87 lines
3.6 KiB
Nim
87 lines
3.6 KiB
Nim
# based on https://github.com/planety/prologue/blob/devel/src/prologue/middlewares/sessions/redissession.nim
|
|
# SQLite session storage adapter
|
|
|
|
import std/[options, strtabs, asyncdispatch]
|
|
|
|
from prologue/core/types import len, Session, newSession
|
|
from prologue/core/context import Context, HandlerAsync, getCookie, setCookie, deleteCookie
|
|
from prologue/core/response import addHeader
|
|
from prologue/core/middlewaresbase import switch
|
|
from prologue/core/nativesettings import Settings
|
|
from cookiejar import SameSite
|
|
from strutils import parseBiggestInt
|
|
import tiny_sqlite
|
|
import times
|
|
import json
|
|
import std/jsonutils
|
|
from sequtils import toSeq, map
|
|
import strtabs
|
|
from sugar import `=>`
|
|
|
|
import prologue/core/request
|
|
import prologue/core/types
|
|
|
|
import ./util
|
|
|
|
proc sessionMiddleware*(
|
|
settings: Settings,
|
|
getDB: (proc(): DbConn),
|
|
sessionName = "session",
|
|
maxAge: int = 14 * 24 * 60 * 60, # 14 days, in seconds
|
|
domain = "",
|
|
sameSite = Lax,
|
|
httpOnly = false
|
|
): HandlerAsync =
|
|
return proc(ctx: Context) {.async.} =
|
|
# TODO: this may be invoking dark gods slightly (fix?)
|
|
# the proc only accesses the DB threadvar, but it itself is GC memory
|
|
{.gcsafe.}:
|
|
let db = getDB()
|
|
var sessionIDString = ctx.getCookie(sessionName)
|
|
|
|
# no session ID results in sessionIDString being empty, which will also result in a parse failure
|
|
# this will cause a new session ID to be generated
|
|
var sessionID: int64 = -1
|
|
try:
|
|
sessionID = int64(parseBiggestInt sessionIDString)
|
|
except ValueError: discard
|
|
|
|
var sessionTS = getTime()
|
|
var newlyCreated = false
|
|
|
|
if sessionID != -1:
|
|
# fetch session from database
|
|
let info = db.one("SELECT * FROM sessions WHERE id = ?", sessionID)
|
|
if info.isSome:
|
|
let (sid, ts, data) = info.get().unpack((int64, Time, string))
|
|
sessionTS = ts
|
|
ctx.session = newSession(data = newStringTable(modeCaseSensitive))
|
|
for (key, value) in parseJson(data).jsonTo(seq[(string, string)]):
|
|
ctx.session[key] = value
|
|
else:
|
|
ctx.session = newSession(data = newStringTable(modeCaseSensitive), newCreated = true)
|
|
else:
|
|
ctx.session = newSession(data = newStringTable(modeCaseSensitive), newCreated = true)
|
|
sessionID = randomID()
|
|
|
|
await switch(ctx)
|
|
|
|
if ctx.session.len == 0: # empty or modified (del or clear)
|
|
if ctx.session.modified: # modified
|
|
db.exec("DELETE FROM sessions WHERE id = ?", sessionID)
|
|
ctx.deleteCookie(sessionName, domain = domain) # delete session data in cookie
|
|
return
|
|
elif ctx.session.modified:
|
|
ctx.setCookie(sessionName, $sessionID, maxAge = some(maxAge), domain = domain, sameSite = sameSite, httpOnly = httpOnly)
|
|
|
|
if ctx.session.accessed:
|
|
ctx.response.addHeader("Vary", "Cookie")
|
|
|
|
if ctx.session.modified:
|
|
let serializedSessionData = $(toSeq(ctx.session.data.pairs).map(x => (x.key, x.value)).toJson())
|
|
db.exec("INSERT OR REPLACE INTO sessions VALUES (?, ?, ?)", sessionID, sessionTS, serializedSessionData)
|
|
# garbage collect old sessions
|
|
# TODO: consider checking elsewhere, as not doing so leads to a bit of an exploit where
|
|
# old session IDs can be used for a while
|
|
let oldSessionThreshold = getTime() + initDuration(seconds = -maxAge)
|
|
db.exec("DELETE FROM sessions WHERE timestamp < ?", oldSessionThreshold) |