mirror of
https://github.com/caddyserver/caddy.git
synced 2024-11-29 12:16:16 +08:00
209 lines
6.7 KiB
Go
209 lines
6.7 KiB
Go
|
// Package htpasswd provides HTTP Basic Authentication using Apache-style htpasswd files
|
||
|
// for the user and password data.
|
||
|
//
|
||
|
// It supports most common hashing systems used over the decades and can be easily extended
|
||
|
// by the programmer to support others. (See the sha.go source file as a guide.)
|
||
|
//
|
||
|
// You will want to use something like...
|
||
|
// myauth := htpasswd.New("My Realm", "./my-htpasswd-file", htpasswd.DefaultSystems, nil)
|
||
|
// m.Use(myauth.Handler)
|
||
|
// ...to configure your authentication and then use the myauth.Handler as a middleware handler in your Martini stack.
|
||
|
// You should read about that nil, as well as Reread() too.
|
||
|
package basic
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"encoding/base64"
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"os/signal"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
)
|
||
|
|
||
|
// An EncodedPasswd is created from the encoded password in a password file by a PasswdParser.
|
||
|
//
|
||
|
// The password files consist of lines like "user:passwd-encoding". The user part is stripped off and
|
||
|
// the passwd-encoding part is captured in an EncodedPasswd.
|
||
|
type EncodedPasswd interface {
|
||
|
// Return true if the string matches the password.
|
||
|
// This may cache the result in the case of expensive comparison functions.
|
||
|
MatchesPassword(pw string) bool
|
||
|
}
|
||
|
|
||
|
// Examine an encoded password, and if it is formatted correctly and sane, return an
|
||
|
// EncodedPasswd which will recognize it.
|
||
|
//
|
||
|
// If the format is not understood, then return nil
|
||
|
// so that another parser may have a chance. If the format is understood but not sane,
|
||
|
// return an error to prevent other formats from possibly claiming it
|
||
|
//
|
||
|
// You may write and supply one of these functions to support a format (e.g. bcrypt) not
|
||
|
// already included in this package. Use sha.c as a template, it is simple but not too simple.
|
||
|
type PasswdParser func(pw string) (EncodedPasswd, error)
|
||
|
|
||
|
type passwdTable map[string]EncodedPasswd
|
||
|
|
||
|
// A BadLineHandler is used to notice bad lines in a password file. If not nil, it will be
|
||
|
// called for each bad line with a descriptive error. Think about what you do with these, they
|
||
|
// will sometimes contain hashed passwords.
|
||
|
type BadLineHandler func(err error)
|
||
|
|
||
|
// An HtpasswdFile encompasses an Apache-style htpasswd file for HTTP Basic authentication
|
||
|
type HtpasswdFile struct {
|
||
|
realm string
|
||
|
filePath string
|
||
|
mutex sync.Mutex
|
||
|
passwds passwdTable
|
||
|
parsers []PasswdParser
|
||
|
}
|
||
|
|
||
|
// An array of PasswdParser including all builtin parsers. Notice that Plain is last, since it accepts anything
|
||
|
var DefaultSystems []PasswdParser = []PasswdParser{AcceptMd5, AcceptSha, RejectBcrypt, AcceptPlain}
|
||
|
|
||
|
// New creates an HtpasswdFile from an Apache-style htpasswd file for HTTP Basic Authentication.
|
||
|
//
|
||
|
// The realm is presented to the user in the login dialog.
|
||
|
//
|
||
|
// The filename must exist and be accessible to the process, as well as being a valid htpasswd file.
|
||
|
//
|
||
|
// parsers is a list of functions to handle various hashing systems. In practice you will probably
|
||
|
// just pass htpasswd.DefaultSystems, but you could make your own to explicitly reject some formats or
|
||
|
// implement your own.
|
||
|
//
|
||
|
// bad is a function, which if not nil will be called for each malformed or rejected entry in
|
||
|
// the password file.
|
||
|
func New(realm string, filename string, parsers []PasswdParser, bad BadLineHandler) (*HtpasswdFile, error) {
|
||
|
bf := HtpasswdFile{
|
||
|
realm: realm,
|
||
|
filePath: filename,
|
||
|
parsers: parsers,
|
||
|
}
|
||
|
|
||
|
if err := bf.Reload(bad); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &bf, nil
|
||
|
}
|
||
|
|
||
|
// A Martini middleware handler to enforce HTTP Basic Auth using the policy read from the htpasswd file.
|
||
|
func (bf *HtpasswdFile) ServeHTTP(res http.ResponseWriter, req *http.Request) {
|
||
|
// if everything works, we return, otherwise we get to the
|
||
|
// end where we do an http.Error to stop the request
|
||
|
auth := req.Header.Get("Authorization")
|
||
|
|
||
|
if auth != "" {
|
||
|
userPassword, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic "))
|
||
|
if err == nil {
|
||
|
parts := strings.SplitN(string(userPassword), ":", 2)
|
||
|
if len(parts) == 2 {
|
||
|
user := parts[0]
|
||
|
pw := parts[1]
|
||
|
|
||
|
bf.mutex.Lock()
|
||
|
matcher, ok := bf.passwds[user]
|
||
|
bf.mutex.Unlock()
|
||
|
|
||
|
if ok && matcher.MatchesPassword(pw) {
|
||
|
// we are good
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
res.Header().Set("WWW-Authenticate", "Basic realm=\""+bf.realm+"\"")
|
||
|
http.Error(res, "Not Authorized", http.StatusUnauthorized)
|
||
|
}
|
||
|
|
||
|
// Reread the password file for this HtpasswdFile.
|
||
|
// You will need to call this to notice any changes to the password file.
|
||
|
// This function is thread safe. Someone versed in fsnotify might make it
|
||
|
// happen automatically. Likewise you might also connect a SIGHUP handler to
|
||
|
// this function.
|
||
|
func (bf *HtpasswdFile) Reload(bad BadLineHandler) error {
|
||
|
// with the file...
|
||
|
f, err := os.Open(bf.filePath)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer f.Close()
|
||
|
|
||
|
// ... and a new map ...
|
||
|
newPasswdMap := passwdTable{}
|
||
|
|
||
|
// ... for each line ...
|
||
|
scanner := bufio.NewScanner(f)
|
||
|
for scanner.Scan() {
|
||
|
line := scanner.Text()
|
||
|
|
||
|
// ... add it to the map, noting errors along the way
|
||
|
if perr := bf.addHtpasswdUser(&newPasswdMap, line); perr != nil && bad != nil {
|
||
|
bad(perr)
|
||
|
}
|
||
|
}
|
||
|
if err := scanner.Err(); err != nil {
|
||
|
return fmt.Errorf("Error scanning htpasswd file: %s", err.Error())
|
||
|
}
|
||
|
|
||
|
// .. finally, safely swap in the new map
|
||
|
bf.mutex.Lock()
|
||
|
bf.passwds = newPasswdMap
|
||
|
bf.mutex.Unlock()
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Reload the htpasswd file on a signal. If there is an error, the old data will be kept instead.
|
||
|
// Typically you would use syscall.SIGHUP for the value of "when"
|
||
|
func (bf *HtpasswdFile) ReloadOn(when os.Signal, onbad BadLineHandler) {
|
||
|
// this is rather common with code in digest, but I don't have a common area...
|
||
|
c := make(chan os.Signal, 1)
|
||
|
signal.Notify(c, when)
|
||
|
|
||
|
go func() {
|
||
|
for {
|
||
|
_ = <-c
|
||
|
bf.Reload(onbad)
|
||
|
}
|
||
|
}()
|
||
|
}
|
||
|
|
||
|
// Process a line from an htpasswd file and add it to the user/password map. We may
|
||
|
// encounter some malformed lines, this will not be an error, but we will log them if
|
||
|
// the caller has given us a logger.
|
||
|
func (bf *HtpasswdFile) addHtpasswdUser(pwmap *passwdTable, rawLine string) error {
|
||
|
// ignore white space lines
|
||
|
line := strings.TrimSpace(rawLine)
|
||
|
if line == "" {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// split "user:encoding" at colon
|
||
|
parts := strings.SplitN(line, ":", 2)
|
||
|
if len(parts) != 2 {
|
||
|
return fmt.Errorf("malformed line, no colon: %s", line)
|
||
|
}
|
||
|
|
||
|
user := parts[0]
|
||
|
encoding := parts[1]
|
||
|
|
||
|
// give each parser a shot. The first one to produce a matcher wins.
|
||
|
// If one produces an error then stop (to prevent Plain from catching it)
|
||
|
for _, p := range bf.parsers {
|
||
|
matcher, err := p(encoding)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if matcher != nil {
|
||
|
(*pwmap)[user] = matcher
|
||
|
return nil // we are done, we took to first match
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// No one liked this line
|
||
|
return fmt.Errorf("unable to recognize password for %s in %s", user, encoding)
|
||
|
}
|