mirror of https://github.com/jlelse/GoBlog
Use sessions instead of jwt
This commit is contained in:
parent
d7da17bda1
commit
1b2eed9897
|
@ -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)
|
||||
}
|
||||
|
|
100
captcha.go
100
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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
9
go.mod
9
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
|
||||
|
|
17
go.sum
17
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=
|
||||
|
|
1
main.go
1
main.go
|
@ -136,6 +136,7 @@ func main() {
|
|||
}
|
||||
initTelegram()
|
||||
initBlogStats()
|
||||
initSessions()
|
||||
|
||||
// Start cron hooks
|
||||
startHourlyHooks()
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
<input type="hidden" name="captchaheaders" value="{{ .Data.captchaheaders }}">
|
||||
<input type="hidden" name="captchabody" value="{{ .Data.captchabody }}">
|
||||
<input type="hidden" name="captchaid" value="{{ .Data.captchaid }}">
|
||||
<input type="text" name="digits" placeholder="{{ string .Blog.Lang "captchainstructions" }}">
|
||||
<input type="text" name="digits" placeholder="{{ string .Blog.Lang "captchainstructions" }}" required>
|
||||
<input class="fw" type="submit" value="{{ string .Blog.Lang "submit" }}">
|
||||
</form>
|
||||
</main>
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue