large parts of the usage is now dynamic.

Still need to generate instructions for authorized keys. A lot of troubleshooting for the form to cookie persistence.
This commit is contained in:
Erik Brakkee 2024-08-03 18:29:14 +02:00
parent 62b51a6d09
commit 49db7578a7
6 changed files with 255 additions and 175 deletions

View File

@ -3,11 +3,10 @@ package main
import (
templates2 "converge/pkg/server/templates"
"net/http"
"os"
)
func pageHandler(w http.ResponseWriter, r *http.Request) {
username, _ := os.LookupEnv("CONVERGE_USERNAME")
username := getAgentSshUser()
access := getConvergeAccess(r, username)
switch r.URL.Path {

View File

@ -1,9 +1,12 @@
package main
import (
"converge/pkg/server/templates"
"log"
"math/rand"
"net/http"
"time"
"os"
"strconv"
)
func generateCLIExammple(w http.ResponseWriter, r *http.Request) {
@ -14,11 +17,31 @@ func generateCLIExammple(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Error parsing form", http.StatusBadRequest)
return
}
remote_shells := r.Form["remote-shell"]
local_shells := r.Form["local-shhell"]
ids := r.Form["rendez-vous-id"]
id := ""
if len(ids) > 0 {
id = ids[0]
}
if id == "" {
id = strconv.Itoa(rand.Int() % 1000000)
}
remoteShells := r.Form["remote-shell"]
localShells := r.Form["local-shell"]
keys := r.FormValue("ssh-keys")
log.Printf("remote_shells %v", remote_shells)
log.Printf("local_shells %v", local_shells)
log.Printf("remote_shells %v", remoteShells)
log.Printf("local_shells %v", localShells)
log.Printf("ssh-keys %v", keys)
w.Write([]byte(time.Now().Format(time.DateTime)))
access := getConvergeAccess(r, getAgentSshUser())
usageInputs := templates.NewUsageInputs(id, remoteShells, localShells)
err = templates.ShellUsage(access, usageInputs).Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), 500)
}
}
func getAgentSshUser() string {
username, _ := os.LookupEnv("CONVERGE_USERNAME")
return username
}

View File

@ -39,6 +39,49 @@ templ About() {
When the timeout of a session is near the user is informed about this with messages
in the shell.
</p>
<p>end-to-end encryoption</p>
<p> ssh keys</p>
<p>agent options </p>
<p>client access </p>
<h2>Local clients: using ssh with a 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.html">downloads</a>). 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>
<h2>Local clients: using SSH with a local TCP forwarding proxy</h2>
<p>
This option is less convenient than the proxy command because it requires two separate
commands to execute.
</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.html">downloads</a> section.
This runs a local client that allows SSH to port 10000 and connects to converge using
a websocket connection.
</p>
<h2>Remote shell usage</h2>
<p>
The agent supports a --shells command-line option by which a comma-separated
list of shells can be prepended to the default search path for shells, e.g.
<code>--shells zsh,csh,sh</code> (linux) or <code>cmd,powershell</code> for
windows.
</p>
<p>
The agent sets a <coder>agentdir</coder> environment variable that points to
the directory where the agent is running.
</p>
</div>
}

View File

@ -1 +1,5 @@
package templates
const BASH = "*.sh"
const CMD = "cmd"
const POWERSHELL = "powershell"

View File

