=encoding utf8
=head1 NAME
std/web/session - File and database backed sessions for std/web.
=head1 SYNOPSIS
from std/web import Request, Response;
from std/web/session import FileSessionHandler;
from std/io import Path;
Request.set_session_handler(
new FileSessionHandler(
dir: new Path( path: "/tmp/zsessions" ),
secret: "change-me",
max_age: 6 * 60 * 60,
),
);
function __request__ ( env ) {
let req := new Request( env: env );
let sess := req.session();
sess{data}.set( "seen", sess{data}.get( "seen", 0 ) + 1 );
return new Response(
session: sess.finalize(),
body: [ "seen ", sess{data}{seen}, "\n" ],
);
}
=head1 DESCRIPTION
This module provides server-side session handlers for C<std/web>. The
contracts C<SessionHandler> and C<Session> are defined by C<std/web>;
this module imports those traits and provides file and database storage
implementations.
The browser receives only a signed opaque session id. Session data is
stored server-side as trusted C<std/marshal> data. Applications which
need encrypted or separately authenticated server-side storage should
provide their own C<SessionHandler>.
=head1 EXPORTS
=head2 C<Session>
Concrete session object implementing C<std/web> C<Session>. The public
C<data> slot contains marshalable application data.
=head2 C<FileSessionHandler>
Stores one marshalled blob per session in the configured C<dir>.
=head2 C<DbSessionHandler>
Stores sessions in the configured database handle. The handler creates
and manages a C<zuzu_web_sessions> table.
=head1 COPYRIGHT AND LICENCE
B<< std/web/session >> is copyright Toby Inkster.
It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.
=cut
from std/digest/sha import hmac_sha256_hex;
from std/io import Path;
from std/marshal import dump, load;
from std/secure import SecureRandom;
from std/string import split;
from std/string/base64 import encode, decode;
from std/time import Time;
from std/web import Request, Session as WebSession, SessionHandler;
function _now () {
return ( new Time() ).epoch();
}
function _bin ( value ) {
return value instanceof BinaryString ? value : to_binary( "" _ value );
}
function _valid_id ( id ) {
return id instanceof String and id ~ /^[A-Za-z0-9_-]+$/;
}
function _empty_data ( data ) {
return data instanceof Dict ? data : {};
}
class Session with WebSession {
let data := {};
let String _id := "";
let _handler := null;
let Number _created := 0;
let Number _updated := 0;
let Boolean _secure := false;
let Boolean _finalized := false;
method id () {
return _id;
}
method handler () {
return _handler;
}
method created () {
return _created;
}
method updated () {
return _updated;
}
method secure () {
return _secure;
}
method age () {
return _now() - _created;
}
method is_finalized () {
return _finalized;
}
method finalize () {
if ( not _finalized ) {
_handler.save_session(self);
_finalized := true;
}
return self;
}
method __demolish__ () {
self.finalize() unless _finalized;
}
method cookie_name () {
return _handler._cookie_name();
}
method cookie_value () {
return _handler._signed_cookie_value(_id);
}
method cookie_options () {
return _handler._cookie_options(self);
}
}
class _SessionHandlerBase with SessionHandler {
let secret := null;
let String cookie_name := "zzsession";
let Number max_age := 21600;
let String cookie_path := "/";
let cookie_domain := null;
let String same_site := "Lax";
let secure := null;
let Boolean http_only := true;
method __build__ () {
die "session handler secret is required" if secret ≡ null;
}
method _cookie_name () {
return cookie_name;
}
method _max_age () {
return max_age;
}
method _signature ( id ) {
return hmac_sha256_hex( _bin(id), _bin(secret) );
}
method _signed_cookie_value ( id ) {
return id _ "." _ self._signature(id);
}
method _cookie_options ( session ) {
let out := {
Path: cookie_path,
HttpOnly: http_only,
SameSite: same_site,
"Max-Age": max_age,
};
out.set( "Domain", cookie_domain ) if cookie_domain ≢ null;
out.set(
"Secure",
secure ≡ null ? session.secure() : secure,
);
return out;
}
method _cookie_session_id ( Request request ) {
let raw := request.cookies().get( cookie_name, null );
return null if raw ≡ null;
let parts := split( raw, "." );
return null unless parts.length() = 2;
let id := parts[0];
return null unless _valid_id(id);
return null unless parts[1] ≡ self._signature(id);
return id;
}
method _new_id () {
return SecureRandom.token(32);
}
method _session ( Request request, id, created, updated, data ) {
return new Session(
data: _empty_data(data),
_id: id,
_handler: self,
_created: created,
_updated: updated,
_secure: request.secure(),
);
}
method _new_session ( Request request ) {
let now := _now();
return self._session( request, self._new_id(), now, now, {} );
}
method _expired ( created ) {
return created + max_age < _now();
}
method _maybe_cleanup () {
self.cleanup() if SecureRandom.int(100) = 0;
return self;
}
method session_for ( Request request ) {
self._maybe_cleanup();
let id := self._cookie_session_id(request);
return self._new_session(request) if id ≡ null;
let session := self._load_session( request, id );
return session ≢ null ? session : self._new_session(request);
}
method _load_session ( request, id ) {
die "_load_session is not implemented";
}
method save_session ( session ) {
die "save_session is not implemented";
}
method cleanup () {
die "cleanup is not implemented";
}
}
class FileSessionHandler extends _SessionHandlerBase {
let dir := null;
method __build__ () {
super();
die "FileSessionHandler dir is required" if dir ≡ null;
dir.mkdir() unless dir.exists();
self.cleanup();
}
method _path ( id ) {
die "invalid session id" unless _valid_id(id);
return dir.child( id _ ".session" );
}
method _load_blob ( path ) {
try {
return load( path.slurp() );
}
catch {
return null;
}
}
method _load_session ( request, id ) {
let path := self._path(id);
return null unless path.exists();
let blob := self._load_blob(path);
if ( blob ≡ null or self._expired( blob{created} ) ) {
path.remove();
return null;
}
return self._session(
request,
id,
blob{created},
blob.get( "updated", blob{created} ),
blob.get( "data", {} ),
);
}
method save_session ( session ) {
let now := _now();
self._path( session.id() ).spew(
dump({
created: session.created(),
updated: now,
data: session{data},
}),
);
return session;
}
method cleanup () {
return self unless dir.exists();
for ( let child in dir.children() ) {
next unless child.basename() ~ /\.session$/;
let blob := self._load_blob(child);
child.remove()
if blob ≡ null or self._expired( blob{created} );
}
return self;
}
}
class DbSessionHandler extends _SessionHandlerBase {
let dbh := null;
method __build__ () {
super();
die "DbSessionHandler dbh is required" if dbh ≡ null;
dbh.prepare(
"create table if not exists zuzu_web_sessions " _
"(id text primary key, created real, updated real, data text)",
).execute();
self.cleanup();
}
method _load_session ( request, id ) {
let q := dbh.prepare(
"select created, updated, data from zuzu_web_sessions " _
"where id = ?",
);
q.execute(id);
let row := q.next_dict();
return null if row ≡ null;
if ( self._expired( row{created} ) ) {
dbh.prepare(
"delete from zuzu_web_sessions where id = ?",
).execute(id);
return null;
}
return self._session(
request,
id,
row{created},
row{updated},
load( decode( row{data} ) ),
);
}
method save_session ( session ) {
dbh.prepare(
"delete from zuzu_web_sessions where id = ?",
).execute( session.id() );
dbh.prepare(
"insert into zuzu_web_sessions " _
"(id, created, updated, data) values (?, ?, ?, ?)",
).execute(
session.id(),
session.created(),
_now(),
encode( dump( session{data} ) ),
);
return session;
}
method cleanup () {
dbh.prepare(
"delete from zuzu_web_sessions where created + ? < ?",
).execute( self._max_age(), _now() );
return self;
}
}
std/web/session
Standard Library source code
File and database backed sessions for std/web.
Module
- Name
std/web/session- Area
- Standard Library
- Source
modules/std/web/session.zzm