Lots of work on docuemtation. The docs page now shows the correct

installation dependent URLs. For now using ServerALiveInterval
to avoid disconnects.
This commit is contained in:
Erik Brakkee 2024-07-21 21:41:53 +02:00
parent 19c728938a
commit aa46ed7b5c
9 changed files with 163 additions and 23 deletions

View File

@ -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{ server := ssh.Server{
PasswordHandler: passwordAuth, PasswordHandler: passwordAuth,
SubsystemHandlers: map[string]ssh.SubsystemHandler{ SubsystemHandlers: map[string]ssh.SubsystemHandler{
@ -164,6 +164,8 @@ func main() {
log.Println("WebSocket connection error:", err) log.Println("WebSocket connection error:", err)
return return
} }
conn.SetReadDeadline(time.Time{})
conn.SetWriteDeadline(time.Time{})
wsConn := websocketutil.NewWebSocketConn(conn) wsConn := websocketutil.NewWebSocketConn(conn)
defer wsConn.Close() defer wsConn.Close()

View File

@ -1,14 +1,18 @@
package main package main
import ( import (
"bytes"
"cidebug/pkg/converge" "cidebug/pkg/converge"
"cidebug/pkg/websocketutil" "cidebug/pkg/websocketutil"
"fmt" "fmt"
"io"
"log" "log"
"net" "net"
"net/http" "net/http"
"os" "os"
"regexp" "regexp"
"strings"
"sync"
) )
func parsePublicId(path string) (publicId string, _ error) { func parsePublicId(path string) (publicId string, _ error) {
@ -25,6 +29,90 @@ func catchAllHandler(w http.ResponseWriter, r *http.Request) {
return 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() { func main() {
downloadDir := "downloads" downloadDir := "downloads"
@ -65,7 +153,14 @@ func main() {
http.HandleFunc("/agent/", registrationService.Handle) http.HandleFunc("/agent/", registrationService.Handle)
http.HandleFunc("/client/", clientService.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) http.HandleFunc("/", catchAllHandler)
// Start HTTP server // Start HTTP server

View File

@ -7,6 +7,7 @@ import (
"log" "log"
"net" "net"
"os" "os"
"time"
) )
func closeConnection(conn net.Conn) { func closeConnection(conn net.Conn) {
@ -25,6 +26,8 @@ func handleConnection(conn net.Conn, wsURL string) {
log.Println("WebSocket connection error:", err) log.Println("WebSocket connection error:", err)
return return
} }
_wsConn.SetReadDeadline(time.Time{})
_wsConn.SetWriteDeadline(time.Time{})
wsConn := websocketutil.NewWebSocketConn(_wsConn) wsConn := websocketutil.NewWebSocketConn(_wsConn)
defer wsConn.Close() defer wsConn.Close()

View File

@ -7,6 +7,7 @@ import (
"log" "log"
"net" "net"
"os" "os"
"time"
) )
func closeConnection(conn net.Conn) { func closeConnection(conn net.Conn) {
@ -29,6 +30,8 @@ func main() {
wsURL := os.Args[1] wsURL := os.Args[1]
_wsConn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) _wsConn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
_wsConn.SetReadDeadline(time.Time{})
_wsConn.SetWriteDeadline(time.Time{})
if err != nil { if err != nil {
log.Println("WebSocket connection error:", err) log.Println("WebSocket connection error:", err)
panic(err) panic(err)

View File

@ -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. The session expires automatically after %d time.
If there are no more sessions after logging out, the agent If there are no more sessions after logging out, the agent
@ -8,6 +8,6 @@ You can extend this time using
touch $agentdir/.hold 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. also use the above command in any shell.

View File

@ -144,9 +144,9 @@ func fileExists(filename string) bool {
func (state *AgentState) expiryTime(filename string) time.Time { func (state *AgentState) expiryTime(filename string) time.Time {
stats, err := os.Stat(filename) stats, err := os.Stat(filename)
if err != nil { if err != nil {
return state.startTime return state.startTime.Add(state.agentExpriryTime)
} }
return stats.ModTime() return stats.ModTime().Add(state.agentExpriryTime)
} }
// Behavior to implement // Behavior to implement
@ -177,6 +177,11 @@ func check() {
PrintHelpMessage(session.sshSession) 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) { func messageUsers(message string) {

View File

@ -72,6 +72,8 @@ func ConnectWebSocket(conn net.Conn, urlStr string) (net.Conn, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
wsConn.SetReadDeadline(time.Time{})
wsConn.SetWriteDeadline(time.Time{})
return NewWebSocketConn(wsConn), nil return NewWebSocketConn(wsConn), nil
} }

View File

@ -5,6 +5,7 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"time"
) )
type WebSocketAddr string type WebSocketAddr string
@ -28,6 +29,8 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request,
log.Println("Error upgrading to WebSocket:", err) log.Println("Error upgrading to WebSocket:", err)
return return
} }
conn.SetReadDeadline(time.Time{})
conn.SetWriteDeadline(time.Time{})
wsConn := NewWebSocketConn(conn) wsConn := NewWebSocketConn(conn)
defer wsConn.Close() defer wsConn.Close()

View File

@ -14,6 +14,39 @@
<div class="container"> <div class="container">
<h1>About</h1>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
Both ssh and sftp are supported. Multiple shells are also allowed.
</p>
<p>
There is a timeout mechanism in the agent such that jobs do not hang indefinetely waiting
for a connection.
</p>
<h1>Usage</h1> <h1>Usage</h1>
<h2>Continous integration jobs</h2> <h2>Continous integration jobs</h2>
@ -22,12 +55,12 @@
In a continous integration job, download the agent, chmod it and run it. In a continous integration job, download the agent, chmod it and run it.
</p> </p>
<pre> <pre>
curl https://HOSTPORT/docs/agent > agent curl http@secure@://@host@/docs/agent > agent
./agent wss://HOST:PORT/agent/ID chmod 755 agent
chmod 755 agent ./agent ws@secure@://@host@/agent/ID
</pre> </pre>
<p> <p>
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. for the job. This should not conflict with other ids.
This connects the agent to the converge server. Clients can now connect to converge This connects the agent to the converge server. Clients can now connect to converge
@ -37,7 +70,7 @@
<h2>Local clients: using ssh proxy command </h2> <h2>Local clients: using ssh proxy command </h2>
<pre> <pre>
curl https://HOST:PORT/docs/wsproxy > wsproxy curl http@secure@://@host@/docs/wsproxy > wsproxy
chmod 755 wsproxy chmod 755 wsproxy
</pre> </pre>
@ -49,8 +82,8 @@
</p> </p>
<pre> <pre>
ssh -oProxyCommand="wsproxy https://HOST:PORT/client/ID" -p 10000 abc@localhost ssh -oServerAliveInterval=10 -oProxyCommand="wsproxy ws@secure@://@host@/client/ID" abc@localhost
sftp -oProxyCommand="wsproxy https://HOST:PORT/client/ID" -oPort 10000 abc@localhost sftp -oServerAliveInterval=10 -oProxyCommand="wsproxy ws@secure@://@host@/client/ID" abc@localhost
</pre> </pre>
<p> <p>
@ -70,15 +103,9 @@
</p> </p>
<pre> <pre>
# for HTTP hosted server curl http@secure@://@host@/docs/tcptows > tcptows
curl http://HOST:PORT/docs/tcptows > tcptows
chmod 755 tcptows chmod 755 tcptows
./tcptows 10000 ws://HOST:PORT/client/ID ./tcptows 10000 ws@secure@://@host@/client/ID
# for HTTPS hosted server
curl https://HOST:PORT/docs/tcptows > tcptows
chmod 755 tcptows
./tcptows 10000 wss://HOST:PORT/client/ID
</pre> </pre>
<p> <p>
@ -90,8 +117,8 @@
</p> </p>
<pre> <pre>
ssh -p 10000 abc@localhost ssh -oServerAliveInterval=10 -p 10000 abc@localhost
sftp -oPort 10000 abc@localhost sftp -oServerAliveInterval=10 -oPort 10000 abc@localhost
</pre> </pre>
<p> <p>