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:
parent
62b51a6d09
commit
49db7578a7
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
||||
|
@ -1 +1,5 @@
|
||||
package templates
|
||||
|
||||
const BASH = "*.sh"
|
||||
const CMD = "cmd"
|
||||
const POWERSHELL = "powershell"
|
||||
|
@ -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 {
|
||||
element.value = value;
|
||||
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();
|
||||
});
|
||||
|
||||
|
23
pkg/server/templates/usageinputs.go
Normal file
23
pkg/server/templates/usageinputs.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user