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()