doc/passwords.texi | 5 ++++- main.go | 13 +++++++------ passwd.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++++---- upload.go | 20 ++++++++------------ usage.go | 1 + diff --git a/doc/passwords.texi b/doc/passwords.texi index 2562ac01c8a03450e00fa17f59838fe57245aa63d29b95814f749216dfc4d2d4..be2f8ff7197502842d35c5b33e59b7e37a4e0eff34ec647e4200dbf7f0ee8bda 100644 --- a/doc/passwords.texi +++ b/doc/passwords.texi @@ -14,7 +14,7 @@ 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: @@ -52,6 +52,9 @@ foo:$sha256$fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 @end verbatim @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: diff --git a/main.go b/main.go index 61c0147cb00ad84c51cf425ff9fd49c86fa09c3a0b95640dd2e60d71562c806a..5689c5c4467fce057ca675c5e15431378178ba092f1cf1a22a565d10410b2ae4 100644 --- a/main.go +++ b/main.go @@ -42,7 +42,7 @@ "golang.org/x/net/netutil" ) const ( - Version = "4.1.0" + Version = "4.2.0" UserAgent = "GoCheese/" + Version ) @@ -66,6 +66,7 @@ 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 @@ server := &http.Server{ 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 376a8e4c3cfe7d4eee18356c5051a9e7168ea0ebd2ffb909bd2af458d9cc7715..4340231e4a11dd2aeba3965159093c751adea0eb8ce9d5c7bf0fe93102b64759 100644 --- a/passwd.go +++ b/passwd.go @@ -18,20 +18,32 @@ 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) { @@ -62,8 +74,8 @@ if len(t) == 0 { 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 @@ log.Println("login:", login, "invalid password:", err) 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 @@ for _, login := range logins { 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 ffadd3ad90f51133837f843632fc016d4d61cb2c04a321f06c968a67121d7248..f2a265b43269f43706561434dd24eb9d110811ad4a6d93ceaa49e10381a65240 100644 --- a/upload.go +++ b/upload.go @@ -36,19 +36,15 @@ 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 @@ } 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 02683bf293046f75a10a118aa31d09f12023595c46664397b43e28cc4957689c..7c73f1ff3d9f72ca17b75841459029561cbfbc01a54831b6168d1cfe32caa4bf 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