Files
amery 0fb985026f htpasswd: clean up parser
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-29 19:14:05 +00:00

235 lines
5.0 KiB
Go

// Package htpasswd contains utilities for manipulating .htpasswd files
package htpasswd
import (
"fmt"
"os"
"strings"
)
// Passwds name => hash
type Passwds map[string]string
// Hasher interface implemented by hash algos
type Hasher interface {
Hash(password string) (string, error)
Match(password, hashedPassword string) error
Name() string
Prefix() string
}
// ParseFile parses a .htpasswd file
// and returns a Passwd type
func ParseFile(file string) (Passwds, error) {
htpasswdBytes, err := os.ReadFile(file)
if err != nil {
return nil, err
}
return Parse(htpasswdBytes)
}
// Parse parses a slice of bytes in htpasswd style
func Parse(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)
if len(line) == 0 {
// skipping empty lines
continue
}
user, password, err := splitLine(line, lineNumber)
if err != nil {
return passwords, err
}
_, exists := passwords[user]
if exists {
err = &UserError{
Name: user,
Err: ErrExists,
}
return passwords, err
}
passwords[user] = password
}
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)
if err != nil {
return err
}
err = pp.CreateUser(user, passwd, algo)
if err != nil {
return err
}
return pp.WriteFile(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,
}
}
h, err := algo.Hash(passwd)
if err != nil {
return err
}
pp[user] = h
return nil
}
// 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)
if err != nil {
return err
}
err = pp.UpdateUser(user, passwd, algo)
if err != nil {
return err
}
return pp.WriteFile(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,
}
}
h, err := algo.Hash(passwd)
if err != nil {
return err
}
pp[user] = h
return nil
}
// DeleteUser deletes the named user from the named file
func DeleteUser(file, user string) error {
pp, err := ParseFile(file)
if err != nil {
return err
}
err = pp.DeleteUser(user)
if err != nil {
return err
}
return pp.WriteFile(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,
}
}
delete(pp, user)
return nil
}
// 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)
if err != nil {
return err
}
return pp.VerifyUser(user, passwd)
}
// VerifyUser will check if the given user and password are matching
// with the given Passwd object
func (pp Passwds) VerifyUser(user, passwd string) error {
if _, ok := pp[user]; !ok {
return &UserError{
Name: user,
Err: ErrNotExists,
}
}
alg := identifyHash(pp[user])
if alg == nil {
return &UserError{
Name: user,
Err: ErrInvalidAlgorithm,
}
}
return alg.Match(passwd, pp[user])
}
// WriteFile will write the Passwds object to the given file
func (pp Passwds) WriteFile(file string) error {
return os.WriteFile(file, pp.Bytes(), os.ModePerm)
}
// Bytes will return the Passwd as a byte slice
func (pp Passwds) Bytes() []byte {
pass := []byte{}
for name, hash := range pp {
pass = append(pass, []byte(name+":"+hash+"\n")...)
}
return pass
}
func identifyHash(h string) Hasher {
switch {
case strings.HasPrefix(h, "$2a$"), strings.HasPrefix(h, "$2y$"),
strings.HasPrefix(h, "$2x$"), strings.HasPrefix(h, "$2b$"):
return new(Bcrypt)
case strings.HasPrefix(h, "$apr1$"):
return new(Apr1)
case strings.HasPrefix(h, "{SHA}"):
return new(Sha)
case strings.HasPrefix(h, "{SSHA}"):
return new(Ssha)
case strings.HasPrefix(h, "$5$"):
return new(Sha256)
case strings.HasPrefix(h, "$6$"):
return new(Sha512)
default:
return nil
}
}
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)
}
user = strings.TrimSpace(user)
password = strings.TrimSpace(password)
if h := identifyHash(password); h != nil {
return "", "", fmt.Errorf("invalid algorithm on line %v", lineNumber+1)
}
return user, password, nil
}