From c5ca3f66f1b3cd24e55d6e5fc95fdf61cac7aec6 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Thu, 1 Feb 2024 14:16:26 +0300 Subject: [PATCH] -auth-required option and optional :ro per-user attribute --- doc/passwords.texi | 5 ++++- main.go | 13 ++++++----- passwd.go | 55 ++++++++++++++++++++++++++++++++++++++++++---- upload.go | 20 +++++++---------- usage.go | 1 + 5 files changed, 71 insertions(+), 23 deletions(-) diff --git a/doc/passwords.texi b/doc/passwords.texi index 6cdcc0a..fd8bd2e 100644 --- a/doc/passwords.texi +++ b/doc/passwords.texi @@ -14,7 +14,7 @@ $ gocheese -passwd passwd -passwd-list passwd-list ... Then you must feed it newline-separated records in following format: @example -username:hashed-password +username:hashed-password[:ro] @end example Where @code{hashed-password} is in one of following algorithms: @@ -53,6 +53,9 @@ foo:$sha256$fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 @end table +Optional @code{:ro} flag forbids user to upload packages, but allows +read-only access if @option{-auth-required} is enabled. + To add or update password entry: @example diff --git a/main.go b/main.go index bd3734a..cdab06e 100644 --- a/main.go +++ b/main.go @@ -42,7 +42,7 @@ import ( ) const ( - Version = "4.1.0" + Version = "4.2.0" UserAgent = "GoCheese/" + Version ) @@ -66,6 +66,7 @@ var ( PasswdPath = flag.String("passwd", "", "") PasswdListPath = flag.String("passwd-list", "", "") PasswdCheck = flag.Bool("passwd-check", false, "") + AuthRequired = flag.Bool("auth-required", false, "") LogTimestamped = flag.Bool("log-timestamped", false, "") FSCK = flag.Bool("fsck", false, "") @@ -239,11 +240,11 @@ func main() { ReadTimeout: time.Minute, WriteTimeout: time.Minute, } - http.HandleFunc("/", serveHRRoot) - http.HandleFunc("/hr/", serveHRPkg) - http.HandleFunc(*JSONURLPath, serveJSON) - http.HandleFunc(*NoRefreshURLPath, handler) - http.HandleFunc(*RefreshURLPath, handler) + http.HandleFunc("/", checkAuth(serveHRRoot)) + http.HandleFunc("/hr/", checkAuth(serveHRPkg)) + http.HandleFunc(*JSONURLPath, checkAuth(serveJSON)) + http.HandleFunc(*NoRefreshURLPath, checkAuth(handler)) + http.HandleFunc(*RefreshURLPath, checkAuth(handler)) if *DoUCSPI { server.SetKeepAlivesEnabled(false) diff --git a/passwd.go b/passwd.go index 95566e1..e5edc52 100644 --- a/passwd.go +++ b/passwd.go @@ -18,22 +18,34 @@ package main import ( "bufio" + "context" "errors" "log" + "net/http" "os" "strings" "sync" ) var ( - Passwords map[string]Auther = make(map[string]Auther) + Passwords map[string]*User = make(map[string]*User) PasswordsM sync.RWMutex ) +type CtxUserKeyType struct{} + +var CtxUserKey CtxUserKeyType + type Auther interface { Auth(password string) bool } +type User struct { + name string + ro bool + auther Auther +} + func strToAuther(verifier string) (string, Auther, error) { st := strings.SplitN(verifier, "$", 3) if len(st) != 3 || st[0] != "" { @@ -62,8 +74,8 @@ func passwdReader(fd *os.File) bool { continue } splitted := strings.Split(t, ":") - if len(splitted) != 2 { - log.Println("wrong login:password format:", t) + if len(splitted) < 2 { + log.Println("wrong login:password[:ro] format:", t) isGood = false continue } @@ -82,9 +94,20 @@ func passwdReader(fd *os.File) bool { isGood = false continue } + var ro bool + if len(splitted) > 2 { + switch splitted[2] { + case "ro": + ro = true + default: + log.Println("wrong format of optional field:", t) + isGood = false + continue + } + } log.Println("adding password for:", login) PasswordsM.Lock() - Passwords[login] = auther + Passwords[login] = &User{name: login, ro: ro, auther: auther} PasswordsM.Unlock() } return isGood @@ -101,3 +124,27 @@ func passwdLister(fd *os.File) { fd.WriteString(login + "\n") } } + +func checkAuth(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + username, password, gotAuth := r.BasicAuth() + var user *User + if gotAuth { + PasswordsM.RLock() + user = Passwords[username] + PasswordsM.RUnlock() + } + var passwordValid bool + if gotAuth && user != nil { + passwordValid = user.auther.Auth(password) + } + if (gotAuth && user == nil) || + (user != nil && !passwordValid) || + (*AuthRequired && !gotAuth) { + log.Println(r.RemoteAddr, "unauthenticated", username) + http.Error(w, "unauthenticated", http.StatusUnauthorized) + return + } + handler(w, r.WithContext(context.WithValue(r.Context(), CtxUserKey, user))) + } +} diff --git a/upload.go b/upload.go index c053705..7e5ed2a 100644 --- a/upload.go +++ b/upload.go @@ -36,19 +36,15 @@ import ( var NormalizationRe = regexp.MustCompilePOSIX("[-_.]+") func serveUpload(w http.ResponseWriter, r *http.Request) { - // Authentication - username, password, ok := r.BasicAuth() - if !ok { - log.Println(r.RemoteAddr, "unauthenticated", username) - http.Error(w, "unauthenticated", http.StatusUnauthorized) + user := r.Context().Value(CtxUserKey).(*User) + if user == nil { + log.Println(r.RemoteAddr, "unauthorised") + http.Error(w, "unauthorised", http.StatusUnauthorized) return } - 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) + if user.ro { + log.Println(r.RemoteAddr, "ro user", user.name) + http.Error(w, "unauthorised", http.StatusUnauthorized) return } @@ -93,7 +89,7 @@ func serveUpload(w http.ResponseWriter, r *http.Request) { for _, file := range r.MultipartForm.File["content"] { filename := file.Filename - log.Println(r.RemoteAddr, "put", filename, "by", username) + log.Println(r.RemoteAddr, "put", filename, "by", user.name) path := filepath.Join(dirPath, filename) if _, err = os.Stat(path); err == nil { log.Println(r.RemoteAddr, filename, "already exists") diff --git a/usage.go b/usage.go index 825f7d4..7fbedb2 100644 --- a/usage.go +++ b/usage.go @@ -69,6 +69,7 @@ Password management: -passwd PATH -- Path to readable FIFO for loading passwords -passwd-list PATH -- Path to writeable FIFO for listing logins -passwd-check -- Verify passwords format from stdin, then exit + -auth-required -- Require authorisation even for read-only endpoints Other options: -log-timestamped -- Prepend timestamp to log messages -- 2.44.0