asciigoat's .htaccess and .htpasswd parser https://asciigoat.org/httools
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

216 lines
5.2 KiB

// Package htpasswd contains utilities for manipulating .htpasswd files
package htpasswd
import (
"errors"
"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
}
// ParseHtpasswdFile parses a .htpasswd file
// and returns a Passwd type
func ParseHtpasswdFile(file string) (Passwds, error) {
htpasswdBytes, err := os.ReadFile(file)
if err != nil {
return nil, err
}
return ParseHtpasswd(htpasswdBytes)
}
// 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.Trim(line, " ")
if len(line) == 0 {
// skipping empty lines
continue
}
parts := strings.Split(line, ":")
if ok, err := validLine(parts, lineNumber, line); !ok {
return passwords, err
}
parts = trimParts(parts)
_, exists := passwords[parts[0]]
if exists {
err = errors.New("invalid htpasswords file - user " +
parts[0] + " defined more than once")
return passwords, err
}
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 := ParseHtpasswdFile(file)
if err != nil {
return err
}
err = pp.CreateUser(user, passwd, algo)
if err != nil {
return err
}
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 fmt.Errorf("user %s already exists", user)
}
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 := ParseHtpasswdFile(file)
if err != nil {
return err
}
err = pp.UpdateUser(user, passwd, algo)
if err != nil {
return err
}
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 fmt.Errorf("user %s does not exist", user)
}
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 := ParseHtpasswdFile(file)
if err != nil {
return err
}
err = pp.DeleteUser(user)
if err != nil {
return err
}
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 fmt.Errorf("user %s does not exist", user)
}
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 := ParseHtpasswdFile(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 fmt.Errorf("user %s does not exist", user)
}
alg, err := identifyHash(pp[user])
if err != nil {
return fmt.Errorf("cannot identify algo %v", alg)
}
return alg.Match(passwd, pp[user])
}
// 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)
}
// 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, error) {
switch {
case strings.HasPrefix(h, "$2a$"), strings.HasPrefix(h, "$2y$"),
strings.HasPrefix(h, "$2x$"), strings.HasPrefix(h, "$2b$"):
return new(Bcrypt), nil
case strings.HasPrefix(h, "$apr1$"):
return new(Apr1), nil
case strings.HasPrefix(h, "{SHA}"):
return new(Sha), nil
case strings.HasPrefix(h, "{SSHA}"):
return new(Ssha), nil
case strings.HasPrefix(h, "$5$"):
return new(Sha256), nil
case strings.HasPrefix(h, "$6$"):
return new(Sha512), nil
}
return nil, fmt.Errorf("unsupported hash algorithm")
}
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
}
func trimParts(parts []string) []string {
for i, part := range parts {
parts[i] = strings.Trim(part, " ")
}
return parts
}