work in progress:

* usage page now has more dynamic part where user
  can enter id and publis ssh keys and the server will
  generate the appropriate commmands to execute depending on the local and remote shell.
This commit is contained in:
Erik Brakkee 2024-08-03 12:54:32 +02:00
parent 4f6d9c209d
commit 62b51a6d09
8 changed files with 237 additions and 40 deletions

View File

@ -161,6 +161,9 @@ func main() {
http.FileServer(http.Dir(downloadDir)))) http.FileServer(http.Dir(downloadDir))))
http.HandleFunc("/", catchAllHandler) http.HandleFunc("/", catchAllHandler)
// create usage generator
http.HandleFunc("/usage", generateCLIExammple)
// Start HTTP server // Start HTTP server
fmt.Println("Rendez-vous server listening on :8000") fmt.Println("Rendez-vous server listening on :8000")
log.Fatal(http.ListenAndServe(":8000", nil)) log.Fatal(http.ListenAndServe(":8000", nil))

View File

@ -0,0 +1,36 @@
package main
import (
"converge/pkg/models"
"converge/pkg/server/converge"
"net/http"
"strings"
)
func getConvergeAccess(r *http.Request, sshRemoteUser string) models.ConvergeAccess {
secure := ""
if r.TLS == nil {
secure = ""
} else {
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" {
secure = "s"
}
}
}
location, err := converge.GetUserLocation(r)
if err != nil {
panic(err)
}
return models.ConvergeAccess{
Secure: secure,
HostPort: r.Host,
Location: location,
Username: sshRemoteUser,
}
}

View File

@ -1,34 +1,14 @@
package main package main
import ( import (
"converge/pkg/server/converge"
templates2 "converge/pkg/server/templates" templates2 "converge/pkg/server/templates"
"net/http" "net/http"
"os" "os"
"strings"
) )
func pageHandler(w http.ResponseWriter, r *http.Request) { func pageHandler(w http.ResponseWriter, r *http.Request) {
secure := ""
if r.TLS == nil {
secure = ""
} else {
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" {
secure = "s"
}
}
}
username, _ := os.LookupEnv("CONVERGE_USERNAME") username, _ := os.LookupEnv("CONVERGE_USERNAME")
access := getConvergeAccess(r, username)
location, err := converge.GetUserLocation(r)
if err != nil {
panic(err)
}
switch r.URL.Path { switch r.URL.Path {
case "": case "":
@ -37,12 +17,13 @@ func pageHandler(w http.ResponseWriter, r *http.Request) {
fallthrough fallthrough
case "index.html": case "index.html":
templates2.AboutTab().Render(r.Context(), w) templates2.AboutTab().Render(r.Context(), w)
// TODO use contexts later.
case "usage.html": case "usage.html":
templates2.UsageTab(secure, r.Host, username).Render(r.Context(), w) templates2.UsageTab(access).Render(r.Context(), w)
case "downloads.html": case "downloads.html":
templates2.DownloadsTab().Render(r.Context(), w) templates2.DownloadsTab().Render(r.Context(), w)
case "sessions.html": case "sessions.html":
templates2.SessionsTab(nil, location).Render(r.Context(), w) templates2.SessionsTab(nil, access.Location).Render(r.Context(), w)
default: default:
http.NotFound(w, r) http.NotFound(w, r)
} }

24
cmd/converge/usage.go Normal file
View File

@ -0,0 +1,24 @@
package main
import (
"log"
"net/http"
"time"
)
func generateCLIExammple(w http.ResponseWriter, r *http.Request) {
log.Println("usage: got ", r.URL.Path)
err := r.ParseForm()
if err != nil {
http.Error(w, "Error parsing form", http.StatusBadRequest)
return
}
remote_shells := r.Form["remote-shell"]
local_shells := r.Form["local-shhell"]
keys := r.FormValue("ssh-keys")
log.Printf("remote_shells %v", remote_shells)
log.Printf("local_shells %v", local_shells)
log.Printf("ssh-keys %v", keys)
w.Write([]byte(time.Now().Format(time.DateTime)))
}

View File

@ -37,11 +37,18 @@ func main() {
panic(err) panic(err)
} }
access := models.ConvergeAccess{
Secure: "s",
HostPort: "example.com",
Location: netherlands,
Username: "converge",
}
fullindex := func() templ.Component { fullindex := func() templ.Component {
return templates2.Index("s", "example.com", "converge") return templates2.Index(access)
} }
usage := func() templ.Component { usage := func() templ.Component {
return templates2.UsageTab("s", "example.com", "converge") return templates2.UsageTab(access)
} }
render(dir, "fullindex.html", fullindex) render(dir, "fullindex.html", fullindex)

View File

@ -0,0 +1,11 @@
package models
import "time"
type ConvergeAccess struct {
// 's" when secure, "" otherwise
Secure string
HostPort string
Location *time.Location
Username string
}

View File

@ -1,9 +1,12 @@
package templates package templates
templ Index(secure string, host string, username string) { import "converge/pkg/models"
templ Index(access models.ConvergeAccess) {
@BasePage(0) { @BasePage(0) {
@About() @About()
@Usage(secure, host, username) @Usage(access)
@Downloads() @Downloads()
} }
} }

View File

