the login code lives in a library now

This commit is contained in:
Ted Unangst 2019-04-24 23:57:01 -04:00
parent d728004cad
commit 723efee364
4 changed files with 42 additions and 374 deletions

3
go.mod
View File

@ -3,7 +3,8 @@ module humungus.tedunangst.com/r/honk
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
)

9
go.sum
View File

@ -3,13 +3,14 @@ github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
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=

73
honk.go
View File

@ -39,13 +39,9 @@ import (
"time"
"github.com/gorilla/mux"
"humungus.tedunangst.com/r/webs/login"
)
type UserInfo struct {
UserID int64
Username string
}
type WhatAbout struct {
ID int64
Name string
@ -101,14 +97,14 @@ func getInfo(r *http.Request) map[string]interface{} {
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 @@ func homepage(w http.ResponseWriter, r *http.Request) {
} else {
honks = gethonksforuser(u.UserID)
}
templinfo["HonkCSRF"] = GetCSRF("honkhonk", r)
templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
} else {
honks = getpublichonks()
}
@ -414,27 +410,27 @@ func viewuser(w http.ResponseWriter, r *http.Request) {
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 @@ func viewhonk(w http.ResponseWriter, r *http.Request) {
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 honkpage(w http.ResponseWriter, r *http.Request, u *UserInfo, user *WhatAbo
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 @@ func savebonk(w http.ResponseWriter, r *http.Request) {
}
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 @@ func savehonk(w http.ResponseWriter, r *http.Request) {
}
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 @@ func gofish(name string) string {
}
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 @@ type Zonker struct {
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 @@ func killzone(w http.ResponseWriter, r *http.Request) {
}
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 killzone(w http.ResponseWriter, r *http.Request) {
}
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 servefile(w http.ResponseWriter, r *http.Request) {
func serve() {
db := opendatabase()
LoginInit(db)
login.Init(db)
listener, err := openListener()
if err != nil {
@ -1126,7 +1123,7 @@ func serve() {
}
mux := mux.NewRouter()
mux.Use(LoginChecker)
mux.Use(login.Checker)
posters := mux.Methods("POST").Subrouter()
getters := mux.Methods("GET").Subrouter()
@ -1147,22 +1144,22 @@ func serve() {
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 {

331
login.go
View File

@ -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)
}