From cc8232897ceab7f8dcfb7fce13de6ca75f1bdb74 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Thu, 23 Sep 2021 22:36:41 +0300 Subject: [PATCH] FIFO-based password management --- doc/passwords.texi | 50 ++++++++++++++++++++++++++---------------- gocheese.go | 35 ++++++++++++++++++------------ passwd.go | 54 +++++++++++++++++++++++++++------------------- upload.go | 4 +++- 4 files changed, 87 insertions(+), 56 deletions(-) diff --git a/doc/passwords.texi b/doc/passwords.texi index 18b8f6b..d3d9ad4 100644 --- a/doc/passwords.texi +++ b/doc/passwords.texi @@ -1,27 +1,34 @@ @node Passwords @unnumbered Password authentication -Password authentication is required for packages uploading. -You have to store your authentication data in @option{-passwd} file in -following format: +Password authentication is required for packages uploading. Passwords +are dynamically changed through the FIFO file. You have to create it and +use in @option{-passwd} option: @example -username:hashed-password +$ mkfifo passwd +$ gocheese -passwd passwd ... @end example -Empty lines and having @verb{|#|} at the beginning are skipped. +Then you must feed it newline-separated records in following format: + +@example +username:hashed-password +@end example -Supported hashing algorithms are: +Where @code{hashed-password} is in one of following algorithms: @table @asis @item @url{https://www.argon2i.com/, Argon2i} (recommended one!) To get Argon2i hashed-password you can use any of following tools: + @itemize - @item go get @url{https://github.com/balakhonova/argon2i, - github.com/balakhonova/argon2i} (Go) + @item @code{go get @url{https://github.com/balakhonova/argon2i, + github.com/balakhonova/argon2i}} (Go) @item @url{https://github.com/p-h-c/phc-winner-argon2} (C) @end itemize + Example user @code{foo} with password @code{bar} can have the following password file entry: @@ -33,11 +40,7 @@ foo:$argon2i$v=19$m=32768,t=3,p=4$OGU5MTM3YjVlYzQwZjhkZA$rVn53v6Ckpf7WH0676ZQLr9 You can use your operating system tools: @example -# BSD-based systems: -$ echo -n "password" | sha256 - -# GNU/Linux-based systems -$ echo -n "password" | sha256sum +$ echo -n "password" | `command -v sha256 || echo sha256sum` @end example Example user @code{foo} with password @code{bar} will have the @@ -49,13 +52,22 @@ foo:$sha256$fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 @end table -You can refresh passwords by sending @code{SIGHUP} signal to the working daemon: +To add or update password entry: @example -$ pkill -HUP gocheese -$ kill -HUP `pidof gocheese` -$ svc -h /var/service/gocheese +$ echo foo:$sha256$... > passwd +$ cat passwords.txt > passwd @end example -Before refreshing it's recommended to check @option{-passwd} file with -@option{-passwd-check} option to prevent daemon failure. +To delete login entry use empty password: + +@example +$ echo foo: > passwd +@end example + +You can also check you passwords file with: + +@example +$ gocheese -passwd-check < passwords.txt +$ echo $? +@end example diff --git a/gocheese.go b/gocheese.go index 67bc1c1..cc30de2 100644 --- a/gocheese.go +++ b/gocheese.go @@ -46,7 +46,7 @@ import ( ) const ( - Version = "2.6.0" + Version = "3.0.0" HTMLBegin = ` @@ -99,9 +99,9 @@ var ( gpgUpdateURLPath = flag.String("gpgupdate", "/gpgupdate/", "GPG forceful refreshing URL path") pypiURL = flag.String("pypi", "https://pypi.org/simple/", "Upstream (PyPI) URL") pypiCertHash = flag.String("pypi-cert-hash", "", "Authenticate upstream by its X.509 certificate's SPKI SHA256 hash") - passwdPath = flag.String("passwd", "passwd", "Path to file with authenticators") logTimestamped = flag.Bool("log-timestamped", false, "Prepend timestmap to log messages") - passwdCheck = flag.Bool("passwd-check", false, "Test the -passwd file for syntax errors and exit") + passwdPath = flag.String("passwd", "", "Path to FIFO for upload authentication") + passwdCheck = flag.Bool("passwd-check", false, "Run password checker") fsck = flag.Bool("fsck", false, "Check integrity of all packages (errors are in stderr)") maxClients = flag.Int("maxclients", 128, "Maximal amount of simultaneous clients") version = flag.Bool("version", false, "Print version information") @@ -288,8 +288,24 @@ func main() { } if *passwdCheck { - refreshPasswd() - return + if passwdReader(os.Stdin) { + os.Exit(0) + } else { + os.Exit(1) + } + } + + if *passwdPath != "" { + go func() { + for { + fd, err := os.OpenFile(*passwdPath, os.O_RDONLY, os.FileMode(0666)) + if err != nil { + log.Fatalln(err) + } + passwdReader(fd) + fd.Close() + } + }() } if (*tlsCert != "" && *tlsKey == "") || (*tlsCert == "" && *tlsKey != "") { @@ -301,7 +317,6 @@ func main() { if err != nil { log.Fatalln(err) } - refreshPasswd() tlsConfig := tls.Config{ ClientSessionCache: tls.NewLRUClientSessionCache(16), NextProtos: []string{"h2", "http/1.1"}, @@ -340,17 +355,9 @@ func main() { http.HandleFunc(*gpgUpdateURLPath, handler) } - needsRefreshPasswd := make(chan os.Signal, 0) needsShutdown := make(chan os.Signal, 0) exitErr := make(chan error, 0) - signal.Notify(needsRefreshPasswd, syscall.SIGHUP) signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT) - go func() { - for range needsRefreshPasswd { - log.Println("refreshing passwords") - refreshPasswd() - } - }() go func(s *http.Server) { <-needsShutdown killed = true diff --git a/passwd.go b/passwd.go index f2ff9a4..1a32f70 100644 --- a/passwd.go +++ b/passwd.go @@ -19,14 +19,18 @@ along with this program. If not, see . package main import ( + "bufio" "errors" - "io/ioutil" "log" "os" "strings" + "sync" ) -var passwords map[string]Auther = make(map[string]Auther) +var ( + Passwords map[string]Auther = make(map[string]Auther) + PasswordsM sync.RWMutex +) type Auther interface { Auth(password string) bool @@ -51,33 +55,39 @@ func strToAuther(verifier string) (string, Auther, error) { return algorithm, auther, err } -func refreshPasswd() { - passwd, err := ioutil.ReadFile(*passwdPath) - if os.IsNotExist(err) { - return - } - if err != nil { - log.Fatal(err) - } - passwordsNew := make(map[string]Auther) - for i, credentials := range strings.Split(strings.TrimRight(string(passwd), "\n"), "\n") { - if len(credentials) == 0 || strings.HasPrefix(credentials, "#") { +func passwdReader(fd *os.File) bool { + isGood := true + scanner := bufio.NewScanner(fd) + for scanner.Scan() { + t := scanner.Text() + if len(t) == 0 { continue } - splitted := strings.Split(credentials, ":") + splitted := strings.Split(t, ":") if len(splitted) != 2 { - log.Fatalf("%s:%d: Wrong login:password format", *passwdPath, i) + log.Println("wrong login:password format:", t) + isGood = false + continue } login := splitted[0] - if _, exists := passwordsNew[login]; exists { - log.Fatalf("%s:%d: %s: already exists", *passwdPath, i, login) + passwd := splitted[1] + if passwd == "" { + log.Println("deleting login:", login) + PasswordsM.Lock() + delete(Passwords, login) + PasswordsM.Unlock() + continue } - _, auther, err := strToAuther(splitted[1]) + _, auther, err := strToAuther(passwd) if err != nil { - log.Fatalf("%s:%d: %s: %s", *passwdPath, i, login, err) + log.Println("login:", login, "invalid password:", err) + isGood = false + continue } - passwordsNew[login] = auther - log.Println("added password for:", login) + log.Println("adding password for:", login) + PasswordsM.Lock() + Passwords[login] = auther + PasswordsM.Unlock() } - passwords = passwordsNew + return isGood } diff --git a/upload.go b/upload.go index e46fa6d..e958108 100644 --- a/upload.go +++ b/upload.go @@ -39,7 +39,9 @@ func serveUpload(w http.ResponseWriter, r *http.Request) { http.Error(w, "unauthenticated", http.StatusUnauthorized) return } - auther, ok := passwords[username] + PasswordsM.RLock() + auther, ok := Passwords[username] + PasswordsM.RUnlock() if !ok || !auther.Auth(password) { log.Println(r.RemoteAddr, "unauthenticated", username) http.Error(w, "unauthenticated", http.StatusUnauthorized) -- 2.44.0