xenotime/src/sqlitesession.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)