forked from tg123/go-htpasswd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
htpasswd.go
196 lines (168 loc) · 6.22 KB
/
htpasswd.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
// Package htpasswd provides HTTP Basic Authentication using Apache-style htpasswd files
// for the user and password data.
//
// It supports most common hashing systems used over the decades and can be easily extended
// by the programmer to support others. (See the sha.go source file as a guide.)
//
// You will want to use something like...
// myauth := htpasswd.New("./my-htpasswd-file", htpasswd.DefaultSystems, nil)
// ok := myauth.Match(user, password)
// ...to use in your handler code.
// You should read about that nil, as well as Reread() too.
package htpasswd
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"sync"
)
// An EncodedPasswd is created from the encoded password in a password file by a PasswdParser.
//
// The password files consist of lines like "user:passwd-encoding". The user part is stripped off and
// the passwd-encoding part is captured in an EncodedPasswd.
type EncodedPasswd interface {
// Return true if the string matches the password.
// This may cache the result in the case of expensive comparison functions.
MatchesPassword(pw string) bool
}
// PasswdParser examines an encoded password, and if it is formatted correctly and sane, return an
// EncodedPasswd which will recognize it.
//
// If the format is not understood, then return nil
// so that another parser may have a chance. If the format is understood but not sane,
// return an error to prevent other formats from possibly claiming it
//
// You may write and supply one of these functions to support a format (e.g. bcrypt) not
// already included in this package. Use sha.c as a template, it is simple but not too simple.
type PasswdParser func(pw string) (EncodedPasswd, error)
type passwdTable map[string]EncodedPasswd
// A BadLineHandler is used to notice bad lines in a password file. If not nil, it will be
// called for each bad line with a descriptive error. Think about what you do with these, they
// will sometimes contain hashed passwords.
type BadLineHandler func(err error)
// An File encompasses an Apache-style htpasswd file for HTTP Basic authentication
type File struct {
filePath string
mutex sync.Mutex
passwds passwdTable
parsers []PasswdParser
}
// DefaultSystems is an array of PasswdParser including all builtin parsers. Notice that Plain is last, since it accepts anything
var DefaultSystems = []PasswdParser{AcceptMd5, AcceptSha, AcceptBcrypt, AcceptSsha, AcceptCryptSha, AcceptPlain}
// New creates an File from an Apache-style htpasswd file for HTTP Basic Authentication.
//
// The realm is presented to the user in the login dialog.
//
// The filename must exist and be accessible to the process, as well as being a valid htpasswd file.
//
// parsers is a list of functions to handle various hashing systems. In practice you will probably
// just pass htpasswd.DefaultSystems, but you could make your own to explicitly reject some formats or
// implement your own.
//
// bad is a function, which if not nil will be called for each malformed or rejected entry in
// the password file.
func New(filename string, parsers []PasswdParser, bad BadLineHandler) (*File, error) {
bf := File{
filePath: filename,
parsers: parsers,
}
if err := bf.Reload(bad); err != nil {
return nil, err
}
return &bf, nil
}
// NewFromReader is like new but reads from r instead of a named file. Calling
// Reload on the returned File will result in an error; use
// ReloadFromReader instead.
func NewFromReader(r io.Reader, parsers []PasswdParser, bad BadLineHandler) (*File, error) {
bf := File{
parsers: parsers,
}
if err := bf.ReloadFromReader(r, bad); err != nil {
return nil, err
}
return &bf, nil
}
// Match checks the username and password combination to see if it represents
// a valid account from the htpassword file.
func (bf *File) Match(username, password string) bool {
bf.mutex.Lock()
matcher, ok := bf.passwds[username]
bf.mutex.Unlock()
if ok && matcher.MatchesPassword(password) {
// we are good
return true
}
return false
}
// Reload rereads the htpassword file..
// You will need to call this to notice any changes to the password file.
// This function is thread safe. Someone versed in fsnotify might make it
// happen automatically. Likewise you might also connect a SIGHUP handler to
// this function.
func (bf *File) Reload(bad BadLineHandler) error {
// with the file...
f, err := os.Open(bf.filePath)
if err != nil {
return err
}
defer f.Close()
return bf.ReloadFromReader(f, bad)
}
// ReloadFromReader is like Reload but reads credentials from r instead of a named
// file. If File was created by New, it is okay to call Reload and
// ReloadFromReader as desired.
func (bf *File) ReloadFromReader(r io.Reader, bad BadLineHandler) error {
// ... and a new map ...
newPasswdMap := passwdTable{}
// ... for each line ...
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
// ... add it to the map, noting errors along the way
if perr := bf.addHtpasswdUser(&newPasswdMap, line); perr != nil && bad != nil {
bad(perr)
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("Error scanning htpasswd file: %s", err.Error())
}
// .. finally, safely swap in the new map
bf.mutex.Lock()
bf.passwds = newPasswdMap
bf.mutex.Unlock()
return nil
}
// addHtpasswdUser processes a line from an htpasswd file and add it to the user/password map. We may
// encounter some malformed lines, this will not be an error, but we will log them if
// the caller has given us a logger.
func (bf *File) addHtpasswdUser(pwmap *passwdTable, rawLine string) error {
// ignore white space lines
line := strings.TrimSpace(rawLine)
if line == "" {
return nil
}
// split "user:encoding" at colon
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
return fmt.Errorf("malformed line, no colon: %s", line)
}
user := parts[0]
encoding := parts[1]
// give each parser a shot. The first one to produce a matcher wins.
// If one produces an error then stop (to prevent Plain from catching it)
for _, p := range bf.parsers {
matcher, err := p(encoding)
if err != nil {
return err
}
if matcher != nil {
(*pwmap)[user] = matcher
return nil // we are done, we took to first match
}
}
// No one liked this line
return fmt.Errorf("unable to recognize password for %s in %s", user, encoding)
}