Skip to content

Commit

Permalink
feat(api): hosts are now configured through API
Browse files Browse the repository at this point in the history
Hosts are no longer provided in config
but they are provided through the api

BREAKING CHANGE: config no longer accepts hosts
  • Loading branch information
gnur committed Sep 14, 2020
1 parent 8776f61 commit 7b00963
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 241 deletions.
85 changes: 67 additions & 18 deletions cmd/tobab/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"crypto/rand"
"fmt"
"html/template"
Expand All @@ -9,14 +10,15 @@ import (
"net/http/httputil"
"net/url"
"os"
"os/signal"
"strings"
"sync"
"time"

"github.com/BurntSushi/toml"
"github.com/caddyserver/certmagic"
"github.com/gnur/tobab"
"github.com/gnur/tobab/muxlogger"
"github.com/gnur/tobab/storm"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
Expand All @@ -33,9 +35,9 @@ type Tobab struct {
logger *logrus.Entry
maxAge time.Duration
templates *template.Template
lock sync.Mutex
dirty bool
confLoc string
db tobab.Database
server *http.Server
}

func main() {
Expand Down Expand Up @@ -94,44 +96,75 @@ func main() {
version = "unknown"
}

db, err := storm.New(cfg.DatabasePath)
if err != nil {
logger.WithError(err).WithField("location", cfg.DatabasePath).Fatal("Unable to initialize database")
}

app := Tobab{
key: key,
config: cfg,
logger: logger.WithField("version", version),
maxAge: 720 * time.Hour,
fqdn: "https://" + cfg.Hostname,
confLoc: confLoc,
db: db,
}

app.templates, err = loadTemplates()
if err != nil {
logger.WithError(err).Fatal("unable to load templates")
}
go app.startServer()
c := make(chan os.Signal, 1)
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
signal.Notify(c, os.Interrupt)

// Block until we receive our signal.
<-c
app.logger.Info("shutting down")
}

func (app *Tobab) restartServer() {

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
app.logger.Info("shutting down server")
err := app.server.Shutdown(ctx)
app.logger.WithError(err).Info("server was shut down")

go app.startServer()
}

func (app *Tobab) startServer() {
app.logger.Info("starting server")
r := mux.NewRouter()
hosts := []string{app.config.Hostname}
certHosts := []string{app.config.Hostname}
var err error

for h, conf := range cfg.Hosts {
app.logger.Debug("loading hosts")
hosts, err := app.db.GetHosts()
if err != nil {
app.logger.WithError(err).Fatal("unable to load hosts")
}

for _, conf := range hosts {
if conf.Type != "http" {
app.logger.WithField("type", conf.Type).Fatal("Unsupported type, currently only http is supported")
}

proxy, err := generateProxy(h, conf.Backend)
proxy, err := generateProxy(conf.Hostname, conf.Backend)
if err != nil {
fmt.Println(err)
return
app.logger.WithError(err).WithField("host", conf.Hostname).Error("Failed creating proxy")
continue
}

r.Host(h).PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.logger.WithField("host", conf.Hostname).Debug("starting proxy listener")
r.Host(conf.Hostname).PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
})
hosts = append(hosts, h)
}

if _, ok := cfg.Hosts[app.config.Hostname]; !ok {
cfg.Hosts[app.config.Hostname] = tobab.Host{
Public: true,
}
certHosts = append(certHosts, conf.Hostname)
}

tobabRoutes := r.Host(app.config.Hostname).Subrouter()
Expand Down Expand Up @@ -172,10 +205,26 @@ func main() {
}
}

err = certmagic.HTTPS(hosts, r)
magicListener, err := certmagic.Listen(certHosts)
if err != nil {
fmt.Println(err)
app.logger.WithError(err).Fatal("Failed getting certmagic listener")
}

srv := &http.Server{
WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60,
Handler: r,
}
go func() {
err = srv.Serve(magicListener)
if err != nil {
if err != http.ErrServerClosed {
app.logger.WithError(err).Fatal("Failed starting magic listener")
}
}
}()
app.server = srv
}

