author | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-06-06 02:57:06 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-06-06 02:57:06 UTC |
parent | e1a6d1a9f867bfa86c173e4a5c08553c66cb60a2 |
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