@ -1,8 +1,62 @@
package templates package templates
templ Usage(secure string, host string, username string) { import "converge/pkg/models"
templ Usage(access models.ConvergeAccess) {
<div> <div>
<h1>usage</h1>
<h1>Usage</h1>
<style>
.minimal-width {
width: 1%;
white-space: nowrap;
}
</style>
<form id="inputs" novalidate
hx-post="/usage"
method="post"
hx-trigger="load,input delay:500ms,change"
hx-target="#example-cli"
hx-on::after-request="saveFormToCookie()">
<table class="table table-responsive">
<tr>
<td class="minimal-width"><label for="rendez-vous-id">rendez-vous id</label></td>
<td>
<input id="rendez-vous-id" class="form-control" name="rendez-vous-id" type="text" maxlength="40"></input>
</td>
</tr>
<tr>
<td class="minimal-width"><label for="ssh-keys">public ssh keys</label></td>
<td>
<textarea id="ssh-keys" class="form-control autogrow" name="ssh-keys" rows="5"></textarea>
</td>
</tr>
<tr>
<td class="minimal-width"><label for="remote-shell">remote shells</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>
</td>
</tr>
<tr>
<td class="minimal-width"><label for="local-shell">local shell</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>
</td>
</tr>
</table>
</form>
<div id="example-cli">
</div>
<h1>usage old</h1>
<h2>Continuous integration jobs</h2> <h2>Continuous integration jobs</h2>
@ -12,14 +66,14 @@ templ Usage(secure string, host string, username string) {
</p> </p>
<pre>{` <pre>{`
# linux # linux
`}curl http{secure}://{host}/static/agent > agent{` `}curl http{access.Secure}://{access.HostPort}/static/agent > agent{`
chmod 755 agent chmod 755 agent
`}./agent --id ID ws{secure}://{host}{` `}./agent --id ID ws{access.Secure}://{access.HostPort}{`
rm -f agent rm -f agent
# windows # windows
`}curl http{secure}://{host}/static/agent.exe > agent.exe{` `}curl http{access.Secure}://{access.HostPort}/static/agent.exe > agent.exe{`
`}agent --id ID ws{secure}://{host}{` `}agent --id ID ws{access.Secure}://{access.HostPort}{`
del agent.exe del agent.exe
`}</pre> `}</pre>
<p> <p>
@ -66,8 +120,8 @@ templ Usage(secure string, host string, username string) {
<pre> <pre>
{` {`
`}ssh -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{secure}://{host}/client/ID" { username }{"@localhost"} {` `}ssh -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{access.Secure}://{access.HostPort}/client/ID" { access.Username }{"@localhost"} {`
`}sftp -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{secure}://{host}/client/ID" { username }{"@localhost"} {` `}sftp -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{access.Secure}://{access.HostPort}/client/ID" { access.Username }{"@localhost"} {`
`}</pre> `}</pre>
<h2>Local clients: using SSH with a local TCP forwarding proxy</h2> <h2>Local clients: using SSH with a local TCP forwarding proxy</h2>
@ -91,8 +145,8 @@ templ Usage(secure string, host string, username string) {
</p> </p>
<pre> {` <pre> {`
`}ssh -oServerAliveInterval=10 -p 10000 { username }{"@localhost"} {` `}ssh -oServerAliveInterval=10 -p 10000 { access.Username }{"@localhost"} {`
`}sftp -oServerAliveInterval=10 -oPort=10000 { username }{"@localhost"} {` `}sftp -oServerAliveInterval=10 -oPort=10000 { access.Username }{"@localhost"} {`
`}</pre> `}</pre>
<h2>Remote shell usage</h2> <h2>Remote shell usage</h2>
@ -145,7 +199,7 @@ templ Usage(secure string, host string, username string) {
<h2>Authentication</h2> <h2>Authentication</h2>
<p> <p>
The <code>{ username }</code> user above is configured in the The <code>{ access.Username }</code> user above is configured in the
Converge server and is communicated to the agent when the agent is Converge server and is communicated to the agent when the agent is
started as well as the password. started as well as the password.
</p> </p>
@ -168,13 +222,91 @@ templ Usage(secure string, host string, username string) {
Note that on windows you should not used quotes. Note that on windows you should not used quotes.
</p> </p>
<script>
function setCookie(name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + encodeURIComponent(value) + expires + "; path=/";
}
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for(let i=0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return decodeURIComponent(c.substring(nameEQ.length,c.length));
}
return null;
}
function saveFormToCookie() {
const form = document.getElementById('inputs');
const formData = new FormData(form);
for (let element of form.elements) {
if (element.id) {
console.log("Saving " + element.id)
if (element.type === 'checkbox') {
console.log("Checkbox " + element.checked)
setCookie(element.id, element.checked ? 'true' : 'false', 7);
} else if (element.type === 'radio') {
if (element.checked) {
setCookie(element.id, element.value, 7);
}
} else {
setCookie(element.id, element.value, 7);
}
}
}
}
function loadFormFromCookie() {
const form = document.getElementById('inputs');
for (let element of form.elements) {
if (element.id) {
const value = getCookie(element.id);
console.log("Loading " + element.id + " value: " + value)
if (value !== null) {
if (element.type === 'checkbox') {
element.checked = value === 'true';
} else if (element.type === 'radio') {
element.checked = (element.value === value);
} else {
element.value = value;
}
}
}
}
}
// Save form data to cookie on change
document.getElementById('inputs').addEventListener('change', saveFormToCookie);
document.body.addEventListener('htmx:load', function(event) {
loadFormFromCookie();
});
document.body.addEventListener('htmx:afterSettle', function(event) {
loadFormFromCookie();
});
// when hx-boost=false
// Load form data from cookie on page load
//document.addEventListener('DOMContentLoaded', loadFormFromCookie);
</script>
</div> </div>
} }
templ UsageTab(secure string, host string, username string) { templ UsageTab(access models.ConvergeAccess) {
@BasePage(2) { @BasePage(2) {
@Usage(secure, host, username) @Usage(access)
} }
} }