Refactor session to use gorilla like sessions
This commit is contained in:
parent
e99a604095
commit
3d9aecf214
@ -22,13 +22,11 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.happydns.org/happyDomain/config"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/storage"
|
||||
)
|
||||
|
||||
@ -47,13 +45,7 @@ func deleteSessions(c *gin.Context) {
|
||||
}
|
||||
|
||||
func sessionHandler(c *gin.Context) {
|
||||
sessionid, err := base64.StdEncoding.DecodeString(c.Param("sessionid"))
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
session, err := storage.MainStore.GetSession(sessionid)
|
||||
session, err := storage.MainStore.GetSession(c.Param("sessionid"))
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
@ -65,13 +57,9 @@ func sessionHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
func getSession(c *gin.Context) {
|
||||
session := c.MustGet("session").(*happydns.Session)
|
||||
|
||||
c.JSON(http.StatusOK, session)
|
||||
c.JSON(http.StatusOK, c.MustGet("session"))
|
||||
}
|
||||
|
||||
func deleteSession(c *gin.Context) {
|
||||
session := c.MustGet("session").(*happydns.Session)
|
||||
|
||||
ApiResponse(c, true, storage.MainStore.DeleteSession(session))
|
||||
ApiResponse(c, true, storage.MainStore.DeleteSession(c.Param("sessionid")))
|
||||
}
|
||||
|
156
api/auth.go
156
api/auth.go
@ -25,9 +25,9 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
@ -50,8 +50,6 @@ type UserClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
const COOKIE_NAME = "happydomain_session"
|
||||
|
||||
var signingMethod = jwt.SigningMethodHS512
|
||||
|
||||
func updateUserFromClaims(user *happydns.User, claims *UserClaims) {
|
||||
@ -98,35 +96,6 @@ func retrieveUserFromClaims(claims *UserClaims) (user *happydns.User, err error)
|
||||
return
|
||||
}
|
||||
|
||||
func retrieveSessionFromClaims(claims *UserClaims, user *happydns.User, session_id []byte) (session *happydns.Session, err error) {
|
||||
session, err = storage.MainStore.GetSession(session_id)
|
||||
if err != nil {
|
||||
// The session doesn't exists yet: create it!
|
||||
session = &happydns.Session{
|
||||
Id: session_id,
|
||||
IdUser: claims.Profile.UserId,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = storage.MainStore.UpdateSession(session)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("has a correct JWT, but an error occured when trying to create the session: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update user's data
|
||||
updateUserFromClaims(user, claims)
|
||||
|
||||
err = storage.MainStore.UpdateUser(user)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("has a correct JWT, session has been created, but an error occured when trying to update the user's information: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func requireLogin(opts *config.Options, c *gin.Context, msg string) {
|
||||
if opts.ExternalAuth.URL != nil {
|
||||
customurl := *opts.ExternalAuth.URL
|
||||
@ -141,17 +110,73 @@ func requireLogin(opts *config.Options, c *gin.Context, msg string) {
|
||||
|
||||
func authMiddleware(opts *config.Options, optional bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var token string
|
||||
// Load user from session
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Retrieve the token from cookie or header
|
||||
if cookie, err := c.Cookie(COOKIE_NAME); err == nil {
|
||||
var userid happydns.Identifier
|
||||
if iu, ok := session.Get("iduser").([]uint8); ok {
|
||||
userid = happydns.Identifier(iu)
|
||||
}
|
||||
|
||||
// Authentication through JWT
|
||||
var token string
|
||||
if c.GetHeader("X-User-Token") != "" {
|
||||
token = c.GetHeader("X-User-Token")
|
||||
} else if cookie, err := c.Cookie("happydomain-account"); err == nil {
|
||||
token = cookie
|
||||
} else if flds := strings.Fields(c.GetHeader("Authorization")); len(flds) == 2 && flds[0] == "Bearer" {
|
||||
token = flds[1]
|
||||
}
|
||||
if len(opts.JWTSecretKey) > 0 && len(token) > 0 {
|
||||
// Validate the token and retrieve claims
|
||||
claims := &UserClaims{}
|
||||
_, err := jwt.ParseWithClaims(token, claims,
|
||||
func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(opts.JWTSecretKey), nil
|
||||
}, jwt.WithValidMethods([]string{signingMethod.Name}))
|
||||
if err != nil {
|
||||
if opts.NoAuth {
|
||||
claims = displayNotAuthToken(opts, c)
|
||||
}
|
||||
|
||||
log.Printf("%s provide a bad JWT claims: %s", c.ClientIP(), err.Error())
|
||||
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
|
||||
return
|
||||
}
|
||||
|
||||
// Check that required fields are filled
|
||||
if claims == nil || len(claims.Profile.UserId) == 0 {
|
||||
log.Printf("%s: no UserId found in JWT claims", c.ClientIP())
|
||||
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Profile.Email == "" {
|
||||
log.Printf("%s: no Email found in JWT claims", c.ClientIP())
|
||||
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve corresponding user
|
||||
user, err := retrieveUserFromClaims(claims)
|
||||
userid = user.Id
|
||||
|
||||
if userid != nil {
|
||||
if userid == nil || userid.IsEmpty() || !userid.Equals(user.Id) {
|
||||
completeAuth(opts, c, claims.Profile)
|
||||
session.Clear()
|
||||
session.Set("iduser", user.Id)
|
||||
err = session.Save()
|
||||
if err != nil {
|
||||
log.Printf("%s: unable to recreate session: %s", c.ClientIP(), err.Error())
|
||||
requireLogin(opts, c, "Something went wrong with your session. Please contact your administrator.")
|
||||
return
|
||||
}
|
||||
userid = user.Id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop here if there is no cookie
|
||||
if len(token) == 0 {
|
||||
if userid == nil {
|
||||
if optional {
|
||||
c.Next()
|
||||
} else {
|
||||
@ -160,67 +185,16 @@ func authMiddleware(opts *config.Options, optional bool) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the token and retrieve claims
|
||||
claims := &UserClaims{}
|
||||
_, err := jwt.ParseWithClaims(token, claims,
|
||||
func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(opts.JWTSecretKey), nil
|
||||
}, jwt.WithValidMethods([]string{signingMethod.Name}))
|
||||
if err != nil {
|
||||
if opts.NoAuth {
|
||||
claims = displayNotAuthToken(opts, c)
|
||||
}
|
||||
|
||||
log.Printf("%s provide a bad JWT claims: %s", c.ClientIP(), err.Error())
|
||||
c.SetCookie(COOKIE_NAME, "", -1, opts.BaseURL+"/", "", opts.DevProxy == "", true)
|
||||
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
|
||||
return
|
||||
}
|
||||
|
||||
// Check that required fields are filled
|
||||
if claims == nil || len(claims.Profile.UserId) == 0 {
|
||||
log.Printf("%s: no UserId found in JWT claims", c.ClientIP())
|
||||
c.SetCookie(COOKIE_NAME, "", -1, opts.BaseURL+"/", "", opts.DevProxy == "", true)
|
||||
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Profile.Email == "" {
|
||||
log.Printf("%s: no Email found in JWT claims", c.ClientIP())
|
||||
c.SetCookie(COOKIE_NAME, "", -1, opts.BaseURL+"/", "", opts.DevProxy == "", true)
|
||||
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve corresponding user
|
||||
user, err := retrieveUserFromClaims(claims)
|
||||
user, err := storage.MainStore.GetUser(userid)
|
||||
if err != nil {
|
||||
log.Printf("%s %s", c.ClientIP(), err.Error())
|
||||
c.SetCookie(COOKIE_NAME, "", -1, opts.BaseURL+"/", "", opts.DevProxy == "", true)
|
||||
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
|
||||
requireLogin(opts, c, "Unable to retrieve your user. Please reauthenticate.")
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("LoggedUser", user)
|
||||
|
||||
// Retrieve the session
|
||||
session_id := append([]byte(claims.Profile.UserId), []byte(claims.ID)...)
|
||||
session, err := retrieveSessionFromClaims(claims, user, session_id)
|
||||
if err != nil {
|
||||
log.Printf("%s %s", c.ClientIP(), err.Error())
|
||||
c.SetCookie(COOKIE_NAME, "", -1, opts.BaseURL+"/", "", opts.DevProxy == "", true)
|
||||
requireLogin(opts, c, "Your session has expired. Please reconnect.")
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("MySession", session)
|
||||
|
||||
// We are now ready to continue
|
||||
c.Next()
|
||||
|
||||
// On return, check if the session has changed
|
||||
if session.HasChanged() {
|
||||
storage.MainStore.UpdateSession(session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.happydns.org/happyDomain/config"
|
||||
@ -44,7 +45,7 @@ type FormState struct {
|
||||
}
|
||||
|
||||
func formDoState(cfg *config.Options, c *gin.Context, fs *FormState, data interface{}, defaultForm func(interface{}) *forms.CustomForm) (form *forms.CustomForm, d map[string]interface{}, err error) {
|
||||
session := c.MustGet("MySession").(*happydns.Session)
|
||||
session := sessions.Default(c)
|
||||
|
||||
csf, ok := data.(forms.CustomSettingsForm)
|
||||
if !ok {
|
||||
@ -55,7 +56,7 @@ func formDoState(cfg *config.Options, c *gin.Context, fs *FormState, data interf
|
||||
}
|
||||
return
|
||||
} else {
|
||||
return csf.DisplaySettingsForm(fs.State, cfg, session, func() string {
|
||||
return csf.DisplaySettingsForm(fs.State, cfg, &session, func() string {
|
||||
return fs.Recall
|
||||
})
|
||||
}
|
||||
|
@ -22,17 +22,17 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"git.happydns.org/happyDomain/config"
|
||||
"git.happydns.org/happyDomain/internal/session"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/storage"
|
||||
)
|
||||
@ -51,7 +51,7 @@ func declareAuthenticationRoutes(opts *config.Options, router *gin.RouterGroup)
|
||||
apiAuthRoutes.Use(authMiddleware(opts, true))
|
||||
|
||||
apiAuthRoutes.GET("", func(c *gin.Context) {
|
||||
if _, exists := c.Get("MySession"); exists {
|
||||
if _, exists := c.Get("LoggedUser"); exists {
|
||||
displayAuthToken(c)
|
||||
} else {
|
||||
displayNotAuthToken(opts, c)
|
||||
@ -140,7 +140,7 @@ func displayNotAuthToken(opts *config.Options, c *gin.Context) *UserClaims {
|
||||
// @Router /auth/logout [post]
|
||||
func logout(opts *config.Options, c *gin.Context) {
|
||||
c.SetCookie(
|
||||
COOKIE_NAME,
|
||||
session.COOKIE_NAME,
|
||||
"",
|
||||
-1,
|
||||
opts.BaseURL+"/",
|
||||
@ -194,7 +194,7 @@ func checkAuth(opts *config.Options, c *gin.Context) {
|
||||
}
|
||||
|
||||
if user.EmailVerification == nil {
|
||||
log.Printf("%s tries to login as %q, but sent an invalid password", c.ClientIP(), lf.Email)
|
||||
log.Printf("%s tries to login as %q, but has not verified email", c.ClientIP(), lf.Email)
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": "Please validate your e-mail address before your first login.", "href": "/email-validation"})
|
||||
return
|
||||
}
|
||||
@ -223,38 +223,17 @@ func checkAuth(opts *config.Options, c *gin.Context) {
|
||||
}
|
||||
|
||||
func completeAuth(opts *config.Options, c *gin.Context, userprofile UserProfile) (*UserClaims, error) {
|
||||
// Issue a new JWT token
|
||||
jti := make([]byte, 16)
|
||||
_, err := rand.Read(jti)
|
||||
session := sessions.Default(c)
|
||||
|
||||
session.Clear()
|
||||
session.Set("iduser", userprofile.UserId)
|
||||
err := session.Save()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read enough random bytes: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iat := jwt.NewNumericDate(time.Now())
|
||||
claims := &UserClaims{
|
||||
return &UserClaims{
|
||||
userprofile,
|
||||
jwt.RegisteredClaims{
|
||||
IssuedAt: iat,
|
||||
ID: base64.StdEncoding.EncodeToString(jti),
|
||||
},
|
||||
}
|
||||
jwtToken := jwt.NewWithClaims(signingMethod, claims)
|
||||
jwtToken.Header["kid"] = "1"
|
||||
|
||||
token, err := jwtToken.SignedString([]byte(opts.JWTSecretKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to sign user claims: %w", err)
|
||||
}
|
||||
|
||||
c.SetCookie(
|
||||
COOKIE_NAME, // name
|
||||
token, // value
|
||||
30*24*3600, // maxAge
|
||||
opts.BaseURL+"/", // path
|
||||
"", // domain
|
||||
opts.DevProxy == "" && opts.ExternalURL.URL.Scheme != "http", // secure
|
||||
true, // httpOnly
|
||||
)
|
||||
|
||||
return claims, nil
|
||||
jwt.RegisteredClaims{},
|
||||
}, nil
|
||||
}
|
||||
|
87
api/users.go
87
api/users.go
@ -30,6 +30,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rrivera/identicon"
|
||||
|
||||
@ -58,6 +59,10 @@ func declareUsersAuthRoutes(opts *config.Options, router *gin.RouterGroup) {
|
||||
router.GET("/session", getSession)
|
||||
router.DELETE("/session", clearSession)
|
||||
|
||||
router.GET("/sessions", getSessions)
|
||||
apiSessionsRoutes := router.Group("/session/:sid")
|
||||
apiSessionsRoutes.DELETE("", deleteSession)
|
||||
|
||||
apiUserRoutes := router.Group("/users/:uid")
|
||||
apiUserRoutes.Use(userHandler)
|
||||
|
||||
@ -449,7 +454,7 @@ func changePassword(opts *config.Options, c *gin.Context) {
|
||||
log.Printf("%s changes password for user %s", c.ClientIP(), user.Email)
|
||||
|
||||
for _, session := range sessions {
|
||||
err = storage.MainStore.DeleteSession(session)
|
||||
err = storage.MainStore.DeleteSession(session.Id)
|
||||
if err != nil {
|
||||
log.Printf("%s: unable to delete session (password changed): %s", c.ClientIP(), err.Error())
|
||||
}
|
||||
@ -507,7 +512,7 @@ func deleteUser(opts *config.Options, c *gin.Context) {
|
||||
log.Printf("%s: deletes user: %s", c.ClientIP(), user.Email)
|
||||
|
||||
for _, session := range sessions {
|
||||
err = storage.MainStore.DeleteSession(session)
|
||||
err = storage.MainStore.DeleteSession(session.Id)
|
||||
if err != nil {
|
||||
log.Printf("%s: unable to delete session (drop account): %s", c.ClientIP(), err.Error())
|
||||
}
|
||||
@ -682,11 +687,17 @@ func recoverUserAccount(c *gin.Context) {
|
||||
// @Security securitydefinitions.basic
|
||||
// @Success 200 {object} happydns.Session
|
||||
// @Failure 401 {object} happydns.Error "Authentication failure"
|
||||
// @Router /sessions [get]
|
||||
// @Router /session [get]
|
||||
func getSession(c *gin.Context) {
|
||||
session := c.MustGet("MySession").(*happydns.Session)
|
||||
session := sessions.Default(c)
|
||||
|
||||
c.JSON(http.StatusOK, session)
|
||||
s, err := storage.MainStore.GetSession(session.ID())
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, s)
|
||||
}
|
||||
|
||||
// clearSession removes the content of the current user's session.
|
||||
@ -700,11 +711,71 @@ func getSession(c *gin.Context) {
|
||||
// @Security securitydefinitions.basic
|
||||
// @Success 204 {null} null
|
||||
// @Failure 401 {object} happydns.Error "Authentication failure"
|
||||
// @Router /sessions [delete]
|
||||
// @Router /session [delete]
|
||||
func clearSession(c *gin.Context) {
|
||||
session := c.MustGet("MySession").(*happydns.Session)
|
||||
session := sessions.Default(c)
|
||||
|
||||
session.ClearSession()
|
||||
session.Clear()
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// getSessions lists the sessions open for the current user.
|
||||
//
|
||||
// @Summary List user's sessions
|
||||
// @Schemes
|
||||
// @Description List the sessions open for the current user
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Success 200 {object} happydns.Session
|
||||
// @Failure 401 {object} happydns.Error "Authentication failure"
|
||||
// @Router /sessions [get]
|
||||
func getSessions(c *gin.Context) {
|
||||
myuser := c.MustGet("LoggedUser").(*happydns.User)
|
||||
|
||||
s, err := storage.MainStore.GetUserSessions(myuser)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, s)
|
||||
}
|
||||
|
||||
// deleteSession delete a session owned by the current user
|
||||
//
|
||||
// @Summary Delete a session owned by the current user.
|
||||
// @Schemes
|
||||
// @Description Delete a session owned by the current user.
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Param sessionId path string true "Session identifier"
|
||||
// @Produce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Success 200 {object} happydns.Session
|
||||
// @Failure 401 {object} happydns.Error "Authentication failure"
|
||||
// @Router /sessions/{sessionId} [delete]
|
||||
func deleteSession(c *gin.Context) {
|
||||
myuser := c.MustGet("LoggedUser").(*happydns.User)
|
||||
|
||||
s, err := storage.MainStore.GetSession(c.Param("sid"))
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if !myuser.Id.Equals(s.IdUser) {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "You are not allowed to drop this session."})
|
||||
return
|
||||
}
|
||||
|
||||
err = storage.MainStore.DeleteSession(c.Param("sid"))
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, nil)
|
||||
}
|
||||
|
@ -24,8 +24,9 @@ package forms // import "git.happydns.org/happyDomain/forms"
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
|
||||
"git.happydns.org/happyDomain/config"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// GenRecallID
|
||||
@ -34,7 +35,7 @@ type GenRecallID func() string
|
||||
// CustomSettingsForm are functions to declare when we want to display a custom user experience when asking for Source's settings.
|
||||
type CustomSettingsForm interface {
|
||||
// DisplaySettingsForm generates the CustomForm corresponding to the asked target state.
|
||||
DisplaySettingsForm(int32, *config.Options, *happydns.Session, GenRecallID) (*CustomForm, map[string]interface{}, error)
|
||||
DisplaySettingsForm(int32, *config.Options, *sessions.Session, GenRecallID) (*CustomForm, map[string]interface{}, error)
|
||||
}
|
||||
|
||||
var (
|
||||
|
9
go.mod
9
go.mod
@ -6,11 +6,16 @@ toolchain go1.22.3
|
||||
|
||||
require (
|
||||
github.com/StackExchange/dnscontrol/v4 v4.3.0
|
||||
github.com/coreos/go-oidc/v3 v3.10.0
|
||||
github.com/fatih/color v1.17.0
|
||||
github.com/gin-contrib/sessions v1.0.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-mail/mail v2.3.1+incompatible
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/gorilla/securecookie v1.1.2
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/miekg/dns v1.1.59
|
||||
github.com/mileusna/useragent v1.3.4
|
||||
github.com/ovh/go-ovh v1.5.1
|
||||
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d
|
||||
github.com/swaggo/files v1.0.1
|
||||
@ -20,6 +25,7 @@ require (
|
||||
github.com/yuin/goldmark v1.7.1
|
||||
go.uber.org/multierr v1.11.0
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/oauth2 v0.20.0
|
||||
)
|
||||
|
||||
require (
|
||||
@ -75,6 +81,7 @@ require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-gandi/go-gandi v0.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
@ -94,6 +101,7 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.4 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
@ -145,7 +153,6 @@ require (
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/oauth2 v0.20.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
|
16
go.sum
16
go.sum
@ -90,6 +90,8 @@ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@ -127,6 +129,8 @@ github.com/getkin/kin-openapi v0.87.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSy
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI=
|
||||
github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
@ -135,6 +139,8 @@ github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T
|
||||
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
|
||||
github.com/go-gandi/go-gandi v0.7.0 h1:gsP33dUspsN1M+ZW9HEgHchK9HiaSkYnltO73RHhSZA=
|
||||
github.com/go-gandi/go-gandi v0.7.0/go.mod h1:9NoYyfWCjFosClPiWjkbbRK5UViaZ4ctpT8/pKSSFlw=
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
|
||||
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@ -216,6 +222,8 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@ -229,7 +237,13 @@ github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoF
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/happyDomain/dnscontrol/v4 v4.11.101 h1:+FvKJRSWYPknhotWzJIuHLOAQ1/e+Si1SlMtvv7RjuI=
|
||||
@ -305,6 +319,8 @@ github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04
|
||||
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||
github.com/mileusna/useragent v1.3.4 h1:MiuRRuvGjEie1+yZHO88UBYg8YBC/ddF6T7F56i3PCk=
|
||||
github.com/mileusna/useragent v1.3.4/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
|
@ -27,10 +27,13 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.happydns.org/happyDomain/api"
|
||||
"git.happydns.org/happyDomain/config"
|
||||
"git.happydns.org/happyDomain/internal/session"
|
||||
"git.happydns.org/happyDomain/storage"
|
||||
"git.happydns.org/happyDomain/ui"
|
||||
)
|
||||
|
||||
@ -48,6 +51,7 @@ func NewApp(cfg *config.Options) App {
|
||||
gin.ForceConsoleColor()
|
||||
router := gin.New()
|
||||
router.Use(gin.Logger(), gin.Recovery())
|
||||
router.Use(sessions.Sessions(session.COOKIE_NAME, session.NewSessionStore(cfg, storage.MainStore, []byte(cfg.JWTSecretKey))))
|
||||
|
||||
api.DeclareRoutes(cfg, router)
|
||||
ui.DeclareRoutes(cfg, router)
|
||||
|
202
internal/session/sessions.go
Normal file
202
internal/session/sessions.go
Normal file
@ -0,0 +1,202 @@
|
||||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2024 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package session // import "git.happydns.org/happyDomain/internal/session"
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ginsessions "github.com/gin-contrib/sessions"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/mileusna/useragent"
|
||||
|
||||
"git.happydns.org/happyDomain/config"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/storage"
|
||||
)
|
||||
|
||||
const COOKIE_NAME = "happydomain_session"
|
||||
|
||||
// SessionStore is an implementation of Gorilla Sessions, using happyDomain storages
|
||||
type SessionStore struct {
|
||||
Codecs []securecookie.Codec
|
||||
options *sessions.Options
|
||||
Path string
|
||||
storage storage.Storage
|
||||
}
|
||||
|
||||
func NewSessionStore(opts *config.Options, storage storage.Storage, keyPairs ...[]byte) *SessionStore {
|
||||
store := &SessionStore{
|
||||
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
||||
options: &sessions.Options{
|
||||
Path: opts.BaseURL + "/",
|
||||
MaxAge: 86400 * 30,
|
||||
Secure: opts.DevProxy == "" && opts.ExternalURL.URL.Scheme != "http",
|
||||
HttpOnly: true,
|
||||
},
|
||||
storage: storage,
|
||||
}
|
||||
store.MaxAge(store.options.MaxAge)
|
||||
return store
|
||||
}
|
||||
|
||||
// Get Fetches a session for a given name after it has been added to the registry.
|
||||
func (s *SessionStore) Get(r *http.Request, name string) (*sessions.Session, error) {
|
||||
return sessions.GetRegistry(r).Get(s, name)
|
||||
}
|
||||
|
||||
// New returns a new session for the given name without adding it to the registry.
|
||||
func (s *SessionStore) New(r *http.Request, name string) (*sessions.Session, error) {
|
||||
session := sessions.NewSession(s, name)
|
||||
options := *s.options
|
||||
session.Options = &options
|
||||
session.IsNew = true
|
||||
|
||||
var token string
|
||||
if cookie, err := r.Cookie(name); err == nil {
|
||||
token = cookie.Value
|
||||
} else if _, ok := r.Header["Authorization"]; ok && len(r.Header["Authorization"]) > 0 {
|
||||
if flds := strings.Fields(r.Header["Authorization"][0]); len(flds) == 2 && flds[0] == "Bearer" {
|
||||
token = flds[1]
|
||||
}
|
||||
}
|
||||
|
||||
if len(token) == 0 {
|
||||
// Cookie not found, this is a new session
|
||||
return session, nil
|
||||
}
|
||||
|
||||
err := securecookie.DecodeMulti(name, token, &session.ID, s.Codecs...)
|
||||
if err != nil {
|
||||
// Value could not be decrypted, consider this is a new session
|
||||
return session, err
|
||||
}
|
||||
|
||||
err = s.load(session)
|
||||
session.IsNew = false
|
||||
return session, err
|
||||
}
|
||||
|
||||
// Save saves the given session into the database and deletes cookies if needed.
|
||||
func (s *SessionStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
|
||||
var cookieValue string
|
||||
if s.options.MaxAge < 0 {
|
||||
s.storage.DeleteSession(session.ID)
|
||||
} else {
|
||||
if session.ID == "" {
|
||||
session.ID = strings.TrimRight(base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)), "=")
|
||||
}
|
||||
encrypted, err := securecookie.EncodeMulti(session.Name(), session.ID, s.Codecs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cookieValue = encrypted
|
||||
|
||||
err = s.save(session, r.UserAgent())
|
||||
}
|
||||
http.SetCookie(w, sessions.NewCookie(session.Name(), cookieValue, session.Options))
|
||||
return nil
|
||||
}
|
||||
|
||||
// MaxAge sets the maximum age for the store and the underlying cookie
|
||||
// implementation. Individual sessions can be deleted by setting Options.MaxAge
|
||||
// = -1 for that session.
|
||||
func (s *SessionStore) MaxAge(age int) {
|
||||
s.options.MaxAge = age
|
||||
|
||||
// Set the maxAge for each securecookie instance.
|
||||
for _, codec := range s.Codecs {
|
||||
if sc, ok := codec.(*securecookie.SecureCookie); ok {
|
||||
sc.MaxAge(age)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SessionStore) Options(options ginsessions.Options) {
|
||||
s.options = options.ToGorillaOptions()
|
||||
}
|
||||
|
||||
func (s *SessionStore) load(session *sessions.Session) error {
|
||||
mysession, err := s.storage.GetSession(session.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return securecookie.DecodeMulti(session.Name(), mysession.Content, &session.Values, s.Codecs...)
|
||||
}
|
||||
|
||||
// save writes encoded session.Values to a database record.
|
||||
func (s *SessionStore) save(session *sessions.Session, ua string) error {
|
||||
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, s.Codecs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
crOn := session.Values["created_on"]
|
||||
exOn := session.Values["expires_on"]
|
||||
|
||||
var expiresOn time.Time
|
||||
|
||||
createdOn, ok := crOn.(time.Time)
|
||||
if !ok {
|
||||
createdOn = time.Now()
|
||||
}
|
||||
|
||||
if exOn == nil {
|
||||
expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
|
||||
} else {
|
||||
expiresOn = exOn.(time.Time)
|
||||
if expiresOn.Sub(time.Now().Add(time.Second*time.Duration(session.Options.MaxAge))) < 0 {
|
||||
expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
|
||||
}
|
||||
}
|
||||
|
||||
var iduser happydns.Identifier
|
||||
if iu, ok := session.Values["iduser"].([]byte); ok {
|
||||
iduser = iu
|
||||
}
|
||||
|
||||
var description string
|
||||
if descr, ok := session.Values["description"].(string); ok {
|
||||
description = descr
|
||||
} else {
|
||||
browser := useragent.Parse(ua)
|
||||
description = fmt.Sprintf("%s on %s", browser.Name, browser.OS)
|
||||
session.Values["description"] = description
|
||||
}
|
||||
|
||||
mysession := &happydns.Session{
|
||||
Id: session.ID,
|
||||
IdUser: iduser,
|
||||
Content: encoded,
|
||||
Description: description,
|
||||
IssuedAt: createdOn,
|
||||
ExpiresOn: expiresOn,
|
||||
ModifiedOn: time.Now(),
|
||||
}
|
||||
|
||||
return s.storage.UpdateSession(mysession)
|
||||
}
|
@ -22,107 +22,34 @@
|
||||
package happydns
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
mrand "math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Session holds informatin about a User's currently connected.
|
||||
type Session struct {
|
||||
// Id is the Session's identifier.
|
||||
Id Identifier `json:"id" swaggertype:"string"`
|
||||
Id string `json:"id"`
|
||||
|
||||
// IdUser is the User's identifier of the Session.
|
||||
IdUser Identifier `json:"login" swaggertype:"string"`
|
||||
|
||||
// Description is a user defined string aims to identify each session.
|
||||
Description string `json:"description"`
|
||||
|
||||
// IssuedAt holds the creation date of the Session.
|
||||
IssuedAt time.Time `json:"time"`
|
||||
|
||||
// ExpiresOn holds the expirate date of the Session.
|
||||
ExpiresOn time.Time `json:"exp"`
|
||||
|
||||
// ModifiedOn is the last time the session has been updated.
|
||||
ModifiedOn time.Time `json:"upd"`
|
||||
|
||||
// Content stores data filled by other modules.
|
||||
Content map[string][]byte `json:"content,omitempty"`
|
||||
|
||||
// changed indicates if Content has changed since its loading.
|
||||
changed bool
|
||||
}
|
||||
|
||||
// NewSession fills a new Session structure.
|
||||
func NewSession(user *User) (s *Session, err error) {
|
||||
session_id := make([]byte, 16)
|
||||
_, err = rand.Read(session_id)
|
||||
if err == nil {
|
||||
s = &Session{
|
||||
Id: session_id,
|
||||
IdUser: user.Id,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// HasChanged tells if the Session has changed since its last loading.
|
||||
func (s *Session) HasChanged() bool {
|
||||
return s.changed
|
||||
}
|
||||
|
||||
// FindNewKey returns a key and an identifier appended to the given
|
||||
// prefix, that is available in the User's Session.
|
||||
func (s *Session) FindNewKey(prefix string) (key string, id int64) {
|
||||
for {
|
||||
// max random id is 2^53 to fit on float64 without loosing precision (JSON limitation)
|
||||
id = mrand.Int63n(1 << 53)
|
||||
key = fmt.Sprintf("%s%d", prefix, id)
|
||||
|
||||
if _, ok := s.Content[key]; !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetValue defines, erase or delete a content to stores at the given
|
||||
// key. If the key is already defined, it erases its content. If the
|
||||
// given value is nil, it deletes the key.
|
||||
func (s *Session) SetValue(key string, value interface{}) {
|
||||
if s.Content == nil && value != nil {
|
||||
s.Content = map[string][]byte{}
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
if s.Content == nil {
|
||||
return
|
||||
} else if _, ok := s.Content[key]; !ok {
|
||||
return
|
||||
} else {
|
||||
delete(s.Content, key)
|
||||
s.changed = true
|
||||
}
|
||||
} else {
|
||||
s.Content[key], _ = json.Marshal(value)
|
||||
s.changed = true
|
||||
}
|
||||
}
|
||||
|
||||
// GetValue retrieves data stored at the given key. Returns true if
|
||||
// the key exists and if the value has been filled correctly.
|
||||
func (s *Session) GetValue(key string, value interface{}) bool {
|
||||
if v, ok := s.Content[key]; !ok {
|
||||
return false
|
||||
} else if json.Unmarshal(v, value) != nil {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// DropKey removes the given key from the Session's Content.
|
||||
func (s *Session) DropKey(key string) {
|
||||
s.SetValue(key, nil)
|
||||
Content string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
// ClearSession removes all content from the Session.
|
||||
func (s *Session) ClearSession() {
|
||||
s.Content = nil
|
||||
s.changed = true
|
||||
s.Content = ""
|
||||
}
|
||||
|
@ -25,11 +25,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/ovh/go-ovh/ovh"
|
||||
|
||||
"git.happydns.org/happyDomain/config"
|
||||
"git.happydns.org/happyDomain/forms"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
func settingsForm(edit bool) *forms.CustomForm {
|
||||
@ -61,7 +61,7 @@ func settingsForm(edit bool) *forms.CustomForm {
|
||||
return form
|
||||
}
|
||||
|
||||
func settingsAskCredentials(cfg *config.Options, recallid string, session *happydns.Session) (*forms.CustomForm, map[string]interface{}, error) {
|
||||
func settingsAskCredentials(cfg *config.Options, recallid string, session *sessions.Session) (*forms.CustomForm, map[string]interface{}, error) {
|
||||
client, err := ovh.NewClient("ovh-eu", appKey, appSecret, "")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Unable to generate Consumer key, as OVH client can't be created: %w", err)
|
||||
@ -89,7 +89,7 @@ func settingsAskCredentials(cfg *config.Options, recallid string, session *happy
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OVHAPI) DisplaySettingsForm(state int32, cfg *config.Options, session *happydns.Session, genRecallId forms.GenRecallID) (*forms.CustomForm, map[string]interface{}, error) {
|
||||
func (s *OVHAPI) DisplaySettingsForm(state int32, cfg *config.Options, session *sessions.Session, genRecallId forms.GenRecallID) (*forms.CustomForm, map[string]interface{}, error) {
|
||||
switch state {
|
||||
case 0:
|
||||
return settingsForm(s.ConsumerKey != ""), nil, nil
|
||||
|
@ -129,7 +129,7 @@ type Storage interface {
|
||||
// SESSIONS ---------------------------------------------------
|
||||
|
||||
// GetSession retrieves the Session with the given identifier.
|
||||
GetSession(id happydns.Identifier) (*happydns.Session, error)
|
||||
GetSession(id string) (*happydns.Session, error)
|
||||
|
||||
// GetAuthUserSessions retrieves all Session for the given AuthUser.
|
||||
GetAuthUserSessions(user *happydns.UserAuth) ([]*happydns.Session, error)
|
||||
@ -137,14 +137,11 @@ type Storage interface {
|
||||
// GetUserSessions retrieves all Session for the given User.
|
||||
GetUserSessions(user *happydns.User) ([]*happydns.Session, error)
|
||||
|
||||
// CreateSession creates a record in the database for the given Session.
|
||||
CreateSession(session *happydns.Session) error
|
||||
|
||||
// UpdateSession updates the fields of the given Session.
|
||||
UpdateSession(session *happydns.Session) error
|
||||
|
||||
// DeleteSession removes the given Session from the database.
|
||||
DeleteSession(session *happydns.Session) error
|
||||
DeleteSession(id string) error
|
||||
|
||||
// ClearSessions deletes all Sessions present in the database.
|
||||
ClearSessions() error
|
||||
|
@ -37,8 +37,8 @@ func (s *LevelDBStorage) getSession(id string) (session *happydns.Session, err e
|
||||
return
|
||||
}
|
||||
|
||||
func (s *LevelDBStorage) GetSession(id happydns.Identifier) (session *happydns.Session, err error) {
|
||||
return s.getSession(fmt.Sprintf("user.session-%s", id.String()))
|
||||
func (s *LevelDBStorage) GetSession(id string) (session *happydns.Session, err error) {
|
||||
return s.getSession(fmt.Sprintf("user.session-%s", id))
|
||||
}
|
||||
|
||||
func (s *LevelDBStorage) GetAuthUserSessions(user *happydns.UserAuth) (sessions []*happydns.Session, err error) {
|
||||
@ -52,7 +52,9 @@ func (s *LevelDBStorage) GetAuthUserSessions(user *happydns.UserAuth) (sessions
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sessions = append(sessions, &s)
|
||||
if s.IdUser.Equals(user.Id) {
|
||||
sessions = append(sessions, &s)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
@ -69,29 +71,20 @@ func (s *LevelDBStorage) GetUserSessions(user *happydns.User) (sessions []*happy
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sessions = append(sessions, &s)
|
||||
if s.IdUser.Equals(user.Id) {
|
||||
sessions = append(sessions, &s)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *LevelDBStorage) CreateSession(session *happydns.Session) error {
|
||||
key, id, err := s.findIdentifierKey("user.session-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.Id = id
|
||||
|
||||
return s.put(key, session)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorage) UpdateSession(session *happydns.Session) error {
|
||||
return s.put(fmt.Sprintf("user.session-%s", session.Id.String()), session)
|
||||
return s.put(fmt.Sprintf("user.session-%s", session.Id), session)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorage) DeleteSession(session *happydns.Session) error {
|
||||
return s.delete(fmt.Sprintf("user.session-%s", session.Id.String()))
|
||||
func (s *LevelDBStorage) DeleteSession(id string) error {
|
||||
return s.delete(fmt.Sprintf("user.session-%s", id))
|
||||
}
|
||||
|
||||
func (s *LevelDBStorage) ClearSessions() error {
|
||||
|
31
storage/leveldb/updates-from-6.go
Normal file
31
storage/leveldb/updates-from-6.go
Normal file
@ -0,0 +1,31 @@
|
||||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2024 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
func migrateFrom6(s *LevelDBStorage) error {
|
||||
log.Println("Drop all sessions to use new format")
|
||||
return s.ClearSessions()
|
||||
}
|
@ -35,6 +35,7 @@ var migrations []LevelDBMigrationFunc = []LevelDBMigrationFunc{
|
||||
migrateFrom3,
|
||||
migrateFrom4,
|
||||
migrateFrom5,
|
||||
migrateFrom6,
|
||||
}
|
||||
|
||||
func (s *LevelDBStorage) DoMigration() (err error) {
|
||||
|
Loading…
Reference in New Issue
Block a user