2017-06-21 15:23:33 -06:00
|
|
|
package restserver
|
2015-09-19 14:28:43 +02:00
|
|
|
|
|
|
|
/*
|
2017-10-24 17:35:51 +08:00
|
|
|
Original version copied from: github.com/bitly/oauth2_proxy
|
2015-09-19 14:28:43 +02:00
|
|
|
|
|
|
|
MIT License
|
|
|
|
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
|
|
in the Software without restriction, including without limitation the rights
|
|
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
|
|
all copies or substantial portions of the Software.
|
|
|
|
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
|
THE SOFTWARE.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha1"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/csv"
|
|
|
|
"log"
|
2016-11-06 20:09:42 +01:00
|
|
|
"os"
|
2018-02-11 14:54:25 -07:00
|
|
|
"regexp"
|
2017-10-24 17:35:51 +08:00
|
|
|
"sync"
|
|
|
|
"time"
|
2018-02-11 14:54:25 -07:00
|
|
|
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
2015-09-19 14:28:43 +02:00
|
|
|
)
|
|
|
|
|
2017-10-25 18:14:07 +02:00
|
|
|
// CheckInterval represents how often we check for changes in htpasswd file.
|
2017-10-24 18:48:04 +08:00
|
|
|
const CheckInterval = 30 * time.Second
|
|
|
|
|
2016-11-06 11:15:33 +01:00
|
|
|
// Lookup passwords in a htpasswd file. The entries must have been created with -s for SHA encryption.
|
2015-09-19 14:28:43 +02:00
|
|
|
|
2016-11-05 17:18:42 +01:00
|
|
|
// HtpasswdFile is a map for usernames to passwords.
|
2015-09-19 14:28:43 +02:00
|
|
|
type HtpasswdFile struct {
|
2017-10-24 17:48:18 +08:00
|
|
|
mutex sync.Mutex
|
|
|
|
path string
|
|
|
|
stat os.FileInfo
|
2017-10-24 17:35:51 +08:00
|
|
|
throttle chan struct{}
|
2017-10-24 17:48:18 +08:00
|
|
|
Users map[string]string
|
2015-09-19 14:28:43 +02:00
|
|
|
}
|
|
|
|
|
2016-11-06 11:15:33 +01:00
|
|
|
// NewHtpasswdFromFile reads the users and passwords from a htpasswd file and returns them. If an error is encountered,
|
|
|
|
// it is returned, together with a nil-Pointer for the HtpasswdFile.
|
2015-09-19 14:28:43 +02:00
|
|
|
func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) {
|
2017-10-24 17:35:51 +08:00
|
|
|
stat, err := os.Stat(path)
|
2015-09-19 14:28:43 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-10-24 17:35:51 +08:00
|
|
|
|
|
|
|
h := &HtpasswdFile{
|
2017-10-24 17:48:18 +08:00
|
|
|
mutex: sync.Mutex{},
|
|
|
|
path: path,
|
|
|
|
stat: stat,
|
2017-10-24 17:35:51 +08:00
|
|
|
throttle: make(chan struct{}),
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := h.Reload(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-10-24 18:48:04 +08:00
|
|
|
// Start a goroutine that limits reload checks to once per CheckInterval
|
2017-10-24 17:35:51 +08:00
|
|
|
go h.throttleTimer()
|
|
|
|
|
|
|
|
return h, nil
|
|
|
|
}
|
|
|
|
|
2017-10-24 18:48:04 +08:00
|
|
|
// throttleTimer sends at most one message per CheckInterval to throttle file change checks.
|
2017-10-24 17:35:51 +08:00
|
|
|
func (h *HtpasswdFile) throttleTimer() {
|
|
|
|
var check struct{}
|
|
|
|
for {
|
2017-10-24 18:48:04 +08:00
|
|
|
time.Sleep(CheckInterval)
|
2017-10-24 17:35:51 +08:00
|
|
|
h.throttle <- check
|
|
|
|
}
|
2015-09-19 14:28:43 +02:00
|
|
|
}
|
|
|
|
|
2017-10-24 17:35:51 +08:00
|
|
|
// Reload reloads the htpasswd file. If the reload fails, the Users map is not changed and the error is returned.
|
|
|
|
func (h *HtpasswdFile) Reload() error {
|
|
|
|
r, err := os.Open(h.path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
cr := csv.NewReader(r)
|
2016-11-05 17:18:42 +01:00
|
|
|
cr.Comma = ':'
|
|
|
|
cr.Comment = '#'
|
|
|
|
cr.TrimLeadingSpace = true
|
2015-09-19 14:28:43 +02:00
|
|
|
|
2016-11-05 17:18:42 +01:00
|
|
|
records, err := cr.ReadAll()
|
2015-09-19 14:28:43 +02:00
|
|
|
if err != nil {
|
2017-10-25 18:31:34 +02:00
|
|
|
_ = r.Close()
|
2017-10-24 17:35:51 +08:00
|
|
|
return err
|
2015-09-19 14:28:43 +02:00
|
|
|
}
|
2017-10-24 17:35:51 +08:00
|
|
|
users := make(map[string]string)
|
2015-09-19 14:28:43 +02:00
|
|
|
for _, record := range records {
|
2017-10-24 17:35:51 +08:00
|
|
|
users[record[0]] = record[1]
|
2015-09-19 14:28:43 +02:00
|
|
|
}
|
2017-10-24 17:35:51 +08:00
|
|
|
|
|
|
|
// Replace the Users map
|
|
|
|
h.mutex.Lock()
|
|
|
|
h.Users = users
|
|
|
|
h.mutex.Unlock()
|
2017-10-25 18:31:34 +02:00
|
|
|
|
|
|
|
_ = r.Close()
|
2017-10-24 17:35:51 +08:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-10-24 18:48:04 +08:00
|
|
|
// ReloadCheck checks at most once per CheckInterval if the file changed and will reload the file if it did.
|
2017-10-24 17:35:51 +08:00
|
|
|
// It logs errors and successful reloads, and returns an error if any was encountered.
|
|
|
|
func (h *HtpasswdFile) ReloadCheck() error {
|
|
|
|
select {
|
|
|
|
case <-h.throttle:
|
|
|
|
stat, err := os.Stat(h.path)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Could not stat htpasswd file: %v", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
reload := false
|
|
|
|
|
|
|
|
h.mutex.Lock()
|
|
|
|
if stat.ModTime() != h.stat.ModTime() || stat.Size() != h.stat.Size() {
|
|
|
|
reload = true
|
|
|
|
h.stat = stat
|
|
|
|
}
|
|
|
|
h.mutex.Unlock()
|
|
|
|
|
|
|
|
if reload {
|
|
|
|
err := h.Reload()
|
|
|
|
if err == nil {
|
|
|
|
log.Printf("Reloaded htpasswd file")
|
|
|
|
} else {
|
|
|
|
log.Printf("Could not reload htpasswd file: %v", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
// No need to check
|
|
|
|
}
|
|
|
|
return nil
|
2015-09-19 14:28:43 +02:00
|
|
|
}
|
|
|
|
|
2016-11-06 11:15:33 +01:00
|
|
|
// Validate returns true if password matches the stored password for user. If no password for user is stored, or the
|
|
|
|
// password is wrong, false is returned.
|
2015-09-19 14:28:43 +02:00
|
|
|
func (h *HtpasswdFile) Validate(user string, password string) bool {
|
2017-10-24 17:35:51 +08:00
|
|
|
_ = h.ReloadCheck()
|
|
|
|
|
|
|
|
h.mutex.Lock()
|
2015-09-19 14:28:43 +02:00
|
|
|
realPassword, exists := h.Users[user]
|
2017-10-24 17:35:51 +08:00
|
|
|
h.mutex.Unlock()
|
|
|
|
|
2015-09-19 14:28:43 +02:00
|
|
|
if !exists {
|
|
|
|
return false
|
|
|
|
}
|
2018-02-11 14:54:25 -07:00
|
|
|
|
|
|
|
var shaRe = regexp.MustCompile(`^{SHA}`)
|
|
|
|
var bcrRe = regexp.MustCompile(`^\$2b\$`)
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case shaRe.MatchString(realPassword):
|
2015-09-19 14:28:43 +02:00
|
|
|
d := sha1.New()
|
2017-10-25 18:31:34 +02:00
|
|
|
_, _ = d.Write([]byte(password))
|
2015-09-19 14:28:43 +02:00
|
|
|
if realPassword[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) {
|
|
|
|
return true
|
|
|
|
}
|
2018-02-11 14:54:25 -07:00
|
|
|
case bcrRe.MatchString(realPassword):
|
|
|
|
err := bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password))
|
|
|
|
if err == nil {
|
|
|
|
return true
|
|
|
|
}
|
2015-09-19 14:28:43 +02:00
|
|
|
}
|
2018-02-11 14:54:25 -07:00
|
|
|
log.Printf("Invalid htpasswd entry for %s.", user)
|
2015-09-19 14:28:43 +02:00
|
|
|
return false
|
|
|
|
}
|