diff --git a/api.go b/api.go index 97918a7..0df9528 100644 --- a/api.go +++ b/api.go @@ -160,6 +160,28 @@ func (a *Api) DeleteTunnel(tokenData TokenData, params url.Values) error { return nil } +func (a *Api) CreateToken(tokenData TokenData, params url.Values) (string, error) { + + owner := params.Get("owner") + if owner == "" { + return "", errors.New("Invalid owner paramater") + } + + if tokenData.Owner != owner { + user, _ := a.db.GetUser(tokenData.Owner) + if !user.IsAdmin { + return "", errors.New("Unauthorized") + } + } + + token, err := a.db.AddToken(owner) + if err != nil { + return "", errors.New("Failed to create token") + } + + return token, nil +} + func (a *Api) handleTunnels(w http.ResponseWriter, r *http.Request) { token, err := extractToken("access_token", r) diff --git a/todo.md b/todo.md index 7f14be5..ce94ad9 100644 --- a/todo.md +++ b/todo.md @@ -1,7 +1,7 @@ # 31 Oct 2020 Launch List - [ ] I think it's possible to create tokens for arbitrary user, even if you're not that user. -- [ ] Responses to unauthorized requests are leaking information about the current tunnels through the genereated CSS. +- [ ] QR codes for admin are broken - [ ] General security review. - [ ] Invalid database is wiping out tunnels - [ ] Improve SSH key download UI. @@ -11,6 +11,8 @@ - [ ] Demo video - [ ] Demo auto email signup - [ ] Post on /r/selfhosted +- [x] Head can be rendered before h.headHtml is ever set, ie if login page is visited before any other page +- [x] Responses to unauthorized requests are leaking information about the current tunnels through the genereated CSS. # Eventually diff --git a/ui_handler.go b/ui_handler.go index 8c0245c..6563d5b 100644 --- a/ui_handler.go +++ b/ui_handler.go @@ -72,10 +72,6 @@ type HeadData struct { Styles template.CSS } -type StylesData struct { - Tunnels map[string]Tunnel -} - type MenuData struct { IsAdmin bool } @@ -107,6 +103,39 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request homePath := "/#/tunnel" + // Note: h.box and h.headHtml need to be ready before pretty much + // everything else, including sendLoginPage + + box, err := rice.FindBox("webui") + if err != nil { + w.WriteHeader(500) + io.WriteString(w, "Error opening webui") + return + } + h.box = box + + stylesText, err := box.String("styles.css") + if err != nil { + w.WriteHeader(500) + io.WriteString(w, "Error reading styles.css") + return + } + headTmplStr, err := box.String("head.tmpl") + if err != nil { + w.WriteHeader(500) + io.WriteString(w, "Error reading head.tmpl") + return + } + headTmpl, err := template.New("head").Parse(headTmplStr) + if err != nil { + w.WriteHeader(500) + io.WriteString(w, "Error compiling head.tmpl") + return + } + var headBuilder strings.Builder + headTmpl.Execute(&headBuilder, HeadData{Styles: template.CSS(stylesText)}) + h.headHtml = template.HTML(headBuilder.String()) + token, err := extractToken("access_token", r) if err != nil { h.sendLoginPage(w, r, 401) @@ -121,29 +150,6 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request user, _ := h.db.GetUser(tokenData.Owner) - box, err := rice.FindBox("webui") - if err != nil { - w.WriteHeader(500) - io.WriteString(w, "Error opening webui") - return - } - - h.box = box - - stylesText, err := box.String("styles.css") - if err != nil { - w.WriteHeader(500) - io.WriteString(w, "Error reading styles.css") - return - } - - stylesTmpl, err := template.New("styles").Parse(stylesText) - if err != nil { - w.WriteHeader(500) - h.alertDialog(w, r, err.Error(), homePath) - return - } - tunnels := h.api.GetTunnels(tokenData) for domain, tun := range tunnels { @@ -152,28 +158,6 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request tunnels[domain] = tun } - var stylesBuilder strings.Builder - stylesTmpl.Execute(&stylesBuilder, StylesData{Tunnels: tunnels}) - - headTmplStr, err := box.String("head.tmpl") - if err != nil { - w.WriteHeader(500) - io.WriteString(w, "Error reading head.tmpl") - return - } - - headTmpl, err := template.New("head").Parse(headTmplStr) - if err != nil { - w.WriteHeader(500) - io.WriteString(w, "Error compiling head.tmpl") - return - } - - var headBuilder strings.Builder - headTmpl.Execute(&headBuilder, HeadData{Styles: template.CSS(stylesBuilder.String())}) - - h.headHtml = template.HTML(headBuilder.String()) - switch r.URL.Path { case "/login": h.handleLogin(w, r) @@ -396,26 +380,10 @@ func (h *WebUiHandler) handleTokens(w http.ResponseWriter, r *http.Request, user r.ParseForm() - if len(r.Form["owner"]) != 1 { - w.WriteHeader(400) - h.alertDialog(w, r, "Invalid owner parameter", "/#/tokens") - return - } - owner := r.Form["owner"][0] - - users := h.db.GetUsers() - - _, exists := users[owner] - if !exists { - w.WriteHeader(400) - h.alertDialog(w, r, "Owner doesn't exist", "/#/tokens") - return - } - - _, err := h.db.AddToken(owner) + _, err := h.api.CreateToken(tokenData, r.Form) if err != nil { w.WriteHeader(500) - h.alertDialog(w, r, "Failed creating token", "/#/tokens") + h.alertDialog(w, r, err.Error(), "/#/tokens") return } @@ -578,7 +546,7 @@ func (h *WebUiHandler) sendLoginPage(w http.ResponseWriter, r *http.Request, cod return } - loginTemplate, err := template.New("test").Parse(loginTemplateStr) + loginTemplate, err := template.New("login").Parse(loginTemplateStr) if err != nil { w.WriteHeader(500) io.WriteString(w, "Error compiling login.tmpl") diff --git a/webui/index.tmpl b/webui/index.tmpl index dc5d6c7..4e86fbc 100644 --- a/webui/index.tmpl +++ b/webui/index.tmpl @@ -2,6 +2,29 @@ {{.Head}} + + diff --git a/webui/styles.css b/webui/styles.css index 4bf51fb..ed5d9fc 100644 --- a/webui/styles.css +++ b/webui/styles.css @@ -229,27 +229,6 @@ main *:target { z-index: 1010; } -{{range $domain, $tunnel:= .Tunnels}} -#toggle-tunnel-delete-dialog-{{$tunnel.CssId}} { - display: none; -} -#toggle-tunnel-delete-dialog-{{$tunnel.CssId}}:checked + .dialog { - display: block; -} - -#toggle-tunnel-hide-deleted-{{$tunnel.CssId}}:checked + .list-item { - /* This is a trick to make the delete request after the delete button is - * clicked. The background will never actually be displayed, because it's - * moved offscreen. */ - position: absolute; - left: -999em; - background: url("/delete-tunnel?domain={{$domain}}"); -} -#toggle-tunnel-hide-deleted-{{$tunnel.CssId}}:checked ~ .dialog { - display: none; -} -{{end}} - @media (min-width: 640px) { main {