fileserver now uses go template language.

updated docs for windows.
This commit is contained in:
Erik Brakkee 2024-07-23 20:47:51 +02:00
parent cd477b7b0a
commit ff1c13cc98
3 changed files with 190 additions and 187 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -12,136 +12,168 @@
<script src="js/bootstrap.bundle.min.js"
crossorigin="anonymous"></script>
<div class="container">
<div class="container-fluid">
<div class="row">
<div class="col">
<h1>About</h1>
<div class="container">
<p>
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.
</p>
<h1>About</h1>
<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-user can connect to the Converge server, a rendez-vous server, that connects
the client and server together.
</p>
<p>
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.
</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>
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.
</p>
<p>
Both ssh and sftp are supported. Multiple shells are also allowed.
</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>
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.
</p>
<p>
Both ssh and sftp are supported. Multiple shells are also allowed.
</p>
<h1>Usage</h1>
<p>
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.
</p>
<h2>Continous integration jobs</h2>
<h1>Usage</h1>
<p>
In a continous integration job, download the agent, chmod it and run it.
</p>
<pre>
curl http@secure@://@host@/docs/agent > agent
<h2>Continous integration jobs</h2>
<p>
In a continous integration job, download the agent, chmod it and run it.
</p>
<pre>
# 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
</pre>
<p>
Above, ID is a unique id
for the job. This should not conflict with other ids.
<p>
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.
</p>
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.
</p>
<h2>Local clients: using ssh proxy command </h2>
<p>
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.
</p>
<pre>
curl http@secure@://@host@/docs/wsproxy > wsproxy
chmod 755 wsproxy
<h2>Local clients: using ssh proxy command </h2>
<p><code>wsproxy</code> 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 <a href="#downloads">downloads</a> 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.
</p>
<p>
Next step is to run a local SSH or SFTP client:
</p>
<pre>
ssh -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{{.secure}}://{{.host}}/client/ID" abc@localhost
sftp -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{{.secure}}://{{.host}}/client/ID" abc@localhost
</pre>
<p>This is a command that can be used as a proxy command for SSH which performs the connection to the remote
server.</p>
<p>
<code>abc</code> is a fixed user defined by converge. It has a very exciting password.
</p>
<p>
Next step is to run a local SSH or SFTP client:
</p>
<h2>Local clients: with a local TCP forwarding proxy</h2>
<pre>
ssh -oServerAliveInterval=10 -oProxyCommand="wsproxy ws@secure@://@host@/client/ID" abc@localhost
sftp -oServerAliveInterval=10 -oProxyCommand="wsproxy ws@secure@://@host@/client/ID" abc@localhost
</pre>
<p>
This option is less convenient than the proxy command because it requires two separate
commands to execute.
</p>
<p>
<code>abc</code> is a fixed user defined by converge. It has a very exciting password.
</p>
<p>
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 <a href="#downloads">downloads</a> section.
This runs a local client that allows SSH to port 10000 and connects to converge using
a websocket connection.
</p>
<h2>Local clients: with a local TCP forwarding proxy</h2>
This option is less convenient than the proxy command because it requires two separate
commands to execute.
<p>
Next step is to run a local SSH of SFTP client:
</p>
<p>
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):
</p>
<pre>
curl http@secure@://@host@/docs/tcptows > tcptows
chmod 755 tcptows
./tcptows 10000 ws@secure@://@host@/client/ID
</pre>
<p>
This runs a local client that allows SSH to port 10000 and connects to converge using
a websocket connection.</p>
<p>
Next step is to run a local SSH of SFTP client:
</p>
<pre>
<pre>
ssh -oServerAliveInterval=10 -p 10000 abc@localhost
sftp -oServerAliveInterval=10 -oPort=10000 abc@localhost
</pre>
<p>
<code>abc</code> is a fixed user defined by converge. It has a very exciting password.
</p>
<p>
<code>abc</code> is a fixed user defined by converge. It has a very exciting password.
</p>
<h1>Downloads</h1>
<h1 id="downloads">Downloads</h1>
<ul>
<li><a href="/docs/agent">agent</a>: The agent to run inside aa CI job
</li>
<li><a href="/docs/tcptows">tcptows</a>: TCP to WS tunnel for allowing regular
SSH and SFTP clients to connect to converge.
</li>
<li><a href="/docs/wsproxy">wsproxy</a>: SSH proxy command that can be directly used by ssh
</li>
</ul>
<table class="table">
<thead>
<tr>
<th>Component</th>
<th>Purpose</th>
<th>Linux</th>
<th>Windows</th>
</tr>
</thead>
<tr>
<td>agent</td>
<td>The agent to run inside aa CI job</td>
<td><a href="/docs/agent">agent</a></td>
<td><a href="/docs/agent.exe">agent.exe</a></td>
</tr>
<tr>
<td>wsproxy</td>
<td>SSH proxy command that can be directly used by ssh</td>
<td><a href="/docs/wsproxy">wsproxy</a></td>
<td><a href="/docs/wsproxy.exe">wsproxy.exe</a></td>
</tr>
<tr>
<td>tcptows</td>
<td>TCP to WS tunnel for allowing regular
SSH and SFTP clients to connect to converge</td>
<td><a href="/docs/tcptows">tcptows</a></td>
<td><a href="/docs/tcptows.exe">tcptows.exe</a></td>
</tr>
</table>
</div>
</div>
</div>
</div>