all repos — honk @ b82c6fd98fb905ceb173ed95d4b52dd2fb874109

my fork of honk

the login code lives in a library now
Ted Unangst tedu@tedunangst.com
Wed, 24 Apr 2019 23:57:01 -0400
commit

b82c6fd98fb905ceb173ed95d4b52dd2fb874109

parent

b5501c4ee6ece99e8f638b9efd566f984ad6ac69

4 files changed, 42 insertions(+), 374 deletions(-)

jump to
M go.modgo.mod

@@ -3,7 +3,8 @@

require ( github.com/gorilla/mux v1.7.1 github.com/mattn/go-runewidth v0.0.4 - golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 + golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 humungus.tedunangst.com/r/go-sqlite3 v1.1.2 + humungus.tedunangst.com/r/webs v0.1.0 )
M go.sumgo.sum

@@ -3,13 +3,14 @@ github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=

github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 h1:bselrhR0Or1vomJZC8ZIjWtbDmn9OYFLX5Ik9alpJpE= -golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d h1:adrbvkTDn9rGnXg2IJDKozEpXXLZN89pdIA+Syt4/u0= +golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= humungus.tedunangst.com/r/go-sqlite3 v1.1.2 h1:bRAXNRZ4VNFRFhhG4tdudK4Lv4ktHQAHEppKlDANUFg= humungus.tedunangst.com/r/go-sqlite3 v1.1.2/go.mod h1:FtEEmQM7U2Ey1TuEEOyY1BmphTZnmiEjPsNLEAkpf/M= +humungus.tedunangst.com/r/webs v0.1.0 h1:TaJBDhgWWL66oK+6aldgn5BdSwTD+9epqhWHoKFc0iI= +humungus.tedunangst.com/r/webs v0.1.0/go.mod h1:6yLLDXBaE4pKURa/3/bxoQPod37uAqc/Kq8J0IopWW0=
M honk.gohonk.go

@@ -39,12 +39,8 @@ "sync"

"time" "github.com/gorilla/mux" + "humungus.tedunangst.com/r/webs/login" ) - -type UserInfo struct { - UserID int64 - Username string -} type WhatAbout struct { ID int64

@@ -101,14 +97,14 @@ templinfo["StyleParam"] = getstyleparam("views/style.css")

templinfo["LocalStyleParam"] = getstyleparam("views/local.css") templinfo["ServerName"] = serverName templinfo["IconName"] = iconName - templinfo["UserInfo"] = GetUserInfo(r) - templinfo["LogoutCSRF"] = GetCSRF("logout", r) + templinfo["UserInfo"] = login.GetUserInfo(r) + templinfo["LogoutCSRF"] = login.GetCSRF("logout", r) return templinfo } func homepage(w http.ResponseWriter, r *http.Request) { templinfo := getInfo(r) - u := GetUserInfo(r) + u := login.GetUserInfo(r) var honks []*Honk if u != nil { if r.URL.Path == "/atme" {

@@ -116,7 +112,7 @@ honks = gethonksforme(u.UserID)

} else { honks = gethonksforuser(u.UserID) } - templinfo["HonkCSRF"] = GetCSRF("honkhonk", r) + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) } else { honks = getpublichonks() }

@@ -414,27 +410,27 @@ WriteJunk(w, j)

return } honks := gethonksbyuser(name) - u := GetUserInfo(r) + u := login.GetUserInfo(r) honkpage(w, r, u, user, honks, "") } func viewhonker(w http.ResponseWriter, r *http.Request) { name := mux.Vars(r)["name"] - u := GetUserInfo(r) + u := login.GetUserInfo(r) honks := gethonksbyhonker(u.UserID, name) honkpage(w, r, u, nil, honks, "honks by honker: " + name) } func viewcombo(w http.ResponseWriter, r *http.Request) { name := mux.Vars(r)["name"] - u := GetUserInfo(r) + u := login.GetUserInfo(r) honks := gethonksbycombo(u.UserID, name) honkpage(w, r, u, nil, honks, "honks by combo: " + name) } func viewconvoy(w http.ResponseWriter, r *http.Request) { c := r.FormValue("c") var userid int64 = -1 - u := GetUserInfo(r) + u := login.GetUserInfo(r) if u != nil { userid = u.UserID }

@@ -512,18 +508,19 @@ w.Header().Set("Content-Type", theonetruename)

WriteJunk(w, j) return } - u := GetUserInfo(r) + u := login.GetUserInfo(r) honkpage(w, r, u, nil, []*Honk{h}, "one honk") } -func honkpage(w http.ResponseWriter, r *http.Request, u *UserInfo, user *WhatAbout, honks []*Honk, infomsg string) { +func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout, +honks []*Honk, infomsg string) { reverbolate(honks) templinfo := getInfo(r) if u != nil { if user != nil && u.Username == user.Name { - templinfo["UserCSRF"] = GetCSRF("saveuser", r) + templinfo["UserCSRF"] = login.GetCSRF("saveuser", r) } - templinfo["HonkCSRF"] = GetCSRF("honkhonk", r) + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) } if u == nil { w.Header().Set("Cache-Control", "max-age=60")

@@ -545,7 +542,7 @@ }

