From 1b2eed9897b7ff701b24b8c8e66dbaddd199451a Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Fri, 14 May 2021 18:24:02 +0200 Subject: [PATCH] Use sessions instead of jwt --- authentication.go | 93 ++++++-------------- captcha.go | 100 +++++++++------------ databaseMigrations.go | 9 ++ go.mod | 9 +- go.sum | 17 ++-- main.go | 1 + sessions.go | 177 ++++++++++++++++++++++++++++++++++++++ templates/captcha.gohtml | 2 +- webmentionVerification.go | 20 +++-- 9 files changed, 282 insertions(+), 146 deletions(-) create mode 100644 sessions.go diff --git a/authentication.go b/authentication.go index c44d337..cf5a3f5 100644 --- a/authentication.go +++ b/authentication.go @@ -7,9 +7,7 @@ import ( "encoding/json" "io" "net/http" - "time" - "github.com/dgrijalva/jwt-go" "github.com/pquerna/otp/totp" ) @@ -19,10 +17,6 @@ func checkCredentials(username, password, totpPasscode string) bool { (appConfig.User.TOTP == "" || totp.Validate(totpPasscode, appConfig.User.TOTP)) } -func checkUsernameTOTP(username string, totp bool) bool { - return username == appConfig.User.Nick && totp == (appConfig.User.TOTP != "") -} - func checkAppPasswords(username, password string) bool { for _, apw := range appConfig.User.AppPasswords { if apw.Username == username && apw.Password == password { @@ -38,22 +32,22 @@ func jwtKey() []byte { func authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check if already logged in + // 1. Check if already logged in if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn { next.ServeHTTP(w, r) return } - // 1. Check BasicAuth (just for app passwords) + // 2. Check BasicAuth (just for app passwords) if username, password, ok := r.BasicAuth(); ok && checkAppPasswords(username, password) { next.ServeHTTP(w, r) return } - // 2. Check JWT - if checkAuthToken(r) { - next.ServeHTTP(w, r) + // 3. Check login cookie + if checkLoginCookie(r) { + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), loggedInKey, true))) return } - // 3. Show login form + // 4. Show login form w.Header().Set("Cache-Control", "no-store,max-age=0") h, _ := json.Marshal(r.Header.Clone()) b, _ := io.ReadAll(io.LimitReader(r.Body, 2000000)) // Only allow 20 Megabyte @@ -74,25 +68,11 @@ func authMiddleware(next http.Handler) http.Handler { }) } -func checkAuthToken(r *http.Request) bool { - if tokenCookie, err := r.Cookie("token"); err == nil { - claims := &authClaims{} - if tkn, err := jwt.ParseWithClaims(tokenCookie.Value, claims, func(t *jwt.Token) (interface{}, error) { - return jwtKey(), nil - }); err == nil && tkn.Valid && - claims.TokenType == "login" && - checkUsernameTOTP(claims.Username, claims.TOTP) { - return true - } - } - return false -} - const loggedInKey requestContextKey = "loggedIn" func checkLoggedIn(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - if checkAuthToken(r) { + if checkLoginCookie(r) { next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), loggedInKey, true))) return } @@ -100,6 +80,16 @@ func checkLoggedIn(next http.Handler) http.Handler { }) } +func checkLoginCookie(r *http.Request) bool { + ses, err := loginSessionsStore.Get(r, "l") + if err == nil && ses != nil { + if login, ok := ses.Values["login"]; ok && login.(bool) { + return true + } + } + return false +} + func checkIsLogin(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if !checkLogin(rw, r) { @@ -134,58 +124,31 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool { req.Header[k] = v } // Cookie - tokenCookie, err := createTokenCookie() + ses, err := loginSessionsStore.Get(r, "l") if err != nil { serveError(w, r, err.Error(), http.StatusInternalServerError) return true } - req.AddCookie(tokenCookie) - http.SetCookie(w, tokenCookie) + ses.Values["login"] = true + cookie, err := loginSessionsStore.SaveGetCookie(r, w, ses) + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return true + } + req.AddCookie(cookie) // Serve original request d.ServeHTTP(w, req) return true } -type authClaims struct { - *jwt.StandardClaims - TokenType string - Username string - TOTP bool -} - -func createTokenCookie() (*http.Cookie, error) { - expiration := time.Now().Add(7 * 24 * time.Hour) - tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &authClaims{ - &jwt.StandardClaims{ExpiresAt: expiration.Unix()}, - "login", - appConfig.User.Nick, - appConfig.User.TOTP != "", - }).SignedString(jwtKey()) - if err != nil { - return nil, err - } - return &http.Cookie{ - Name: "token", - Value: tokenString, - Expires: expiration, - Secure: httpsConfigured(), - HttpOnly: true, - SameSite: http.SameSiteLaxMode, - }, nil -} - // Need to set auth middleware! func serveLogin(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusFound) } func serveLogout(w http.ResponseWriter, r *http.Request) { - http.SetCookie(w, &http.Cookie{ - Name: "token", - MaxAge: -1, - Secure: httpsConfigured(), - HttpOnly: true, - SameSite: http.SameSiteLaxMode, - }) + if ses, err := loginSessionsStore.Get(r, "l"); err == nil && ses != nil { + _ = loginSessionsStore.Delete(r, w, ses) + } http.Redirect(w, r, "/", http.StatusFound) } diff --git a/captcha.go b/captcha.go index 0f08ece..8a57326 100644 --- a/captcha.go +++ b/captcha.go @@ -6,20 +6,16 @@ import ( "encoding/json" "io" "net/http" - "time" "github.com/dchest/captcha" - "github.com/dgrijalva/jwt-go" ) func captchaMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // 1. Check JWT - claims := &captchaClaims{} - if captchaCookie, err := r.Cookie("captcha"); err == nil { - if tkn, err := jwt.ParseWithClaims(captchaCookie.Value, claims, func(t *jwt.Token) (interface{}, error) { - return jwtKey(), nil - }); err == nil && tkn.Valid && claims.TokenType == "captcha" { + // 1. Check Cookie + ses, err := captchaSessionsStore.Get(r, "c") + if err == nil && ses != nil { + if captcha, ok := ses.Values["captcha"]; ok && captcha.(bool) { next.ServeHTTP(w, r) return } @@ -54,59 +50,41 @@ func checkIsCaptcha(next http.Handler) http.Handler { } func checkCaptcha(w http.ResponseWriter, r *http.Request) bool { - if r.Method == http.MethodPost && - r.Header.Get(contentType) == contentTypeWWWForm && - r.FormValue("captchaaction") == "captcha" { - // Do original request - captchabody, _ := base64.StdEncoding.DecodeString(r.FormValue("captchabody")) - req, _ := http.NewRequest(r.FormValue("captchamethod"), r.RequestURI, bytes.NewReader(captchabody)) - // Copy original headers - captchaheaders, _ := base64.StdEncoding.DecodeString(r.FormValue("captchaheaders")) - var headers http.Header - _ = json.Unmarshal(captchaheaders, &headers) - for k, v := range headers { - req.Header[k] = v - } - // Check captcha - if captcha.VerifyString(r.FormValue("captchaid"), r.FormValue("digits")) { - // Create cookie - captchaCookie, err := createCaptchaCookie() - if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return true - } - // Add cookie to original request - req.AddCookie(captchaCookie) - // Send cookie - http.SetCookie(w, captchaCookie) - } - // Serve original request - d.ServeHTTP(w, req) - return true + if r.Method != http.MethodPost { + return false } - return false -} - -type captchaClaims struct { - *jwt.StandardClaims - TokenType string -} - -func createCaptchaCookie() (*http.Cookie, error) { - expiration := time.Now().Add(24 * time.Hour) - tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &captchaClaims{ - &jwt.StandardClaims{ExpiresAt: expiration.Unix()}, - "captcha", - }).SignedString(jwtKey()) - if err != nil { - return nil, err + if r.Header.Get(contentType) != contentTypeWWWForm { + return false } - return &http.Cookie{ - Name: "captcha", - Value: tokenString, - Expires: expiration, - Secure: httpsConfigured(), - HttpOnly: true, - SameSite: http.SameSiteLaxMode, - }, nil + if r.FormValue("captchaaction") != "captcha" { + return false + } + // Prepare original request + captchabody, _ := base64.StdEncoding.DecodeString(r.FormValue("captchabody")) + req, _ := http.NewRequest(r.FormValue("captchamethod"), r.RequestURI, bytes.NewReader(captchabody)) + // Copy original headers + captchaheaders, _ := base64.StdEncoding.DecodeString(r.FormValue("captchaheaders")) + var headers http.Header + _ = json.Unmarshal(captchaheaders, &headers) + for k, v := range headers { + req.Header[k] = v + } + // Check captcha and create cookie + if captcha.VerifyString(r.FormValue("captchaid"), r.FormValue("digits")) { + ses, err := captchaSessionsStore.Get(r, "c") + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return true + } + ses.Values["captcha"] = true + cookie, err := captchaSessionsStore.SaveGetCookie(r, w, ses) + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return true + } + req.AddCookie(cookie) + } + // Serve original request + d.ServeHTTP(w, req) + return true } diff --git a/databaseMigrations.go b/databaseMigrations.go index 1071b28..62f4882 100644 --- a/databaseMigrations.go +++ b/databaseMigrations.go @@ -148,6 +148,15 @@ func migrateDb() error { return err }, }, + &migrator.Migration{ + Name: "00013", + Func: func(tx *sql.Tx) error { + _, err := tx.Exec(` + create table sessions (id integer primary key autoincrement, data text default '', created text default '', modified datetime default '', expires text default ''); + `) + return err + }, + }, ), ) if err != nil { diff --git a/go.mod b/go.mod index f3742da..c5cf1b6 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f github.com/dgraph-io/ristretto v0.0.4-0.20210504190834-0bf2acd73aa3 - github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/elnormous/contenttype v1.0.0 github.com/felixge/httpsnoop v1.0.2 // indirect github.com/go-chi/chi/v5 v5.0.3 @@ -24,6 +23,8 @@ require ( github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe // indirect github.com/gorilla/feeds v1.1.1 github.com/gorilla/handlers v1.5.1 + github.com/gorilla/securecookie v1.1.1 + github.com/gorilla/sessions v1.2.1 github.com/jonboulle/clockwork v0.2.2 // indirect github.com/joncrlsn/dque v0.0.0-20200702023911-3e80e3146ce5 github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9 @@ -39,7 +40,7 @@ require ( github.com/miekg/dns v1.1.42 // indirect github.com/mitchellh/go-server-timing v1.0.1 github.com/mitchellh/mapstructure v1.4.1 // indirect - github.com/pelletier/go-toml v1.9.0 // indirect + github.com/pelletier/go-toml v1.9.1 // indirect github.com/pquerna/otp v1.3.0 github.com/schollz/sqlite3dump v1.2.4 github.com/smartystreets/assertions v1.2.0 // indirect @@ -57,12 +58,12 @@ require ( github.com/yuin/goldmark-emoji v1.0.1 go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.16.0 // indirect - golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf // indirect + golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect golang.org/x/mod v0.4.1 // indirect golang.org/x/net v0.0.0-20210510120150-4163338589ed golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect + golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.62.0 // indirect diff --git a/go.sum b/go.sum index dbb2ed4..f79a796 100644 --- a/go.sum +++ b/go.sum @@ -63,7 +63,6 @@ github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY= github.com/dgraph-io/ristretto v0.0.4-0.20210504190834-0bf2acd73aa3 h1:jU/wpYsEL+8JPLf/QcjkQKI5g0dOjSuwcMjkThxt5x0= github.com/dgraph-io/ristretto v0.0.4-0.20210504190834-0bf2acd73aa3/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= @@ -141,6 +140,10 @@ github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -257,8 +260,8 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.0 h1:NOd0BRdOKpPf0SxkL3HxSQOG7rNh+4kl6PHcBPFs7Q0= -github.com/pelletier/go-toml v1.9.0/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.1 h1:a6qW1EVNZWH9WGI6CsYdD8WAylkoXBS5yv0XHlh17Tc= +github.com/pelletier/go-toml v1.9.1/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -371,8 +374,8 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o= -golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -458,8 +461,8 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/main.go b/main.go index c93bd5f..5312110 100644 --- a/main.go +++ b/main.go @@ -136,6 +136,7 @@ func main() { } initTelegram() initBlogStats() + initSessions() // Start cron hooks startHourlyHooks() diff --git a/sessions.go b/sessions.go new file mode 100644 index 0000000..5c044c7 --- /dev/null +++ b/sessions.go @@ -0,0 +1,177 @@ +package main + +import ( + "database/sql" + "errors" + "fmt" + "log" + "net/http" + "time" + + "github.com/araddon/dateparse" + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" +) + +var loginSessionsStore, captchaSessionsStore *dbSessionStore + +const ( + sessionCreatedOn = "created" + sessionModifiedOn = "modified" + sessionExpiresOn = "expires" +) + +func initSessions() { + deleteExpiredSessions := func() { + if _, err := appDbExec("delete from sessions where expires < @now", + sql.Named("now", time.Now().Local().String())); err != nil { + log.Println("Failed to delete expired sessions:", err.Error()) + } + } + deleteExpiredSessions() + hourlyHooks = append(hourlyHooks, deleteExpiredSessions) + loginSessionsStore = &dbSessionStore{ + codecs: securecookie.CodecsFromPairs(jwtKey()), + options: &sessions.Options{ + Secure: httpsConfigured(), + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + MaxAge: int((7 * 24 * time.Hour).Seconds()), + }, + } + captchaSessionsStore = &dbSessionStore{ + codecs: securecookie.CodecsFromPairs(jwtKey()), + options: &sessions.Options{ + Secure: httpsConfigured(), + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + MaxAge: int((24 * time.Hour).Seconds()), + }, + } +} + +type dbSessionStore struct { + options *sessions.Options + codecs []securecookie.Codec +} + +func (s *dbSessionStore) Get(r *http.Request, name string) (*sessions.Session, error) { + return sessions.GetRegistry(r).Get(s, name) +} + +func (s *dbSessionStore) New(r *http.Request, name string) (session *sessions.Session, err error) { + session = sessions.NewSession(s, name) + opts := *s.options + session.Options = &opts + session.IsNew = true + if cook, errCookie := r.Cookie(name); errCookie == nil { + if err = securecookie.DecodeMulti(name, cook.Value, &session.ID, s.codecs...); err == nil { + session.IsNew = s.load(session) == nil + } + } + return session, err +} + +func (s *dbSessionStore) Save(r *http.Request, w http.ResponseWriter, ss *sessions.Session) error { + _, err := s.SaveGetCookie(r, w, ss) + return err +} + +func (s *dbSessionStore) SaveGetCookie(r *http.Request, w http.ResponseWriter, ss *sessions.Session) (cookie *http.Cookie, err error) { + if ss.ID == "" { + if err = s.insert(ss); err != nil { + return nil, err + } + } else if err = s.save(ss); err != nil { + return nil, err + } + if encoded, err := securecookie.EncodeMulti(ss.Name(), ss.ID, s.codecs...); err != nil { + return nil, err + } else { + cookie = sessions.NewCookie(ss.Name(), encoded, ss.Options) + http.SetCookie(w, cookie) + return cookie, nil + } +} + +func (s *dbSessionStore) Delete(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { + options := *session.Options + options.MaxAge = -1 + http.SetCookie(w, sessions.NewCookie(session.Name(), "", &options)) + for k := range session.Values { + delete(session.Values, k) + } + if _, err := appDbExec("delete from sessions where id = @id", sql.Named("id", session.ID)); err != nil { + return err + } + return nil +} + +func (s *dbSessionStore) load(session *sessions.Session) (err error) { + row, err := appDbQueryRow("select data, created, modified, expires from sessions where id = @id", sql.Named("id", session.ID)) + if err != nil { + return err + } + var data, createdStr, modifiedStr, expiresStr string + if err = row.Scan(&data, &createdStr, &modifiedStr, &expiresStr); err == sql.ErrNoRows { + return nil + } else if err != nil { + return err + } + created, _ := dateparse.ParseLocal(createdStr) + modified, _ := dateparse.ParseLocal(modifiedStr) + expires, _ := dateparse.ParseLocal(expiresStr) + if expires.Before(time.Now()) { + return errors.New("session expired") + } + if err = securecookie.DecodeMulti(session.Name(), data, &session.Values, s.codecs...); err != nil { + return err + } + session.Values[sessionCreatedOn] = created + session.Values[sessionModifiedOn] = modified + session.Values[sessionExpiresOn] = expires + return nil +} + +func (s *dbSessionStore) insert(session *sessions.Session) (err error) { + created := time.Now() + modified := time.Now() + expires := time.Now().Add(time.Second * time.Duration(session.Options.MaxAge)) + delete(session.Values, sessionCreatedOn) + delete(session.Values, sessionExpiresOn) + delete(session.Values, sessionModifiedOn) + encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, s.codecs...) + if err != nil { + return err + } + res, err := appDbExec("insert into sessions(data, created, modified, expires) values(@data, @created, @modified, @expires)", + sql.Named("data", encoded), sql.Named("created", created.Local().String()), sql.Named("modified", modified.Local().String()), sql.Named("expires", expires.Local().String())) + if err != nil { + return err + } + lastInserted, err := res.LastInsertId() + if err != nil { + return err + } + session.ID = fmt.Sprintf("%d", lastInserted) + return nil +} + +func (s *dbSessionStore) save(session *sessions.Session) (err error) { + if session.IsNew { + return s.insert(session) + } + delete(session.Values, sessionCreatedOn) + delete(session.Values, sessionExpiresOn) + delete(session.Values, sessionModifiedOn) + encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, s.codecs...) + if err != nil { + return err + } + _, err = appDbExec("update sessions set data = @data, modified = @modified where id = @id", + sql.Named("data", encoded), sql.Named("modified", time.Now().Local().String()), sql.Named("id", session.ID)) + if err != nil { + return err + } + return nil +} diff --git a/templates/captcha.gohtml b/templates/captcha.gohtml index 95adcd5..badb30e 100644 --- a/templates/captcha.gohtml +++ b/templates/captcha.gohtml @@ -12,7 +12,7 @@ - + diff --git a/webmentionVerification.go b/webmentionVerification.go index 5f17aba..5ef9eac 100644 --- a/webmentionVerification.go +++ b/webmentionVerification.go @@ -2,12 +2,14 @@ package main import ( "bytes" + "context" "database/sql" "errors" "fmt" "io" "log" "net/http" + "net/http/httptest" "net/url" "os" "strings" @@ -77,15 +79,17 @@ func (m *mention) verifyMention() error { if err != nil { return err } - req.Header.Set(userAgent, appUserAgent) + var resp *http.Response if strings.HasPrefix(m.Source, appConfig.Server.PublicAddress) { - // Set authentication - c, _ := createTokenCookie() - req.AddCookie(c) - } - resp, err := appHttpClient.Do(req) - if err != nil { - return err + rec := httptest.NewRecorder() + d.ServeHTTP(rec, req.WithContext(context.WithValue(req.Context(), loggedInKey, true))) + resp = rec.Result() + } else { + req.Header.Set(userAgent, appUserAgent) + resp, err = appHttpClient.Do(req) + if err != nil { + return err + } } err = m.verifyReader(resp.Body) _ = resp.Body.Close()