Implement first draft of HTML UI

The UI was originally written using fancy HTML checkbox toggle
hacks in order to make the UI very fast. It's cool but complicated
and difficult to change. In order to make updates to the UI more
quickly, I'm changing it to use traditional HTML with full page
reloads for navigation. It's not as fast but much simpler.
This commit is contained in:
Anders Pitman
2021-12-14 14:06:25 -07:00
parent aa04f15283
commit 60fbfac081
11 changed files with 758 additions and 43 deletions

16
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/boringproxy/boringproxy
go 1.15
go 1.17
require (
github.com/GeertJohan/go.rice v1.0.0
@@ -8,3 +8,17 @@ require (
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
)
require (
github.com/daaku/go.zipexe v1.0.0 // indirect
github.com/klauspost/cpuid v1.2.5 // indirect
github.com/libdns/libdns v0.1.0 // indirect
github.com/mholt/acmez v0.1.1 // indirect
github.com/miekg/dns v1.1.30 // indirect
go.uber.org/atomic v1.6.0 // indirect
go.uber.org/multierr v1.5.0 // indirect
go.uber.org/zap v1.15.0 // indirect
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect
golang.org/x/text v0.3.0 // indirect
)

9
go.sum
View File

@@ -1,10 +1,8 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/caddyserver/certmagic v0.12.0 h1:1f7kxykaJkOVVpXJ8ZrC6RAO5F6+kKm9U7dBFbLNeug=
github.com/caddyserver/certmagic v0.12.0/go.mod h1:tr26xh+9fY5dN0J6IPAlMj07qpog22PJKa7Nw7j835U=
@@ -14,15 +12,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid v1.2.5 h1:VBd9MyVIiJHzzgnrLQG5Bcv75H4YaWrlKqWHjurxCGo=
github.com/klauspost/cpuid v1.2.5/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/libdns/libdns v0.1.0 h1:0ctCOrVJsVzj53mop1angHp/pE3hmAhP7KiHvR0HD04=
github.com/libdns/libdns v0.1.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
@@ -30,7 +25,6 @@ github.com/mholt/acmez v0.1.1 h1:KQODCqk+hBn3O7qfCRPj6L96uG65T5BSS95FKNEqtdA=
github.com/mholt/acmez v0.1.1/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -43,9 +37,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@@ -89,7 +81,6 @@ golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapK
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=

View File

@@ -0,0 +1,58 @@
{{ template "header.tmpl" . }}
<div class='tunnel-adder'>
<h1>Add Tunnel</h1>
<form action="/tunnels" method="POST">
<div class='input'>
<label for="domain">Domain:</label>
<input type="text" id="domain" name="domain" required>
<input type="hidden" id="tunnel-owner" name="owner" value="{{$.UserId}}">
</div>
<div class='input'>
<label for="tunnel-port">Tunnel Port:</label>
<input type="text" id="tunnel-port" name="tunnel-port" value="Random">
</div>
<div class='input'>
<label for="client-name">Client Name:</label>
<select id="client-name" name="client-name">
<option value="none">No client</option>
{{range $id, $client := (index $.Users $.UserId).Clients}}
<option value="{{$id}}">{{$id}}</option>
{{end}}
</select>
</div>
<div class='input'>
<label for="client-addr">Client Address:</label>
<input type="text" id="client-addr" name="client-addr" value='127.0.0.1'>
</div>
<div class='input'>
<label for="client-port">Client Port:</label>
<input type="text" id="client-port" name="client-port">
</div>
<div class='input'>
<label for="allow-external-tcp">Allow External TCP:</label>
<input type="checkbox" id="allow-external-tcp" name="allow-external-tcp">
</div>
<div class='input'>
<label for="password-protect">Password Protect:</label>
<input type="checkbox" id="password-protect" name="password-protect">
<div id='login-inputs'>
<label for="username">Username:</label>
<input type="text" id="username" name="username">
<label for="password">Password:</label>
<input type="password" id="password" name="password">
</div>
</div>
<div class='input'>
<label for="tls-termination">TLS Termination:</label>
<select id="tls-termination" name="tls-termination">
<option value="server">Server</option>
<option value="client">Client</option>
<option value="passthrough">Passthrough</option>
</select>
</div>
<button class='button' type="submit">Submit</button>
</form>
</div>
{{ template "footer.tmpl" . }}

5
templates/footer.tmpl Normal file
View File

@@ -0,0 +1,5 @@
</div>
</div>
</main>
</body>
</html>

36
templates/header.tmpl Normal file
View File

@@ -0,0 +1,36 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>boringproxy</title>
<link rel="icon" href="/logo.png">
<style>
{{ template "styles.tmpl" }}
</style>
<style>
</style>
</head>
<body>
<main>
<input type='checkbox' id='menu-toggle'/>
<label id='menu-label' for='menu-toggle'>Menu</label>
<div class='page'>
<div class='menu'>
<a class='menu-item' href='/tunnels'>Tunnels</a>
<a class='menu-item' href='/edit-tunnel'>Add Tunnel</a>
<a class='menu-item' href='/tokens'>Tokens</a>
{{ if $.User.IsAdmin }}
<a class='menu-item' href='/users'>Users</a>
{{ end }}
<a class='menu-item' href='/confirm-logout'>Logout</a>
</div>
<div class='content'>

309
templates/styles.tmpl Normal file
View File

@@ -0,0 +1,309 @@
:root {
--main-color: #555555;
--background-color: #fff;
--hover-color: #ddd;
--menu-label-height: 60px;
}
* {
box-sizing: border-box;
font-family: Arial;
}
html {
}
body {
font-family: Helvetica;
display: flex;
justify-content: center;
margin: 0;
}
main {
width: 100%;
}
.tn-tunnel-list-table {
display: none;
}
.tn-tunnel-list-item {
padding: 10px;
border: 1px solid #000;
}
.tn-attribute {
padding: 5px;
}
.tn-attribute__name {
font-weight: bold;
}
.tn-tunnel-table, .tn-tunnel-table__cell {
border: 1px solid black;
border-collapse: collapse;
text-align: center;
padding: 10px;
word-wrap: break-word;
}
.tn-tunnel-table {
width: 100%;
}
.tn-tunnel-table__link {
display: contents;
}
.tn-tunnel-table__row:hover {
background: var(--hover-color);
cursor: pointer;
}
.tn-tunnel-table__cell {
max-width: 256px;
}
.tn-tunnel-link {
color: #000;
text-decoration: none;
}
.tn-tunnel-link:hover {
background: var(--hover-color);
}
#menu-label {
position: fixed;
z-index: 1000;
width: 100vw;
height: var(--menu-label-height);
line-height: var(--menu-label-height);
color: var(--background-color);
background: var(--main-color);
font-size: 18px;
font-weight: bold;
display: block;
/*border: 2px solid var(--main-color);*/
cursor: pointer;
padding: 0 .7em;
user-select: none;
}
#menu-label:hover {
color: var(--main-color);
background: var(--background-color);
border: 2px solid var(--main-color);
}
.menu {
position: fixed;
z-index: 1000;
top: var(--menu-label-height);
display: none;
flex-direction: column;
background: #fff;
border: 1px solid var(--main-color);
/*min-width: 256px;*/
}
.menu-item {
font-weight: bold;
border-bottom: 1px solid var(--main-color);
padding: .7em 2em;
text-decoration: none;
}
.menu-item:hover {
background: var(--hover-color);
}
.menu-item:any-link {
color: var(--main-color);
}
.active-tab.menu-item:any-link {
color: #fff;
}
.active-tab.menu-item {
background: var(--main-color);
}
.toggle {
display: none;
}
#menu-toggle {
display: none;
}
#menu-toggle:checked ~ .page .menu {
display: flex;
}
#menu-toggle:checked ~ #menu-label {
}
.content {
border: 1px solid var(--main-color);
}
.button {
padding: .5em 1em;
margin: 5px;
border: 2px solid var(--main-color);
border-radius: .5em;
color: #fff;
background-color: var(--main-color);
text-decoration: none;
user-select: none;
cursor: pointer;
font-family: -system-ui, sans-serif;
font-size: 1em;
line-height: 1.2;
white-space: nowrap;
}
.button:hover {
color: #555555;
background-color: #fff;
border: 2px solid var(--main-color);
}
.button-row {
display: flex;
justify-content: center;
}
.list {
display: flex;
flex-direction: column;
}
.list-item {
padding: 5px;
border-bottom: 1px solid var(--main-color);
display: flex;
flex-wrap: wrap;
overflow-x: hidden;
justify-content: space-between;
align-items: center;
}
.tunnel:hover {
background: var(--hover-color);
}
.tunnel-adder {
padding: 5px;
}
.tunnel-adder h1 {
margin: .2em;
}
.tunnel-adder form {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.ssh-key-adder form {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.input {
padding: .7em;
margin: .2em;
border: 1px solid var(--main-color);
display: flex;
flex-direction: column;
}
.input label {
padding: .2em;
font-weight: bold;
}
#login-inputs {
display: none;
}
#password-protect:checked ~ #login-inputs {
display: block;
}
.token-adder {
padding: 5px;
display: flex;
}
.token {
font-family: Monospace;
}
.monospace {
font-family: Monospace;
}
.page {
margin-top: var(--menu-label-height);
/*display: none;*/
flex-direction: column;
display: flex;
}
.qr-code {
width: 8em;
height: 8em;
}
.dialog {
display: none;
}
.dialog__overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
z-index: 1000;
}
.dialog__content {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--background-color);
padding: 1em;
border: 1px solid #ccc;
z-index: 1010;
}
@media (min-width: 960px) {
main {
display: flex;
justify-content: flex-start;
width: 900px;
}
#menu-label {
display: none;
}
.menu {
display: flex;
position: static;
}
.page {
margin-top: auto;
flex-direction: row;
width: 100%;
}
.content {
width: 100%;
}
.tn-tunnel-list-table {
display: block;
}
.tn-tunnel-list {
display: none;
}
}

