git » gofer » commit f4f71ed

HTTP Authentication support

author Alberto Bertogli
2020-06-06 02:57:06 UTC
committer Alberto Bertogli
2020-06-06 02:57:06 UTC
parent e1a6d1a9f867bfa86c173e4a5c08553c66cb60a2

HTTP Authentication support

This patch adds HTTP Authentication support.

URLs can be password-protected using basic HTTP authentication. The
user database is static for now, supporting plain and sha256-hashed
passwords.

config/config.go +2 -0
server/auth.go +93 -0
server/http.go +50 -15
test/01-be.yaml +6 -1
test/01-fe.yaml +1 -0
test/test.sh +39 -1

diff --git a/config/config.go b/config/config.go
index 6f715df..3988bb8 100644
--- a/config/config.go
+++ b/config/config.go
@@ -25,6 +25,8 @@ type HTTP struct {
 	Redirect map[string]string
 	CGI      map[string]string
 
+	Auth map[string]string
+
 	DirOpts map[string]DirOpts
 }
 
diff --git a/server/auth.go b/server/auth.go
new file mode 100644
index 0000000..0547b72
--- /dev/null
+++ b/server/auth.go
@@ -0,0 +1,93 @@
+package server
+
+import (
+	"crypto/sha256"
+	"encoding/hex"
+	"io/ioutil"
+	"math/rand"
+	"net/http"
+	"time"
+
+	"blitiri.com.ar/go/gofer/trace"
+	"gopkg.in/yaml.v3"
+)
+
+const authDuration = 10 * time.Millisecond
+
+type AuthWrapper struct {
+	handler http.Handler
+	users   *AuthDB
+}
+
+func (a *AuthWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// Make sure the call takes authDuration + 0-20% regardless of the
+	// outcome, to prevent basic timing attacks.
+	defer func(start time.Time) {
+		elapsed := time.Since(start)
+		delay := authDuration - elapsed
+		if delay > 0 {
+			maxDelta := int64(float64(delay) * 0.2)
+			delay += time.Duration(rand.Int63n(maxDelta))
+			time.Sleep(delay)
+		}
+	}(time.Now())
+
+	tr, _ := trace.FromContext(r.Context())
+
+	user, pass, ok := r.BasicAuth()
+	if !ok {
+		tr.Printf("auth header missing")
+		a.failed(w)
+		return
+	}
+
+	if dbPass, ok := a.users.Plain[user]; ok {
+		if pass == dbPass {
+			tr.Printf("auth for %q successful", user)
+			a.handler.ServeHTTP(w, r)
+		} else {
+			tr.Printf("incorrect password (plain) for %q", user)
+			a.failed(w)
+		}
+		return
+	}
+
+	if dbPass, ok := a.users.SHA256[user]; ok {
+		// Take the sha256 of the given pass, and compare.
+		buf := sha256.Sum256([]byte(pass))
+		shaPass := hex.EncodeToString(buf[:])
+		if shaPass == dbPass {
+			tr.Printf("auth for %q successful", user)
+			a.handler.ServeHTTP(w, r)
+		} else {
+			tr.Printf("incorrect password (sha256) for %q", user)
+			a.failed(w)
+		}
+		return
+	}
+
+	tr.Printf("user %q not found", user)
+	a.failed(w)
+}
+
+func (a *AuthWrapper) failed(w http.ResponseWriter) {
+	w.Header().Set("WWW-Authenticate", `Basic realm="Authentication"`)
+	http.Error(w, "Unauthorized", http.StatusUnauthorized)
+	return
+}
+
+type AuthDB struct {
+	Plain  map[string]string
+	SHA256 map[string]string
+}
+
+func LoadAuthFile(path string) (*AuthDB, error) {
+	buf, err := ioutil.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+
+	db := &AuthDB{}
+	err = yaml.Unmarshal(buf, &db)
+	return db, err
+}
diff --git a/server/http.go b/server/http.go
index 61c8693..1bf6c54 100644
--- a/server/http.go
+++ b/server/http.go
@@ -60,6 +60,28 @@ func httpServer(addr string, conf config.HTTP) *http.Server {
 		}
 	}
 
