@@ -23,11 +23,14 @@ import (
2323 "strconv"
2424 "strings"
2525 "sync"
26+ "sync/atomic"
2627 "time"
2728)
2829
2930const websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
3031
32+ var websocketBridgeSequence atomic.Uint64
33+
3134func main () {
3235 listen := flag .String ("listen" , envDefault ("NOVNC_LISTEN" , ":6080" ), "HTTP listen address" )
3336 vnc := flag .String ("vnc" , envDefault ("VNC_ADDR" , "127.0.0.1:5900" ), "upstream VNC address" )
@@ -345,6 +348,9 @@ func staticHandler(root string, ready readyGate) http.HandlerFunc {
345348 // carries the current bundle hash, so it has to be re-read every load.
346349 base := path .Base (clean )
347350 switch {
351+ case strings .HasPrefix (base , "viewer-fallback-" ) && strings .HasSuffix (base , ".bundle.js" ):
352+ // The recovery path must not inherit the primary's immutable cache.
353+ w .Header ().Set ("Cache-Control" , "no-store, max-age=0, must-revalidate" )
348354 case strings .HasPrefix (base , "viewer-" ) && strings .HasSuffix (base , ".bundle.js" ):
349355 w .Header ().Set ("Cache-Control" , "public, max-age=31536000, immutable" )
350356 case strings .HasSuffix (clean , "/liveview.html" ) || strings .HasSuffix (clean , "/kbd-autofocus.js" ) ||
@@ -987,32 +993,48 @@ func websocketKey() (string, error) {
987993}
988994
989995func proxyWebsocket (w http.ResponseWriter , r * http.Request , upstream string ) {
996+ sid := websocketDiagSID (r )
990997 if ! isWebsocketRequest (r ) {
998+ if sid != "" {
999+ log .Printf ("[websockify sid=%s] rejected: websocket upgrade required" , sid )
1000+ }
9911001 http .Error (w , "websocket upgrade required" , http .StatusBadRequest )
9921002 return
9931003 }
9941004
9951005 key := strings .TrimSpace (r .Header .Get ("Sec-WebSocket-Key" ))
9961006 if key == "" {
1007+ if sid != "" {
1008+ log .Printf ("[websockify sid=%s] rejected: missing Sec-WebSocket-Key" , sid )
1009+ }
9971010 http .Error (w , "missing Sec-WebSocket-Key" , http .StatusBadRequest )
9981011 return
9991012 }
10001013
10011014 vncConn , err := net .DialTimeout ("tcp" , upstream , 5 * time .Second )
10021015 if err != nil {
1016+ if sid != "" {
1017+ log .Printf ("[websockify sid=%s] VNC dial %s failed: %v" , sid , upstream , err )
1018+ }
10031019 http .Error (w , fmt .Sprintf ("failed to connect to VNC upstream: %v" , err ), http .StatusBadGateway )
10041020 return
10051021 }
10061022
10071023 hijacker , ok := w .(http.Hijacker )
10081024 if ! ok {
1025+ if sid != "" {
1026+ log .Printf ("[websockify sid=%s] rejected: hijacking unsupported" , sid )
1027+ }
10091028 _ = vncConn .Close ()
10101029 http .Error (w , "hijacking unsupported" , http .StatusInternalServerError )
10111030 return
10121031 }
10131032
10141033 clientConn , rw , err := hijacker .Hijack ()
10151034 if err != nil {
1035+ if sid != "" {
1036+ log .Printf ("[websockify sid=%s] client hijack failed: %v" , sid , err )
1037+ }
10161038 _ = vncConn .Close ()
10171039 return
10181040 }
@@ -1039,7 +1061,22 @@ func proxyWebsocket(w http.ResponseWriter, r *http.Request, upstream string) {
10391061 reader : rw .Reader ,
10401062 vnc : vncConn ,
10411063 }
1042- bridge .run ()
1064+ bridgeID := websocketBridgeSequence .Add (1 )
1065+ if sid != "" {
1066+ log .Printf ("[websockify sid=%s bridge=%d] upgraded upstream=%s" , sid , bridgeID , upstream )
1067+ }
1068+ result := bridge .run ()
1069+ if sid != "" {
1070+ if result .closeCode != 0 {
1071+ log .Printf ("[websockify sid=%s bridge=%d] closed direction=%s code=%d reason=%q" , sid , bridgeID , result .direction , result .closeCode , result .closeReason )
1072+ } else {
1073+ log .Printf ("[websockify sid=%s bridge=%d] closed direction=%s err=%v" , sid , bridgeID , result .direction , result .err )
1074+ }
1075+ }
1076+ }
1077+
1078+ func websocketDiagSID (r * http.Request ) string {
1079+ return klogSanitize (r .URL .Query ().Get ("diag_sid" ), 24 )
10431080}
10441081
10451082func isWebsocketRequest (r * http.Request ) bool {
@@ -1085,26 +1122,32 @@ type wsBridge struct {
10851122 mu sync.Mutex
10861123}
10871124
1088- func (b * wsBridge ) run () {
1089- done := make (chan struct {}, 2 )
1125+ type wsBridgeResult struct {
1126+ direction string
1127+ err error
1128+ closeCode int
1129+ closeReason string
1130+ }
1131+
1132+ func (b * wsBridge ) run () wsBridgeResult {
1133+ done := make (chan wsBridgeResult , 2 )
10901134 stopPing := make (chan struct {})
10911135
10921136 go func () {
1093- b .copyWebsocketToVNC ()
1094- done <- struct {}{}
1137+ done <- b .copyWebsocketToVNC ()
10951138 }()
10961139
10971140 go func () {
1098- b .copyVNCToWebsocket ()
1099- done <- struct {}{}
1141+ done <- b .copyVNCToWebsocket ()
11001142 }()
11011143
11021144 go b .pingLoop (stopPing )
11031145
1104- <- done
1146+ result := <- done
11051147 close (stopPing )
11061148 _ = b .client .Close ()
11071149 _ = b .vnc .Close ()
1150+ return result
11081151}
11091152
11101153// pingLoop sends server->client WS pings so idle-but-alive connections stay warm
@@ -1124,7 +1167,7 @@ func (b *wsBridge) pingLoop(stop <-chan struct{}) {
11241167 }
11251168}
11261169
1127- func (b * wsBridge ) copyWebsocketToVNC () {
1170+ func (b * wsBridge ) copyWebsocketToVNC () wsBridgeResult {
11281171 for {
11291172 // Refresh the read deadline on every frame (client keystrokes AND the
11301173 // auto-pongs to our pings count as liveness), so a silent half-open
@@ -1135,7 +1178,7 @@ func (b *wsBridge) copyWebsocketToVNC() {
11351178 continue // e.g. an enormous clipboard paste: drop the message, keep the session
11361179 }
11371180 if err != nil {
1138- return
1181+ return wsBridgeResult { direction : "client-read" , err : err }
11391182 }
11401183
11411184 switch opcode {
@@ -1144,39 +1187,48 @@ func (b *wsBridge) copyWebsocketToVNC() {
11441187 continue
11451188 }
11461189 if _ , err := b .vnc .Write (payload ); err != nil {
1147- return
1190+ return wsBridgeResult { direction : "vnc-write" , err : err }
11481191 }
11491192 case 0x8 :
11501193 _ = b .writeFrame (0x8 , payload )
1151- return
1194+ code , reason := websocketCloseInfo (payload )
1195+ return wsBridgeResult {direction : "client-close" , closeCode : code , closeReason : reason }
11521196 case 0x9 :
11531197 _ = b .writeFrame (0xA , payload )
11541198 case 0xA :
11551199 default :
11561200 _ = b .writeFrame (0x8 , []byte {0x03 , 0xEA })
1157- return
1201+ return wsBridgeResult { direction : "client-protocol" , err : fmt . Errorf ( "unsupported opcode %d" , opcode )}
11581202 }
11591203 }
11601204}
11611205
1162- func (b * wsBridge ) copyVNCToWebsocket () {
1206+ func (b * wsBridge ) copyVNCToWebsocket () wsBridgeResult {
11631207 buf := make ([]byte , 32 * 1024 )
11641208 for {
11651209 n , err := b .vnc .Read (buf )
11661210 if n > 0 {
11671211 if writeErr := b .writeFrame (0x2 , buf [:n ]); writeErr != nil {
1168- return
1212+ return wsBridgeResult { direction : "client-write" , err : writeErr }
11691213 }
11701214 }
11711215 if err != nil {
11721216 if ! errors .Is (err , io .EOF ) {
11731217 _ = b .writeFrame (0x8 , nil )
11741218 }
1175- return
1219+ return wsBridgeResult { direction : "vnc-read" , err : err }
11761220 }
11771221 }
11781222}
11791223
1224+ func websocketCloseInfo (payload []byte ) (int , string ) {
1225+ if len (payload ) < 2 {
1226+ return 0 , ""
1227+ }
1228+ code := int (binary .BigEndian .Uint16 (payload [:2 ]))
1229+ return code , klogSanitize (string (payload [2 :]), 120 )
1230+ }
1231+
11801232func (b * wsBridge ) writeFrame (opcode byte , payload []byte ) error {
11811233 _ = b .client .SetWriteDeadline (time .Now ().Add (wsWriteDeadline ))
11821234 return writeFrameToConn (b .client , & b .mu , opcode , payload , false , true )
0 commit comments