From 600eeaef572eaaaf5bfb320614091b10664c66f0 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Mon, 4 Oct 2021 15:40:50 +0300 Subject: [PATCH] Authentication and authorization --- auth.go | 63 +++++++++++++++++++++++++++++++++++++++++++ cfg.go | 9 +++++++ cmd/godlighty/main.go | 7 ++--- doc/index.texi | 2 ++ handler.go | 63 +++++++++++++++++++++++++++++++------------ media.go | 5 +++- tls.go | 54 ++++++++++++++++++++++++++++++++++++- 7 files changed, 179 insertions(+), 24 deletions(-) create mode 100644 auth.go diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..96b9c8e --- /dev/null +++ b/auth.go @@ -0,0 +1,63 @@ +/* +godlighty -- highly-customizable HTTP, HTTP/2, HTTPS server +Copyright (C) 2021 Sergey Matveev + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package godlighty + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +var Unauthorized = errors.New("failed authorization") + +func performAuth(w http.ResponseWriter, r *http.Request, cfg *AuthCfg) (string, error) { + username, password, ok := r.BasicAuth() + if !ok { + w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, cfg.Realm)) + w.WriteHeader(http.StatusUnauthorized) + return username, Unauthorized + } + data, err := ioutil.ReadFile(cfg.Passwords) + if err != nil { + return username, err + } + for _, line := range strings.Split(string(data), "\n") { + if len(line) == 0 || line[0] == '#' { + continue + } + cols := strings.Split(line, ":") + if len(cols) != 2 { + continue + } + if cols[0] == username { + d := sha256.Sum256([]byte(password)) + if hex.EncodeToString(d[:]) == cols[1] { + return username, nil + } else { + w.WriteHeader(http.StatusUnauthorized) + return username, Unauthorized + } + } + } + w.WriteHeader(http.StatusUnauthorized) + return username, Unauthorized +} diff --git a/cfg.go b/cfg.go index 1aa243a..9bc1206 100644 --- a/cfg.go +++ b/cfg.go @@ -25,6 +25,14 @@ type TLSCfg struct { Cert string Key string CACert string + + // Require client authentication + ClientCAs []string +} + +type AuthCfg struct { + Passwords string + Realm string } type Hook func(http.ResponseWriter, *http.Request) bool @@ -35,6 +43,7 @@ type HostCfg struct { DirList bool WebDAV bool Hooks []Hook + Auth *AuthCfg Indexes []string Readmes []string diff --git a/cmd/godlighty/main.go b/cmd/godlighty/main.go index 22f0c22..6c71145 100644 --- a/cmd/godlighty/main.go +++ b/cmd/godlighty/main.go @@ -101,11 +101,8 @@ func main() { }() var ll net.Listener if *doTLS { - tlsCfg := tls.Config{ - GetCertificate: godlighty.GetCertificate, - NextProtos: []string{"h2", "http/1.1"}, - } - ll = tls.NewListener(netutil.LimitListener(l, MaxConns), &tlsCfg) + tlsCfg := godlighty.NewTLSConfig() + ll = tls.NewListener(netutil.LimitListener(l, MaxConns), tlsCfg) } else { ll = netutil.LimitListener(l, MaxConns) } diff --git a/doc/index.texi b/doc/index.texi index 3923ab9..5be4234 100644 --- a/doc/index.texi +++ b/doc/index.texi @@ -40,6 +40,8 @@ handlers are fully used. @item Auto-generated directory listings and read-only @url{https://en.wikipedia.org/wiki/WebDAV, WebDAV} support. +@item Per-domain HTTP basic authorization and TLS client authentication. + @item If corresponding @file{.meta4} files are found, then @code{Link} header is generated automatically to that @url{http://www.metalinker.org/, Metalink}. diff --git a/handler.go b/handler.go index 3aacc29..df16c38 100644 --- a/handler.go +++ b/handler.go @@ -20,6 +20,7 @@ package godlighty import ( "bytes" "compress/gzip" + "errors" "fmt" "io/ioutil" "log" @@ -42,8 +43,6 @@ const ( ) var ( - CompressibleContentTypes = make(map[string]struct{}) - gzPool = sync.Pool{ New: func() interface{} { return gzip.NewWriter(ioutil.Discard) }, } @@ -82,19 +81,49 @@ func (h Handler) Handle( return } - for _, hook := range cfg.Hooks { - if done := hook(w, r); done { - return - } + var username string + var err error + if cfg.Auth != nil { + username, err = performAuth(w, r, cfg.Auth) + } + if username != "" { + username = "user:" + username + " " } - printErr := func(code int, err error) { - fmt.Printf("%s %s \"%s %s %s\" %d \"%s\" \"%s\"\n", + fmt.Printf("%s %s \"%s %s %s\" %d \"%s\" %s\"%s\"\n", r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto, code, err.Error(), - r.Header.Get("User-Agent"), + username, r.Header.Get("User-Agent"), ) } + switch err { + case nil: + break + case Unauthorized: + printErr(http.StatusUnauthorized, err) + return + default: + printErr(http.StatusInternalServerError, err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + if cfg.TLS != nil && len(cfg.TLS.ClientCAs) > 0 { + if r.TLS == nil { + err = errors.New("TLS client authentication required") + printErr(http.StatusForbidden, err) + http.Error(w, err.Error(), http.StatusForbidden) + return + } else { + username += r.TLS.PeerCertificates[0].Subject.String() + " " + } + } + + for _, hook := range cfg.Hooks { + if done := hook(w, r); done { + return + } + } if cfg.Root == "" { notFound() @@ -110,19 +139,19 @@ func (h Handler) Handle( } wc := &CountResponseWriter{ResponseWriter: w} dav.ServeHTTP(wc, r) - fmt.Printf("%s %s \"WebDAV %s\" %d %d \"%s\"\n", + fmt.Printf("%s %s \"WebDAV %s\" %d %d %s\"%s\"\n", r.RemoteAddr, host, r.URL.Path, wc.Status, wc.Size, - r.Header.Get("User-Agent"), + username, r.Header.Get("User-Agent"), ) return } if !(r.Method == "" || r.Method == http.MethodGet) { - fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n", + fmt.Printf("%s %s \"%s %s %s\" %d %s\"%s\"\n", r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto, http.StatusMethodNotAllowed, - r.Header.Get("User-Agent"), + username, r.Header.Get("User-Agent"), ) http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return @@ -256,18 +285,18 @@ IndexLookup: wr := wc.(*gzipResponseWriter) w.WriteHeader(wr.status) w.Write(bufCompressed.Bytes()) - fmt.Printf("%s %s \"%s %s %s\" %d %d \"%s\"\n", + fmt.Printf("%s %s \"%s %s %s\" %d %d %s\"%s\"\n", r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto, wr.status, size, - r.Header.Get("User-Agent"), + username, r.Header.Get("User-Agent"), ) return } wr := wc.(*CountResponseWriter) - fmt.Printf("%s %s \"%s %s %s\" %d %d \"%s\"\n", + fmt.Printf("%s %s \"%s %s %s\" %d %d %s\"%s\"\n", r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto, wr.Status, wr.Size, - r.Header.Get("User-Agent"), + username, r.Header.Get("User-Agent"), ) } diff --git a/media.go b/media.go index f36db17..542b073 100644 --- a/media.go +++ b/media.go @@ -21,7 +21,10 @@ import "path" const OctetStream = "application/octet-stream" -var ContentTypes = make(map[string]string) +var ( + ContentTypes = make(map[string]string) + CompressibleContentTypes = make(map[string]struct{}) +) func mediaType(fn string, overrides map[string]string) string { ext := path.Ext(fn) diff --git a/tls.go b/tls.go index 9ac600c..88f1dc3 100644 --- a/tls.go +++ b/tls.go @@ -19,6 +19,7 @@ package godlighty import ( "crypto/tls" + "crypto/x509" "encoding/pem" "errors" "fmt" @@ -26,7 +27,11 @@ import ( "log" ) -var HostToCertificate map[string]*tls.Certificate +var ( + NextProtos = []string{"h2", "http/1.1"} + HostToCertificate map[string]*tls.Certificate + HostClientAuth map[string]*x509.CertPool +) func GetCertificate(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { cert := HostToCertificate[chi.ServerName] @@ -36,8 +41,22 @@ func GetCertificate(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { return cert, nil } +func GetConfigForClient(chi *tls.ClientHelloInfo) (*tls.Config, error) { + pool := HostClientAuth[chi.ServerName] + if pool == nil { + return nil, nil + } + return &tls.Config{ + GetCertificate: GetCertificate, + NextProtos: NextProtos, + ClientCAs: pool, + ClientAuth: tls.RequireAndVerifyClientCert, + }, nil +} + func LoadCertificates() { HostToCertificate = make(map[string]*tls.Certificate, len(Hosts)) + HostClientAuth = make(map[string]*x509.CertPool) for host, cfg := range Hosts { if cfg.TLS == nil { continue @@ -61,5 +80,38 @@ func LoadCertificates() { cert.Certificate = append(cert.Certificate, block.Bytes) } HostToCertificate[host] = &cert + pool := x509.NewCertPool() + for _, p := range cfg.TLS.ClientCAs { + data, err := ioutil.ReadFile(p) + if err != nil { + log.Fatalln(err) + } + var block *pem.Block + for len(data) > 0 { + block, data = pem.Decode(data) + if block == nil { + log.Fatalln("can not decode PEM:", p) + } + if block.Type != "CERTIFICATE" { + continue + } + ca, err := x509.ParseCertificate(block.Bytes) + if err != nil { + log.Fatalln(err) + } + pool.AddCert(ca) + } + } + if len(pool.Subjects()) > 0 { + HostClientAuth[host] = pool + } + } +} + +func NewTLSConfig() *tls.Config { + return &tls.Config{ + NextProtos: NextProtos, + GetCertificate: GetCertificate, + GetConfigForClient: GetConfigForClient, } } -- 2.44.0