I'm trying to create a session manager for a asyncrounous http server. I have a table with all created sessions. Each session should be deleted after a certain time if there are no user requests. The following prototype code (I don't know if it's correct) creates the sessions, but then when the timeout occurs it delete the session and stops. How can I check if sessions have already expired at regular intervals and continue processing the requests?
import asynchttpserver, asyncdispatch
import tables
import times
import oids
type
Session = object
map: TableRef[string, string]
request_time: DateTime
callback: proc (id: string): Future[void] {.gcsafe.}
var sessions = newTable[string, Session]()
const sessionTimeout = 30
proc sessions_manager() {.async.} =
while true:
await sleepAsync(1000)
echo "check for sessions timeout..."
for key, value in sessions:
if (now() - sessions[key].request_time).inSeconds > sessionTimeout:
echo "session id timeout:", key
await sessions[key].callback(key)
echo "the session will be deleted:", key
sessions.del(key)
proc cb(req: Request) {.async,gcsafe.} =
echo sessions
proc timeout_session(id: string) {.async.} =
echo "expired session:", id
let session_id = genOid()
var session = Session(
map: newTable[string, string](),
request_time: now(),
callback: timeout_session
)
session.map["login"] = "test"
session.map["password"] = "123456789"
sessions[$session_id] = session
await req.respond(Http200, "Hello World")
proc main() =
let server = newAsyncHttpServer()
discard sessions_manager()
waitFor server.serve(Port(8080), cb)
main()
I would create a browser cookie with a session id and also store the session id somewhere on the server (database, memory, etc) then on every request check if the transmitted session id is still in your datastore.
To invalidate session keys you could register a function with asyncCheck that loops over every stored session in you datastore and removes the sessions that are too old. eg:
proc removeOldSessions(): Future[void] {.async.} =
while true:
# code to invalidate old sessions from manager
await sleepAsync(5_000)
# ....
proc main() =
let server = newAsyncHttpServer()
asyncCheck sessions_manager()
asyncCheck removeOldSessions()
waitFor server.serve(Port(8080), cb)
Also, do not discard futures:
discard sessions_manager() should be: asyncCheck sessions_manager()
i also think you need an sessionManager object that has your sessions stored and that you always pass to each relevant procedures.
Thanks, now it works. This is a prototype. I need to store the session in a cookie and check if it already exists.
import asynchttpserver, asyncdispatch
import tables
import times
import oids
type
Session = object
map: TableRef[string, string]
request_time: DateTime
callback: proc (id: string): Future[void] {.gcsafe.}
var sessions = newTable[string, Session]()
const sessionTimeout = 30
proc sessions_manager(): Future[void] {.async.} =
while true:
await sleepAsync(1000)
echo "check for sessions timeout..."
var to_del = newSeq[string]()
for key, value in sessions:
if (now() - sessions[key].request_time).inSeconds > sessionTimeout:
echo "session id timeout:", key
to_del.add(key)
for key in to_del:
if sessions[key].callback != nil:
await sessions[key].callback(key)
echo "the session will be deleted:", key
sessions.del(key)
proc cb(req: Request) {.async,gcsafe.} =
echo sessions
proc timeout_session(id: string) {.async.} =
echo "expired session:", id
let session_id = genOid()
var session = Session(
map: newTable[string, string](),
request_time: now(),
callback: nil
# callback: timeout_session
)
session.map["login"] = "test"
session.map["password"] = "123456789"
sessions[$session_id] = session
await req.respond(Http200, "Hello World")
proc main() =
let server = newAsyncHttpServer()
asyncCheck sessions_manager()
waitFor server.serve(Port(8080), cb)
main()
When the code is compiled gives the following warning message:
template/generic instantiation of newTable from here
and
Warning: Cannot prove that 'result' is initialized. This will become a compile time error in the future. [ProveInit]
How can I fix this?
$ nim c -r test.nim
...
Hint: endians [Processing]
/home/hdias/Downloads/StatorHTTPServer/test.nim(17, 24) template/generic instantiation of `newTable` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/collections/tables.nim(827, 27) template/generic instantiation of `toTable` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/collections/tables.nim(340, 27) template/generic instantiation of `initTable` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/collections/tables.nim(309, 12) Warning: Cannot prove that 'result' is initialized. This will become a compile time error in the future. [ProveInit]
/home/hdias/Downloads/StatorHTTPServer/test.nim(17, 24) template/generic instantiation of `newTable` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/collections/tables.nim(827, 27) template/generic instantiation of `toTable` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/collections/tables.nim(341, 45) template/generic instantiation of `[]=` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/collections/tableimpl.nim(49, 12) template/generic instantiation of `enlarge` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/collections/tables.nim(270, 10) Warning: Cannot prove that 'n' is initialized. This will become a compile time error in the future. [ProveInit]
/home/hdias/Downloads/StatorHTTPServer/test.nim(17, 24) template/generic instantiation of `newTable` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/collections/tables.nim(826, 7) Warning: Cannot prove that 'result' is initialized. This will become a compile time error in the future. [ProveInit]
/home/hdias/Downloads/StatorHTTPServer/test.nim(17, 24) template/generic instantiation of `newTable` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/collections/tables.nim(810, 7) Warning: Cannot prove that 'result' is initialized. This will become a compile time error in the future. [ProveInit]
/home/hdias/Downloads/StatorHTTPServer/test.nim(42, 8) Hint: 'timeout_session' is declared but not used [XDeclaredButNotUsed]
/home/hdias/Downloads/StatorHTTPServer/test.nim(38, 25) template/generic instantiation of `async` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/asyncmacro.nim(272, 31) Warning: 'cbIter' is not GC-safe as it accesses 'sessions' which is a global using GC'ed memory [GcUnsafe2]
Hint: [Link]
Hint: operation successful (65089 lines compiled; 0.968 sec total; 89.48MiB peakmem; Debug Build) [SuccessX]
Hint: /home/hdias/Downloads/StatorHTTPServer/test [Exec]
I made some changes that corrected several warnings now it works as expected with cookies. But it still shows warnings like these:
Warning: Cannot prove that 'n' is initialized. This will become a compile time error in the future. [ProveInit]
Warning: 'set_session' is not GC-safe as it accesses 'sessions' which is a global using GC'ed memory [GcUnsafe2]
Warning: 'get_session' is not GC-safe as it accesses 'sessions' which is a global using GC'ed memory [GcUnsafe2]
Can someone help me fix these warnings?
import asynchttpserver, asyncdispatch
import tables
import times
import oids
import cookies
import strformat
import strtabs
type
Session = object
id: string
map: TableRef[string, string]
request_time: DateTime
callback: proc (id: string): Future[void] {.gcsafe.}
var sessions: Table[string, Session]
const sessionTimeout = 30
proc sessions_manager(session_timeout: int = 3600): Future[void] {.async.} =
while true:
await sleepAsync(1000)
echo "Number of sessions: ", sessions.len
echo "check for sessions timeout..."
var to_del = newSeq[string]()
for key, value in sessions:
if (now() - sessions[key].request_time).inSeconds > session_timeout:
echo "session id timeout:", key
to_del.add(key)
for key in to_del:
if sessions[key].callback != nil:
await sessions[key].callback(key)
echo "the session will be deleted:", key
sessions.del(key)
proc set_session(): Session {.gcsafe.} =
let session_id = genOid()
return (sessions[$session_id] = Session(
id: $session_id,
map: newTable[string, string](),
request_time: now(),
callback: nil
); sessions[$session_id])
proc get_session(id: string): Session = (sessions[id].request_time = now(); sessions[id])
proc cb(req: Request) {.async,gcsafe.} =
proc timeout_session(id: string) {.async.} =
echo "expired session:", id
var id = ""
if req.headers.hasKey("Cookie"):
let cookies = parseCookies(req.headers["Cookie"])
if cookies.hasKey("session"):
id = cookies["session"]
var session: Session
if id != "" and sessions.hasKey(id):
session = get_session(id)
else:
session = set_session()
session.map["login"] = "test"
session.map["password"] = "123456789"
session.callback = timeout_session
let headers = newHttpHeaders([("Set-Cookie", &"session={session.id}")])
await req.respond(Http200, "Hello World", headers)
proc main() =
let server = newAsyncHttpServer()
asyncCheck sessions_manager(sessionTimeout)
waitFor server.serve(Port(8080), cb)
main()
Code compilation messages
$ nim c test.nim
...
/home/hdias/Downloads/StatorHTTPServer/test.nim(49, 19) template/generic instantiation of `[]=` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/collections/tableimpl.nim(49, 12) template/generic instantiation of `enlarge` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/collections/tables.nim(270, 10) Warning: Cannot prove that 'n' is initialized. This will become a compile time error in the future. [ProveInit]
/home/hdias/Downloads/StatorHTTPServer/test.nim(46, 6) Warning: 'set_session' is not GC-safe as it accesses 'sessions' which is a global using GC'ed memory [GcUnsafe2]
/home/hdias/Downloads/StatorHTTPServer/test.nim(58, 25) template/generic instantiation of `async` from here
/home/hdias/Downloads/StatorHTTPServer/test.nim(56, 6) Warning: 'get_session' is not GC-safe as it accesses 'sessions' which is a global using GC'ed memory [GcUnsafe2]
/home/hdias/Downloads/StatorHTTPServer/test.nim(58, 25) template/generic instantiation of `async` from here
/usr/local/programs/x86_64/nim-1.0.4/lib/pure/asyncmacro.nim(272, 31) Warning: 'cbIter' is not GC-safe as it calls 'get_session' [GcUnsafe2]
Hint: [Link]
Hint: operation successful (66967 lines compiled; 0.960 sec total; 89.562MiB peakmem; Debug Build) [SuccessX]
well, as i've already written, i would not use globals to store sessions. I would create a session object that holds all your data and pass it to every procedure.
so feed the session table into your http callback:
import asynchttpserver, asyncdispatch, tables
type
Session = ref object
## your stuff
Sessions = Table[string, Session]
proc cb(req: Request, sessions: Sessions) {.async.} =
## do stuff
echo sessions.len
proc main() =
let server = newAsyncHttpServer()
var sessions: Table[string, Session]
# ...
waitFor server.serve(
Port(8080),
proc (req: Request): Future[void] = cb(req, sessions)
)
main()
Thank you @enthus1ast and everyone for showing me the way. Now all code compiles without errors or warnings.
import asynchttpserver, asyncdispatch
import tables
import times
import oids
import cookies
import strformat
import strtabs
import strutils
type
Session = ref object
id: string
map: TableRef[string, string]
request_time: DateTime
callback: proc (id: string): Future[void]
type
AsyncHttpSessions = ref object of RootObj
pool: TableRef[string, Session]
session_timeout: int
sleep_time: int
proc sessions_manager(self: AsyncHttpSessions): Future[void] {.async.} =
while true:
await sleepAsync(self.sleep_time)
if self.pool == nil:
continue
echo "Number of active sessions: ", self.pool.len
echo "check for sessions timeout..."
var to_del = newSeq[string]()
for key, value in self.pool:
if (now() - self.pool[key].request_time).inSeconds > self.session_timeout:
echo "session id timeout:", key
to_del.add(key)
for key in to_del:
if self.pool[key].callback != nil:
await self.pool[key].callback(key)
echo "the session will be deleted:", key
self.pool.del(key)
proc set_session(self: AsyncHttpSessions): Session =
let session_id = genOid()
return (self.pool[$session_id] = Session(
id: $session_id,
map: newTable[string, string](),
request_time: now(),
callback: nil
); self.pool[$session_id])
proc get_session(self: AsyncHttpSessions, id: string): Session =
(self.pool[id].request_time = now(); self.pool[id])
proc new_async_http_sessions(
sleep_time: int = 5000,
session_timeout: int = 3600): AsyncHttpSessions =
let self = AsyncHttpSessions()
self.sleep_time = sleep_time
self.session_timeout = session_timeout
self.pool = newTable[string, Session]()
asyncCheck self.sessions_manager()
return self
proc login(): string =
return """
<!Doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
</head>
<body>
<form action="/login" method="post">
Username: <input type="text" name="username" value="test"><br/>
Password: <input type="password" name="password" value="12345678"><br/>
<input type="submit">
</form>
</body>
</html>
"""
proc cb(req: Request, sessions: AsyncHttpSessions) {.async.} =
if req.url.path == "/login" and req.reqMethod == HttpPost:
var params = initTable[string, string]()
for part in req.body.split('&'):
let pair = part.split('=')
if pair.len == 2:
params[pair[0]] = pair[1]
if params.hasKey("username") and params["username"] == "test" and
params.hasKey("password") and params["password"] == "12345678":
proc timeout_session(id: string) {.async.} =
echo "expired session:", id
var session = sessions.set_session()
session.map["username"] = params["username"]
session.map["password"] = params["password"]
session.callback = timeout_session
let headers = newHttpHeaders([("Set-Cookie", &"session={session.id}")])
await req.respond(Http200, &"""Hello User {session.map["username"]}""", headers)
else:
await req.respond(Http200, login())
else:
var id = ""
if req.headers.hasKey("Cookie"):
let cookies = parseCookies(req.headers["Cookie"])
if cookies.hasKey("session"):
id = cookies["session"]
if id != "" and sessions.pool.hasKey(id):
var session = sessions.get_session(id)
await req.respond(Http200, &"""Hello User {session.map["username"]} Again :-)""")
else:
await req.respond(Http200, login())
proc main() =
let server = newAsyncHttpServer()
let sessions = newAsyncHttpSessions(
sleep_time=1000,
session_timeout=30
)
waitFor server.serve(
Port(8080),
proc (req: Request): Future[void] = cb(req, sessions)
)
main()