zhangzengfei
2024-03-26 6d9c1b55394bccc0666f957825338d067002f627
添加http basic 认证代码
1个文件已删除
6个文件已添加
932 ■■■■■ 已修改文件
pkg/auth 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
pkg/auth/auth.go 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pkg/auth/basic.go 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pkg/auth/digest.go 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pkg/auth/md5crypt.go 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pkg/auth/misc.go 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pkg/auth/users.go 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pkg/auth
File was deleted
pkg/auth/auth.go
New file
@@ -0,0 +1,110 @@
// Package auth is an implementation of HTTP Basic and HTTP Digest authentication.
package auth
import (
    "context"
    "net/http"
)
// AuthenticatedRequest is passed to AuthenticatedHandlerFunc instead
// of *http.Request.
type AuthenticatedRequest struct {
    http.Request
    // Username is the authenticated user name. Current API implies that
    // Username is never empty, which means that authentication is
    // always done before calling the request handler.
    Username string
}
// AuthenticatedHandlerFunc is like http.HandlerFunc, but takes
// AuthenticatedRequest instead of http.Request
type AuthenticatedHandlerFunc func(http.ResponseWriter, *AuthenticatedRequest)
// Authenticator wraps an AuthenticatedHandlerFunc with
// authentication-checking code.
//
// Typical Authenticator usage is something like:
//
//   authenticator := SomeAuthenticator(...)
//   http.HandleFunc("/", authenticator(my_handler))
//
// Authenticator wrapper checks the user authentication and calls the
// wrapped function only after authentication has succeeded. Otherwise,
// it returns a handler which initiates the authentication procedure.
type Authenticator func(AuthenticatedHandlerFunc) http.HandlerFunc
// Info contains authentication information for the request.
type Info struct {
    // Authenticated is set to true when request was authenticated
    // successfully, i.e. username and password passed in request did
    // pass the check.
    Authenticated bool
    // Username contains a user name passed in the request when
    // Authenticated is true. It's value is undefined if Authenticated
    // is false.
    Username string
    // ResponseHeaders contains extra headers that must be set by server
    // when sending back HTTP response.
    ResponseHeaders http.Header
}
// UpdateHeaders updates headers with this Info's ResponseHeaders. It is
// safe to call this function on nil Info.
func (i *Info) UpdateHeaders(headers http.Header) {
    if i == nil {
        return
    }
    for k, values := range i.ResponseHeaders {
        for _, v := range values {
            headers.Add(k, v)
        }
    }
}
type key int // used for context keys
var infoKey key
// AuthenticatorInterface is the interface implemented by BasicAuth
// and DigestAuth authenticators.
//
// Deprecated: this interface is not coherent. New code should define
// and use your own interfaces with a required subset of authenticator
// methods.
type AuthenticatorInterface interface {
    // NewContext returns a new context carrying authentication
    // information extracted from the request.
    NewContext(ctx context.Context, r *http.Request) context.Context
    // Wrap returns an http.HandlerFunc which wraps
    // AuthenticatedHandlerFunc with this authenticator's
    // authentication checks.
    Wrap(AuthenticatedHandlerFunc) http.HandlerFunc
}
// FromContext returns authentication information from the context or
// nil if no such information present.
func FromContext(ctx context.Context) *Info {
    info, ok := ctx.Value(infoKey).(*Info)
    if !ok {
        return nil
    }
    return info
}
// AuthUsernameHeader is the header set by JustCheck functions. It
// contains an authenticated username (if authentication was
// successful).
const AuthUsernameHeader = "X-Authenticated-Username"
// JustCheck returns a new http.HandlerFunc, which requires
// authenticator to successfully authenticate a user before calling
// wrapped http.HandlerFunc.
func JustCheck(auth AuthenticatorInterface, wrapped http.HandlerFunc) http.HandlerFunc {
    return auth.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) {
        ar.Header.Set(AuthUsernameHeader, ar.Username)
        wrapped(w, &ar.Request)
    })
}
pkg/auth/basic.go
New file
@@ -0,0 +1,161 @@
package auth
import (
    "bytes"
    "context"
    "crypto/sha1"
    "crypto/subtle"
    "encoding/base64"
    "errors"
    "net/http"
    "strings"
    "golang.org/x/crypto/bcrypt"
)
type compareFunc func(hashedPassword, password []byte) error
var (
    errMismatchedHashAndPassword = errors.New("mismatched hash and password")
    compareFuncs = []struct {
        prefix  string
        compare compareFunc
    }{
        {"", compareMD5HashAndPassword}, // default compareFunc
        {"{SHA}", compareShaHashAndPassword},
        // Bcrypt is complicated. According to crypt(3) from
        // crypt_blowfish version 1.3 (fetched from
        // http://www.openwall.com/crypt/crypt_blowfish-1.3.tar.gz), there
        // are three different has prefixes: "$2a$", used by versions up
        // to 1.0.4, and "$2x$" and "$2y$", used in all later
        // versions. "$2a$" has a known bug, "$2x$" was added as a
        // migration path for systems with "$2a$" prefix and still has a
        // bug, and only "$2y$" should be used by modern systems. The bug
        // has something to do with handling of 8-bit characters. Since
        // both "$2a$" and "$2x$" are deprecated, we are handling them the
        // same way as "$2y$", which will yield correct results for 7-bit
        // character passwords, but is wrong for 8-bit character
        // passwords. You have to upgrade to "$2y$" if you want sant 8-bit
        // character password support with bcrypt. To add to the mess,
        // OpenBSD 5.5. introduced "$2b$" prefix, which behaves exactly
        // like "$2y$" according to the same source.
        {"$2a$", bcrypt.CompareHashAndPassword},
        {"$2b$", bcrypt.CompareHashAndPassword},
        {"$2x$", bcrypt.CompareHashAndPassword},
        {"$2y$", bcrypt.CompareHashAndPassword},
    }
)
// BasicAuth is an authenticator implementation for 'Basic' HTTP
// Authentication scheme (RFC 7617).
type BasicAuth struct {
    Realm   string
    Secrets SecretProvider
    // Headers used by authenticator. Set to ProxyHeaders to use with
    // proxy server. When nil, NormalHeaders are used.
    Headers *Headers
}
// check that BasicAuth implements AuthenticatorInterface
var _ = (AuthenticatorInterface)((*BasicAuth)(nil))
// CheckAuth checks the username/password combination from the
// request. Returns either an empty string (authentication failed) or
// the name of the authenticated user.
func (a *BasicAuth) CheckAuth(r *http.Request) string {
    user, password, ok := r.BasicAuth()
    if !ok {
        return ""
    }
    secret := a.Secrets(user, a.Realm)
    if secret == "" {
        return ""
    }
    if !CheckSecret(password, secret) {
        return ""
    }
    return user
}
// CheckSecret returns true if the password matches the encrypted
// secret.
func CheckSecret(password, secret string) bool {
    compare := compareFuncs[0].compare
    for _, cmp := range compareFuncs[1:] {
        if strings.HasPrefix(secret, cmp.prefix) {
            compare = cmp.compare
            break
        }
    }
    return compare([]byte(secret), []byte(password)) == nil
}
func compareShaHashAndPassword(hashedPassword, password []byte) error {
    d := sha1.New()
    d.Write(password)
    if subtle.ConstantTimeCompare(hashedPassword[5:], []byte(base64.StdEncoding.EncodeToString(d.Sum(nil)))) != 1 {
        return errMismatchedHashAndPassword
    }
    return nil
}
func compareMD5HashAndPassword(hashedPassword, password []byte) error {
    parts := bytes.SplitN(hashedPassword, []byte("$"), 4)
    if len(parts) != 4 {
        return errMismatchedHashAndPassword
    }
    magic := []byte("$" + string(parts[1]) + "$")
    salt := parts[2]
    if subtle.ConstantTimeCompare(hashedPassword, MD5Crypt(password, salt, magic)) != 1 {
        return errMismatchedHashAndPassword
    }
    return nil
}
// RequireAuth is an http.HandlerFunc for BasicAuth which initiates
// the authentication process (or requires reauthentication).
func (a *BasicAuth) RequireAuth(w http.ResponseWriter, r *http.Request) {
    w.Header().Set(contentType, a.Headers.V().UnauthContentType)
    w.Header().Set(a.Headers.V().Authenticate, `Basic realm="`+a.Realm+`"`)
    w.WriteHeader(a.Headers.V().UnauthCode)
    w.Write([]byte(a.Headers.V().UnauthResponse))
}
// Wrap returns an http.HandlerFunc, which wraps
// AuthenticatedHandlerFunc with this BasicAuth authenticator's
// authentication checks. Once the request contains valid credentials,
// it calls wrapped AuthenticatedHandlerFunc.
//
// Deprecated: new code should use NewContext instead.
func (a *BasicAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if username := a.CheckAuth(r); username == "" {
            a.RequireAuth(w, r)
        } else {
            ar := &AuthenticatedRequest{Request: *r, Username: username}
            wrapped(w, ar)
        }
    }
}
// NewContext returns a context carrying authentication information for the request.
func (a *BasicAuth) NewContext(ctx context.Context, r *http.Request) context.Context {
    info := &Info{Username: a.CheckAuth(r), ResponseHeaders: make(http.Header)}
    info.Authenticated = (info.Username != "")
    if !info.Authenticated {
        info.ResponseHeaders.Set(a.Headers.V().Authenticate, `Basic realm="`+a.Realm+`"`)
    }
    return context.WithValue(ctx, infoKey, info)
}
// NewBasicAuthenticator returns a BasicAuth initialized with provided
// realm and secrets.
//
// Deprecated: new code should construct BasicAuth values directly.
func NewBasicAuthenticator(realm string, secrets SecretProvider) *BasicAuth {
    return &BasicAuth{Realm: realm, Secrets: secrets}
}
pkg/auth/digest.go
New file
@@ -0,0 +1,289 @@
package auth
import (
    "context"
    "crypto/subtle"
    "fmt"
    "net/http"
    "net/url"
    "sort"
    "strconv"
    "strings"
    "sync"
    "time"
)
type digestClient struct {
    nc       uint64
    lastSeen int64
}
// DigestAuth is an authenticator implementation for 'Digest' HTTP Authentication scheme (RFC 7616).
//
// Note: this implementation was written following now deprecated RFC
// 2617, and supports only MD5 algorithm.
//
// TODO: Add support for SHA-256 and SHA-512/256 algorithms.
type DigestAuth struct {
    Realm            string
    Opaque           string
    Secrets          SecretProvider
    PlainTextSecrets bool
    IgnoreNonceCount bool
    // Headers used by authenticator. Set to ProxyHeaders to use with
    // proxy server. When nil, NormalHeaders are used.
    Headers *Headers
    /*
       Approximate size of Client's Cache. When actual number of
       tracked client nonces exceeds
       ClientCacheSize+ClientCacheTolerance, ClientCacheTolerance*2
       older entries are purged.
    */
    ClientCacheSize      int
    ClientCacheTolerance int
    clients map[string]*digestClient
    mutex   sync.RWMutex
}
// check that DigestAuth implements AuthenticatorInterface
var _ = (AuthenticatorInterface)((*DigestAuth)(nil))
type digestCacheEntry struct {
    nonce    string
    lastSeen int64
}
type digestCache []digestCacheEntry
func (c digestCache) Less(i, j int) bool {
    return c[i].lastSeen < c[j].lastSeen
}
func (c digestCache) Len() int {
    return len(c)
}
func (c digestCache) Swap(i, j int) {
    c[i], c[j] = c[j], c[i]
}
// Purge removes count oldest entries from DigestAuth.clients
func (da *DigestAuth) Purge(count int) {
    da.mutex.Lock()
    da.purgeLocked(count)
    da.mutex.Unlock()
}
func (da *DigestAuth) purgeLocked(count int) {
    entries := make([]digestCacheEntry, 0, len(da.clients))
    for nonce, client := range da.clients {
        entries = append(entries, digestCacheEntry{nonce, client.lastSeen})
    }
    cache := digestCache(entries)
    sort.Sort(cache)
    for _, client := range cache[:count] {
        delete(da.clients, client.nonce)
    }
}
// RequireAuth is an http.HandlerFunc which initiates the
// authentication process (or requires reauthentication).
func (da *DigestAuth) RequireAuth(w http.ResponseWriter, r *http.Request) {
    da.mutex.RLock()
    clientsLen := len(da.clients)
    da.mutex.RUnlock()
    if clientsLen > da.ClientCacheSize+da.ClientCacheTolerance {
        da.Purge(da.ClientCacheTolerance * 2)
    }
    nonce := RandomKey()
    da.mutex.Lock()
    da.clients[nonce] = &digestClient{nc: 0, lastSeen: time.Now().UnixNano()}
    da.mutex.Unlock()
    da.mutex.RLock()
    w.Header().Set(contentType, da.Headers.V().UnauthContentType)
    w.Header().Set(da.Headers.V().Authenticate,
        fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm=MD5, qop="auth"`,
            da.Realm, nonce, da.Opaque))
    w.WriteHeader(da.Headers.V().UnauthCode)
    w.Write([]byte(da.Headers.V().UnauthResponse))
    da.mutex.RUnlock()
}
// DigestAuthParams parses Authorization header from the
// http.Request. Returns a map of auth parameters or nil if the header
// is not a valid parsable Digest auth header.
func DigestAuthParams(authorization string) map[string]string {
    s := strings.SplitN(authorization, " ", 2)
    if len(s) != 2 || s[0] != "Digest" {
        return nil
    }
    return ParsePairs(s[1])
}
// CheckAuth checks whether the request contains valid authentication
// data. Returns a pair of username, authinfo, where username is the
// name of the authenticated user or an empty string and authinfo is
// the contents for the optional Authentication-Info response header.
func (da *DigestAuth) CheckAuth(r *http.Request) (username string, authinfo *string) {
    da.mutex.RLock()
    defer da.mutex.RUnlock()
    username = ""
    authinfo = nil
    auth := DigestAuthParams(r.Header.Get(da.Headers.V().Authorization))
    if auth == nil {
        return "", nil
    }
    // RFC2617 Section 3.2.1 specifies that unset value of algorithm in
    // WWW-Authenticate Response header should be treated as
    // "MD5". According to section 3.2.2 the "algorithm" value in
    // subsequent Request Authorization header must be set to whatever
    // was supplied in the WWW-Authenticate Response header. This
    // implementation always returns an algorithm in WWW-Authenticate
    // header, however there seems to be broken clients in the wild
    // which do not set the algorithm. Assume the unset algorithm in
    // Authorization header to be equal to MD5.
    if _, ok := auth["algorithm"]; !ok {
        auth["algorithm"] = "MD5"
    }
    if da.Opaque != auth["opaque"] || auth["algorithm"] != "MD5" || auth["qop"] != "auth" {
        return "", nil
    }
    // Check if the requested URI matches auth header
    if r.RequestURI != auth["uri"] {
        // We allow auth["uri"] to be a full path prefix of request-uri
        // for some reason lost in history, which is probably wrong, but
        // used to be like that for quite some time
        // (https://tools.ietf.org/html/rfc2617#section-3.2.2 explicitly
        // says that auth["uri"] is the request-uri).
        //
        // TODO: make an option to allow only strict checking.
        switch u, err := url.Parse(auth["uri"]); {
        case err != nil:
            return "", nil
        case r.URL == nil:
            return "", nil
        case len(u.Path) > len(r.URL.Path):
            return "", nil
        case !strings.HasPrefix(r.URL.Path, u.Path):
            return "", nil
        }
    }
    HA1 := da.Secrets(auth["username"], da.Realm)
    if da.PlainTextSecrets {
        HA1 = H(auth["username"] + ":" + da.Realm + ":" + HA1)
    }
    HA2 := H(r.Method + ":" + auth["uri"])
    KD := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], HA2}, ":"))
    if subtle.ConstantTimeCompare([]byte(KD), []byte(auth["response"])) != 1 {
        return "", nil
    }
    // At this point crypto checks are completed and validated.
    // Now check if the session is valid.
    nc, err := strconv.ParseUint(auth["nc"], 16, 64)
    if err != nil {
        return "", nil
    }
    client, ok := da.clients[auth["nonce"]]
    if !ok {
        return "", nil
    }
    if client.nc != 0 && client.nc >= nc && !da.IgnoreNonceCount {
        return "", nil
    }
    client.nc = nc
    client.lastSeen = time.Now().UnixNano()
    respHA2 := H(":" + auth["uri"])
    rspauth := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], respHA2}, ":"))
    info := fmt.Sprintf(`qop="auth", rspauth="%s", cnonce="%s", nc="%s"`, rspauth, auth["cnonce"], auth["nc"])
    return auth["username"], &info
}
// Default values for ClientCacheSize and ClientCacheTolerance for DigestAuth
const (
    DefaultClientCacheSize      = 1000
    DefaultClientCacheTolerance = 100
)
// Wrap returns an http.HandlerFunc wraps AuthenticatedHandlerFunc
// with this DigestAuth authentication checks. Once the request
// contains valid credentials, it calls wrapped
// AuthenticatedHandlerFunc.
//
// Deprecated: new code should use NewContext instead.
func (da *DigestAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if username, authinfo := da.CheckAuth(r); username == "" {
            da.RequireAuth(w, r)
        } else {
            ar := &AuthenticatedRequest{Request: *r, Username: username}
            if authinfo != nil {
                w.Header().Set(da.Headers.V().AuthInfo, *authinfo)
            }
            wrapped(w, ar)
        }
    }
}
// JustCheck returns a new http.HandlerFunc, which requires
// DigestAuth to successfully authenticate a user before calling
// wrapped http.HandlerFunc.
//
// Authenticated Username is passed as an extra
// X-Authenticated-Username header to the wrapped HandlerFunc.
func (da *DigestAuth) JustCheck(wrapped http.HandlerFunc) http.HandlerFunc {
    return da.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) {
        ar.Header.Set(AuthUsernameHeader, ar.Username)
        wrapped(w, &ar.Request)
    })
}
// NewContext returns a context carrying authentication information for the request.
func (da *DigestAuth) NewContext(ctx context.Context, r *http.Request) context.Context {
    username, authinfo := da.CheckAuth(r)
    da.mutex.Lock()
    defer da.mutex.Unlock()
    info := &Info{Username: username, ResponseHeaders: make(http.Header)}
    if username != "" {
        info.Authenticated = true
        info.ResponseHeaders.Set(da.Headers.V().AuthInfo, *authinfo)
    } else {
        // return back digest WWW-Authenticate header
        if len(da.clients) > da.ClientCacheSize+da.ClientCacheTolerance {
            da.purgeLocked(da.ClientCacheTolerance * 2)
        }
        nonce := RandomKey()
        da.clients[nonce] = &digestClient{nc: 0, lastSeen: time.Now().UnixNano()}
        info.ResponseHeaders.Set(da.Headers.V().Authenticate,
            fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm=MD5, qop="auth"`,
                da.Realm, nonce, da.Opaque))
    }
    return context.WithValue(ctx, infoKey, info)
}
// NewDigestAuthenticator generates a new DigestAuth object
func NewDigestAuthenticator(realm string, secrets SecretProvider) *DigestAuth {
    da := &DigestAuth{
        Opaque:               RandomKey(),
        Realm:                realm,
        Secrets:              secrets,
        PlainTextSecrets:     true,
        ClientCacheSize:      DefaultClientCacheSize,
        ClientCacheTolerance: DefaultClientCacheTolerance,
        clients:              map[string]*digestClient{}}
    return da
}
pkg/auth/md5crypt.go
New file
@@ -0,0 +1,73 @@
package auth
import "crypto/md5"
const itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var md5CryptSwaps = [16]int{12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11}
// MD5Crypt is the MD5 password crypt implementation.
func MD5Crypt(password, salt, magic []byte) []byte {
    d := md5.New()
    d.Write(password)
    d.Write(magic)
    d.Write(salt)
    d2 := md5.New()
    d2.Write(password)
    d2.Write(salt)
    d2.Write(password)
    for i, mixin := 0, d2.Sum(nil); i < len(password); i++ {
        d.Write([]byte{mixin[i%16]})
    }
    for i := len(password); i != 0; i >>= 1 {
        if i&1 == 0 {
            d.Write([]byte{password[0]})
        } else {
            d.Write([]byte{0})
        }
    }
    final := d.Sum(nil)
    for i := 0; i < 1000; i++ {
        d2 := md5.New()
        if i&1 == 0 {
            d2.Write(final)
        } else {
            d2.Write(password)
        }
        if i%3 != 0 {
            d2.Write(salt)
        }
        if i%7 != 0 {
            d2.Write(password)
        }
        if i&1 == 0 {
            d2.Write(password)
        } else {
            d2.Write(final)
        }
        final = d2.Sum(nil)
    }
    result := make([]byte, 0, 22)
    v := uint(0)
    bits := uint(0)
    for _, i := range md5CryptSwaps {
        v |= (uint(final[i]) << bits)
        for bits = bits + 8; bits > 6; bits -= 6 {
            result = append(result, itoa64[v&0x3f])
            v >>= 6
        }
    }
    result = append(result, itoa64[v&0x3f])
    return append(append(append(magic, salt...), '$'), result...)
}
pkg/auth/misc.go
New file
@@ -0,0 +1,146 @@
package auth
import (
    "bytes"
    "crypto/md5"
    "crypto/rand"
    "encoding/base64"
    "fmt"
    "net/http"
    "strings"
)
// RandomKey returns a random 16-byte base64 alphabet string
func RandomKey() string {
    k := make([]byte, 12)
    for bytes := 0; bytes < len(k); {
        n, err := rand.Read(k[bytes:])
        if err != nil {
            panic("rand.Read() failed")
        }
        bytes += n
    }
    return base64.StdEncoding.EncodeToString(k)
}
// H function for MD5 algorithm (returns a lower-case hex MD5 digest)
func H(data string) string {
    digest := md5.New()
    digest.Write([]byte(data))
    return fmt.Sprintf("%x", digest.Sum(nil))
}
// ParseList parses a comma-separated list of values as described by
// RFC 2068 and returns list elements.
//
// Lifted from https://code.google.com/p/gorilla/source/browse/http/parser/parser.go
// which was ported from urllib2.parse_http_list, from the Python
// standard library.
func ParseList(value string) []string {
    var list []string
    var escape, quote bool
    b := new(bytes.Buffer)
    for _, r := range value {
        switch {
        case escape:
            b.WriteRune(r)
            escape = false
        case quote:
            if r == '\\' {
                escape = true
            } else {
                if r == '"' {
                    quote = false
                }
                b.WriteRune(r)
            }
        case r == ',':
            list = append(list, strings.TrimSpace(b.String()))
            b.Reset()
        case r == '"':
            quote = true
            b.WriteRune(r)
        default:
            b.WriteRune(r)
        }
    }
    // Append last part.
    if s := b.String(); s != "" {
        list = append(list, strings.TrimSpace(s))
    }
    return list
}
// ParsePairs extracts key/value pairs from a comma-separated list of
// values as described by RFC 2068 and returns a map[key]value. The
// resulting values are unquoted. If a list element doesn't contain a
// "=", the key is the element itself and the value is an empty
// string.
//
// Lifted from https://code.google.com/p/gorilla/source/browse/http/parser/parser.go
func ParsePairs(value string) map[string]string {
    m := make(map[string]string)
    for _, pair := range ParseList(strings.TrimSpace(value)) {
        switch i := strings.Index(pair, "="); {
        case i < 0:
            // No '=' in pair, treat whole string as a 'key'.
            m[pair] = ""
        case i == len(pair)-1:
            // Malformed pair ('key=' with no value), keep key with empty value.
            m[pair[:i]] = ""
        default:
            v := pair[i+1:]
            if v[0] == '"' && v[len(v)-1] == '"' {
                // Unquote it.
                v = v[1 : len(v)-1]
            }
            m[pair[:i]] = v
        }
    }
    return m
}
// Headers contains header and error codes used by authenticator.
type Headers struct {
    Authenticate      string // WWW-Authenticate
    Authorization     string // Authorization
    AuthInfo          string // Authentication-Info
    UnauthCode        int    // 401
    UnauthContentType string // text/plain
    UnauthResponse    string // Unauthorized.
}
// V returns NormalHeaders when h is nil, or h otherwise. Allows to
// use uninitialized *Headers values in structs.
func (h *Headers) V() *Headers {
    if h == nil {
        return NormalHeaders
    }
    return h
}
var (
    // NormalHeaders are the regular Headers used by an HTTP Server for
    // request authentication.
    NormalHeaders = &Headers{
        Authenticate:      "WWW-Authenticate",
        Authorization:     "Authorization",
        AuthInfo:          "Authentication-Info",
        UnauthCode:        http.StatusUnauthorized,
        UnauthContentType: "text/plain",
        UnauthResponse:    fmt.Sprintf("%d %s\n", http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)),
    }
    // ProxyHeaders are Headers used by an HTTP Proxy server for proxy
    // access authentication.
    ProxyHeaders = &Headers{
        Authenticate:      "Proxy-Authenticate",
        Authorization:     "Proxy-Authorization",
        AuthInfo:          "Proxy-Authentication-Info",
        UnauthCode:        http.StatusProxyAuthRequired,
        UnauthContentType: "text/plain",
        UnauthResponse:    fmt.Sprintf("%d %s\n", http.StatusProxyAuthRequired, http.StatusText(http.StatusProxyAuthRequired)),
    }
)
const contentType = "Content-Type"
pkg/auth/users.go
New file
@@ -0,0 +1,151 @@
package auth
import (
    "encoding/csv"
    "os"
    "sync"
)
// SecretProvider is used by authenticators. Takes user name and realm
// as an argument, returns secret required for authentication (HA1 for
// digest authentication, properly encrypted password for basic).
//
// Returning an empty string means failing the authentication.
type SecretProvider func(user, realm string) string
// File handles automatic file reloading on changes.
type File struct {
    Path string
    Info os.FileInfo
    /* must be set in inherited types during initialization */
    Reload func()
    mu     sync.Mutex
}
// ReloadIfNeeded checks file Stat and calls Reload() if any changes
// were detected. File mutex is Locked for the duration of Reload()
// call.
//
// This function will panic() if Stat fails.
func (f *File) ReloadIfNeeded() {
    info, err := os.Stat(f.Path)
    if err != nil {
        panic(err)
    }
    f.mu.Lock()
    defer f.mu.Unlock()
    if f.Info == nil || f.Info.ModTime() != info.ModTime() {
        f.Info = info
        f.Reload()
    }
}
// HtdigestFile is a File holding htdigest authentication data.
type HtdigestFile struct {
    // File is used for automatic reloading of the authentication data.
    File
    // Users is a map of realms to users to HA1 digests.
    Users map[string]map[string]string
    mu    sync.RWMutex
}
func reloadHTDigest(hf *HtdigestFile) {
    r, err := os.Open(hf.Path)
    if err != nil {
        panic(err)
    }
    reader := csv.NewReader(r)
    reader.Comma = ':'
    reader.Comment = '#'
    reader.TrimLeadingSpace = true
    records, err := reader.ReadAll()
    if err != nil {
        panic(err)
    }
    hf.mu.Lock()
    defer hf.mu.Unlock()
    hf.Users = make(map[string]map[string]string)
    for _, record := range records {
        _, exists := hf.Users[record[1]]
        if !exists {
            hf.Users[record[1]] = make(map[string]string)
        }
        hf.Users[record[1]][record[0]] = record[2]
    }
}
// HtdigestFileProvider is a SecretProvider implementation based on
// htdigest-formated files. It will automatically reload htdigest file
// on changes. It panics on syntax errors in htdigest files.
func HtdigestFileProvider(filename string) SecretProvider {
    hf := &HtdigestFile{File: File{Path: filename}}
    hf.Reload = func() { reloadHTDigest(hf) }
    return func(user, realm string) string {
        hf.ReloadIfNeeded()
        hf.mu.RLock()
        defer hf.mu.RUnlock()
        _, exists := hf.Users[realm]
        if !exists {
            return ""
        }
        digest, exists := hf.Users[realm][user]
        if !exists {
            return ""
        }
        return digest
    }
}
// HtpasswdFile is a File holding basic authentication data.
type HtpasswdFile struct {
    // File is used for automatic reloading of the authentication data.
    File
    // Users is a map of users to their secrets (salted encrypted
    // passwords).
    Users map[string]string
    mu    sync.RWMutex
}
func reloadHTPasswd(h *HtpasswdFile) {
    r, err := os.Open(h.Path)
    if err != nil {
        panic(err)
    }
    reader := csv.NewReader(r)
    reader.Comma = ':'
    reader.Comment = '#'
    reader.TrimLeadingSpace = true
    records, err := reader.ReadAll()
    if err != nil {
        panic(err)
    }
    h.mu.Lock()
    defer h.mu.Unlock()
    h.Users = make(map[string]string)
    for _, record := range records {
        h.Users[record[0]] = record[1]
    }
}
// HtpasswdFileProvider is a SecretProvider implementation based on
// htpasswd-formated files. It will automatically reload htpasswd file
// on changes. It panics on syntax errors in htpasswd files. Realm
// argument of the SecretProvider is ignored.
func HtpasswdFileProvider(filename string) SecretProvider {
    h := &HtpasswdFile{File: File{Path: filename}}
    h.Reload = func() { reloadHTPasswd(h) }
    return func(user, realm string) string {
        h.ReloadIfNeeded()
        h.mu.RLock()
        password, exists := h.Users[user]
        h.mu.RUnlock()
        if !exists {
            return ""
        }
        return password
    }
}