diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index 2d96d69..6cb5c2d 100755 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -92,7 +92,7 @@ func sshServer(hostKeyFile string) *ssh.Server { } }) - log.Println("starting ssh server") + log.Println("starting ssh server, waiting for debug sessions") server := ssh.Server{ PasswordHandler: passwordAuth, SubsystemHandlers: map[string]ssh.SubsystemHandler{ @@ -164,6 +164,8 @@ func main() { log.Println("WebSocket connection error:", err) return } + conn.SetReadDeadline(time.Time{}) + conn.SetWriteDeadline(time.Time{}) wsConn := websocketutil.NewWebSocketConn(conn) defer wsConn.Close() diff --git a/cmd/converge/server.go b/cmd/converge/server.go index 4db9826..fc34ae7 100644 --- a/cmd/converge/server.go +++ b/cmd/converge/server.go @@ -1,14 +1,18 @@ package main import ( + "bytes" "cidebug/pkg/converge" "cidebug/pkg/websocketutil" "fmt" + "io" "log" "net" "net/http" "os" "regexp" + "strings" + "sync" ) func parsePublicId(path string) (publicId string, _ error) { @@ -25,6 +29,90 @@ func catchAllHandler(w http.ResponseWriter, r *http.Request) { return } +// filters to filter html content. +var filters map[string]string = make(map[string]string) + +type FileHandlerFilter struct { + http.Handler +} + +func (handler FileHandlerFilter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + mutex := sync.Mutex{} + // pedantic + mutex.Lock() + + if r.TLS == nil { + filters["secure"] = "" + } else { + filters["secure"] = "s" + } + for _, header := range []string{"X-Forwarded-Proto", "X-Scheme", "X-Forwarded-Scheme"} { + values := r.Header.Values(header) + for _, value := range values { + if strings.ToLower(value) == "https" { + filters["secure"] = "s" + } + } + } + filters["host"] = r.Host + mutex.Unlock() + + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + + handler.Handler.ServeHTTP(w, r) +} + +type FilteredFileSystem struct { + fs http.FileSystem +} + +func (ffs FilteredFileSystem) Open(name string) (http.File, error) { + log.Println("Name : " + name) + f, err := ffs.fs.Open(name) + if err != nil { + return nil, err + } + if !strings.HasSuffix(name, ".html") { + return f, nil + } + return NewFilteredFile(f, filters) +} + +type FilteredFile struct { + http.File + + // Read bytes 0..(index-1) already + index int + fullContents *bytes.Buffer +} + +func NewFilteredFile(f http.File, filter map[string]string) (FilteredFile, error) { + file := FilteredFile{ + index: 0, + fullContents: bytes.NewBuffer(make([]byte, 0)), + } + file.File = f + + _, err := io.Copy(file.fullContents, file.File) + if err != nil { + return FilteredFile{}, err + } + contents := file.fullContents.String() + for key, value := range filter { + key = "@" + key + "@" + contents = strings.ReplaceAll(contents, key, value) + } + file.fullContents = bytes.NewBufferString(contents) + + return file, nil +} + +func (ff FilteredFile) Read(p []byte) (n int, err error) { + return ff.fullContents.Read(p) +} + func main() { downloadDir := "downloads" @@ -65,7 +153,14 @@ func main() { http.HandleFunc("/agent/", registrationService.Handle) http.HandleFunc("/client/", clientService.Handle) - http.Handle("/docs/", http.StripPrefix("/docs/", http.FileServer(http.Dir(downloadDir)))) + + filesystem := http.Dir(downloadDir) + filteredFilesystem := FilteredFileSystem{ + fs: filesystem, + } + fileHandler := FileHandlerFilter{http.FileServer(filteredFilesystem)} + + http.Handle("/docs/", http.StripPrefix("/docs/", fileHandler)) http.HandleFunc("/", catchAllHandler) // Start HTTP server diff --git a/cmd/tcptows/tcptows.go b/cmd/tcptows/tcptows.go index c0a72c8..f99a15f 100644 --- a/cmd/tcptows/tcptows.go +++ b/cmd/tcptows/tcptows.go @@ -7,6 +7,7 @@ import ( "log" "net" "os" + "time" ) func closeConnection(conn net.Conn) { @@ -25,6 +26,8 @@ func handleConnection(conn net.Conn, wsURL string) { log.Println("WebSocket connection error:", err) return } + _wsConn.SetReadDeadline(time.Time{}) + _wsConn.SetWriteDeadline(time.Time{}) wsConn := websocketutil.NewWebSocketConn(_wsConn) defer wsConn.Close() diff --git a/cmd/wsproxy/proxy.go b/cmd/wsproxy/proxy.go index 1b6d249..9218ea6 100644 --- a/cmd/wsproxy/proxy.go +++ b/cmd/wsproxy/proxy.go @@ -7,6 +7,7 @@ import ( "log" "net" "os" + "time" ) func closeConnection(conn net.Conn) { @@ -29,6 +30,8 @@ func main() { wsURL := os.Args[1] _wsConn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) + _wsConn.SetReadDeadline(time.Time{}) + _wsConn.SetWriteDeadline(time.Time{}) if err != nil { log.Println("WebSocket connection error:", err) panic(err) diff --git a/pkg/agent/help.txt b/pkg/agent/help.txt index 44435ef..0fed91e 100644 --- a/pkg/agent/help.txt +++ b/pkg/agent/help.txt @@ -1,4 +1,4 @@ -Session is set to expire at %s +Session is set to expire at %v The session expires automatically after %d time. If there are no more sessions after logging out, the agent @@ -8,6 +8,6 @@ You can extend this time using touch $agentdir/.hold -To prevent the agent from exiting after the last sessioni is gone, +To prevent the agent from exiting after the last session is gone, also use the above command in any shell. diff --git a/pkg/agent/session.go b/pkg/agent/session.go index 3c705c6..2c8016d 100644 --- a/pkg/agent/session.go +++ b/pkg/agent/session.go @@ -144,9 +144,9 @@ func fileExists(filename string) bool { func (state *AgentState) expiryTime(filename string) time.Time { stats, err := os.Stat(filename) if err != nil { - return state.startTime + return state.startTime.Add(state.agentExpriryTime) } - return stats.ModTime() + return stats.ModTime().Add(state.agentExpriryTime) } // Behavior to implement @@ -177,6 +177,11 @@ func check() { PrintHelpMessage(session.sshSession) } } + + if !fileExists(holdFilename) && len(state.sessions) == 0 { + log.Printf("All clients disconnected and no '%s' file found, exiting", holdFilename) + os.Exit(0) + } } func messageUsers(message string) { diff --git a/pkg/websocketutil/connections.go b/pkg/websocketutil/connections.go index ea87213..7ff838a 100644 --- a/pkg/websocketutil/connections.go +++ b/pkg/websocketutil/connections.go @@ -72,6 +72,8 @@ func ConnectWebSocket(conn net.Conn, urlStr string) (net.Conn, error) { if err != nil { return nil, err } + wsConn.SetReadDeadline(time.Time{}) + wsConn.SetWriteDeadline(time.Time{}) return NewWebSocketConn(wsConn), nil } diff --git a/pkg/websocketutil/services.go b/pkg/websocketutil/services.go index 37efbd5..a1fc248 100644 --- a/pkg/websocketutil/services.go +++ b/pkg/websocketutil/services.go @@ -5,6 +5,7 @@ import ( "log" "net" "net/http" + "time" ) type WebSocketAddr string @@ -28,6 +29,8 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request, log.Println("Error upgrading to WebSocket:", err) return } + conn.SetReadDeadline(time.Time{}) + conn.SetWriteDeadline(time.Time{}) wsConn := NewWebSocketConn(conn) defer wsConn.Close() diff --git a/static/index.html b/static/index.html index b479d3a..1b178ba 100644 --- a/static/index.html +++ b/static/index.html @@ -14,6 +14,39 @@
+

About

+ +

+ Converge is a utility for troubleshooting builds on continuous integration serves. + It solves a common problem where the cause of job failure is difficult to determine. + This is complicated furhter by the fact that build jobs are usually run on a build + farm where there is no access to the build agents or in more modern envrionments when + jobs are run in ephemeral containers. +

+ +

+ With Converge it is possible to get remote shell access to such jobs. This works + by configuring the build job to connect to a Converge server using an agent program. + The agent program can be downloaded from within the CI job using curl or wget. + Next, an end-use can connect to the Converge server, a rendez-vous server, that connects + the client and server together. +

+ +

+ The setup is such that the connection from client (end-user) to server (agent on CI job) + is end-to-end encrypted. The Converge server itself is no more than a bitpipe which pumps + data between client and agent. +

+ +

+ Both ssh and sftp are supported. Multiple shells are also allowed. +

+ +

+ There is a timeout mechanism in the agent such that jobs do not hang indefinetely waiting + for a connection. +

+

Usage

Continous integration jobs

@@ -22,12 +55,12 @@ In a continous integration job, download the agent, chmod it and run it.

-     curl https://HOSTPORT/docs/agent > agent
-     ./agent wss://HOST:PORT/agent/ID
-    chmod 755 agent
+     curl http@secure@://@host@/docs/agent > agent
+     chmod 755 agent
+     ./agent ws@secure@://@host@/agent/ID
     

- Above, HOST:PORT is the hostname:port of the converge server and ID is a unique id + Above, ID is a unique id for the job. This should not conflict with other ids. This connects the agent to the converge server. Clients can now connect to converge @@ -37,7 +70,7 @@

Local clients: using ssh proxy command

-    curl https://HOST:PORT/docs/wsproxy > wsproxy
+    curl http@secure@://@host@/docs/wsproxy > wsproxy
     chmod 755 wsproxy
     
@@ -49,8 +82,8 @@

-    ssh -oProxyCommand="wsproxy https://HOST:PORT/client/ID" -p 10000 abc@localhost
-    sftp -oProxyCommand="wsproxy https://HOST:PORT/client/ID" -oPort 10000 abc@localhost
+    ssh -oServerAliveInterval=10 -oProxyCommand="wsproxy ws@secure@://@host@/client/ID"  abc@localhost
+    sftp -oServerAliveInterval=10 -oProxyCommand="wsproxy ws@secure@://@host@/client/ID" abc@localhost
     

@@ -70,15 +103,9 @@

-    # for HTTP hosted server
-    curl http://HOST:PORT/docs/tcptows > tcptows
+    curl http@secure@://@host@/docs/tcptows > tcptows
     chmod 755 tcptows
-    ./tcptows 10000 ws://HOST:PORT/client/ID
-
-    # for HTTPS hosted server
-    curl https://HOST:PORT/docs/tcptows > tcptows
-    chmod 755 tcptows
-    ./tcptows 10000 wss://HOST:PORT/client/ID
+    ./tcptows 10000 ws@secure@://@host@/client/ID
     

@@ -90,8 +117,8 @@

-    ssh -p 10000 abc@localhost
-    sftp -oPort 10000 abc@localhost
+    ssh -oServerAliveInterval=10 -p 10000 abc@localhost
+    sftp -oServerAliveInterval=10 -oPort 10000 abc@localhost