Implement Waygate web UI

Moved from waygate library to handle inside boringproxy.
This commit is contained in:
Anders Pitman 2022-03-13 17:25:09 -06:00
parent 31e48bf2e7
commit c91b322a23
5 changed files with 487 additions and 34 deletions

View File

@ -226,7 +226,9 @@ func Listen() {
}
tlsListener := tls.NewListener(httpListener, tlsConfig)
http.Handle("/waygate/", http.StripPrefix("/waygate", waygateServer))
waygateHandler := NewWaygateHandler(waygateServer, db, api)
http.Handle("/waygate/", http.StripPrefix("/waygate", waygateHandler))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
timestamp := time.Now().Format(time.RFC3339)

50
templates/authorize.tmpl Normal file
View File

@ -0,0 +1,50 @@
<p>
A service is requesting to create a tunnel. If you want to approve this
action, create a new Waygate or select an existing one below.
</p>
<form action="/waygate-edit" method="POST">
<input type="hidden" name="return-url" value="{{.ReturnUrl}}" required>
<button class='button' formaction="/waygate-edit">Create new Waygate</button>
</form>
<h1>Select existing Waygate:</h1>
<form action="./approve" method="POST">
<input type="hidden" name="client_id" value="{{.AuthRequest.ClientId}}" required>
<input type="hidden" name="redirect_uri" value="{{.AuthRequest.RedirectUri}}" required>
<input type="hidden" name="scope" value="{{.AuthRequest.Scope}}" required>
<input type="hidden" name="state" value="{{.AuthRequest.State}}" required>
<div class='waygate-list-table'>
<table class='waygate-table'>
<thead>
<tr>
<th class='waygate-table__cell'>Domains</th>
<th class='waygate-table__cell'>Description</th>
<th class='waygate-table__cell'></th>
</tr>
</thead>
<tbody>
{{range $waygateId, $waygate := .Waygates}}
<tr>
<td class='waygate-table__cell'>
{{ range $domain := $waygate.Domains }}
<div>
{{$domain}}
</div>
{{ end }}
</td>
<td class='waygate-table__cell'>
{{$waygate.Description}}
</td>
<td class='waygate-table__cell'>
<button class='button' formaction="/waygate-connect-existing" name="waygate-id" value="{{$waygateId}}">Select</button>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</form>

View File

@ -0,0 +1,56 @@
<!doctype html>
<html>
<body>
<form action="/waygate-create" method="POST">
<input type="hidden" name="return-url" value="{{.ReturnUrl}}" required>
<div>
Description:
</div>
<div>
<input type="text" name="description" placeholder="Description (optional)">
</div>
<div>
Domains:
</div>
{{range $domainName := $.SelectedDomains}}
<input type="hidden" name="selected-domains" value="{{$domainName}}">
<div>
{{$domainName}}
<button formaction="/waygate-delete-selected" name="delete-domain" value="{{$domainName}}">Delete</button>
</div>
{{end}}
<div>
<select id="domain-input" name="add-domain">
{{range $domainName := $.Domains}}
<option>{{$domainName}}</option>
{{ end }}
</select>
<button formaction="/waygate-add-domain">Add Domain</button>
</div>
<div>
<input type="text" name="host" placeholder="Subdomain">
<span>.</span>
<select id="domain-input" name="add-wildcard-domain">
{{range $domainName := $.WildcardDomains}}
<option>{{$domainName}}</option>
{{ end }}
</select>
<button formaction="/waygate-add-wildcard-domain">Add Wildcard</button>
</div>
<div class='button-row'>
<button class='button' type="submit">Submit</button>
<button class='button'>Cancel</button>
</div>
</form>
</body>
</html>

View File

