]> Cypherpunks.ru repositories - gocheese.git/commitdiff
FIFO-based password management
authorSergey Matveev <stargrave@stargrave.org>
Thu, 23 Sep 2021 19:36:41 +0000 (22:36 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Thu, 23 Sep 2021 21:07:07 +0000 (00:07 +0300)
doc/passwords.texi
gocheese.go
passwd.go
upload.go

index 18b8f6b611f0cbcd87aa61f4b4b86b2cfde670ee..d3d9ad4039b09092c0e5969020a8f230359e3850 100644 (file)
@@ -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
index 67bc1c1c40ff157a9fec2edf033689106ef700c8..cc30de23dfc84596f7980a2fef81fc370827521c 100644 (file)
@@ -46,7 +46,7 @@ import (
 )
 
 const (
-       Version   = "2.6.0"
+       Version   = "3.0.0"
        HTMLBegin = `<!DOCTYPE html>
 <html>
   <head>
@@ -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
index f2ff9a472193b1b642eb5d1e8c010acea2217fc7..1a32f70269b16f2d2de2b103b7b542025387144a 100644 (file)
--- a/passwd.go
+++ b/passwd.go
@@ -19,14 +19,18 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 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
 }
index e46fa6db619715d3ecd85833648f120aeeedcdc4..e9581081da4026261a8429daaa50b184db36399e 100644 (file)
--- 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)