27
templates/tokens.tmpl Normal file
View File

@@ -0,0 +1,27 @@
{{ template "header.tmpl" . }}
<div class='list'>
{{range $token, $tokenData := .Tokens}}
<div class='list-item'>
<span class='token'>{{$token}} ({{$tokenData.Owner}})</span>
<a href='/login?access_token={{$token}}'>Login link</a>
<img class='qr-code' src='{{index $.QrCodes $token}}' width=100 height=100>
<a href="/confirm-delete-token?token={{$token}}">
<button class='button'>Delete</button>
</a>
</div>
{{end}}
</div>
<div class='token-adder'>
<form action="/tokens" method="POST">
<label for="token-owner">Owner:</label>
<select id="token-owner" name="owner">
{{range $username, $user := .Users}}
<option value="{{$username}}">{{$username}}</option>
{{end}}
</select>
<button class='button' type="submit">Add Token</button>
</form>
</div>
{{ template "footer.tmpl" . }}

20
templates/tunnel.tmpl Normal file
View File

@@ -0,0 +1,20 @@
{{ template "header.tmpl" . }}
<div class='tn-attribute'>
<div class='tn-attribute__name'>Domain:</div>
<div class='tn-attribute__value'>{{$.Tunnel.Domain}}</div>
</div>
<div class='tn-attribute'>
<div class='tn-attribute__name'>Client:</div>
<div class='tn-attribute__value'>{{$.Tunnel.ClientName}}</div>
</div>
<div class='tn-attribute'>
<div class='tn-attribute__name'>Target:</div>
<div class='tn-attribute__value'>{{$.Tunnel.ClientAddress}}:{{$.Tunnel.ClientPort}}</div>
</div>
<a href="/confirm-delete-tunnel?domain={{$.Tunnel.Domain}}">
<button class='button'>Delete</button>
</a>
{{ template "footer.tmpl" . }}

