Following on from [Websocket on TclHttpd] I was reading that the RFB protocol used by VNC could be layered on top of WebSocket. Searching the Wiki, [Mac Cody] had written [TclRFB] which is a [pure-Tcl] Client and Server for the RFB protocol in 2003. On the [TclRFB] homepage it mentions one of the Future Goals was '''"Combine TclRFB with tclhttpd to allow serving of the Java VNC client to a browser. Alternately, the Java VNC client could be replaced with a [TclRFB] client that would run on the Tcl/Tk plugin." ''' Well 11 years later the "Java VNC client" can be replaced by noVNC, a VNC client using HTML5 WebSockets written in Javascript. I thought if I could combine [TclHttpd] with [TclRFB], WebSocket and noVNC this would be an interesting project to test the WebSocket library. I wanted to run the [TclRFB] server in the same event loop as [TclHttpd] and WebSocket. So I decided to run the [TclRFB] Server in a Slave Interpreter and redirect its socket I/O to the WebSocket Library via alias commands. This worked out well. Testing was done using the rfbcounter demo that came with [TclRFB]. The [TclRFB] homepage says the server needs the VNC client to support BGR233. I had to modify rfbcounter.tcl as noVNC works with 24bit True Colour. I have created a [Starkit] with the latest [TclHttpd] from the Fossil repository. This works with Tcl8.6 and Tcl8.5. It's available from [http://www.freewebs.com/headsup/TclRFB-WebSocket.kit] (Right click on the link and select "Save target as...") Point your browser to http://127.0.0.1:8015 and click on "WebSocket [TclRFB] noVNC Test" on the homepage. [TclRFB] looks to be very interesting. It's a shame it never progressed. Below is the tclRFB-novnc.tcl file in the custom directory of [TclHttpd] ====== # I made a few modifications to the Websocket library to make it work with Tclhttpd. # # 1. In the procedure ::websocket::test changed the following line from # [string equal -nocase $v "upgrade"] } { # to # [string compare -nocase $v "*upgrade*"] } { # Most browsers send the header "Connection: Upgrade" but Firefox sends # "Connection: keep-alive, Upgrade" # # 2. In the procedure ::websocket::takeover changed the following line from # fconfigure $sock -translation binary -blocking on # to # fconfigure $sock -translation binary -blocking off # # 3. In the procedure ::websocket::Receiver changed the following line from # binary scan $dta Iu mask # to # binary scan $dta I mask # Without this change the symptoms were intermittent starting of TclRFB # server. # # So make the above modifications and then save the following to # tclRFB-novnc.tcl and drop in the custom directory. # Url_PrefixInstall /novnc [list ::novnc::domain /novnc] package require websocket namespace eval ::novnc { # ensure ::novnc namespace exists } namespace eval ::rfbcounter { # ensure ::rfbcounter namespace exists } proc ::novnc::domain {prefix sock suffix} { upvar #0 Httpd$sock data # Use the Session Module in TclHttpd to create a slave interpreter to run the # TclRFB package in but rename certain commands to redirect the I/O to the webSocket library. set session [Session_Create Rfb 0] # To get started register the socket as a websocket server. ::websocket::server $sock # The callback procedure when a message/data is present. ::websocket::live $sock /novnc [list ::novnc::TclRFB $session] # Test the Http headers via data(headerlist) to see if it is a websocket request. set wstest [::websocket::test $sock $sock /novnc $data(headerlist) $data(query)] # If ::websocket::test returns 1 it's a valid websocket request so suspend the Http request # in TclHtppd. Start the TclRFB server in its own Slave Interpreter. # Let the websocket library return the correct Http headers via the ::websocket::upgrade # procedure and take control. if {$wstest == 1} { Httpd_Suspend $sock 0 ::rfbcounter::Setup $sock $session rfbcounterNovnc.tcl -clock 1 000000 0000ff ::websocket::upgrade $sock } else { Httpd_ReturnData $sock text/html "Not a valid Websocket connection!" Session_Destroy $session } } # ::novnc::TclRFB -- #This procedure is called when the server #can read data from the client # # Arguments: appended to the callback procedure by the Websocket library. #sockThe socket connection to the client #typeType of message either: #request (initial connection generated by the websocket library.) #close #disconnect #binary #text #msgmessage or data # proc ::novnc::TclRFB {session sock type msg} { upvar #0 Session:$session state set interp $state(interp) switch $type { request { set rfbClientAddr [lindex [fconfigure $sock -peername] 0] set rfbClientPort [lindex [fconfigure $sock -peername] 2] $interp eval ::rfb::AcceptServerSocket $sock $rfbClientAddr $rfbClientPort } close { return } disconnect { Session_Destroy $session unset ::Httpd$sock return } binary { set state(lwsockmsg) 1 set state(wsockmsg) $msg while {$state(lwsockmsg) > 0} { $interp eval ::rfb::ServerConnectionStateMachine $sock } return } text { return } } } # ::rfbcounter::Setup -- # This procedure is called just before the WebSocket Library takes over the socket. # It sets up the rfb server to run using the same event loop as Tclhttpd and the # WebSocket Library before data is transferred. We want to run rfbcounterNovnc.tcl # in its own slave interpreter and this way we can capture the I/O by aliasing commands # and redirecting them to the WebSocket Library. # # Arguments: # sock The socket connection to the client #session The session used to create the interp # sfileThe source file in the bin directory. #args The arguments if rfbcounter.tcl was run on the command line. # proc ::rfbcounter::Setup { sock session sfile args } { upvar #0 Session:$session state set interp $state(interp) # Following taken from httpd.tcl for getting these variables into # a slave interpreter. # # Transfer the scalar global variables foreach var {::v ::auto_path} { $interp eval [list set $var [set $var]] } # Renaming commands and aliasing in the Slave Interpreter # to capture the socket I/O. interp eval $interp {rename puts real_puts} interp alias $interp puts {} ::rfbcounter::Puts $interp interp eval $interp {rename read real_read} interp alias $interp read {} ::rfbcounter::Read $sock $session interp eval $interp {rename close real_close} interp alias $interp close {} ::rfbcounter::Close $interp interp eval $interp {rename fconfigure real_fconfigure} interp alias $interp fconfigure {} ::rfbcounter::Fconfigure $interp interp eval $interp {rename socket real_socket} interp alias $interp socket {} ::rfbcounter::Socket $interp $sock interp eval $interp {rename fileevent real_fileevent} interp alias $interp fileevent {} ::rfbcounter::Fileevent $interp interp share {} $sock $interp interp eval $interp "set argc [llength $args]" set cmdargv "set argv [list $args]" interp eval $interp $cmdargv $interp eval [list set sock $sock] $interp eval [list set tclhttpdport [lindex [fconfigure $sock -sockname] 2]] set cmd [list source [file join $starkit::topdir bin $sfile]] $interp eval $cmd } proc ::rfbcounter::Puts { interp args } { if {[string match "-nonewline" [lindex $args 0]]} { set flag -nonewline set args [lrange $args 1 end] } else { set flag "" } if {[llength $args] == 1} { set chan stdout return [$interp eval real_puts $chan $args] } elseif {[llength $args] == 2} { if {[string match "sock*" [lindex $args 0]]} { set sock [lindex $args 0] set msg [lindex $args 1] ::websocket::send $sock binary $msg } else { return [$interp eval real_puts $flag $args] } } else { return [$interp error "wrong # args: should be \"puts ?-nonewline? ?channelId? string\""] } } proc ::rfbcounter::Read {sock session args} { upvar #0 Session:$session state set interp $state(interp) if { [llength $args] == 2 && [string match "sock*" [lindex $args 0]] } { set range [expr [lindex $args 1]-1] set wsockdata [string range $state(wsockmsg) 0 $range] set state(wsockmsg) [string range $state(wsockmsg) [lindex $args 1] end] set state(lwsockmsg) [string length $state(wsockmsg)] return $wsockdata } else { return [$interp eval real_read $args] } } proc ::rfbcounter::Close {interp args} { if {[string match "sock*" [lindex $args 0]]} { ::websocket::close $sock } else { return [$interp eval real_close $args] } } proc ::rfbcounter::Fconfigure {interp args} { if {[string match "-buffering" [lindex $args 1]]} { return } else { return [$interp eval real_fconfigure $args] } } proc ::rfbcounter::Socket {interp sock args} { if {[string match "-server" [lindex $args 0]]} { return $sock } else { return [$interp eval real_socket $args] } } proc ::rfbcounter::Fileevent {interp args} { return } ====== This is the rfbcounterNovnc.tcl in the bin directory of [TclHttpd] ====== # This is a slightly modified rfbcounter.tcl file that # was originally shipped with TclRFB as a demo. It # allows TclRFB to run with TclHttpd, WebSocket and the # noVNC client. Save this to rfbcounterNovnc.tcl and drop in # the bin directory of TclHttpd. package require tclRFB set ::rfb::rfb($sock,clientBEIfLittle) 1 # TclRFB - rfbcounter.tcl: Counter/clock RFB server # Determine command line arguments set ndx 0 if {[string match {-clock} [lindex $argv 0]]} { set showClock 1 incr ndx } else { set showClock 0 } if {[regexp -- {^[1-9]*[0-9]$} [lindex $argv $ndx]]} { set port [expr 5900 + [lindex $argv $ndx]] incr ndx } else { puts {Incorrect command line options!!!} puts {rfbcouter.tcl [-clock] display-number [fg [bg]]} exit } if {$argc - $showClock > 1} { if {[regexp -- {^[0-9a-f]{6}$} [lindex $argv $ndx]]} { set fg [lindex $argv $ndx] incr ndx } else { puts {Incorrect command line options} puts {rfbcouter.tcl [-clock] display-number [fg [bg]]} exit } } else { set fg 000000 } if {$argc - $showClock > 2} { if {[regexp -- {^[0-9a-f]{6}$} [lindex $argv $ndx]]} { set bg [lindex $argv $ndx] incr ndx } else { puts {Incorrect command line options} puts {rfbcouter.tcl [-clock] display-number [fg [bg]]} exit } } else { set bg ffffff } # Either TclDES or TclDESjr can be used with TclRFB. # If you don't want to use VNC authentication, comment # out the next line. See the ServerSetup proc also. #package require tclDES #package require tclDESjr # sevenseg(number) {element element ... element} set sevenseg(0) {a b c d e f} set sevenseg(1) {b c} set sevenseg(2) {a b d e g} set sevenseg(3) {a b c d g} set sevenseg(4) {b c f g} set sevenseg(5) {a c d f g} set sevenseg(6) {a c d e f g} set sevenseg(7) {a b c} set sevenseg(8) {a b c d e f g} set sevenseg(9) {a b c d f g} # sevenseg(element) {x y w h} set sevenseg(a) {25 10 50 10} set sevenseg(b) {75 20 10 50} set sevenseg(c) {75 80 10 50} set sevenseg(d) {25 130 50 10} set sevenseg(e) {15 80 10 50} set sevenseg(f) {15 20 10 50} set sevenseg(g) {25 70 50 10} set sevenseg(colonH) {0 40 10 10} set sevenseg(colonL) {0 100 10 10} # Procedure: ServerSetup - Set up the RFB server # Inputs: # port - The port number for the RFB server. # (really TclHttpd port number) # Output: # The socket handle for the created server. # # Effect: # The server awaits connection from one or more clients. proc ServerSetup {port} { set optList [list serverVersionMajor 3] set optList [concat $optList [list serverVersionMinor 3]] set optList [concat $optList [list serverBPP 32]] set optList [concat $optList [list serverDepth 24]] set optList [concat $optList [list serverBE 1]] set optList [concat $optList [list serverTC 1]] set optList [concat $optList [list serverRmax 255]] set optList [concat $optList [list serverGmax 255]] set optList [concat $optList [list serverBmax 255]] set optList [concat $optList [list serverRshift 16]] set optList [concat $optList [list serverGshift 8]] set optList [concat $optList [list serverBshift 0]] set optList [concat $optList [list serverShared client]] set optList [concat $optList [list passwd {}]] set optList [concat $optList [list passfile /home/mcody/.vnc/passwd]] # If you don't want to use VNC authentication, replace # "[list scheme 2]" below with "[list scheme 1]". # set optList [concat $optList [list scheme 2]] set optList [concat $optList [list scheme 1]] set optList [concat $optList [list width 600]] set optList [concat $optList [list height 150]] set optList [concat $optList [list name "TclRFB Clock Server $::sock"]] set optList [concat $optList [list updaterequest ProcessUpdateRequest]] set optList [concat $optList [list keyevent ProcessKeyEvent]] set optList [concat $optList [list pointerevent ProcessPointerEvent]] set optList [concat $optList [list servercuttext ProcessServerCutText]] # Initial last value will guarantee transmission of all characters. set optList [concat $optList [list lval -1]] return [::rfb::CreateServerSocket $port $optList] } # Procedure: ProcessUpdateRequest - Process the update request event that # comes fromt the client. # Inputs: # sock - Socket handle ID. # inc - Flag for incremental update (1) or complete update (0). # x - Coordinate of the lefthand side of the requested update region. # y - Coordinate of the top side of the requested update region. # width - Width of the requested update region. # height - Geight of the requested update region. # # Output: # 1 - successful completion. # # Effect: # Appropriate frame buffer update message is sent to the client. proc ProcessUpdateRequest {sock inc x y width height} { global showClock bg fg sevenseg # It appears noVNC doesn't take notice of the Server Endianness # and wants to have the colours in Little Endian order. # (Not sure if this is the correct terminalogy. Instead of # the colours in RGB order it wants BGR order.) Don't know # if the rfb($sock,clientBE) changes depending on the OS running # on the client machine but this should handle it if it does. if {$::rfb::rfb($sock,clientBEIfLittle)} { if {$::rfb::rfb($sock,clientBE) == 0} { set bgRed [string range $bg 0 1] set bgGreen [string range $bg 2 3] set bgBlue [string range $bg 4 5] set bg $bgBlue$bgGreen$bgRed set fgRed [string range $fg 0 1] set fgGreen [string range $fg 2 3] set fgBlue [string range $fg 4 5] set fg $fgBlue$fgGreen$fgRed set ::rfb::rfb($sock,clientBEIfLittle) 0 } else { set ::rfb::rfb($sock,clientBEIfLittle) 0 } } if {$::rfb::rfb($sock,state) eq {halted}} { return 0 } if {$showClock} { # Get the current time set cval [clock format [clock seconds] -format %H%M%S] } else { set cval [string trimleft $::rfb::rfb($sock,lval) 0] if {$cval != {}} { incr cval } else { set cval 1 } if {$cval > 1000000} { set cval 0 } set cval [format %06d $cval] } # encodeList: {{2 x y w h bg {{x y w h fg} ... {x y w h fg}}} ... {2 x y w h bg {{x y w h fg} ... {x y w h fg}}}} if {![string match $::rfb::rfb($sock,lval) $cval] || !$inc} { # start the RRE rectangle list with the background color set i 0 foreach ndx {0 100 200 300 400 500} { set num [string index $cval $i] if {($num ne [string index $::rfb::rfb($sock,lval) $i]) || !$inc} { set rreList [list 2 $ndx 0 100 150 $bg] set rectList {} foreach elm $sevenseg($num) { lappend rectList [concat $sevenseg($elm) $fg] } lappend rreList $rectList lappend encodeList $rreList } incr i } if {$showClock} { # add the semicolons set rectList [list [concat $sevenseg(colonH) $fg]] lappend rectList [concat $sevenseg(colonL) $fg] lappend encodeList [list 2 195 0 10 150 $bg $rectList] lappend encodeList [list 2 395 0 10 150 $bg $rectList] } ::rfb::SendFramebufferUpdate $sock $encodeList set ::rfb::rfb($sock,lval) $cval } else { # Send a dummy frame buffer update. ::rfb::SendFramebufferUpdate $sock {} } return 1 } # Procedure: ProcessKeyEvent - Process the keyboard event that comes fromt the client. # Inputs: # sock - Socket handle ID. # downflag - Key depressed (1) or released (0). # keysym - Keysym value. # # Output: # 1 - Successful completion. # # Effect: # RFB message to ring the console bell is sent to the client. proc ProcessKeyEvent {sock downflag keysym} { if {$downflag} { ::rfb::SendBell $sock } return 1 } # Procedure: ProcessPointerEvent - Process the mouse pointer event that comes fromt the client. # Inputs: # sock - Socket handle ID. # buttonmask - States for the mouse buttons (1 - depressed, 0 - key released). # x - Current x coordinate of the mouse cursor. # y - Current y coordinate of the mouse cursor. # # Output: # 1 - Successful completion. # # Effect: # RFB message to ring the console bell is sent to the client. proc ProcessPointerEvent {sock buttonmask x y} { if {$buttonmask} { ::rfb::SendBell $sock } return 1 } # Procedure: ProcessServerCutText - Process the cut buffer event that comes fromt the client. # Inputs: # sock - Socket handle ID. # text - Cut buffer text sent by the client. # # Output: # 1 - Successful completion. # # Effect: # RFB message to ring the console bell is sent to the client. proc ProcessServerCutText {sock text} { ::rfb::SendBell $sock return 1 } # Start up the server. ServerSetup $tclhttpdport # Wait until the server is shut down (Ctrl-C). #set forever 0 #vwait forever ====== <>TclHttpd