First draft of Waygate implementation

This commit is contained in:
Anders Pitman
2022-02-25 14:28:33 -07:00
parent a602c6cfa7
commit e11e03c8d2
4 changed files with 265 additions and 12 deletions

View File

@@ -11,6 +11,8 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"os/user"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -37,10 +39,11 @@ type SmtpConfig struct {
} }
type Server struct { type Server struct {
db *Database db *Database
tunMan *TunnelManager tunMan *TunnelManager
httpClient *http.Client httpClient *http.Client
httpListener *PassthroughListener httpListener *PassthroughListener
waygateServer *waygate.Server
} }
func Listen() { func Listen() {
@@ -93,7 +96,17 @@ func Listen() {
fmt.Printf("WARNING: Failed to access %s:%d from the internet\n", ip, *httpsPort) fmt.Printf("WARNING: Failed to access %s:%d from the internet\n", ip, *httpsPort)
} }
user, err := user.Current()
if err != nil {
log.Fatalf("Unable to get current user: %v", err)
}
waygateServer := waygate.NewServer(db) waygateServer := waygate.NewServer(db)
waygateServer.SshConfig = &waygate.SshConfig{
ServerAddress: db.GetAdminDomain(),
ServerPort: *sshServerPort,
Username: user.Username,
AuthorizedKeysPath: filepath.Join(user.HomeDir, ".ssh", "authorized_keys"),
}
autoCerts := true autoCerts := true
if *httpPort != 80 || *httpsPort != 443 { if *httpPort != 80 || *httpsPort != 443 {
@@ -148,7 +161,8 @@ func Listen() {
users := db.GetUsers() users := db.GetUsers()
if len(users) == 0 { if len(users) == 0 {
db.AddUser("admin", true) db.AddUser("admin", true)
_, err := db.AddToken("admin", "") clientId := ""
_, err := db.AddToken("admin", clientId)
if err != nil { if err != nil {
log.Fatal("Failed to initialize admin user") log.Fatal("Failed to initialize admin user")
} }
@@ -190,7 +204,7 @@ func Listen() {
httpListener := NewPassthroughListener() httpListener := NewPassthroughListener()
p := &Server{db, tunMan, httpClient, httpListener} p := &Server{db, tunMan, httpClient, httpListener, waygateServer}
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
GetCertificate: certConfig.GetCertificate, GetCertificate: certConfig.GetCertificate,
@@ -199,6 +213,13 @@ func Listen() {
tlsListener := tls.NewListener(httpListener, tlsConfig) tlsListener := tls.NewListener(httpListener, tlsConfig)
http.Handle("/waygate/", http.StripPrefix("/waygate", waygateServer)) http.Handle("/waygate/", http.StripPrefix("/waygate", waygateServer))
// TODO: This feels like a bit of a hack.
http.HandleFunc("/waygate/authorize", func(w http.ResponseWriter, r *http.Request) {
webUiHandler.handleWebUiRequest(w, r)
})
http.HandleFunc("/waygate/authorized", func(w http.ResponseWriter, r *http.Request) {
webUiHandler.handleWebUiRequest(w, r)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
timestamp := time.Now().Format(time.RFC3339) timestamp := time.Now().Format(time.RFC3339)
@@ -357,6 +378,18 @@ func (p *Server) handleConnection(clientConn net.Conn, certConfig *certmagic.Con
return return
} }
port, err := p.waygateServer.GetSSHPortForDomain(clientHello.ServerName)
if err == nil {
fmt.Println("yolo", port)
useTls := true
err := ProxyTcp(clientConn, "127.0.0.1", port, useTls, certConfig)
if err != nil {
log.Println(err.Error())
return
}
return
}
passConn := NewProxyConn(clientConn, clientReader) passConn := NewProxyConn(clientConn, clientReader)
tunnel, exists := p.db.GetTunnel(clientHello.ServerName) tunnel, exists := p.db.GetTunnel(clientHello.ServerName)

View File

@@ -8,17 +8,20 @@ import (
"sync" "sync"
"github.com/takingnames/namedrop-go" "github.com/takingnames/namedrop-go"
"github.com/takingnames/waygate-go"
) )
var DBFolderPath string var DBFolderPath string
type Database struct { type Database struct {
AdminDomain string `json:"admin_domain"` AdminDomain string `json:"admin_domain"`
Tokens map[string]TokenData `json:"tokens"` Tokens map[string]TokenData `json:"tokens"`
Tunnels map[string]Tunnel `json:"tunnels"` Tunnels map[string]Tunnel `json:"tunnels"`
Users map[string]User `json:"users"` Users map[string]User `json:"users"`
dnsRequests map[string]namedrop.DNSRequest `json:"dns_requests"` dnsRequests map[string]namedrop.DNSRequest `json:"dns_requests"`
mutex *sync.Mutex WaygateTunnels map[string]waygate.WaygateTunnel `json:"waygate_tunnels"`
WaygateTalismans map[string]waygate.WaygateTalisman `json:"waygate_talismans"`
mutex *sync.Mutex
} }
type TokenData struct { type TokenData struct {
@@ -96,6 +99,13 @@ func NewDatabase(path string) (*Database, error) {
db.dnsRequests = make(map[string]namedrop.DNSRequest) db.dnsRequests = make(map[string]namedrop.DNSRequest)
} }
if db.WaygateTunnels == nil {
db.WaygateTunnels = make(map[string]waygate.WaygateTunnel)
}
if db.WaygateTalismans == nil {
db.WaygateTalismans = make(map[string]waygate.WaygateTalisman)
}
db.mutex = &sync.Mutex{} db.mutex = &sync.Mutex{}
db.mutex.Lock() db.mutex.Lock()
@@ -322,6 +332,87 @@ func (d *Database) DeleteUser(username string) {
d.persist() d.persist()
} }
func (d *Database) AddWaygateTunnel(domains []string) (string, error) {
d.mutex.Lock()
defer d.mutex.Unlock()
id, err := genRandomCode(32)
if err != nil {
return "", errors.New("Could not generate waygate id")
}
// TODO: verify none of the domains are already in use.
waygate := waygate.WaygateTunnel{
Domains: domains,
}
d.WaygateTunnels[id] = waygate
d.persist()
return id, nil
}
func (d *Database) GetWaygateTunnel(id string) (waygate.WaygateTunnel, error) {
d.mutex.Lock()
defer d.mutex.Unlock()
tun, exists := d.WaygateTunnels[id]
if !exists {
return waygate.WaygateTunnel{}, errors.New("No such waygate")
}
return tun, nil
}
func (d *Database) GetWaygates() map[string]waygate.WaygateTunnel {
d.mutex.Lock()
defer d.mutex.Unlock()
wgs := make(map[string]waygate.WaygateTunnel)
for id, wg := range d.WaygateTunnels {
wgs[id] = wg
}
return wgs
}
func (d *Database) AddWaygateTalisman(waygateId string) (string, error) {
d.mutex.Lock()
defer d.mutex.Unlock()
talisman, err := genRandomCode(32)
if err != nil {
return "", errors.New("Could not generate waygate talisman")
}
_, exists := d.WaygateTunnels[waygateId]
if !exists {
return "", errors.New("No such waygate")
}
talismanData := waygate.WaygateTalisman{
WaygateId: waygateId,
}
d.WaygateTalismans[talisman] = talismanData
d.persist()
return talisman, nil
}
func (d *Database) GetWaygateTalisman(id string) (waygate.WaygateTalisman, error) {
d.mutex.Lock()
defer d.mutex.Unlock()
talisman, exists := d.WaygateTalismans[id]
if !exists {
return waygate.WaygateTalisman{}, errors.New("No such talisman")
}
return talisman, nil
}
func (d *Database) persist() { func (d *Database) persist() {
saveJson(d, DBFolderPath+"boringproxy_db.json") saveJson(d, DBFolderPath+"boringproxy_db.json")
} }

28
templates/authorize.tmpl Normal file
View File

@@ -0,0 +1,28 @@
<p>
A service is requesting to create a tunnel. If you want to approve this action, select a domain below.
</p>
<h1>Select Domain</h1>
<form action="/waygate/authorized" 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>
<input type="text" name="host" placeholder="Subdomain optional">
<span>.</span>
<select id="domain-input" name="domain">
{{ range .Domains}}
<option>{{.DomainName}}</option>
{{ end }}
<option>takingnames.co</option>
</select>
</div>
<div class='tn-button-row'>
<button class='button'>Approve</button>
<button class='button' formaction="/deny">Deny</button>
</div>
</form>

View File

@@ -14,6 +14,8 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/takingnames/waygate-go"
) )
//go:embed logo.png templates //go:embed logo.png templates
@@ -104,6 +106,105 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
} }
switch r.URL.Path { switch r.URL.Path {
case "/waygate/authorized":
if r.Method != "POST" {
w.WriteHeader(405)
io.WriteString(w, "Invalid method")
return
}
r.ParseForm()
authReq, err := waygate.ExtractAuthRequest(r)
if err != nil {
w.WriteHeader(400)
io.WriteString(w, err.Error())
return
}
domain := r.Form.Get("domain")
host := r.Form.Get("host")
fqdn := fmt.Sprintf("%s.%s", host, domain)
fmt.Println(fqdn)
waygateId, err := h.db.AddWaygateTunnel([]string{fqdn})
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/")
return
}
talisman, err := h.db.AddWaygateTalisman(waygateId)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/")
return
}
if authReq.RedirectUri == "urn:ietf:wg:oauth:2.0:oob" {
fmt.Fprintf(w, talisman)
} else {
w.WriteHeader(500)
h.alertDialog(w, r, "Unsupported auth", "/")
return
}
case "/waygate/authorize":
if r.Method != "GET" {
w.WriteHeader(405)
h.alertDialog(w, r, err.Error(), "/")
return
}
r.ParseForm()
authReq, err := waygate.ExtractAuthRequest(r)
if err != nil {
w.WriteHeader(400)
h.alertDialog(w, r, err.Error(), "/")
return
}
//user, exists := db.GetUser(tokenData.Owner)
//if !exists {
// sendLoginPage(w, r)
// return
//}
//domains := db.GetDomainsForUser(tokenData.Owner)
//displayClientId := authReq.ClientId
//if strings.HasSuffix(authReq.ClientId, ".ip.takingnames.live") {
// domainParts := strings.Split(authReq.ClientId, ".ip.takingnames.live")
// ip := strings.Replace(domainParts[0], "-", ".", -1)
// displayClientId = ip
//}
data := struct {
// LoggedIn bool
Domains []string
// FreeDomains []string
// User User
AuthRequest *waygate.AuthRequest
// DisplayClientId string
}{
// LoggedIn: loggedIn,
Domains: []string{},
// FreeDomains: FreeDomains(),
// User: user,
AuthRequest: authReq,
// DisplayClientId: displayClientId,
}
err = h.tmpl.ExecuteTemplate(w, "authorize.tmpl", data)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/")
return
}
case "/login": case "/login":
h.handleLogin(w, r) h.handleLogin(w, r)
case "/users": case "/users":