func generateProxy(host, backend string) (http.Handler, error) {
Expand Down
70 changes: 41 additions & 29 deletions cmd/tobab/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ import (
"net/url"
"strings"

"github.com/asdine/storm"
"github.com/sirupsen/logrus"
)

func (app *Tobab) getRBACMiddleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

h := r.Host
u, err := app.extractUser(r)
if err != nil && err != ErrUnauthenticatedRequest {
hostname := r.Host
u, extractUserErr := app.extractUser(r)
if extractUserErr != nil && extractUserErr != ErrUnauthenticatedRequest {
//this shouldn't happen unless someone tampered with a cookie manually
app.logger.WithError(err).Error("Unable to extract user")
app.logger.WithError(extractUserErr).Error("Unable to extract user")
//invalid cookie is present, delete it and force re-auth
c := http.Cookie{
Name: "X-Tobab-Token",
Expand All @@ -32,38 +33,45 @@ func (app *Tobab) getRBACMiddleware() func(http.Handler) http.Handler {
return
}
app.logger.WithFields(logrus.Fields{
"host": h,
"host": hostname,
"user": u,
"uri": r.RequestURI,
}).Debug("checking auth")

if !hasAccess(u, h, app.config) {
if err == ErrUnauthenticatedRequest {
redirectURL := url.URL{
Host: h,
Path: r.URL.String(),
Scheme: "https",
}
c := http.Cookie{
Domain: app.config.CookieScope,
Secure: true,
HttpOnly: true,
Value: redirectURL.String(),
Path: "/",
Name: "X-Tobab-Source",
}
http.SetCookie(w, &c)
http.Redirect(w, r, app.fqdn, 302)
} else {
http.Error(w, "access denied", http.StatusUnauthorized)
r.Header.Add("X-Tobab-User", u)

//configured hostname is always accessible
if hostname != app.config.Hostname {
h, err := app.db.GetHost(hostname)
if err == storm.ErrNotFound {
http.Error(w, "not found", 404)
return
}

return
}
if !h.HasAccess(u) {
if extractUserErr == ErrUnauthenticatedRequest {
redirectURL := url.URL{
Host: hostname,
Path: r.URL.String(),
Scheme: "https",
}
c := http.Cookie{
Domain: app.config.CookieScope,
Secure: true,
HttpOnly: true,
Value: redirectURL.String(),
Path: "/",
Name: "X-Tobab-Source",
}
http.SetCookie(w, &c)
http.Redirect(w, r, app.fqdn, 302)
} else {
http.Error(w, "access denied", http.StatusUnauthorized)
}

r.Header.Add("X-Tobab-User", u)
return
}

if h != app.config.Hostname {
//get all cookies, clear them, and then re-add the ones that are not tobab specific
cookies := r.Cookies()
r.Header.Del("Cookie")
Expand All @@ -84,7 +92,11 @@ func (app *Tobab) adminMiddleware() func(http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

u := r.Header.Get("X-Tobab-User")
if u == "" || !allowAdmin(u, app.config) {
if u == "" || !allowAdmin(u, app.config.AdminGlobs) {
app.logger.WithFields(logrus.Fields{
"user": u,
"globs": app.config.AdminGlobs,
}).Debug("denying request")
http.Error(w, "access denied", http.StatusUnauthorized)
return
}
Expand Down
46 changes: 3 additions & 43 deletions cmd/tobab/rbac.go
Original file line number Diff line number Diff line change
@@ -1,56 +1,16 @@
package main

import (
"strings"

"github.com/gnur/tobab"
matcher "github.com/ryanuber/go-glob"
)

func hasAccess(user, host string, cfg tobab.Config) bool {
h, ok := cfg.Hosts[host]
if !ok {
return false
}

if h.Public {
return true
} else if user == "" {
return false
}

for _, g := range cfg.Globs {
if matcher.Glob(g.Matcher, user) {
if contains(h.AllowedGlobs, g.Name) {
return true
}
}
}

return false
}
func allowAdmin(user string, adminGlobs []tobab.Glob) bool {

func contains(s []string, e string) bool {
for _, a := range s {
if strings.EqualFold(a, e) {
for _, g := range adminGlobs {
if g.Match(user) {
return true
}
}
return false
}

func allowAdmin(user string, cfg tobab.Config) bool {

for _, globName := range cfg.AdminGlobs {
for _, g := range cfg.Globs {
if g.Name != globName {
continue
}
if matcher.Glob(g.Matcher, user) {
return true
}
}
}

return false
}
Loading

0 comments on commit 7b00963

Please sign in to comment.