@ -14,8 +14,6 @@ import (
"strings"
"sync"
"time"
"github.com/takingnames/waygate-go"
)
//go:embed logo.png templates
@ -242,8 +240,6 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
h.handleClients(w, r, user, tokenData)
case "/domains":
h.handleDomains(w, r, user, tokenData)
case "/waygates":
h.handleWaygates(w, r, user, tokenData)
case "/confirm-delete-token":
h.confirmDeleteToken(w, r)
case "/delete-token":
@ -295,6 +291,22 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
namedropLink := h.config.namedropClient.DomainRequestLink()
http.Redirect(w, r, namedropLink, 303)
case "/waygates":
h.handleWaygates(w, r, user, tokenData)
case "/waygate-edit":
h.handleWaygateEdit(w, r)
case "/waygate-add-domain":
h.handleWaygateAddNormalDomain(w, r)
case "/waygate-add-wildcard-domain":
h.handleWaygateAddWildcardDomain(w, r)
case "/waygate-delete-selected":
h.handleWaygateDeleteSelected(w, r)
case "/waygate-create":
h.handleWaygateCreate(w, r)
case "/waygate-connect-existing":
h.handleWaygateConnectExisting(w, r)
default:
if strings.HasPrefix(r.URL.Path, "/tunnels/") {
@ -535,35 +547,6 @@ func (h *WebUiHandler) handleDomains(w http.ResponseWriter, r *http.Request, use
}
}
func (h *WebUiHandler) handleWaygates(w http.ResponseWriter, r *http.Request, user User, tokenData TokenData) {
r.ParseForm()
switch r.Method {
case "GET":
waygates := h.api.GetWaygates(tokenData)
templateData := struct {
User User
Waygates map[string]waygate.Waygate
}{
User: user,
Waygates: waygates,
}
err := h.tmpl.ExecuteTemplate(w, "waygates.tmpl", templateData)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
default:
w.WriteHeader(405)
h.alertDialog(w, r, "Invalid method for /waygates", "/waygates")
return
}
}
func (h *WebUiHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {

362
waygate.go Normal file
View File

@ -0,0 +1,362 @@
package boringproxy
import (
"fmt"
"html/template"
"io"
"net/http"
"strings"
"github.com/takingnames/waygate-go"
)
type WaygateHandler struct {
db *Database
api *Api
tmpl *template.Template
mux *http.ServeMux
}
func (h *WaygateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mux.ServeHTTP(w, r)
}
func NewWaygateHandler(waygateServer *waygate.Server, db *Database, api *Api) *WaygateHandler {
tmpl, err := template.ParseFS(fs, "templates/*.tmpl")
if err != nil {
fmt.Println(err.Error())
return nil
}
mux := &http.ServeMux{}
h := &WaygateHandler{
db: db,
api: api,
tmpl: tmpl,
}
mux.HandleFunc("/authorize", func(w http.ResponseWriter, r *http.Request) {
h.authorize(w, r)
})
mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
waygateServer.ServeHTTP(w, r)
})
mux.HandleFunc("/open", func(w http.ResponseWriter, r *http.Request) {
waygateServer.ServeHTTP(w, r)
})
h.mux = mux
return h
}
func (h *WaygateHandler) authorize(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(405)
fmt.Fprintf(w, "Invalid method")
return
}
r.ParseForm()
authReq, err := waygate.ExtractAuthRequest(r)
if err != nil {
w.WriteHeader(400)
fmt.Fprintf(w, err.Error())
return
}
wildcardDomains := []string{}
domains, err := h.api.GetDomainNames(r)
if err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, err.Error())
return
}
for _, domainName := range domains {
if strings.HasPrefix(domainName, "*.") {
wildcardDomains = append(wildcardDomains, domainName[2:])
}
}
waygates := h.db.GetWaygates()
returnUrl := "/waygate" + r.URL.String()
data := struct {
Domains []string
Waygates map[string]waygate.Waygate
AuthRequest *waygate.AuthRequest
ReturnUrl string
}{
Domains: wildcardDomains,
Waygates: waygates,
AuthRequest: authReq,
ReturnUrl: returnUrl,
}
err = h.tmpl.ExecuteTemplate(w, "authorize.tmpl", data)
if err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, err.Error())
return
}
}
func (h *WebUiHandler) handleWaygates(w http.ResponseWriter, r *http.Request, user User, tokenData TokenData) {
r.ParseForm()
switch r.Method {
case "GET":
waygates := h.api.GetWaygates(tokenData)
templateData := struct {
User User
Waygates map[string]waygate.Waygate
}{
User: user,
Waygates: waygates,
}
err := h.tmpl.ExecuteTemplate(w, "waygates.tmpl", templateData)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
default:
w.WriteHeader(405)
h.alertDialog(w, r, "Invalid method for /waygates", "/waygates")
return
}
}
func (h *WebUiHandler) handleWaygateAddNormalDomain(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
domain := r.Form.Get("add-domain")
h.handleWaygateAddDomain(w, r, domain)
}
func (h *WebUiHandler) handleWaygateAddWildcardDomain(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
host := r.Form.Get("host")
domain := r.Form.Get("add-wildcard-domain")
if host != "" {
domain = fmt.Sprintf("%s.%s", host, domain)
}
h.handleWaygateAddDomain(w, r, domain)
}
func (h *WebUiHandler) handleWaygateAddDomain(w http.ResponseWriter, r *http.Request, addDomain string) {
if r.Method != "POST" {
w.WriteHeader(405)
io.WriteString(w, "Invalid method")
return
}
r.ParseForm()
dup := false
for _, domain := range r.Form["selected-domains"] {
if addDomain == domain {
dup = true
break
}
}
if !dup {
r.Form["selected-domains"] = append(r.Form["selected-domains"], addDomain)
}
h.handleWaygateEdit(w, r)
}
func (h *WebUiHandler) handleWaygateDeleteSelected(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(405)
io.WriteString(w, "Invalid method")
return
}
r.ParseForm()
deleteDomain := r.Form.Get("delete-domain")
newSelected := []string{}
for _, sel := range r.Form["selected-domains"] {
fmt.Println(sel, deleteDomain)
if sel != deleteDomain {
newSelected = append(newSelected, sel)
}
}
r.Form["selected-domains"] = newSelected
h.handleWaygateEdit(w, r)
}
func (h *WebUiHandler) handleWaygateEdit(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(405)
io.WriteString(w, "Invalid method")
return
}
r.ParseForm()
selectedDomains := r.Form["selected-domains"]
allDomains, err := h.api.GetDomainNames(r)
if err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, err.Error())
return
}
domains := []string{}
wildcardDomains := []string{}
for _, domainName := range allDomains {
if strings.HasPrefix(domainName, "*.") {
wildcardDomains = append(wildcardDomains, domainName[2:])
} else {
domains = append(domains, domainName)
}
}
data := struct {
SelectedDomains []string
Domains []string
WildcardDomains []string
ReturnUrl string
}{
SelectedDomains: selectedDomains,
Domains: domains,
WildcardDomains: wildcardDomains,
ReturnUrl: r.Form.Get("return-url"),
}
err = h.tmpl.ExecuteTemplate(w, "edit_waygate.tmpl", data)
if err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, err.Error())
return
}
}
func (h *WebUiHandler) handleWaygateCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(405)
io.WriteString(w, "Invalid method")
return
}
r.ParseForm()
selectedDomains := r.Form["selected-domains"]
description := r.Form.Get("description")
domains, err := h.api.GetDomainNames(r)
if err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, err.Error())
return
}
for _, fqdn := range selectedDomains {
matched := false
for _, domain := range domains {
fmt.Println("comp", fqdn, domain)
if domain == fqdn {
matched = true
break
} else if strings.HasPrefix(domain, "*.") {
baseDomain := domain[1:]
if strings.HasSuffix(fqdn, baseDomain) {
matched = true
break
}
}
}
if !matched {
w.WriteHeader(403)
fmt.Fprintf(w, "No permissions for domain")
return
}
}
wg := waygate.Waygate{
Domains: selectedDomains,
Description: description,
}
_, err = h.db.AddWaygate(wg)
if err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, err.Error())
return
}
returnUrl := r.Form.Get("return-url")
http.Redirect(w, r, returnUrl, 303)
}
func (h *WebUiHandler) handleWaygateConnectExisting(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(405)
io.WriteString(w, "Invalid method")
return
}
r.ParseForm()
waygateId := r.Form.Get("waygate-id")
h.completeAuth(w, r, waygateId)
}
func (h *WebUiHandler) completeAuth(w http.ResponseWriter, r *http.Request, waygateId string) {
// TODO: Make sure this is secure, ie users can't connect to waygates
// owned by others.
waygateToken, err := h.db.AddWaygateToken(waygateId)
if err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, err.Error())
return
}
authReq, err := waygate.ExtractAuthRequest(r)
if err != nil {
w.WriteHeader(400)
io.WriteString(w, err.Error())
return
}
if authReq.RedirectUri == "urn:ietf:wg:oauth:2.0:oob" {
fmt.Fprintf(w, waygateToken)
} else {
code, err := genRandomCode(32)
if err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, err.Error())
return
}
err = h.db.SetTokenCode(waygateToken, code)
if err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, err.Error())
return
}
url := fmt.Sprintf("http://%s?code=%s&state=%s", authReq.RedirectUri, code, authReq.State)
http.Redirect(w, r, url, 303)
}
}