Compare commits

..

No commits in common. 'master' and 'pr-karasz-refactor' have entirely different histories.

  1. 7
      .vscode/settings.json
  2. 2
      go.mod
  3. 1
      go.sum
  4. 49
      htpasswd/errors.go
  5. 124
      htpasswd/htpasswd.go
  6. 6
      htpasswd/htpasswd_test.go

7
.vscode/settings.json vendored

@ -1,7 +0,0 @@
{
"cSpell.words": [
"hasher",
"htpasswd",
"Passwds"
]
}

2
go.mod

@ -1,6 +1,6 @@
module asciigoat.org/httools module asciigoat.org/httools
go 1.20 go 1.21.1
require github.com/mgechev/revive v1.3.3 require github.com/mgechev/revive v1.3.3

1
go.sum

@ -40,6 +40,7 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

49
htpasswd/errors.go

@ -1,49 +0,0 @@
package htpasswd
import (
"errors"
"strings"
)
var (
// ErrExists indicates the specified user already exists
ErrExists = errors.New("user already exists")
// ErrNotExists indicates the specified user does not exist
ErrNotExists = errors.New("user does not exist")
// ErrInvalidAlgorithm indicates the htpasswd entry doesn't contain a
// valid algorithm signature
ErrInvalidAlgorithm = errors.New("invalid algorithm")
// ErrInvalidPassword indicates the offered password doesn't match
ErrInvalidPassword = errors.New("invalid password")
)
// UserError indicates an error associated to the specified user
type UserError struct {
Name string
Hint string
Err error
}
func (e *UserError) Error() string {
var buf strings.Builder
var reason string
if e.Hint != "" {
reason = e.Hint
} else {
reason = e.Err.Error()
}
if e.Name == "" {
return reason
}
_, _ = buf.WriteString(e.Name)
_, _ = buf.WriteString(": ")
_, _ = buf.WriteString(reason)
return buf.String()
}
func (e *UserError) Unwrap() error {
return e.Err
}

124
htpasswd/htpasswd.go

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

6
htpasswd/htpasswd_test.go

@ -6,7 +6,7 @@ import (
) )
func TestParseHtpasswd(t *testing.T) { func TestParseHtpasswd(t *testing.T) {
passwords, err := Parse([]byte("sha:{SHA}IRRjboXT92QSYXm8lpGPCZUvU1E=\n")) passwords, err := ParseHtpasswd([]byte("sha:{SHA}IRRjboXT92QSYXm8lpGPCZUvU1E=\n"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -51,7 +51,7 @@ func TestVerifyPassword(t *testing.T) {
TestCreateUser(t) TestCreateUser(t)
} }
_, err = ParseFile(f.Name()) _, err = ParseHtpasswdFile(f.Name())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -83,7 +83,7 @@ func TestVerifyUser(t *testing.T) {
TestCreateUser(t) TestCreateUser(t)
} }
pp, err := ParseFile(f.Name()) pp, err := ParseHtpasswdFile(f.Name())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

Loading…
Cancel
Save