diff --git a/cmd/converge/fileserver.go b/cmd/converge/fileserver.go index 5009e53..1d38bec 100644 --- a/cmd/converge/fileserver.go +++ b/cmd/converge/fileserver.go @@ -1,59 +1,67 @@ package main import ( - "bytes" - "io" + "html/template" "net/http" + "os" + "path/filepath" "strings" ) -// filters to filter html content. -var filters map[string]string = make(map[string]string) - -type FilteredFileSystem struct { - fs http.FileSystem +type FileHandlerFilter struct { + dir string + fileHandler http.Handler } -func (ffs FilteredFileSystem) Open(name string) (http.File, error) { - f, err := ffs.fs.Open(name) +func NewFileHandler(dir string) *FileHandlerFilter { + handler := FileHandlerFilter{ + dir: dir, + fileHandler: http.FileServer(http.Dir(dir)), + } + return &handler +} + +func (handler FileHandlerFilter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + contextPath := r.URL.Path + path := filepath.Join(handler.dir, contextPath) + fileInfo, err := os.Stat(path) + if err == nil && fileInfo.IsDir() { + contextPath = filepath.Join(contextPath, "index.html") + } + + if !strings.HasSuffix(contextPath, ".html") { + handler.fileHandler.ServeHTTP(w, r) + return + } + + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + + filters := make(map[string]string) + 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 + + tmpl, err := template.ParseFiles(filepath.Join(handler.dir, contextPath)) if err != nil { - return nil, err + // let the filehandler generate the rror + handler.fileHandler.ServeHTTP(w, r) } - 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) + err = tmpl.Execute(w, filters) if err != nil { - return FilteredFile{}, err + http.Error(w, err.Error(), http.StatusInternalServerError) } - 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) + // ok, tmpl has written the response } diff --git a/cmd/converge/server.go b/cmd/converge/server.go index 9feac0f..bb65d33 100644 --- a/cmd/converge/server.go +++ b/cmd/converge/server.go @@ -9,8 +9,6 @@ import ( "net/http" "os" "regexp" - "strings" - "sync" ) func parsePublicId(path string) (publicId string, _ error) { @@ -27,38 +25,6 @@ func catchAllHandler(w http.ResponseWriter, r *http.Request) { return } -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) -} - func main() { downloadDir := "downloads" @@ -97,15 +63,12 @@ func main() { }, } + // websocket endpoints http.HandleFunc("/agent/", registrationService.Handle) http.HandleFunc("/client/", clientService.Handle) - filesystem := http.Dir(downloadDir) - filteredFilesystem := FilteredFileSystem{ - fs: filesystem, - } - fileHandler := FileHandlerFilter{http.FileServer(filteredFilesystem)} - + // create filehandler with templating for html files. + fileHandler := NewFileHandler(downloadDir) http.Handle("/docs/", http.StripPrefix("/docs/", fileHandler)) http.HandleFunc("/", catchAllHandler) diff --git a/static/index.html b/static/index.html index acffb94..2295fe9 100644 --- a/static/index.html +++ b/static/index.html @@ -12,136 +12,168 @@ -
- Converge is a utility for troubleshooting builds on continuous integration servers. - It solves a common problem where the cause of job failure is difficult to determine. - This is complicated further 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-user can connect to the Converge server, a rendez-vous server, that connects - the client and server together. -
++ Converge is a utility for troubleshooting builds on continuous integration servers. + It solves a common problem where the cause of job failure is difficult to determine. + This is complicated further 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. +
-- 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. -
++ 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-user can connect to the Converge server, a rendez-vous server, that connects + the client and server together. +
-- Both ssh and sftp are supported. Multiple shells are also allowed. -
++ 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. +
-- There is a timeout mechanism in the agent such that jobs do not hang indefinitely waiting - for a connection. This mechanism is useful to make sure build agents do not keep - build agents occupied for a long time. By default, the agent exits with status 0 when - the first client exits after logging in. This behavior as well as general expiry can be - controlled from within a shell session by touching a .hold file. After logging in, the - user can control expiry of the session as instructed by messages in the ssh session. - When the timeout of a session is near the user is informed about this with messages - in the shell. -
++ 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 indefinitely waiting + for a connection. This mechanism is useful to make sure build agents do not keep + build agents occupied for a long time. By default, the agent exits with status 0 when + the first client exits after logging in. This behavior as well as general expiry can be + controlled from within a shell session by touching a .hold file. After logging in, the + user can control expiry of the session as instructed by messages in the ssh session. + When the timeout of a session is near the user is informed about this with messages + in the shell. +
-- In a continous integration job, download the agent, chmod it and run it. -
-- curl http@secure@://@host@/docs/agent > agent +Continous integration jobs
+ ++ In a continous integration job, download the agent, chmod it and run it. +
++ # linux + curl http{{.secure}}://{{.host}}/docs/agent > agent chmod 755 agent - ./agent ws@secure@://@host@/agent/ID + ./agent ws{{.secure}}://{{.host}}/agent/ID + + # windows + curl http{{.secure}}://{{.host}}/docs/agent.exe > agent.exe + agent ws{{.secure}}://{{.host}}/agent/ID-- Above, ID is a unique id - for the job. This should not conflict with other ids. +
+ 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 the Converge - server to establish a connection to the CI job through converge. -
+ This connects the agent to the converge server. Clients can now connect to the Converge + server to establish a connection to the CI job through converge. + -Local clients: using ssh proxy command
++ NOTE: When running the agent on windows, an exit of the remote session using + exit in powershell or command prompt does not terminate the shell completely. + To terminate the client ssh session must be killed by closing the terminal. + Cleanup of remote processes on the agent appears to work properly despite this + problem. It is just that exit of the windows shell (powershell or command prompt) + is not detected properly. +
-- curl http@secure@://@host@/docs/wsproxy > wsproxy - chmod 755 wsproxy +Local clients: using ssh proxy command
+ ++
wsproxy
is a command that can be used as a proxy command for SSH which performs the connection to the + remote server. This command needs to be downloaded only once (see downloads below). It does not depend on + the converge implementation but only on the websocket standards. Other tools that + provide a mapping of stdio to a websocket can also be used instead of wsproxy. ++ Next step is to run a local SSH or SFTP client: +
+ ++ ssh -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{{.secure}}://{{.host}}/client/ID" abc@localhost + sftp -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{{.secure}}://{{.host}}/client/ID" abc@localhost-This is a command that can be used as a proxy command for SSH which performs the connection to the remote - server.
++
-abc
is a fixed user defined by converge. It has a very exciting password. +- Next step is to run a local SSH or SFTP client: -
+Local clients: with a local TCP forwarding proxy
-- ssh -oServerAliveInterval=10 -oProxyCommand="wsproxy ws@secure@://@host@/client/ID" abc@localhost - sftp -oServerAliveInterval=10 -oProxyCommand="wsproxy ws@secure@://@host@/client/ID" abc@localhost -++ This option is less convenient than the proxy command because it requires two separate + commands to execute. +
--
+abc
is a fixed user defined by converge. It has a very exciting password. -+ Local clients can connect using regular ssh and sftp commands through a tunnel that + translates a local TCP port to a websocket connection in converge. See + the downloads section. + This runs a local client that allows SSH to port 10000 and connects to converge using + a websocket connection. +
-Local clients: with a local TCP forwarding proxy
- This option is less convenient than the proxy command because it requires two separate - commands to execute. ++ Next step is to run a local SSH of SFTP client: +
-- Local clients can connect using regular ssh and sftp commands through a tunnel that - translates a local TCP port to a websocket connection in converge. - - First step is to download the tcptows program (see below): -
- -- curl http@secure@://@host@/docs/tcptows > tcptows - chmod 755 tcptows - ./tcptows 10000 ws@secure@://@host@/client/ID -- -- This runs a local client that allows SSH to port 10000 and connects to converge using - a websocket connection.
- -- Next step is to run a local SSH of SFTP client: -
- -+ssh -oServerAliveInterval=10 -p 10000 abc@localhost sftp -oServerAliveInterval=10 -oPort=10000 abc@localhost--
+abc
is a fixed user defined by converge. It has a very exciting password. -+
-abc
is a fixed user defined by converge. It has a very exciting password. +Downloads
+Downloads
-
Component | +Purpose | +Linux | +Windows | +
---|---|---|---|
agent | +The agent to run inside aa CI job | +agent | +agent.exe | +
wsproxy | +SSH proxy command that can be directly used by ssh | +wsproxy | +wsproxy.exe | +
tcptows | +TCP to WS tunnel for allowing regular + SSH and SFTP clients to connect to converge | +tcptows | +tcptows.exe | +