func saveuser(w http.ResponseWriter, r *http.Request) { whatabout := r.FormValue("whatabout") - u := GetUserInfo(r) + u := login.GetUserInfo(r) db := opendatabase() _, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username) if err != nil {

@@ -721,7 +718,7 @@ xonk.XID = fmt.Sprintf("https://%s/u/%s/h/%s", serverName, xonk.Username, xonk.XID)

} convoy := xonk.Convoy - userinfo := GetUserInfo(r) + userinfo := login.GetUserInfo(r) dt := time.Now().UTC() bonk := Honk{

@@ -767,7 +764,7 @@ func zonkit(w http.ResponseWriter, r *http.Request) {

xid := r.FormValue("xid") log.Printf("zonking %s", xid) - userinfo := GetUserInfo(r) + userinfo := login.GetUserInfo(r) stmtZonkIt.Exec(userinfo.UserID, xid) }

@@ -775,7 +772,7 @@ func savehonk(w http.ResponseWriter, r *http.Request) {

rid := r.FormValue("rid") noise := r.FormValue("noise") - userinfo := GetUserInfo(r) + userinfo := login.GetUserInfo(r) dt := time.Now().UTC() xid := xfiltrate()

@@ -909,10 +906,10 @@ http.Redirect(w, r, "/", http.StatusSeeOther)

} func viewhonkers(w http.ResponseWriter, r *http.Request) { - userinfo := GetUserInfo(r) + userinfo := login.GetUserInfo(r) templinfo := getInfo(r) templinfo["Honkers"] = gethonkers(userinfo.UserID) - templinfo["HonkerCSRF"] = GetCSRF("savehonker", r) + templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r) err := readviews.ExecuteTemplate(w, "honkers.html", templinfo) if err != nil { log.Print(err)

@@ -960,7 +957,7 @@ return ""

} func savehonker(w http.ResponseWriter, r *http.Request) { - u := GetUserInfo(r) + u := login.GetUserInfo(r) name := r.FormValue("name") url := r.FormValue("url") peep := r.FormValue("peep")

@@ -1009,7 +1006,7 @@ }

func killzone(w http.ResponseWriter, r *http.Request) { db := opendatabase() - userinfo := GetUserInfo(r) + userinfo := login.GetUserInfo(r) rows, err := db.Query("select name, wherefore from zonkers where userid = ?", userinfo.UserID) if err != nil { log.Printf("err: %s", err)

@@ -1023,7 +1020,7 @@ zonkers = append(zonkers, z)

} templinfo := getInfo(r) templinfo["Zonkers"] = zonkers - templinfo["KillCSRF"] = GetCSRF("killitwithfire", r) + templinfo["KillCSRF"] = login.GetCSRF("killitwithfire", r) err = readviews.ExecuteTemplate(w, "zonkers.html", templinfo) if err != nil { log.Print(err)

@@ -1031,7 +1028,7 @@ }

} func killitwithfire(w http.ResponseWriter, r *http.Request) { - userinfo := GetUserInfo(r) + userinfo := login.GetUserInfo(r) wherefore := r.FormValue("wherefore") name := r.FormValue("name") if name == "" {

@@ -1099,7 +1096,7 @@ }

func serve() { db := opendatabase() - LoginInit(db) + login.Init(db) listener, err := openListener() if err != nil {

@@ -1126,7 +1123,7 @@ savedstyleparams[s] = getstyleparam(s)

} mux := mux.NewRouter() - mux.Use(LoginChecker) + mux.Use(login.Checker) posters := mux.Methods("POST").Subrouter() getters := mux.Methods("GET").Subrouter()

@@ -1147,22 +1144,22 @@

getters.HandleFunc("/style.css", servecss) getters.HandleFunc("/local.css", servecss) getters.HandleFunc("/login", servehtml) - posters.HandleFunc("/dologin", dologin) - getters.HandleFunc("/logout", dologout) + posters.HandleFunc("/dologin", login.LoginFunc) + getters.HandleFunc("/logout", login.LogoutFunc) loggedin := mux.NewRoute().Subrouter() - loggedin.Use(LoginRequired) + loggedin.Use(login.Required) loggedin.HandleFunc("/atme", homepage) loggedin.HandleFunc("/killzone", killzone) - loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk))) - loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk))) - loggedin.Handle("/zonkit", CSRFWrap("honkhonk", http.HandlerFunc(zonkit))) - loggedin.Handle("/killitwithfire", CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire))) - loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser))) + loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk))) + loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk))) + loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit))) + loggedin.Handle("/killitwithfire", login.CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire))) + loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser))) loggedin.HandleFunc("/honkers", viewhonkers) loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker) loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", viewcombo) - loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker))) + loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker))) err = http.Serve(listener, mux) if err != nil {
D login.go