54
templates/tunnels.tmpl Normal file
View File

@@ -0,0 +1,54 @@
{{ template "header.tmpl" . }}
<div class='tn-tunnel-list'>
{{ range $domain, $tunnel:= .Tunnels }}
<div class='tn-tunnel-list-item'>
<div class='tn-attribute'>
<div class='tn-attribute__name'>Domain:</div>
<div class='tn-attribute__value'>{{$domain}}</div>
</div>
<div class='tn-attribute'>
<div class='tn-attribute__name'>Client:</div>
<div class='tn-attribute__value'>{{$tunnel.ClientName}}</div>
</div>
<div class='tn-attribute'>
<div class='tn-attribute__name'>Target:</div>
<div class='tn-attribute__value'>{{$tunnel.ClientAddress}}:{{$tunnel.ClientPort}}</div>
</div>
<div class='button-row'>
<a class='button' href="/tunnels/{{$domain}}">View</a>
<a class='button' href="/confirm-delete-tunnel?domain={{$domain}}">Delete</a>
</div>
</div>
{{ end }}
</div>
<div class='tn-tunnel-list-table'>
<table class='tn-tunnel-table'>
<thead>
<tr>
<th class='tn-tunnel-table__cell'>Domain</th>
<th class='tn-tunnel-table__cell'>Client</th>
<th class='tn-tunnel-table__cell'>Target</th>
<th class='tn-tunnel-table__cell'>Actions</th>
</tr>
</thead>
<tbody>
{{range $domain, $tunnel:= .Tunnels}}
<tr>
<td class='tn-tunnel-table__cell'>
{{$domain}}
</td>
<td class='tn-tunnel-table__cell'>{{$tunnel.ClientName}}</td>
<td class='tn-tunnel-table__cell'>{{$tunnel.ClientAddress}}:{{$tunnel.ClientPort}}</td>
<td class='tn-tunnel-table__cell'>
<div class='button-row'>
<a class='button' href="/tunnels/{{$domain}}">View</a>
<a class='button' href="/confirm-delete-tunnel?domain={{$domain}}">Delete</a>
</div>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ template "footer.tmpl" . }}

