htpasswd autoreload

This commit is contained in:
Vladislav Yarmak 2022-09-06 21:43:56 +03:00
parent ac1ade4c69
commit 6adb7bacba
3 changed files with 82 additions and 2 deletions

64
auth.go
View File

@ -11,6 +11,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/tg123/go-htpasswd" "github.com/tg123/go-htpasswd"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -23,6 +24,7 @@ const EPOCH_EXPIRE = "Thu, 01 Jan 1970 00:00:01 GMT"
type Auth interface { type Auth interface {
Validate(wr http.ResponseWriter, req *http.Request) bool Validate(wr http.ResponseWriter, req *http.Request) bool
Stop()
} }
func NewAuth(paramstr string, logger *CondLogger) (Auth, error) { func NewAuth(paramstr string, logger *CondLogger) (Auth, error) {
@ -77,6 +79,7 @@ func NewStaticAuth(param_url *url.URL, logger *CondLogger) (*BasicAuth, error) {
hiddenDomain: strings.ToLower(values.Get("hidden_domain")), hiddenDomain: strings.ToLower(values.Get("hidden_domain")),
logger: logger, logger: logger,
pwFile: pwFile, pwFile: pwFile,
stopChan: make(chan struct{}),
}, nil }, nil
} }
@ -99,6 +102,9 @@ type BasicAuth struct {
pwMux sync.RWMutex pwMux sync.RWMutex
logger *CondLogger logger *CondLogger
hiddenDomain string hiddenDomain string
stopOnce sync.Once
stopChan chan struct{}
lastReloaded time.Time
} }
func NewBasicFileAuth(param_url *url.URL, logger *CondLogger) (*BasicAuth, error) { func NewBasicFileAuth(param_url *url.URL, logger *CondLogger) (*BasicAuth, error) {
@ -115,16 +121,30 @@ func NewBasicFileAuth(param_url *url.URL, logger *CondLogger) (*BasicAuth, error
hiddenDomain: strings.ToLower(values.Get("hidden_domain")), hiddenDomain: strings.ToLower(values.Get("hidden_domain")),
pwFilename: filename, pwFilename: filename,
logger: logger, logger: logger,
stopChan: make(chan struct{}),
} }
if err := auth.reload(); err != nil { if err := auth.reload(); err != nil {
return nil, fmt.Errorf("unable to load initial password list: %w", err) return nil, fmt.Errorf("unable to load initial password list: %w", err)
} }
reloadIntervalOption := values.Get("reload")
reloadInterval, err := time.ParseDuration(reloadIntervalOption)
if err != nil {
reloadInterval = 0
}
if reloadInterval == 0 {
reloadInterval = 15 * time.Second
}
if reloadInterval > 0 {
go auth.reloadLoop(reloadInterval)
}
return auth, nil return auth, nil
} }
func (auth *BasicAuth) reload() error { func (auth *BasicAuth) reload() error {
auth.logger.Info("reloading password file from %q...", auth.pwFilename)
newPwFile, err := htpasswd.New(auth.pwFilename, htpasswd.DefaultSystems, func(parseErr error) { newPwFile, err := htpasswd.New(auth.pwFilename, htpasswd.DefaultSystems, func(parseErr error) {
auth.logger.Error("failed to parse line in %q: %v", auth.pwFilename, parseErr) auth.logger.Error("failed to parse line in %q: %v", auth.pwFilename, parseErr)
}) })
@ -132,13 +152,45 @@ func (auth *BasicAuth) reload() error {
return err return err
} }
now := time.Now()
auth.pwMux.Lock() auth.pwMux.Lock()
defer auth.pwMux.Unlock()
auth.pwFile = newPwFile auth.pwFile = newPwFile
auth.lastReloaded = now
auth.pwMux.Unlock()
auth.logger.Info("password file reloaded.")
return nil return nil
} }
func (auth *BasicAuth) condReload() error {
reload := func() bool {
pwFileModTime, err := fileModTime(auth.pwFilename)
if err != nil {
auth.logger.Warning("can't get password file modtime: %v", err)
return true
}
return !pwFileModTime.Before(auth.lastReloaded)
}()
if reload {
return auth.reload()
}
return nil
}
func (auth *BasicAuth) reloadLoop(interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-auth.stopChan:
return
case <-ticker.C:
auth.condReload()
}
}
}
func (auth *BasicAuth) Validate(wr http.ResponseWriter, req *http.Request) bool { func (auth *BasicAuth) Validate(wr http.ResponseWriter, req *http.Request) bool {
hdr := req.Header.Get("Proxy-Authorization") hdr := req.Header.Get("Proxy-Authorization")
if hdr == "" { if hdr == "" {
@ -190,12 +242,20 @@ func (auth *BasicAuth) Validate(wr http.ResponseWriter, req *http.Request) bool
return false return false
} }
func (auth *BasicAuth) Stop() {
auth.stopOnce.Do(func() {
close(auth.stopChan)
})
}
type NoAuth struct{} type NoAuth struct{}
func (_ NoAuth) Validate(wr http.ResponseWriter, req *http.Request) bool { func (_ NoAuth) Validate(wr http.ResponseWriter, req *http.Request) bool {
return true return true
} }
func (_ NoAuth) Stop() {}
type CertAuth struct{} type CertAuth struct{}
func (_ CertAuth) Validate(wr http.ResponseWriter, req *http.Request) bool { func (_ CertAuth) Validate(wr http.ResponseWriter, req *http.Request) bool {
@ -206,3 +266,5 @@ func (_ CertAuth) Validate(wr http.ResponseWriter, req *http.Request) bool {
return true return true
} }
} }
func (_ CertAuth) Stop() {}

View File

@ -128,6 +128,7 @@ func run() int {
mainLogger.Critical("Failed to instantiate auth provider: %v", err) mainLogger.Critical("Failed to instantiate auth provider: %v", err)
return 3 return 3
} }
defer auth.Stop()
server := http.Server{ server := http.Server{
Addr: args.bind_address, Addr: args.bind_address,

View File

@ -6,11 +6,13 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
"os"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -189,3 +191,18 @@ func makeCipherList(ciphers string) []uint16 {
return cipherIDList return cipherIDList
} }
func fileModTime(filename string) (time.Time, error) {
f, err := os.Open(filename)
if err != nil {
return time.Time{}, fmt.Errorf("fileModTime(): can't open file %q: %w", filename, err)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return time.Time{}, fmt.Errorf("fileModTime(): can't stat file %q: %w", filename, err)
}
return fi.ModTime(), nil
}