@@ -1,331 +0,0 @@

-// -// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> -// -// Permission to use, copy, modify, and distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -package main - -import ( - "context" - "crypto/rand" - "crypto/sha512" - "crypto/subtle" - "database/sql" - "fmt" - "hash" - "io" - "log" - "net/http" - "reflect" - "regexp" - "strings" - "sync" - "time" - - "golang.org/x/crypto/bcrypt" -) - -type keytype struct{} - -var thekey keytype - -func LoginChecker(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userinfo, ok := checkauthcookie(r) - if ok { - ctx := context.WithValue(r.Context(), thekey, userinfo) - r = r.WithContext(ctx) - } - handler.ServeHTTP(w, r) - }) -} - -func LoginRequired(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ok := GetUserInfo(r) != nil - if !ok { - loginredirect(w, r) - return - } - handler.ServeHTTP(w, r) - }) -} - -func GetUserInfo(r *http.Request) *UserInfo { - userinfo, ok := r.Context().Value(thekey).(*UserInfo) - if !ok { - return nil - } - return userinfo -} - -func calculateCSRF(salt, action, auth string) string { - hasher := sha512.New512_256() - zero := []byte{0} - hasher.Write(zero) - hasher.Write([]byte(auth)) - hasher.Write(zero) - hasher.Write([]byte(csrfkey)) - hasher.Write(zero) - hasher.Write([]byte(salt)) - hasher.Write(zero) - hasher.Write([]byte(action)) - hasher.Write(zero) - hash := hexsum(hasher) - - return salt + hash -} - -func GetCSRF(action string, r *http.Request) string { - auth := getauthcookie(r) - if auth == "" { - return "" - } - hasher := sha512.New512_256() - io.CopyN(hasher, rand.Reader, 32) - salt := hexsum(hasher) - - return calculateCSRF(salt, action, auth) -} - -func CheckCSRF(action string, r *http.Request) bool { - auth := getauthcookie(r) - if auth == "" { - return false - } - csrf := r.FormValue("CSRF") - if len(csrf) != authlen*2 { - return false - } - salt := csrf[0:authlen] - rv := calculateCSRF(salt, action, auth) - ok := subtle.ConstantTimeCompare([]byte(rv), []byte(csrf)) == 1 - return ok -} - -func CSRFWrap(action string, handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ok := CheckCSRF(action, r) - if !ok { - http.Error(w, "invalid csrf", 403) - return - } - handler.ServeHTTP(w, r) - }) -} - -func loginredirect(w http.ResponseWriter, r *http.Request) { - http.SetCookie(w, &http.Cookie{ - Name: "auth", - Value: "", - MaxAge: -1, - Secure: securecookies, - HttpOnly: true, - }) - http.Redirect(w, r, "/login", http.StatusSeeOther) -} - -var authregex = regexp.MustCompile("^[[:alnum:]]+$") -var authlen = 32 - -var stmtUserName, stmtUserAuth, stmtSaveAuth, stmtDeleteAuth *sql.Stmt -var csrfkey string -var securecookies bool - -func LoginInit(db *sql.DB) { - var err error - stmtUserName, err = db.Prepare("select userid, hash from users where username = ?") - if err != nil { - log.Fatal(err) - } - var userinfo UserInfo - t := reflect.TypeOf(userinfo) - var fields []string - for i := 0; i < t.NumField(); i++ { - f := t.Field(i) - fields = append(fields, strings.ToLower(f.Name)) - } - stmtUserAuth, err = db.Prepare(fmt.Sprintf("select %s from users where userid = (select userid from auth where hash = ?)", strings.Join(fields, ", "))) - if err != nil { - log.Fatal(err) - } - stmtSaveAuth, err = db.Prepare("insert into auth (userid, hash) values (?, ?)") - if err != nil { - log.Fatal(err) - } - stmtDeleteAuth, err = db.Prepare("delete from auth where userid = ?") - if err != nil { - log.Fatal(err) - } - debug := false - getconfig("debug", &debug) - securecookies = !debug - getconfig("csrfkey", &csrfkey) -} - -var authinprogress = make(map[string]bool) -var authprogressmtx sync.Mutex - -func rateandwait(username string) bool { - authprogressmtx.Lock() - defer authprogressmtx.Unlock() - if authinprogress[username] { - return false - } - authinprogress[username] = true - go func(name string) { - time.Sleep(1 * time.Second / 2) - authprogressmtx.Lock() - authinprogress[name] = false - authprogressmtx.Unlock() - }(username) - return true -} - -func getauthcookie(r *http.Request) string { - cookie, err := r.Cookie("auth") - if err != nil { - return "" - } - auth := cookie.Value - if !(len(auth) == authlen && authregex.MatchString(auth)) { - log.Printf("login: bad auth: %s", auth) - return "" - } - return auth -} - -func checkauthcookie(r *http.Request) (*UserInfo, bool) { - auth := getauthcookie(r) - if auth == "" { - return nil, false - } - hasher := sha512.New512_256() - hasher.Write([]byte(auth)) - authhash := hexsum(hasher) - row := stmtUserAuth.QueryRow(authhash) - var userinfo UserInfo - v := reflect.ValueOf(&userinfo).Elem() - var ptrs []interface{} - for i := 0; i < v.NumField(); i++ { - f := v.Field(i) - ptrs = append(ptrs, f.Addr().Interface()) - } - err := row.Scan(ptrs...) - if err != nil { - if err == sql.ErrNoRows { - log.Printf("login: no auth found") - } else { - log.Printf("login: error scanning auth row: %s", err) - } - return nil, false - } - return &userinfo, true -} - -func loaduser(username string) (int64, string, bool) { - row := stmtUserName.QueryRow(username) - var userid int64 - var hash string - err := row.Scan(&userid, &hash) - if err != nil { - if err == sql.ErrNoRows { - log.Printf("login: no username found") - } else { - log.Printf("login: error loading username: %s", err) - } - return -1, "", false - } - return userid, hash, true -} - -var userregex = regexp.MustCompile("^[[:alnum:]]+$") -var userlen = 32 -var passlen = 128 - -func hexsum(h hash.Hash) string { - return fmt.Sprintf("%x", h.Sum(nil))[0:authlen] -} - -func dologin(w http.ResponseWriter, r *http.Request) { - username := r.FormValue("username") - password := r.FormValue("password") - - if len(username) == 0 || len(username) > userlen || - !userregex.MatchString(username) || len(password) == 0 || - len(password) > passlen { - log.Printf("login: invalid password attempt") - loginredirect(w, r) - return - } - userid, hash, ok := loaduser(username) - if !ok { - loginredirect(w, r) - return - } - - if !rateandwait(username) { - loginredirect(w, r) - return - } - - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - if err != nil { - log.Printf("login: incorrect password") - loginredirect(w, r) - return - } - hasher := sha512.New512_256() - io.CopyN(hasher, rand.Reader, 32) - hash = hexsum(hasher) - - http.SetCookie(w, &http.Cookie{ - Name: "auth", - Value: hash, - MaxAge: 3600 * 24 * 30, - Secure: securecookies, - HttpOnly: true, - }) - - hasher.Reset() - hasher.Write([]byte(hash)) - authhash := hexsum(hasher) - - _, err = stmtSaveAuth.Exec(userid, authhash) - if err != nil { - log.Printf("error saving auth: %s", err) - } - - log.Printf("login: successful login") - http.Redirect(w, r, "/", http.StatusSeeOther) -} - -func dologout(w http.ResponseWriter, r *http.Request) { - userinfo, ok := checkauthcookie(r) - if ok && CheckCSRF("logout", r) { - _, err := stmtDeleteAuth.Exec(userinfo.UserID) - if err != nil { - log.Printf("login: error deleting old auth: %s", err) - http.Redirect(w, r, "/", http.StatusSeeOther) - return - } - http.SetCookie(w, &http.Cookie{ - Name: "auth", - Value: "", - MaxAge: -1, - Secure: securecookies, - HttpOnly: true, - }) - } - http.Redirect(w, r, "/", http.StatusSeeOther) -}