|
|
|
@ -2,6 +2,7 @@
|
|
|
|
|
package htpasswd |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"errors" |
|
|
|
|
"fmt" |
|
|
|
|
"os" |
|
|
|
|
"strings" |
|
|
|
@ -18,53 +19,51 @@ type Hasher interface {
|
|
|
|
|
Prefix() string |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ParseFile parses a .htpasswd file
|
|
|
|
|
// ParseHtpasswdFile parses a .htpasswd file
|
|
|
|
|
// and returns a Passwd type
|
|
|
|
|
func ParseFile(file string) (Passwds, error) { |
|
|
|
|
func ParseHtpasswdFile(file string) (Passwds, error) { |
|
|
|
|
htpasswdBytes, err := os.ReadFile(file) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
return Parse(htpasswdBytes) |
|
|
|
|
return ParseHtpasswd(htpasswdBytes) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Parse parses a slice of bytes in htpasswd style
|
|
|
|
|
func Parse(htpasswdBytes []byte) (Passwds, error) { |
|
|
|
|
// ParseHtpasswd parses a slice of bytes in htpasswd style
|
|
|
|
|
func ParseHtpasswd(htpasswdBytes []byte) (Passwds, error) { |
|
|
|
|
lines := strings.Split(string(htpasswdBytes), "\n") |
|
|
|
|
passwords := make(map[string]string) |
|
|
|
|
var err error |
|
|
|
|
|
|
|
|
|
for lineNumber, line := range lines { |
|
|
|
|
line = strings.TrimSpace(line) |
|
|
|
|
line = strings.Trim(line, " ") |
|
|
|
|
if len(line) == 0 { |
|
|
|
|
// skipping empty lines
|
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
user, password, err := splitLine(line, lineNumber) |
|
|
|
|
if err != nil { |
|
|
|
|
parts := strings.Split(line, ":") |
|
|
|
|
if ok, err := validLine(parts, lineNumber, line); !ok { |
|
|
|
|
return passwords, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
_, exists := passwords[user] |
|
|
|
|
parts = trimParts(parts) |
|
|
|
|
_, exists := passwords[parts[0]] |
|
|
|
|
|
|
|
|
|
if exists { |
|
|
|
|
err = &UserError{ |
|
|
|
|
Name: user, |
|
|
|
|
Err: ErrExists, |
|
|
|
|
} |
|
|
|
|
err = errors.New("invalid htpasswords file - user " + |
|
|
|
|
parts[0] + " defined more than once") |
|
|
|
|
return passwords, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
passwords[user] = password |
|
|
|
|
passwords[parts[0]] = parts[1] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return passwords, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// CreateUser creates a record in the named file with
|
|
|
|
|
// the named password and hash algorithm
|
|
|
|
|
func CreateUser(file, user, passwd string, algo Hasher) error { |
|
|
|
|
pp, err := ParseFile(file) |
|
|
|
|
pp, err := ParseHtpasswdFile(file) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
@ -72,19 +71,15 @@ func CreateUser(file, user, passwd string, algo Hasher) error {
|
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
return pp.WriteFile(file) |
|
|
|
|
return pp.Write(file) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// CreateUser will create a new user in the given Passwd object
|
|
|
|
|
// using the given name, password and hashing algorithm
|
|
|
|
|
func (pp Passwds) CreateUser(user, passwd string, algo Hasher) error { |
|
|
|
|
if _, exists := pp[user]; exists { |
|
|
|
|
return &UserError{ |
|
|
|
|
Name: user, |
|
|
|
|
Err: ErrExists, |
|
|
|
|
} |
|
|
|
|
return fmt.Errorf("user %s already exists", user) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
h, err := algo.Hash(passwd) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
@ -96,7 +91,7 @@ func (pp Passwds) CreateUser(user, passwd string, algo Hasher) error {
|
|
|
|
|
// UpdateUser will update the password for the named user
|
|
|
|
|
// in the named file
|
|
|
|
|
func UpdateUser(file, user, passwd string, algo Hasher) error { |
|
|
|
|
pp, err := ParseFile(file) |
|
|
|
|
pp, err := ParseHtpasswdFile(file) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
@ -104,19 +99,15 @@ func UpdateUser(file, user, passwd string, algo Hasher) error {
|
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
return pp.WriteFile(file) |
|
|
|
|
return pp.Write(file) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// UpdateUser will update the password for the named user
|
|
|
|
|
// using the given name, password and hashing algorithm
|
|
|
|
|
func (pp Passwds) UpdateUser(user, passwd string, algo Hasher) error { |
|
|
|
|
if _, exists := pp[user]; !exists { |
|
|
|
|
return &UserError{ |
|
|
|
|
Name: user, |
|
|
|
|
Err: ErrNotExists, |
|
|
|
|
} |
|
|
|
|
return fmt.Errorf("user %s does not exist", user) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
h, err := algo.Hash(passwd) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
@ -127,7 +118,7 @@ func (pp Passwds) UpdateUser(user, passwd string, algo Hasher) error {
|
|
|
|
|
|
|
|
|
|
// DeleteUser deletes the named user from the named file
|
|
|
|
|
func DeleteUser(file, user string) error { |
|
|
|
|
pp, err := ParseFile(file) |
|
|
|
|
pp, err := ParseHtpasswdFile(file) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
@ -136,16 +127,13 @@ func DeleteUser(file, user string) error {
|
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return pp.WriteFile(file) |
|
|
|
|
return pp.Write(file) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// DeleteUser deletes the named user from the named file
|
|
|
|
|
func (pp Passwds) DeleteUser(user string) error { |
|
|
|
|
if _, exists := pp[user]; !exists { |
|
|
|
|
return &UserError{ |
|
|
|
|
Name: user, |
|
|
|
|
Err: ErrNotExists, |
|
|
|
|
} |
|
|
|
|
return fmt.Errorf("user %s does not exist", user) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
delete(pp, user) |
|
|
|
@ -155,7 +143,7 @@ func (pp Passwds) DeleteUser(user string) error {
|
|
|
|
|
// VerifyUser will check if the given user and password are matching
|
|
|
|
|
// with the content of the given file
|
|
|
|
|
func VerifyUser(file, user, passwd string) error { |
|
|
|
|
pp, err := ParseFile(file) |
|
|
|
|
pp, err := ParseHtpasswdFile(file) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
@ -166,25 +154,17 @@ func VerifyUser(file, user, passwd string) error {
|
|
|
|
|
// with the given Passwd object
|
|
|
|
|
func (pp Passwds) VerifyUser(user, passwd string) error { |
|
|
|
|
if _, ok := pp[user]; !ok { |
|
|
|
|
return &UserError{ |
|
|
|
|
Name: user, |
|
|
|
|
Err: ErrNotExists, |
|
|
|
|
} |
|
|
|
|
return fmt.Errorf("user %s does not exist", user) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
alg := identifyHash(pp[user]) |
|
|
|
|
if alg == nil { |
|
|
|
|
return &UserError{ |
|
|
|
|
Name: user, |
|
|
|
|
Err: ErrInvalidAlgorithm, |
|
|
|
|
} |
|
|
|
|
alg, err := identifyHash(pp[user]) |
|
|
|
|
if err != nil { |
|
|
|
|
return fmt.Errorf("cannot identify algo %v", alg) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return alg.Match(passwd, pp[user]) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WriteFile will write the Passwds object to the given file
|
|
|
|
|
func (pp Passwds) WriteFile(file string) error { |
|
|
|
|
// Write will cwrite the Passwd object to the given file
|
|
|
|
|
func (pp Passwds) Write(file string) error { |
|
|
|
|
return os.WriteFile(file, pp.Bytes(), os.ModePerm) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -197,38 +177,40 @@ func (pp Passwds) Bytes() []byte {
|
|
|
|
|
return pass |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func identifyHash(h string) Hasher { |
|
|
|
|
func identifyHash(h string) (Hasher, error) { |
|
|
|
|
switch { |
|
|
|
|
case strings.HasPrefix(h, "$2a$"), strings.HasPrefix(h, "$2y$"), |
|
|
|
|
strings.HasPrefix(h, "$2x$"), strings.HasPrefix(h, "$2b$"): |
|
|
|
|
return new(Bcrypt) |
|
|
|
|
return new(Bcrypt), nil |
|
|
|
|
case strings.HasPrefix(h, "$apr1$"): |
|
|
|
|
return new(Apr1) |
|
|
|
|
return new(Apr1), nil |
|
|
|
|
case strings.HasPrefix(h, "{SHA}"): |
|
|
|
|
return new(Sha) |
|
|
|
|
return new(Sha), nil |
|
|
|
|
case strings.HasPrefix(h, "{SSHA}"): |
|
|
|
|
return new(Ssha) |
|
|
|
|
return new(Ssha), nil |
|
|
|
|
case strings.HasPrefix(h, "$5$"): |
|
|
|
|
return new(Sha256) |
|
|
|
|
return new(Sha256), nil |
|
|
|
|
case strings.HasPrefix(h, "$6$"): |
|
|
|
|
return new(Sha512) |
|
|
|
|
default: |
|
|
|
|
return nil |
|
|
|
|
return new(Sha512), nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("unsupported hash algorithm") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func splitLine(line string, lineNumber int) (user, password string, err error) { |
|
|
|
|
user, password, ok := strings.Cut(line, ":") |
|
|
|
|
if !ok { |
|
|
|
|
return "", "", fmt.Errorf("invalid line %v", lineNumber+1) |
|
|
|
|
func validLine(parts []string, lineNumber int, line string) (bool, error) { |
|
|
|
|
var err error |
|
|
|
|
if len(parts) != 2 { |
|
|
|
|
err = errors.New(fmt.Sprintln("invalid line", lineNumber+1, |
|
|
|
|
"unexpected number of parts split by", ":", len(parts), |
|
|
|
|
"instead of 2 in\"", line, "\"")) |
|
|
|
|
return false, err |
|
|
|
|
} |
|
|
|
|
return true, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
user = strings.TrimSpace(user) |
|
|
|
|
password = strings.TrimSpace(password) |
|
|
|
|
|
|
|
|
|
if h := identifyHash(password); h != nil { |
|
|
|
|
return "", "", fmt.Errorf("invalid algorithm on line %v", lineNumber+1) |
|
|
|
|
func trimParts(parts []string) []string { |
|
|
|
|
for i, part := range parts { |
|
|
|
|
parts[i] = strings.Trim(part, " ") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return user, password, nil |
|
|
|
|
return parts |
|
|
|
|
} |
|
|
|
|