+	// Wrap the authentication handlers.
+	if len(conf.Auth) > 0 {
+		authMux := http.NewServeMux()
+		authMux.Handle("/", srv.Handler)
+		for path, dbPath := range conf.Auth {
+			users, err := LoadAuthFile(dbPath)
+			if err != nil {
+				log.Fatalf(
+					"failed to load auth file %q: %v", dbPath, err)
+			}
+			authMux.Handle(path,
+				WithTrace("http:auth",
+					&AuthWrapper{
+						handler: mux,
+						users:   users,
+					}))
+
+			log.Infof("%s auth %q -> %q", srv.Addr, path, dbPath)
+		}
+		srv.Handler = authMux
+	}
+
 	return srv
 }
 
@@ -208,7 +230,7 @@ func makeDir(from string, to url.URL, conf *config.HTTP) http.Handler {
 	path := pathOrOpaque(to)
 
 	fs := http.FileServer(NewFS(http.Dir(path), conf.DirOpts[from]))
-	return WithLogging("http:dir",
+	return WithTrace("http:dir", WithLogging(
 		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			tr, _ := trace.FromContext(r.Context())
 			tr.Printf("serving dir root %q", path)
@@ -220,19 +242,19 @@ func makeDir(from string, to url.URL, conf *config.HTTP) http.Handler {
 			tr.Printf("adjusted path: %q", r.URL.Path)
 			fs.ServeHTTP(w, r)
 		}),
-	)
+	))
 }
 
 func makeStatic(from string, to url.URL, conf *config.HTTP) http.Handler {
 	path := pathOrOpaque(to)
 
-	return WithLogging("http:static",
+	return WithTrace("http:static", WithLogging(
 		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			tr, _ := trace.FromContext(r.Context())
 			tr.Printf("statically serving %q", path)
 			http.ServeFile(w, r, path)
 		}),
-	)
+	))
 }
 
 func makeCGI(from string, to url.URL, conf *config.HTTP) http.Handler {
@@ -240,7 +262,7 @@ func makeCGI(from string, to url.URL, conf *config.HTTP) http.Handler {
 	path := pathOrOpaque(to)
 	args := queryToArgs(to.RawQuery)
 
-	return WithLogging("http:cgi",
+	return WithTrace("http:cgi", WithLogging(
 		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			tr, _ := trace.FromContext(r.Context())
 			tr.Debugf("exec %q %q", path, args)
@@ -253,7 +275,7 @@ func makeCGI(from string, to url.URL, conf *config.HTTP) http.Handler {
 			}
 			h.ServeHTTP(w, r)
 		}),
-	)
+	))
 }
 
 func queryToArgs(query string) []string {
@@ -277,7 +299,7 @@ func queryToArgs(query string) []string {
 func makeRedirect(from string, to url.URL, conf *config.HTTP) http.Handler {
 	from = stripDomain(from)
 
-	return WithLogging("http:redirect",
+	return WithTrace("http:redirect", WithLogging(
 		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			tr, _ := trace.FromContext(r.Context())
 			target := to
@@ -287,7 +309,7 @@ func makeRedirect(from string, to url.URL, conf *config.HTTP) http.Handler {
 
 			http.Redirect(w, r, target.String(), http.StatusTemporaryRedirect)
 		}),
-	)
+	))
 }
 
 type loggingTransport struct{}
@@ -372,19 +394,32 @@ func (w *statusWriter) Write(b []byte) (int, error) {
 	return n, err
 }
 
-func WithLogging(name string, parent http.Handler) http.Handler {
+func WithTrace(name string, parent http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		tr := trace.New(name, r.Host+r.URL.String())
-		defer tr.Finish()
+		tr, ok := trace.FromContext(r.Context())
+		if !ok {
+			tr = trace.New(name, r.Host+r.URL.String())
+			defer tr.Finish()
+
+			// Associate the trace with this request.
+			r = r.WithContext(trace.NewContext(r.Context(), tr))
 
-		// Associate the trace with this request.
-		r = r.WithContext(trace.NewContext(r.Context(), tr))
+			// Log the request on creation.
+			tr.Printf("%s %s %s %s %s",
+				r.RemoteAddr, r.Proto, r.Method, r.Host, r.URL.String())
+		}
+
+		parent.ServeHTTP(w, r)
+	})
+}
+
+func WithLogging(parent http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		tr, _ := trace.FromContext(r.Context())
 
 		// Wrap the writer so we can get output information.
 		sw := statusWriter{ResponseWriter: w}
 
-		tr.Printf("%s %s %s %s %s",
-			r.RemoteAddr, r.Proto, r.Method, r.Host, r.URL.String())
 		parent.ServeHTTP(&sw, r)
 		tr.Printf("%d %s", sw.status, http.StatusText(sw.status))
 		tr.Printf("%d bytes", sw.length)
diff --git a/test/01-be.yaml b/test/01-be.yaml
index 5b12a06..09d10ef 100644
--- a/test/01-be.yaml
+++ b/test/01-be.yaml
@@ -5,11 +5,12 @@ http:
   ":8450":
     dir:
       "/dir/": "testdata/dir"
+      "/authdir/": "testdata/dir"
     static:
       "/file": "testdata/file"
       "/file/second": "testdata/dir/ñaca"
     cgi:
-      "/cgi": "testdata/cgi.sh?param 1;param 2"
+      "/cgi/": "testdata/cgi.sh?param 1;param 2"
 
     diropts:
       "/dir/":
@@ -19,3 +20,7 @@ http:
           "/withoutindex/": false
         exclude: ["/ignored\\..*"]
 
+    auth:
+      "/authdir/ñaca": "testdata/authdb.yaml"
+      "/authdir/withoutindex/": "testdata/authdb.yaml"
+
diff --git a/test/01-fe.yaml b/test/01-fe.yaml
index 7c4fd69..3aeb1d3 100644
--- a/test/01-fe.yaml
+++ b/test/01-fe.yaml
@@ -3,6 +3,7 @@ control_addr: "127.0.0.1:8440"
 
 _proxy: &proxyroutes
   "/dir/": "http://localhost:8450/dir/"
+  "/authdir/": "http://localhost:8450/authdir/"
   "/file": "http://localhost:8450/file"
   "/cgi/": "http://localhost:8450/cgi/"
   "/bad/unreacheable": "http://localhost:1/"
diff --git a/test/test.sh b/test/test.sh
index 1ae0321..4e19450 100755
--- a/test/test.sh
+++ b/test/test.sh
@@ -108,6 +108,7 @@ for base in \
 	http://localhost:8441 \
 	https://localhost:8442 ;
 do
+	echo "### Common tests for $base"
 	exp $base/file -body "ñaca\n"
 
 	exp $base/dir -status 301 -redir /dir/
@@ -144,8 +145,43 @@ do
 	# Interesting case because neither has a trailing "/", so check that
 	# the striping is done correctly.
 	exp $base/file/ -status 404
+
+	# Files in authdir/; only some are covered by auth.
+	exp $base/authdir/hola -body 'hola marola\n'
+	exp $base/authdir/ñaca -status 401
+	exp $base/authdir/withoutindex -status 301
+	exp $base/authdir/withoutindex/ -status 401
+	exp $base/authdir/withoutindex/chau -status 401
+done
+
+# Good auth.
+for base in \
+	http://oneuser:onepass@localhost:8441 \
+	https://twouser:twopass@localhost:8442 ;
+do
+	echo "### Good auth for $base"
+	exp $base/authdir/hola -body 'hola marola\n'
+	exp $base/authdir/ñaca -body "tracañaca\n"
+	exp $base/authdir/withoutindex -status 301
+	exp $base/authdir/withoutindex/ -status 404
+	exp $base/authdir/withoutindex/chau -body 'chau\n'
 done
 
+# Bad auth.
+for base in \
+	http://oneuser:bad@localhost:8441 \
+	http://twouser:bad@localhost:8441 ;
+do
+	echo "### Bad auth for $base"
+	exp $base/authdir/hola -body 'hola marola\n'
+	exp $base/authdir/ñaca -status 401
+	exp $base/authdir/withoutindex -status 301
+	exp $base/authdir/withoutindex/ -status 401
+	exp $base/authdir/withoutindex/chau -status 401
+done
+
+echo "### Miscellaneous"
+
 # HTTPS-only tests.
 exp https://localhost:8442/dar/ -bodyre '<a href="%C3%B1aca">ñaca</a>'
 
@@ -153,8 +189,10 @@ exp https://localhost:8442/dar/ -bodyre '<a href="%C3%B1aca">ñaca</a>'
 # misconfiguration.
 exp http://localhost:8450/file/second -body "tracañaca\n"
 
-# Raw proxying.
+
+echo "### Raw proxying"
 exp http://localhost:8445/file -body "ñaca\n"
 exp https://localhost:8446/file -body "ñaca\n"
 
+echo "## Success"
 snoop