@ -2,10 +2,118 @@ package templates
import "converge/pkg/models"
templ AgentUsage(access models.ConvergeAccess, shells map[string]bool, usageInputs UsageInputs) {
<div>
<h2>Downloading and running the agent</h2>
<p>
This is what you run on a remote server, typically in your continuous integration job.
</p>
if shells[BASH] {
<pre>{`
`}curl http{access.Secure}://{access.HostPort}/static/agent > agent{`
chmod 755 agent
`}./agent --id {usageInputs.Id} ws{access.Secure}://{access.HostPort}{`
rm -f agent
`}</pre>
}
if shells[CMD] || shells[POWERSHELL] {
<pre>{`
`}curl http{access.Secure}://{access.HostPort}/static/agent.exe > agent.exe{`
`}agent --id {usageInputs.Id} ws{access.Secure}://{access.HostPort}{`
del agent.exe
`}</pre>
}
<p>
The agent has more command-line options than shown here.
Download the agent and run it without arguments to
see all options.
</p>
<p>
<b>Tip</b>: Run the above command locally in a similar shell (for instance in a
docker container) then try to connect to the shell using the commands below. If that
works, then you are all set.
</p>
<h2>Connecting to the agent</h2>
<pre>{`
`}ssh -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{access.Secure}://{access.HostPort}/client/{usageInputs.Id}" { access.Username }{"@localhost"} {`
`}sftp -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{access.Secure}://{access.HostPort}/client/{usageInputs.Id}" { access.Username }{"@localhost"} {`
`}</pre>
<h2>Working with the agent</h2>
if shells[BASH] {
<pre>{`
# cd back to the agent directory
cd $agentdir
# prevent logout when last user exits
touch $agentdir/.hold
`}</pre>
}
if shells[CMD] {
<pre>{`
# cd back to the agent directory
cd %agentdir%
# prevent logout when last user exits
echo > %agentdir%\.hold
`}</pre>
}
if shells[POWERSHELL] {
<pre>{`
# cd back to the agent directory
cd $env:agentdir
# prevent logout when last user exits
$null > $env:agentdir\.hold
`}</pre>
}
if shells[CMD] || shells[POWERSHELL] {
<p>windows</p>
<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>
}
</div>
}
templ LocalShellUsage(access models.ConvergeAccess, shells map[string]bool, usageInput UsageInputs) {
<div>
if shells[BASH] {
<p>bash</p>
}
if shells[CMD] {
<p>cmd</p>
}
if shells[POWERSHELL] {
<p>powershell</p>
}
</div>
}
templ ShellUsage(access models.ConvergeAccess, usageInputs UsageInputs) {
<div>
@AgentUsage(access, usageInputs.RemoteShells, usageInputs)
</div>
@LocalShellUsage(access, usageInputs.LocalShells, usageInputs)
}
templ Usage(access models.ConvergeAccess) {
<div>
<h1>Usage</h1>
<style>
@ -18,7 +126,7 @@ templ Usage(access models.ConvergeAccess) {
<form id="inputs" novalidate
hx-post="/usage"
method="post"
hx-trigger="load,input delay:500ms,change"
hx-trigger="load,input delay:500ms,change,formdataloaded"
hx-target="#example-cli"
hx-on::after-request="saveFormToCookie()">
<table class="table table-responsive">
@ -35,19 +143,19 @@ templ Usage(access models.ConvergeAccess) {
</td>
</tr>
<tr>
<td class="minimal-width"><label for="remote-shell">remote shells</label></td>
<td class="minimal-width"><label for="remote-shell">agent environment</label></td>
<td>
<input checked id="remote-shell-0" name="remote-shell" type="checkbox" value="*sh"> <label for="remote-shell-0">*.sh</label>
<input id="remote-shell-1" name="remote-shell" type="checkbox" value="cmd"> <label for="remote-shell-1">cmd.exe</label>
<input id="remote-shell-2" name="remote-shell" type="checkbox" value="cmd"> <label for="remote-shell-2">powerpoint.exe</label>
<input checked id="remote-shell-0" name="remote-shell" type="radio" value={BASH}> <label for="remote-shell-0">*.sh</label>
<input id="remote-shell-1" name="remote-shell" type="radio" value={CMD}> <label for="remote-shell-1">command prompt</label>
<input id="remote-shell-2" name="remote-shell" type="radio" value={POWERSHELL}> <label for="remote-shell-2">power shell</label>
</td>
</tr>
<tr>
<td class="minimal-width"><label for="local-shell">local shell</label></td>
<td class="minimal-width"><label for="local-shell">local environment</label></td>
<td>
<input id="checked local-shell-0" name="local-shell" type="checkbox" value="*sh"> <label for="local-shell-0">*.sh</label>
<input id="local-shell-1" name="local-shell" type="checkbox" value="cmd"> <label for="local-shell-1">cmd.exe</label>
<input id="local-shell-2" name="local-shell" type="checkbox" value="cmd"> <label for="local-shell-2">powerpoint.exe</label>
<input id="checked local-shell-0" name="local-shell" type="radio" value={BASH}> <label for="local-shell-0">*.sh</label>
<input id="local-shell-1" name="local-shell" type="radio" value={CMD}> <label for="local-shell-1">command prompt</label>
<input id="local-shell-2" name="local-shell" type="radio" value={POWERSHELL}> <label for="local-shell-2">powershell</label>
</td>
</tr>
</table>
@ -56,159 +164,10 @@ templ Usage(access models.ConvergeAccess) {
<div id="example-cli">
</div>
<h1>usage old</h1>
<h2>Continuous integration jobs</h2>
<p>
In a
continuous integration job, download the agent, chmod it and run it.
</p>
<pre>{`
# linux
`}curl http{access.Secure}://{access.HostPort}/static/agent > agent{`
chmod 755 agent
`}./agent --id ID ws{access.Secure}://{access.HostPort}{`
rm -f agent
# windows
`}curl http{access.Secure}://{access.HostPort}/static/agent.exe > agent.exe{`
`}agent --id ID ws{access.Secure}://{access.HostPort}{`
del agent.exe
`}</pre>
<p>
Above, ID is a unique id for the job, the so-called rendez-cous ID. This should not conflict with IDs
used by other agents. The ID is used for a rendez-vous between the end-user on a local system and
the continuous integration job running on a build agent. If you don't specify an id, a random
id will be generated. If you specify a duplicate ID, the server will generate a new one andd the
agent will tell you what id to use.
Clients can now connect to the Converge server with the ID to establish a connection to
the CI job through Converge.
Communication between
end-user and agent is encrypted using SSH and the rendez-vous server is unable to
read the contents. The rendez-vous server is nothing more then a glorified bit pipe,
simply transferring data between end-user SSH client and the agent which runs an
embedded SSH server.
</p>
<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>
<p>
The agent has more command-line options than shown here.
Download the agent and run it without arguments to
see all options.
</p>
<h2>Local clients: using ssh with a 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.html">downloads</a>). 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{access.Secure}://{access.HostPort}/client/ID" { access.Username }{"@localhost"} {`
`}sftp -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{access.Secure}://{access.HostPort}/client/ID" { access.Username }{"@localhost"} {`
`}</pre>
<h2>Local clients: using SSH with a local TCP forwarding proxy</h2>
<p>
This option is less convenient than the proxy command because it requires two separate
commands to execute.
</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.html">downloads</a> section.
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> {`
`}ssh -oServerAliveInterval=10 -p 10000 { access.Username }{"@localhost"} {`
`}sftp -oServerAliveInterval=10 -oPort=10000 { access.Username }{"@localhost"} {`
`}</pre>
<h2>Remote shell usage</h2>
<p>
The agent supports a --shells command-line option by which a comma-separated
list of shells can be prepended to the default search path for shells, e.g.
<code>--shells zsh,csh,sh</code> (linux) or <code>cmd,powershell</code> for
windows.
</p>
<p>
The agent sets a <coder>agentdir</coder> environment variable that points to
the directory where the agent is running.
</p>
<h3>Linux</h3>
<pre>{`
# cd back to the agent directory
cd $agentdir
# prevent logout when last user exits
touch $agentdir/.hold
`}</pre>
<pre>
</pre>
<h3>Windows Command Prompt</h3>
<pre>{`
# cd back to the agent directory
cd %agentdir%
# prevent logout when last user exits
echo > %agentdir%\.hold
`}</pre>
<h3>Windows Powershell</h3>
<pre>{`
# cd back to the agent directory
cd $env:agentdir
# prevent logout when last user exits
$null > $env:agentdir\.hold
`}</pre>
<h2>Authentication</h2>
<p>
The <code>{ access.Username }</code> user above is configured in the
Converge server and is communicated to the agent when the agent is
started as well as the password.
</p>
<p>
Another way to authenticate is through an .authorized_keys file in the
same directory as where the agent is started.
This can be setup as follows before starting the agent:
</p>
<pre> {`
`}# linux {`
`}echo "ssh-rsa dkddkdkkk a@b.c" > .authorized_keys {`
@ -250,13 +209,17 @@ templ Usage(access models.ConvergeAccess) {
const formData = new FormData(form);
for (let element of form.elements) {
if (element.id) {
console.log("Saving " + element.id)
//console.log("Saving " + element.id)
if (element.type === 'checkbox') {
console.log("Checkbox " + element.checked)
//console.log("Checkbox " + element.checked)
setCookie(element.id, element.checked ? 'true' : 'false', 7);
} else if (element.type === 'radio') {
if (element.checked) {
//console.log("Set cookie " + element.id + " " + element.value)
setCookie(element.id, element.value, 7);
} else {
//console.log("Set cookie " + element.id + " EMPTY")
setCookie(element.id, "", 7)
}
} else {
setCookie(element.id, element.value, 7);
@ -266,32 +229,57 @@ templ Usage(access models.ConvergeAccess) {
}
function loadFormFromCookie() {
console.log("Load form from cookie")
const form = document.getElementById('inputs');
if (!form) {
return
}
updated = false
for (let element of form.elements) {
if (element.id) {
const value = getCookie(element.id);
console.log("Loading " + element.id + " value: " + value)
//console.log("Loading " + element.id + " value: " + value)
if (value !== null) {
if (element.type === 'checkbox') {
element.checked = value === 'true';
newvalue = value === "true"
if (element.checked != newvalue) {
console.log("Setting " + element.id + " checked " + newvalue)
element.checked = newvalue
updated = true
}
} else if (element.type === 'radio') {
element.checked = (element.value === value);
newvalue = element.value === value
if (element.checked != newvalue) {
console.log("Setting " + element.id + " selected " + newvalue)
element.checked = newvalue;
updated = true
}
} else {
if (element.value != value) {
console.log("Setting " + element.id + " " + element.value + " -> " + value)
element.value = value;
updated = true
}
}
}
}
}
if (updated) {
console.log("Sending htmx event to trigger form")
htmx.trigger(form, 'formdataloaded')
}
}
// Save form data to cookie on change
document.getElementById('inputs').addEventListener('change', saveFormToCookie);
document.body.addEventListener('htmx:load', function(event) {
loadFormFromCookie();
//console.log("htmx:load")
//loadFormFromCookie();
});
document.body.addEventListener('htmx:afterSettle', function(event) {
console.log("htmx:afterSettle")
loadFormFromCookie();
});

View File

@ -0,0 +1,23 @@
package templates
type UsageInputs struct {
Id string
SshKeys []string
RemoteShells map[string]bool
LocalShells map[string]bool
}
func NewUsageInputs(id string, remoteShells []string, localShells []string) UsageInputs {
inputs := UsageInputs{
Id: id,
RemoteShells: make(map[string]bool),
LocalShells: make(map[string]bool),
}
for _, remoteShell := range remoteShells {
inputs.RemoteShells[remoteShell] = true
}
for _, localShell := range localShells {
inputs.LocalShells[localShell] = true
}
return inputs
}