21
templates/users.tmpl Normal file
View File

@@ -0,0 +1,21 @@
{{ template "header.tmpl" . }}
<div class='list'>
{{range $username, $user := .Users}}
<div class='list-item'>
{{$username}}
<a href="/confirm-delete-user?username={{$username}}">
<button class='button'>Delete</button>
</a>
</div>
{{end}}
</div>
<div class='user-adder'>
<form action="/users" method="POST">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<label for="is-admin">Is Admin:</label>
<input type="checkbox" id="is-admin" name="is-admin">
<button class='button' type="submit">Add User</button>
</form>
</div>
{{ template "footer.tmpl" . }}

View File

@@ -11,8 +11,12 @@ import (
"strings"
"sync"
"time"
"embed"
)
//go:embed templates
var fs embed.FS
type WebUiHandler struct {
config *Config
db *Database
@@ -21,6 +25,7 @@ type WebUiHandler struct {
tunMan *TunnelManager
box *rice.Box
headHtml template.HTML
tmpl *template.Template
pendingRequests map[string]chan ReqResult
mutex *sync.Mutex
}
@@ -103,6 +108,13 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
homePath := "/#/tunnel"
var err error
h.tmpl, err = template.ParseFS(fs, "templates/*.tmpl")
if err != nil {
fmt.Println(err.Error())
return
}
// Note: h.box and h.headHtml need to be ready before pretty much
// everything else, including sendLoginPage
@@ -162,7 +174,7 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
case "/login":
h.handleLogin(w, r)
case "/users":
h.handleUsers(w, r, tokenData)
h.handleUsers(w, r, tokenData, user)
case "/confirm-delete-user":
h.confirmDeleteUser(w, r)
@@ -253,7 +265,7 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
}
case "/tunnels":
h.handleTunnels(w, r, tokenData)
h.handleTunnels(w, r, tokenData, user)
case "/confirm-delete-tunnel":
r.ParseForm()
@@ -276,11 +288,41 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
Head: h.headHtml,
Message: fmt.Sprintf("Are you sure you want to delete %s?", domain),
ConfirmUrl: fmt.Sprintf("/delete-tunnel?domain=%s", domain),
CancelUrl: "/#/tunnels",
CancelUrl: "/tunnels",
}
tmpl.Execute(w, data)
case "/edit-tunnel":
r.ParseForm()
var users map[string]User
// TODO: handle security checks in api
if user.IsAdmin {
users = h.db.GetUsers()
} else {
users = make(map[string]User)
users[tokenData.Owner] = user
}
templateData := struct {
UserId string
User User
Users map[string]User
}{
UserId: tokenData.Owner,
User: user,
Users: users,
}
err := h.tmpl.ExecuteTemplate(w, "edit_tunnel.tmpl", templateData)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
case "/delete-tunnel":
r.ParseForm()
@@ -292,6 +334,8 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
return
}
http.Redirect(w, r, "/tunnels", 303)
case "/tunnel-private-key":
r.ParseForm()
@@ -356,22 +400,111 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
case "/loading":
h.handleLoading(w, r)
default:
if strings.HasPrefix(r.URL.Path, "/tunnels/") {
r.ParseForm()
parts := strings.Split(r.URL.Path, "/")
if len(parts) != 3 {
w.WriteHeader(400)
h.alertDialog(w, r, "Invalid path", "/#/tunnels")
return
}
domain := parts[2]
r.Form.Set("domain", domain)
tunnel, err := h.api.GetTunnel(tokenData, r.Form)
if err != nil {
w.WriteHeader(400)
h.alertDialog(w, r, err.Error(), "/#/tunnels")
return
}
templateData := struct {
Tunnel Tunnel
}{
Tunnel: tunnel,
}
err = h.tmpl.ExecuteTemplate(w, "tunnel.tmpl", templateData)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
} else {
w.WriteHeader(404)
h.alertDialog(w, r, "Unknown page "+r.URL.Path, "/#/tunnels")
return
}
}
}
func (h *WebUiHandler) handleTokens(w http.ResponseWriter, r *http.Request, user User, tokenData TokenData) {
if r.Method != "POST" {
w.WriteHeader(405)
h.alertDialog(w, r, "Invalid method for tokens", "/#/tokens")
r.ParseForm()
switch r.Method {
case "GET":
var tokens map[string]TokenData
var users map[string]User
// TODO: handle security checks in api
if user.IsAdmin {
tokens = h.db.GetTokens()
users = h.db.GetUsers()
} else {
tokens = make(map[string]TokenData)
for token, td := range h.db.GetTokens() {
if tokenData.Owner == td.Owner {
tokens[token] = td
}
}
users = make(map[string]User)
users[tokenData.Owner] = user
}
qrCodes := make(map[string]template.URL)
for token := range tokens {
loginUrl := fmt.Sprintf("https://%s/login?access_token=%s", h.config.WebUiDomain, token)
var png []byte
png, err := qrcode.Encode(loginUrl, qrcode.Medium, 256)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/#/tokens")
return
}
r.ParseForm()
data := base64.StdEncoding.EncodeToString(png)
qrCodes[token] = template.URL("data:image/png;base64," + data)
}
templateData := struct {
Tokens map[string]TokenData
User User
Users map[string]User
QrCodes map[string]template.URL
}{
Tokens: tokens,
User: user,
Users: users,
QrCodes: qrCodes,
}
err := h.tmpl.ExecuteTemplate(w, "tokens.tmpl", templateData)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
case "POST":
_, err := h.api.CreateToken(tokenData, r.Form)
if err != nil {
w.WriteHeader(500)
@@ -380,6 +513,11 @@ func (h *WebUiHandler) handleTokens(w http.ResponseWriter, r *http.Request, user
}
http.Redirect(w, r, "/#/tokens", 303)
default:
w.WriteHeader(405)
h.alertDialog(w, r, "Invalid method for tokens", "/#/tokens")
return
}
}
func (h *WebUiHandler) handleSshKeys(w http.ResponseWriter, r *http.Request, user User, tokenData TokenData) {
@@ -460,11 +598,28 @@ func (h *WebUiHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
}
}
func (h *WebUiHandler) handleTunnels(w http.ResponseWriter, r *http.Request, tokenData TokenData) {
func (h *WebUiHandler) handleTunnels(w http.ResponseWriter, r *http.Request, tokenData TokenData, user User) {
switch r.Method {
case "POST":
h.handleCreateTunnel(w, r, tokenData)
case "GET":
tunnels := h.api.GetTunnels(tokenData)
templateData := struct {
User User
Tunnels map[string]Tunnel
}{
User: user,
Tunnels: tunnels,
}
err := h.tmpl.ExecuteTemplate(w, "tunnels.tmpl", templateData)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
default:
w.WriteHeader(405)
w.Write([]byte("Invalid method for /#/tunnels"))
@@ -553,16 +708,37 @@ func (h *WebUiHandler) sendLoginPage(w http.ResponseWriter, r *http.Request, cod
loginTemplate.Execute(w, loginData)
}
func (h *WebUiHandler) handleUsers(w http.ResponseWriter, r *http.Request, tokenData TokenData) {
if r.Method != "POST" {
w.WriteHeader(405)
h.alertDialog(w, r, "Invalid method for users", "/#/users")
return
}
func (h *WebUiHandler) handleUsers(w http.ResponseWriter, r *http.Request, tokenData TokenData, user User) {
r.ParseForm()
switch r.Method {
case "GET":
var users map[string]User
// TODO: handle security checks in api
if user.IsAdmin {
users = h.db.GetUsers()
} else {
users = make(map[string]User)
users[tokenData.Owner] = user
}
templateData := struct {
User User
Users map[string]User
}{
User: user,
Users: users,
}
err := h.tmpl.ExecuteTemplate(w, "users.tmpl", templateData)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
case "POST":
err := h.api.CreateUser(tokenData, r.Form)
if err != nil {
w.WriteHeader(500)
@@ -571,6 +747,10 @@ func (h *WebUiHandler) handleUsers(w http.ResponseWriter, r *http.Request, token
}
http.Redirect(w, r, "/#/users", 303)
default:
w.WriteHeader(405)
h.alertDialog(w, r, "Invalid method for users", "/#/users")
}
}
func (h *WebUiHandler) confirmDeleteUser(w http.ResponseWriter, r *http.Request) {