]> Sergey Matveev's repositories - godlighty.git/commitdiff
Authentication and authorization
authorSergey Matveev <stargrave@stargrave.org>
Mon, 4 Oct 2021 12:40:50 +0000 (15:40 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Mon, 4 Oct 2021 13:49:28 +0000 (16:49 +0300)
auth.go [new file with mode: 0644]
cfg.go
cmd/godlighty/main.go
doc/index.texi
handler.go
media.go
tls.go

diff --git a/auth.go b/auth.go
new file mode 100644 (file)
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 <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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 1aa243a8c901064442cfcca2f2ba7978d9d79848..9bc1206076bd399dae9d8c752f3f0a28f6b87a24 100644 (file)
--- 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
index 22f0c22131a4e235315d8998c9f1069df854379c..6c7114511e455f2134eca5f797ea0716ef73fcd0 100644 (file)
@@ -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)
        }
index 3923ab9c8b6edd53c0d7031aa7d8238bcdb03c16..5be423428f06edb29e35a9f06530201bd3142ac3 100644 (file)
@@ -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}.
index 3aacc29f6b74d502616308481e159445bdafc95b..df16c38d5e11bf05092f7c6b1bcaf606be8a87c7 100644 (file)
@@ -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"),
        )
 }
 
index f36db17c27870b9c6a2f716545c6ffba54977167..542b073eaa4d22842c35b806605074ffc65511b1 100644 (file)
--- 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 9ac600c364e25a4ed1a9f853d784d3fe9f9a28c4..88f1dc38ff2d458477a77f068c8655aaadb38d7c 100644 (file)
--- 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,
        }
 }