mirror of
https://github.com/rclone/rclone.git
synced 2026-11-04 11:43:22 +00:00
lib/http: replace github.com/abbot/go-http-auth with built in htpasswd support
The go-http-auth library is unmaintained and rclone only used a small
part of it, so this replaces it with a small implementation of basic
auth and htpasswd file checking supporting the same bcrypt,
MD5-crypt ($1$ and $apr1$) and {SHA} formats.
It is tested against hashes made by htpasswd, openssl, mkpasswd and
Python bcrypt, expecting the same results as go-http-auth gave.
A missing or unreadable htpasswd file now logs an error and rejects
requests rather than panicking, malformed lines are logged and skipped,
and entries in unsupported formats are logged when the file is loaded.
This commit is contained in:
1 parent
ba11197157
commit
f0cb5dd6d3
7 files changed
+1472
-41
No files matched your search
@@ -20,7 +20,6 @@ require (
|
||||
github.com/a1ex3/zstd-seekable-format-go/pkg v0.10.0
|
||||
github.com/a8m/tree v0.0.0-20240104212747-2c8764a5f17e
|
||||
github.com/aalpar/deheap v1.1.2
|
||||
github.com/abbot/go-http-auth v0.4.0
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/anacrolix/dms v1.8.0
|
||||
github.com/apache/arrow-go/v18 v18.7.0 // indirect
|
||||
|
||||
@@ -63,8 +63,6 @@ github.com/a8m/tree v0.0.0-20240104212747-2c8764a5f17e h1:KMVieI1/Ub++GYfnhyFPoG
|
||||
github.com/a8m/tree v0.0.0-20240104212747-2c8764a5f17e/go.mod h1:j5astEcUkZQX8lK+KKlQ3NRQ50f4EE8ZjyZpCz3mrH4=
|
||||
github.com/aalpar/deheap v1.1.2 h1:MABHLcnjqsffb8GLkUFDigqpBBxOMz0DoKM9QfELeTw=
|
||||
github.com/aalpar/deheap v1.1.2/go.mod h1:A+nfkD4JbS05sewV0he/MYgR/90vfqyMoNNROgs+rmA=
|
||||
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
|
||||
github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rclone/rclone/fs"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// md5CryptAlphabet is the base64 variant used by crypt(3)
|
||||
const md5CryptAlphabet = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
// md5Crypt returns the MD5-crypt hash of password as used by "$1$"
|
||||
// and Apache "$apr1$" entries, in the form magic+salt+"$"+hash.
|
||||
//
|
||||
// Unlike crypt(3) the salt isn't truncated to 8 bytes, for
|
||||
// compatibility with entries rclone accepted previously.
|
||||
func md5Crypt(password, salt, magic string) string {
|
||||
alt := md5.Sum([]byte(password + salt + password))
|
||||
h := md5.New()
|
||||
_, _ = io.WriteString(h, password+magic+salt)
|
||||
for n := len(password); n > 0; n -= md5.Size {
|
||||
_, _ = h.Write(alt[:min(n, md5.Size)])
|
||||
}
|
||||
// The algorithm adds a NUL or the first byte of the password
|
||||
// for each bit of the password length
|
||||
for n := len(password); n > 0; n >>= 1 {
|
||||
if n&1 != 0 {
|
||||
_, _ = h.Write([]byte{0})
|
||||
} else {
|
||||
_, _ = io.WriteString(h, password[:1])
|
||||
}
|
||||
}
|
||||
sum := h.Sum(nil)
|
||||
for i := range 1000 {
|
||||
h.Reset()
|
||||
if i&1 != 0 {
|
||||
_, _ = io.WriteString(h, password)
|
||||
} else {
|
||||
_, _ = h.Write(sum)
|
||||
}
|
||||
if i%3 != 0 {
|
||||
_, _ = io.WriteString(h, salt)
|
||||
}
|
||||
if i%7 != 0 {
|
||||
_, _ = io.WriteString(h, password)
|
||||
}
|
||||
if i&1 != 0 {
|
||||
_, _ = h.Write(sum)
|
||||
} else {
|
||||
_, _ = io.WriteString(h, password)
|
||||
}
|
||||
sum = h.Sum(sum[:0])
|
||||
}
|
||||
out := []byte(magic + salt + "$")
|
||||
encode := func(v uint32, n int) {
|
||||
for range n {
|
||||
out = append(out, md5CryptAlphabet[v&0x3f])
|
||||
v >>= 6
|
||||
}
|
||||
}
|
||||
for _, i := range [][3]int{{0, 6, 12}, {1, 7, 13}, {2, 8, 14}, {3, 9, 15}, {4, 10, 5}} {
|
||||
encode(uint32(sum[i[0]])<<16|uint32(sum[i[1]])<<8|uint32(sum[i[2]]), 4)
|
||||
}
|
||||
encode(uint32(sum[11]), 2)
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// parseMD5Crypt returns the magic and salt of an MD5-crypt hash or
|
||||
// ok=false if secret isn't one.
|
||||
func parseMD5Crypt(secret string) (magic, salt string, ok bool) {
|
||||
for _, magic = range []string{"$1$", "$apr1$"} {
|
||||
if rest, found := strings.CutPrefix(secret, magic); found {
|
||||
salt, _, ok = strings.Cut(rest, "$")
|
||||
return magic, salt, ok
|
||||
}
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// isBcrypt returns true if secret is a bcrypt hash
|
||||
func isBcrypt(secret string) bool {
|
||||
for _, prefix := range []string{"$2a$", "$2b$", "$2x$", "$2y$"} {
|
||||
if strings.HasPrefix(secret, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// supportedHash returns true if secret is in a format checkHash
|
||||
// can check.
|
||||
func supportedHash(secret string) bool {
|
||||
if _, _, ok := parseMD5Crypt(secret); ok {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(secret, "{SHA}") || isBcrypt(secret)
|
||||
}
|
||||
|
||||
// checkHash returns true if password matches the hashed secret.
|
||||
//
|
||||
// It returns false if secret isn't in a supported format.
|
||||
func checkHash(secret, password string) bool {
|
||||
switch {
|
||||
case strings.HasPrefix(secret, "{SHA}"):
|
||||
sum := sha1.Sum([]byte(password))
|
||||
return subtle.ConstantTimeCompare([]byte(secret[len("{SHA}"):]), []byte(base64.StdEncoding.EncodeToString(sum[:]))) == 1
|
||||
case isBcrypt(secret):
|
||||
return bcrypt.CompareHashAndPassword([]byte(secret), []byte(password)) == nil
|
||||
}
|
||||
if magic, salt, ok := parseMD5Crypt(secret); ok {
|
||||
return subtle.ConstantTimeCompare([]byte(secret), []byte(md5Crypt(password, salt, magic))) == 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseHtpasswd reads user:hash lines from in, returning a map of
|
||||
// user to hash. Blank lines and lines starting with # are ignored and
|
||||
// malformed lines are logged and skipped.
|
||||
func parseHtpasswd(name string, in io.Reader) (map[string]string, error) {
|
||||
users := make(map[string]string)
|
||||
scanner := bufio.NewScanner(in)
|
||||
for lineNumber := 1; scanner.Scan(); lineNumber++ {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
user, secret, ok := strings.Cut(line, ":")
|
||||
// Some tools write extra fields after the hash
|
||||
secret, _, _ = strings.Cut(secret, ":")
|
||||
user, secret = strings.TrimSpace(user), strings.TrimSpace(secret)
|
||||
if !ok || user == "" || secret == "" {
|
||||
// Don't log the line as it may contain a password
|
||||
fs.Errorf(name, "Ignoring malformed htpasswd line %d", lineNumber)
|
||||
continue
|
||||
}
|
||||
if !supportedHash(secret) {
|
||||
fs.Errorf(name, "Password for htpasswd user %q is in an unsupported format - use bcrypt, MD5 or SHA1", user)
|
||||
}
|
||||
users[user] = secret
|
||||
}
|
||||
return users, scanner.Err()
|
||||
}
|
||||
|
||||
// htpasswdFile checks passwords against an htpasswd file, reloading
|
||||
// it whenever it changes.
|
||||
type htpasswdFile struct {
|
||||
path string
|
||||
mu sync.Mutex
|
||||
loaded bool
|
||||
modTime time.Time
|
||||
size int64
|
||||
users map[string]string // user => hashed password
|
||||
}
|
||||
|
||||
// load reads the users from the file. On error it leaves no users and
|
||||
// the load is retried on the next lookup, as fixing errors such as
|
||||
// permissions needn't change the file's size or modification time.
|
||||
//
|
||||
// Call with mu held.
|
||||
func (h *htpasswdFile) load(info os.FileInfo) {
|
||||
h.loaded, h.users = false, nil
|
||||
f, err := os.Open(h.path)
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Failed to read htpasswd file: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
users, err := parseHtpasswd(h.path, f)
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Failed to read htpasswd file %q: %v", h.path, err)
|
||||
return
|
||||
}
|
||||
h.loaded, h.modTime, h.size = true, info.ModTime(), info.Size()
|
||||
h.users = users
|
||||
}
|
||||
|
||||
// lookup returns the hashed password for user, or "" if there isn't
|
||||
// one.
|
||||
func (h *htpasswdFile) lookup(user string) string {
|
||||
info, err := os.Stat(h.path)
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if err != nil {
|
||||
fs.Errorf(nil, "Failed to read htpasswd file: %v", err)
|
||||
h.loaded, h.users = false, nil
|
||||
return ""
|
||||
}
|
||||
if !h.loaded || !info.ModTime().Equal(h.modTime) || info.Size() != h.size {
|
||||
h.load(info)
|
||||
}
|
||||
return h.users[user]
|
||||
}
|
||||
|
||||
// check returns true if user and password match an entry in the file.
|
||||
func (h *htpasswdFile) check(user, password string) bool {
|
||||
return checkHash(h.lookup(user), password)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMD5Crypt(t *testing.T) {
|
||||
// Expected values made with "openssl passwd"
|
||||
for _, test := range []struct {
|
||||
password, salt, magic, want string
|
||||
}{
|
||||
{"password", "saltsalt", "$1$", "$1$saltsalt$qjXMvbEw8oaL.CzflDtaK/"},
|
||||
{"", "ab", "$1$", "$1$ab$rn6aQS/o7141mj179E/zA."},
|
||||
{"md5", "s7fogein", "$apr1$", "$apr1$s7fogein$IK9ItbnGM14ct0bY4Uyik1"},
|
||||
{"averyveryverylongpasswordthatislongerthan32bytes", "12345678", "$apr1$", "$apr1$12345678$AzbvZTnrGfP3.51TG02ZD1"},
|
||||
{"pässwörd", "x£y", "$1$", "$1$x£y$DQwRxj42VvbTcvJrXRM8k1"},
|
||||
} {
|
||||
assert.Equal(t, test.want, md5Crypt(test.password, test.salt, test.magic), test.password)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckHashGolden checks checkHash against hashes made by
|
||||
// htpasswd, openssl, mkpasswd and Python bcrypt with
|
||||
// testdata/make-htpasswd-vectors.sh. The expected results were
|
||||
// recorded from github.com/abbot/go-http-auth v0.4.0 which rclone
|
||||
// used previously.
|
||||
func TestCheckHashGolden(t *testing.T) {
|
||||
f, err := os.Open("testdata/htpasswd-golden.tsv")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = f.Close() }()
|
||||
scanner := bufio.NewScanner(f)
|
||||
n := 0
|
||||
for scanner.Scan() {
|
||||
fields := strings.Split(scanner.Text(), "\t")
|
||||
require.Len(t, fields, 4, scanner.Text())
|
||||
label, secret, password, want := fields[0], fields[1], fields[2], fields[3] == "true"
|
||||
assert.Equal(t, want, checkHash(secret, password), "%s %q %q", label, secret, password)
|
||||
n++
|
||||
}
|
||||
require.NoError(t, scanner.Err())
|
||||
assert.Greater(t, n, 1000)
|
||||
}
|
||||
|
||||
func TestCheckHash(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
secret, password string
|
||||
want bool
|
||||
}{
|
||||
{"{SHA}2PRZAyDhNDqRW2OUFwZQqPNdaSY=", "sha", true},
|
||||
{"{SHA}2PRZAyDhNDqRW2OUFwZQqPNdaSY=", "wrong", false},
|
||||
{"$apr1$s7fogein$IK9ItbnGM14ct0bY4Uyik1", "md5", true},
|
||||
{"$apr1$s7fogein$IK9ItbnGM14ct0bY4Uyik1", "wrong", false},
|
||||
{"$1$saltsalt$qjXMvbEw8oaL.CzflDtaK/", "password", true},
|
||||
{"$2y$10$K/b3mVXUA6X857TOTYIL9.Lbaeg9oBjMQwUX5NefpVUCcYP0Z5KY2", "bcrypt", true},
|
||||
{"$2y$10$K/b3mVXUA6X857TOTYIL9.Lbaeg9oBjMQwUX5NefpVUCcYP0Z5KY2", "wrong", false},
|
||||
// Unsupported formats never match
|
||||
{"plain", "plain", false},
|
||||
{"abJnggxhB/yWI", "des", false},
|
||||
{"", "", false},
|
||||
} {
|
||||
assert.Equal(t, test.want, checkHash(test.secret, test.password), "%q %q", test.secret, test.password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHtpasswd(t *testing.T) {
|
||||
in := strings.NewReader(`# comment
|
||||
user1:{SHA}2PRZAyDhNDqRW2OUFwZQqPNdaSY=
|
||||
|
||||
user2 : $apr1$s7fogein$IK9ItbnGM14ct0bY4Uyik1
|
||||
user3:$1$saltsalt$qjXMvbEw8oaL.CzflDtaK/:extra:fields
|
||||
malformed
|
||||
:nouser
|
||||
nopass:
|
||||
user1:{SHA}overridden=
|
||||
`)
|
||||
users, err := parseHtpasswd("test", in)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{
|
||||
"user1": "{SHA}overridden=",
|
||||
"user2": "$apr1$s7fogein$IK9ItbnGM14ct0bY4Uyik1",
|
||||
"user3": "$1$saltsalt$qjXMvbEw8oaL.CzflDtaK/",
|
||||
}, users)
|
||||
}
|
||||
|
||||
func TestHtpasswdFileUnsupported(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), ".htpasswd")
|
||||
require.NoError(t, os.WriteFile(path, []byte("des:abJnggxhB/yWI\nplain:plain\nmd5:$apr1$s7fogein$IK9ItbnGM14ct0bY4Uyik1\n"), 0600))
|
||||
h := &htpasswdFile{path: path}
|
||||
assert.False(t, h.check("des", "des"))
|
||||
assert.False(t, h.check("plain", "plain"))
|
||||
assert.True(t, h.check("md5", "md5"))
|
||||
assert.False(t, h.check("nosuchuser", "md5"))
|
||||
}
|
||||
|
||||
// The users must follow changes to the htpasswd file
|
||||
func TestHtpasswdFileReload(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), ".htpasswd")
|
||||
require.NoError(t, os.WriteFile(path, []byte("sha:{SHA}2PRZAyDhNDqRW2OUFwZQqPNdaSY=\n"), 0600))
|
||||
h := &htpasswdFile{path: path}
|
||||
assert.True(t, h.check("sha", "sha"))
|
||||
|
||||
require.NoError(t, os.WriteFile(path, []byte("bcrypt:$2y$10$K/b3mVXUA6X857TOTYIL9.Lbaeg9oBjMQwUX5NefpVUCcYP0Z5KY2\n"), 0600))
|
||||
future := time.Now().Add(time.Hour)
|
||||
require.NoError(t, os.Chtimes(path, future, future))
|
||||
assert.False(t, h.check("sha", "sha"))
|
||||
assert.True(t, h.check("bcrypt", "bcrypt"))
|
||||
|
||||
// A missing file lets nobody in
|
||||
require.NoError(t, os.Remove(path))
|
||||
assert.False(t, h.check("bcrypt", "bcrypt"))
|
||||
}
|
||||
|
||||
// A file which can't be read must be retried even if it doesn't change
|
||||
func TestHtpasswdFileUnreadable(t *testing.T) {
|
||||
if runtime.GOOS == "windows" || os.Geteuid() == 0 {
|
||||
t.Skip("can't make file unreadable")
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), ".htpasswd")
|
||||
require.NoError(t, os.WriteFile(path, []byte("sha:{SHA}2PRZAyDhNDqRW2OUFwZQqPNdaSY=\n"), 0000))
|
||||
h := &htpasswdFile{path: path}
|
||||
assert.False(t, h.check("sha", "sha"))
|
||||
|
||||
// chmod changes neither the size nor the modification time
|
||||
require.NoError(t, os.Chmod(path, 0600))
|
||||
assert.True(t, h.check("sha", "sha"))
|
||||
}
|
||||
+23
-38
@@ -2,6 +2,7 @@ package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -9,7 +10,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
goauth "github.com/abbot/go-http-auth"
|
||||
"github.com/rclone/rclone/fs"
|
||||
)
|
||||
|
||||
@@ -34,36 +34,27 @@ func parseAuthorization(r *http.Request) (user, pass string, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// LoggedBasicAuth simply wraps the goauth.BasicAuth struct
|
||||
type LoggedBasicAuth struct {
|
||||
goauth.BasicAuth
|
||||
// requireBasicAuth replies to the request asking the client for
|
||||
// basic auth credentials.
|
||||
func requireBasicAuth(w http.ResponseWriter, realm string) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = fmt.Fprintf(w, "%d %s\n", http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized))
|
||||
}
|
||||
|
||||
// CheckAuth extends BasicAuth.CheckAuth to emit a log entry for unauthorised requests
|
||||
func (a *LoggedBasicAuth) CheckAuth(r *http.Request) string {
|
||||
username := a.BasicAuth.CheckAuth(r)
|
||||
if username == "" {
|
||||
user, _, _ := parseAuthorization(r)
|
||||
fs.Infof(r.URL.Path, "%s: Unauthorized request from %s", r.RemoteAddr, user)
|
||||
}
|
||||
return username
|
||||
}
|
||||
|
||||
// NewLoggedBasicAuthenticator instantiates a new instance of LoggedBasicAuthenticator
|
||||
func NewLoggedBasicAuthenticator(realm string, secrets goauth.SecretProvider) *LoggedBasicAuth {
|
||||
return &LoggedBasicAuth{BasicAuth: goauth.BasicAuth{Realm: realm, Secrets: secrets}}
|
||||
}
|
||||
|
||||
// Helper to generate required interface for middleware
|
||||
func basicAuth(authenticator *LoggedBasicAuth) func(next http.Handler) http.Handler {
|
||||
// basicAuth returns middleware which authenticates with basic auth,
|
||||
// using check to see whether the user and password are valid.
|
||||
func basicAuth(realm string, check func(user, pass string) bool) Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
username := authenticator.CheckAuth(r)
|
||||
if username == "" {
|
||||
authenticator.RequireAuth(w, r)
|
||||
user, pass, ok := parseAuthorization(r)
|
||||
if !ok || !check(user, pass) {
|
||||
fs.Infof(r.URL.Path, "%s: Unauthorized request from %s", r.RemoteAddr, user)
|
||||
requireBasicAuth(w, realm)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), ctxKeyUser, username)
|
||||
ctx := context.WithValue(r.Context(), ctxKeyUser, user)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
@@ -94,23 +85,17 @@ func MiddlewareAuthCertificateUser() Middleware {
|
||||
// MiddlewareAuthHtpasswd instantiates middleware that authenticates against the passed htpasswd file
|
||||
func MiddlewareAuthHtpasswd(path, realm string) Middleware {
|
||||
fs.Infof(nil, "Using %q as htpasswd storage", path)
|
||||
secretProvider := goauth.HtpasswdFileProvider(path)
|
||||
authenticator := NewLoggedBasicAuthenticator(realm, secretProvider)
|
||||
return basicAuth(authenticator)
|
||||
h := &htpasswdFile{path: path}
|
||||
return basicAuth(realm, h.check)
|
||||
}
|
||||
|
||||
// MiddlewareAuthBasic instantiates middleware that authenticates for a single user
|
||||
func MiddlewareAuthBasic(user, pass, realm, salt string) Middleware {
|
||||
fs.Infof(nil, "Using --user %s --pass XXXX as authenticated user", user)
|
||||
pass = string(goauth.MD5Crypt([]byte(pass), []byte(salt), []byte("$1$")))
|
||||
secretProvider := func(u, r string) string {
|
||||
if user == u {
|
||||
return pass
|
||||
}
|
||||
return ""
|
||||
}
|
||||
authenticator := NewLoggedBasicAuthenticator(realm, secretProvider)
|
||||
return basicAuth(authenticator)
|
||||
pass = md5Crypt(pass, salt, "$1$")
|
||||
return basicAuth(realm, func(u, p string) bool {
|
||||
return u == user && subtle.ConstantTimeCompare([]byte(md5Crypt(p, salt, "$1$")), []byte(pass)) == 1
|
||||
})
|
||||
}
|
||||
|
||||
// MiddlewareAuthCustom instantiates middleware that authenticates using a custom function
|
||||
@@ -133,7 +118,7 @@ func MiddlewareAuthCustom(fn CustomAuthFn, realm string, userFromContext bool) M
|
||||
value, err := fn(r, user, pass)
|
||||
if err != nil {
|
||||
fs.Infof(r.URL.Path, "%s: Auth failed from %s: %v", r.RemoteAddr, user, err)
|
||||
goauth.NewBasicAuthenticator(realm, func(user, realm string) string { return "" }).RequireAuth(w, r) //Reuse BasicAuth error reporting
|
||||
requireBasicAuth(w, realm)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+1080
File diff suppressed because it is too large.
Load diff
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Prints label<TAB>secret<TAB>password lines made by real htpasswd
|
||||
# tools, used to make htpasswd-golden.tsv which adds variations of the
|
||||
# password and whether each should be accepted.
|
||||
pws=("" "a" "password" "md5" "exactly16chars!!" "seventeen_chars!!" "thirty-three-characters-long-pass" "$(printf 'x%.0s' {1..80})" "pässwörd" "has:colon" "has space" "日本語パスワード" 'dol$lar' '#hash')
|
||||
out() { printf '%s\t%s\t%s\n' "$1" "$2" "$3"; }
|
||||
for pw in "${pws[@]}"; do
|
||||
out htpasswd-m "$(htpasswd -nbm u "$pw" | cut -d: -f2-)" "$pw"
|
||||
out htpasswd-B4 "$(htpasswd -nbB -C 4 u "$pw" | cut -d: -f2-)" "$pw"
|
||||
out htpasswd-s "$(htpasswd -nbs u "$pw" | cut -d: -f2-)" "$pw"
|
||||
out htpasswd-d "$(htpasswd -nbd u "$pw" 2>/dev/null | cut -d: -f2-)" "$pw"
|
||||
out htpasswd-p "$(htpasswd -nbp u "$pw" 2>/dev/null | cut -d: -f2-)" "$pw"
|
||||
out openssl-1 "$(openssl passwd -1 "$pw")" "$pw"
|
||||
out openssl-apr1 "$(openssl passwd -apr1 "$pw")" "$pw"
|
||||
out openssl-1-shortsalt "$(openssl passwd -1 -salt ab "$pw")" "$pw"
|
||||
out openssl-apr1-emptysalt "$(openssl passwd -apr1 -salt '' "$pw")" "$pw"
|
||||
out openssl-5 "$(openssl passwd -5 "$pw")" "$pw"
|
||||
out openssl-6 "$(openssl passwd -6 "$pw")" "$pw"
|
||||
[ -n "$pw" ] && out mkpasswd-md5 "$(mkpasswd -m md5crypt "$pw")" "$pw"
|
||||
for p in 2a 2b; do
|
||||
out python-$p "$(python3 -I -c 'import bcrypt,sys; print(bcrypt.hashpw(sys.argv[1].encode(), bcrypt.gensalt(4, prefix=sys.argv[2].encode())).decode())' "$pw" $p)" "$pw"
|
||||
done
|
||||
h=$(python3 -I -c 'import bcrypt,sys; print(bcrypt.hashpw(sys.argv[1].encode(), bcrypt.gensalt(4)).decode())' "$pw")
|
||||
out bcrypt-2x "\$2x\$${h:4}" "$pw"
|
||||
out bcrypt-2 "\$2\$${h:4}" "$pw"
|
||||
done
|
||||
out htpasswd-B10 "$(htpasswd -nbB -C 10 u password | cut -d: -f2-)" password
|
||||
Reference in new issue
Block a user