From 91e1139881ad82006804888900324d13bee117ac Mon Sep 17 00:00:00 2001 From: Erik Brakkee <erik@brakkee.org> Date: Sat, 3 Aug 2024 12:54:32 +0200 Subject: [PATCH] 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. --- cmd/converge/converge.go | 3 + cmd/converge/convergeaccess.go | 36 +++++++ cmd/converge/pagehandler.go | 27 +----- cmd/converge/usage.go | 24 +++++ cmd/templaterender/render.go | 11 ++- pkg/models/convergeaccess.go | 11 +++ pkg/server/templates/index.templ | 7 +- pkg/server/templates/usage.templ | 158 ++++++++++++++++++++++++++++--- 8 files changed, 237 insertions(+), 40 deletions(-) create mode 100644 cmd/converge/convergeaccess.go create mode 100644 cmd/converge/usage.go create mode 100644 pkg/models/convergeaccess.go diff --git a/cmd/converge/converge.go b/cmd/converge/converge.go index bae64aa..5c4edf4 100644 --- a/cmd/converge/converge.go +++ b/cmd/converge/converge.go @@ -161,6 +161,9 @@ func main() { http.FileServer(http.Dir(downloadDir)))) http.HandleFunc("/", catchAllHandler) + // create usage generator + http.HandleFunc("/usage", generateCLIExammple) + // Start HTTP server fmt.Println("Rendez-vous server listening on :8000") log.Fatal(http.ListenAndServe(":8000", nil)) diff --git a/cmd/converge/convergeaccess.go b/cmd/converge/convergeaccess.go new file mode 100644 index 0000000..f69407f --- /dev/null +++ b/cmd/converge/convergeaccess.go @@ -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, + } +} diff --git a/cmd/converge/pagehandler.go b/cmd/converge/pagehandler.go index e0909cc..bdc256e 100644 --- a/cmd/converge/pagehandler.go +++ b/cmd/converge/pagehandler.go @@ -1,34 +1,14 @@ package main import ( - "converge/pkg/server/converge" templates2 "converge/pkg/server/templates" "net/http" "os" - "strings" ) 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") - - location, err := converge.GetUserLocation(r) - if err != nil { - panic(err) - } + access := getConvergeAccess(r, username) switch r.URL.Path { case "": @@ -37,12 +17,13 @@ func pageHandler(w http.ResponseWriter, r *http.Request) { fallthrough case "index.html": templates2.AboutTab().Render(r.Context(), w) + // TODO use contexts later. case "usage.html": - templates2.UsageTab(secure, r.Host, username).Render(r.Context(), w) + templates2.UsageTab(access).Render(r.Context(), w) case "downloads.html": templates2.DownloadsTab().Render(r.Context(), w) case "sessions.html": - templates2.SessionsTab(nil, location).Render(r.Context(), w) + templates2.SessionsTab(nil, access.Location).Render(r.Context(), w) default: http.NotFound(w, r) } diff --git a/cmd/converge/usage.go b/cmd/converge/usage.go new file mode 100644 index 0000000..63304ff --- /dev/null +++ b/cmd/converge/usage.go @@ -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))) +} diff --git a/cmd/templaterender/render.go b/cmd/templaterender/render.go index 01bffee..3ea7024 100644 --- a/cmd/templaterender/render.go +++ b/cmd/templaterender/render.go @@ -37,11 +37,18 @@ func main() { panic(err) } + access := models.ConvergeAccess{ + Secure: "s", + HostPort: "example.com", + Location: netherlands, + Username: "converge", + } + fullindex := func() templ.Component { - return templates2.Index("s", "example.com", "converge") + return templates2.Index(access) } usage := func() templ.Component { - return templates2.UsageTab("s", "example.com", "converge") + return templates2.UsageTab(access) } render(dir, "fullindex.html", fullindex) diff --git a/pkg/models/convergeaccess.go b/pkg/models/convergeaccess.go new file mode 100644 index 0000000..007c321 --- /dev/null +++ b/pkg/models/convergeaccess.go @@ -0,0 +1,11 @@ +package models + +import "time" + +type ConvergeAccess struct { + // 's" when secure, "" otherwise + Secure string + HostPort string + Location *time.Location + Username string +} diff --git a/pkg/server/templates/index.templ b/pkg/server/templates/index.templ index 8914d90..947e97e 100644 --- a/pkg/server/templates/index.templ +++ b/pkg/server/templates/index.templ @@ -1,9 +1,12 @@ package templates -templ Index(secure string, host string, username string) { +import "converge/pkg/models" + + +templ Index(access models.ConvergeAccess) { @BasePage(0) { @About() - @Usage(secure, host, username) + @Usage(access) @Downloads() } } diff --git a/pkg/server/templates/usage.templ b/pkg/server/templates/usage.templ index 252f746..4fe25d8 100644 --- a/pkg/server/templates/usage.templ +++ b/pkg/server/templates/usage.templ @@ -1,8 +1,62 @@ package templates -templ Usage(secure string, host string, username string) { +import "converge/pkg/models" + +templ Usage(access models.ConvergeAccess) { <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> @@ -12,14 +66,14 @@ templ Usage(secure string, host string, username string) { </p> <pre>{` # linux - `}curl http{secure}://{host}/static/agent > agent{` + `}curl http{access.Secure}://{access.HostPort}/static/agent > agent{` chmod 755 agent - `}./agent --id ID ws{secure}://{host}{` + `}./agent --id ID ws{access.Secure}://{access.HostPort}{` rm -f agent # windows - `}curl http{secure}://{host}/static/agent.exe > agent.exe{` - `}agent --id ID ws{secure}://{host}{` + `}curl http{access.Secure}://{access.HostPort}/static/agent.exe > agent.exe{` + `}agent --id ID ws{access.Secure}://{access.HostPort}{` del agent.exe `}</pre> <p> @@ -66,8 +120,8 @@ templ Usage(secure string, host string, username string) { <pre> {` - `}ssh -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{secure}://{host}/client/ID" { username }{"@localhost"} {` - `}sftp -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{access.Secure}://{access.HostPort}/client/ID" { access.Username }{"@localhost"} {` `}</pre> <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> <pre> {` - `}ssh -oServerAliveInterval=10 -p 10000 { username }{"@localhost"} {` - `}sftp -oServerAliveInterval=10 -oPort=10000 { username }{"@localhost"} {` + `}ssh -oServerAliveInterval=10 -p 10000 { access.Username }{"@localhost"} {` + `}sftp -oServerAliveInterval=10 -oPort=10000 { access.Username }{"@localhost"} {` `}</pre> <h2>Remote shell usage</h2> @@ -145,7 +199,7 @@ templ Usage(secure string, host string, username string) { <h2>Authentication</h2> <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 started as well as the password. </p> @@ -168,13 +222,91 @@ templ Usage(secure string, host string, username string) { Note that on windows you should not used quotes. </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> } -templ UsageTab(secure string, host string, username string) { +templ UsageTab(access models.ConvergeAccess) { @BasePage(2) { - @Usage(secure, host, username) + @Usage(access) } }