mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 23:50:32 +00:00
4e41a5583d
The common package defines the interfaces that a protocol must implement and contain code that can be shared among supported protocols. This way should be easier to support new protocols
150 lines
3.6 KiB
Go
150 lines
3.6 KiB
Go
package httpd
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
unixcrypt "github.com/nathanaelle/password/v2"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/drakkan/sftpgo/logger"
|
|
"github.com/drakkan/sftpgo/utils"
|
|
)
|
|
|
|
const (
|
|
authenticationHeader = "WWW-Authenticate"
|
|
authenticationRealm = "SFTPGo Web"
|
|
unauthResponse = "Unauthorized"
|
|
)
|
|
|
|
var (
|
|
md5CryptPwdPrefixes = []string{"$1$", "$apr1$"}
|
|
bcryptPwdPrefixes = []string{"$2a$", "$2$", "$2x$", "$2y$", "$2b$"}
|
|
)
|
|
|
|
type httpAuthProvider interface {
|
|
getHashedPassword(username string) (string, bool)
|
|
isEnabled() bool
|
|
}
|
|
|
|
type basicAuthProvider struct {
|
|
Path string
|
|
sync.RWMutex
|
|
Info os.FileInfo
|
|
Users map[string]string
|
|
}
|
|
|
|
func newBasicAuthProvider(authUserFile string) (httpAuthProvider, error) {
|
|
basicAuthProvider := basicAuthProvider{
|
|
Path: authUserFile,
|
|
Info: nil,
|
|
Users: make(map[string]string),
|
|
}
|
|
return &basicAuthProvider, basicAuthProvider.loadUsers()
|
|
}
|
|
|
|
func (p *basicAuthProvider) isEnabled() bool {
|
|
return len(p.Path) > 0
|
|
}
|
|
|
|
func (p *basicAuthProvider) isReloadNeeded(info os.FileInfo) bool {
|
|
p.RLock()
|
|
defer p.RUnlock()
|
|
return p.Info == nil || p.Info.ModTime() != info.ModTime() || p.Info.Size() != info.Size()
|
|
}
|
|
|
|
func (p *basicAuthProvider) loadUsers() error {
|
|
if !p.isEnabled() {
|
|
return nil
|
|
}
|
|
info, err := os.Stat(p.Path)
|
|
if err != nil {
|
|
logger.Debug(logSender, "", "unable to stat basic auth users file: %v", err)
|
|
return err
|
|
}
|
|
if p.isReloadNeeded(info) {
|
|
r, err := os.Open(p.Path)
|
|
if err != nil {
|
|
logger.Debug(logSender, "", "unable to open basic auth users file: %v", err)
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
reader := csv.NewReader(r)
|
|
reader.Comma = ':'
|
|
reader.Comment = '#'
|
|
reader.TrimLeadingSpace = true
|
|
records, err := reader.ReadAll()
|
|
if err != nil {
|
|
logger.Debug(logSender, "", "unable to parse basic auth users file: %v", err)
|
|
return err
|
|
}
|
|
p.Lock()
|
|
defer p.Unlock()
|
|
p.Users = make(map[string]string)
|
|
for _, record := range records {
|
|
if len(record) == 2 {
|
|
p.Users[record[0]] = record[1]
|
|
}
|
|
}
|
|
logger.Debug(logSender, "", "number of users loaded for httpd basic auth: %v", len(p.Users))
|
|
p.Info = info
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *basicAuthProvider) getHashedPassword(username string) (string, bool) {
|
|
err := p.loadUsers()
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
p.RLock()
|
|
defer p.RUnlock()
|
|
pwd, ok := p.Users[username]
|
|
return pwd, ok
|
|
}
|
|
|
|
func checkAuth(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !validateCredentials(r) {
|
|
w.Header().Set(authenticationHeader, fmt.Sprintf("Basic realm=\"%v\"", authenticationRealm))
|
|
if strings.HasPrefix(r.RequestURI, apiPrefix) {
|
|
sendAPIResponse(w, r, errors.New(unauthResponse), "", http.StatusUnauthorized)
|
|
} else {
|
|
http.Error(w, unauthResponse, http.StatusUnauthorized)
|
|
}
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func validateCredentials(r *http.Request) bool {
|
|
if !httpAuth.isEnabled() {
|
|
return true
|
|
}
|
|
username, password, ok := r.BasicAuth()
|
|
if !ok {
|
|
return false
|
|
}
|
|
if hashedPwd, ok := httpAuth.getHashedPassword(username); ok {
|
|
if utils.IsStringPrefixInSlice(hashedPwd, bcryptPwdPrefixes) {
|
|
err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(password))
|
|
return err == nil
|
|
}
|
|
if utils.IsStringPrefixInSlice(hashedPwd, md5CryptPwdPrefixes) {
|
|
crypter, ok := unixcrypt.MD5.CrypterFound(hashedPwd)
|
|
if !ok {
|
|
err := errors.New("cannot found matching MD5 crypter")
|
|
logger.Debug(logSender, "", "error comparing password with MD5 crypt hash: %v", err)
|
|
return false
|
|
}
|
|
return crypter.Verify([]byte(password))
|
|
}
|
|
}
|
|
return false
|
|
}
|