Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+346
View File
@@ -0,0 +1,346 @@
# Sessions
A sessions module for web projects.
## Usage
The sessions module provides an implementation for [session stores](#custom-stores).
The session store handles the saving, storing and retrieving of data. You can
either use a store directly yourself, or you can use the `session.Sessions` struct
which is easier to use since it also handles session verification and integrates nicely
with veb.
If you want to use `session.Sessions` in your web app the session id's will be
stored using cookies. The best way to get started is to follow the
[getting started](#getting-started) section.
Otherwise have a look at the [advanced usage](#advanced-usage) section.
## Getting Started
The examples in this section use `x.veb`. See the [advanced usage](#advanced-usage) section
for examples without `x.veb`.
To start using sessions in veb embed `sessions.CurrentSession` on the
Context struct and add `sessions.Sessions` to the app struct. We must also pass the type
of our session data.
For any further example code we will use the `User` struct.
**Example:**
```v ignore
import x.sessions
import veb
pub struct User {
pub mut:
name string
verified bool
}
pub struct Context {
veb.Context
// By embedding the CurrentSession struct we can directly access the current session id
// and any associated session data. Set the session data type to `User`
sessions.CurrentSession[User]
}
pub struct App {
pub mut:
// this struct contains the store that holds all session data it also provides
// an easy way to manage sessions in your veb app. Set the session data type to `User`
sessions &sessions.Sessions[User]
}
```
Next we need to create the `&sessions.Sessions[User]` instance for our app. This
struct provides functionality to easier manage sessions in a veb app.
### Session Stores
To create `sessions.Sessions` We must specify a "store" which handles the session data.
Currently veb provides two options for storing session data:
1. The `MemoryStore[T]` stores session data in memory only using the `map` datatype.
2. The `DBStore[T]` stores session data in a database by encoding the session data to JSON.
It will create the table `DBStoreSessions` in your database, to store the session data.
It is possible to create your own session store, see [custom stores](#custom-stores).
### Starting the App
For this example we will use the memory store.
**Example:**
```v ignore
fn main() {
mut app := &App{
store: sessions.MemoryStore[User]{}
// use your own secret which will be used to verify session id's
secret: 'my secret'.bytes()
}
veb.run[App, Context](mut app, 8080)
}
```
### Middleware
The `sessions.veb2_middleware` module provides a middleware handler. This handler will execute
before your own route handlers and will verify the current session and fetch any associated
session data and load it into `sessions.CurrentSession`, which is embedded on the Context struct.
> **Note:**
> It is recommended to use the middleware, so the sessions are always verfied
> and loaded correctly.
**Example:**
```v ignore
// add this import at the top of your file
import x.sessions.veb2_middleware
pub struct App {
// embed the Middleware struct from veb
veb.Middleware[Context]
pub mut:
// this struct contains the store that holds all session data it also provides
// an easy way to manage sessions in your veb app. Set the session data type to `User`
sessions &sessions.Sessions[User]
}
fn main() {
mut app := &App{
store: sessions.MemoryStore[User]{}
// use your own secret which will be used to verify session id's
secret: 'my secret'.bytes()
}
// register the sessions middleware
app.use(veb2_middleware.create[User, Context](mut app.sessions))
veb.run[App, Context](mut app, 8080)
}
```
You can now start using sessions with veb!
### Usage in endpoint handlers
#### Using Session Data
Because `sessions.CurrentSession` is embedded on the Context struct we can directly
access any session data via `ctx.session_data`. This field is an option, it will be `none`
if no data is set.
**Example:**
```v ignore
pub fn (app &App) index(mut ctx Context) veb.Result {
// check if a user is logged in
if user := ctx.session_data {
return ctx.text('Welcome ${user.name}! Verification status: ${user.verified}')
} else {
// user is not logged in
return ctx.text('You are not logged in :(')
}
}
```
#### Saving / updating session data
You can use the `save` method to update and save any session data.
When the user logs in, the `save` method is called and a new session id is generated
and set as cookie. Assuming there wasn't already a session going on. If you want to
be sure that a new session id is generated when you save data, you can use the `resave`
method. This method will save the data and *always* set a new session id.
**Example:**
```v ignore
pub fn (mut app App) login(mut ctx Context) veb.Result {
// set a session id cookie and save data for the new user
app.sessions.save(mut ctx, User{
name: '[no name provided]'
}) or { return ctx.server_error('could not save session data, please try again') }
return ctx.text('You are now logged in!')
}
```
The following endpoint checks if a session exists, if it doesn't inform the
user that they need to login.
If a session does exists the users name is updated to the `name` query parameter,
you can use this route via `http://localhost:8080/save?name=myname`. And if the
query parameter is not passed an error 400 (bad request) is returned.
**Example:**
```v ignore
pub fn (mut app App) save(mut ctx Context) veb.Result {
// check if there is a session
app.sessions.get(ctx) or { return ctx.request_error('You are not logged in :(') }
if name := ctx.query['name'] {
// update the current user
app.sessions.save(mut ctx, User{
name: name
}) or { return ctx.server_error('could not save session data, please try again') }
return ctx.redirect('/', typ: .see_other)
} else {
// send HTTP 400 error
return ctx.request_error('query parameter "name" must be present!')
}
}
```
#### Destroying data / logging out
If a user logs out you can use the `logout` method to destroy the session data and
clear the session id cookie. If you only want to destroy the session data use the `destroy`
method.
**Example:**
```v ignore
pub fn (mut app App) logout(mut ctx Context) veb.Result {
app.sessions.logout(mut ctx) or { return ctx.server_error('could not logout, please try again') }
return ctx.text('You are now logged out!')
}
```
### Configuration
Change the `cookie_options` field to modify how the session cookie is stored.
**Example:**
```v ignore
mut app := &App{
sessions: &sessions.Sessions[User]{
// ...
cookie_options: sessions.CookieOptions{
// cookie can only be stored on an HTTPS site.
secure: true
}
}
}
```
#### Mag-age
By default the expiration date of a session is 30 days. You can change this by
setting the `max_age` field. If `max_age = 0`, then session expiration times are not
checked and sessions will be stored forever until they are destroyed.
#### Pre-sessions
By default a session cookie is only generated when you call `save`, or `resave`.
By setting `save_uninitialized` to `true` a session cookie will always be set,
even if there is no data for the session yet. This is useful when you need session
data to be always available.
Or, for example, you could use pre-sessions to mitigate login-csrf,
since you can bind a csrf-token to the "pre-session" id. Then when the user logs
in, you can set a new session id with `resave`..
## Advanced Usage
If you want to store session id's in another manner than cookies, or if you want
to use this sessions module outside of veb, the easiest way is to create an
instance of a `Store` and directly interact with it.
First we create an instance of the `MemoryStore` and pass the user struct as data type.
**Example:**
```v
import x.sessions
const secret = 'my secret'.bytes()
pub struct User {
pub mut:
name string
verified bool
}
fn main() {
mut store := sessions.MemoryStore[User]{}
user := User{
name: 'vaesel'
}
}
```
### Generating and validating session id's
The session module provides a function for generating a new signed session id
and for verifying a signed session id. You can ofcourse generate your own session id's.
**Example:**
```v ignore
// fn main
// generate a new session id and sign it
session_id, signed_session_id := sessions.new_session_id(secret)
// save session data to our store
store.set(session_id, user)!
// get a normal session id from the signed version and verify it
verified_session_id, valid := sessions.verify_session_id(signed_session_id, secret)
assert verified_session_id == session_id && valid == true
```
We can retrieve the saved user and verify that the data we saved can be retrieved
from the verified session id.
**Example:**
```v ignore
// fn main
// pass `max_age = 0` to ignore the expiration time.
if saved_user := store.get(verified_session_id, 0) {
assert user == saved_user
println('Retrieved a valid user! ${saved_user}')
} else {
println(':(')
}
```
## Custom Stores
You can easily create your own custom store in order to control how session data is
stored and retrieved. Each session store needs to implement the `Store[T]` interface.
```v ignore
pub interface Store[T] {
mut:
// get the current session data if the id exists and if it's not expired.
// If the session is expired, any associated data should be destroyed.
// If `max_age=0` the store will not check for expiration of the session.
get(sid string, max_age time.Duration) !T
// destroy session data for `sid`
destroy(sid string) !
// set session data for `sid`
set(sid string, val T) !
}
// get data from all sessions, optional to implement
pub fn (mut s Store) all[T]() ![]T {
return []T{}
}
// clear all session data, optional to implement
pub fn (mut s Store) clear[T]() ! {}
```
Only the `get`, `destroy` and `set` methods are required to implement.
### Session Expire time
The `max_age` argument in `get` can be used to check whether the session is still valid.
The database and memory store both check the expiration time from the time the session data
first inserted. But if `max_age = 0`, the stores will not check for expiration time.
+100
View File
@@ -0,0 +1,100 @@
module sessions
import json
import orm
import time
// DBStoreSessions is the table that is created in your database and represents a session data record.
pub struct DBStoreSessions {
pub mut:
session_id string @[primary]
created_at time.Time
data string
}
// DBStore stores sessions in a database.
@[noinit]
pub struct DBStore[T] {
pub mut:
db orm.Connection @[required]
}
// create a new Database store with a connection to a database.
pub fn DBStore.create[T](db orm.Connection) !DBStore[T] {
sql db {
create table DBStoreSessions
}!
return DBStore[T]{
db: db
}
}
// all gets the data from all sessions.
pub fn (mut store DBStore[T]) all() ![]T {
rows := sql store.db {
select from DBStoreSessions
}!
// decode should never fail
return rows.map(json.decode(T, it.data)!)
}
// get session for session id `sid`. The session can be `max_age` old.
// `max_age` will be ignored when set to `0`
pub fn (mut store DBStore[T]) get(sid string, max_age time.Duration) !T {
rows := sql store.db {
select from DBStoreSessions where session_id == sid
}!
if rows.len == 1 {
record := rows[0]
// session is expired
if max_age != 0 && record.created_at.add(max_age) < time.now() {
store.destroy(sid)!
return error('session is expired')
}
return json.decode(T, record.data)!
} else {
return error('session does not exist')
}
}
// destroy data for session id `sid`.
pub fn (mut store DBStore[T]) destroy(sid string) ! {
sql store.db {
delete from DBStoreSessions where session_id == sid
}!
}
// clear all sessions.
pub fn (mut store DBStore[T]) clear() ! {
sql store.db {
delete from DBStoreSessions where session_id != ''
}!
}
// set session data for session id `sid`.
pub fn (mut store DBStore[T]) set(sid string, val T) ! {
count := sql store.db {
select count from DBStoreSessions where session_id == sid
}!
if count == 1 {
stringified := json.encode(val)
sql store.db {
update DBStoreSessions set data = stringified where session_id == sid
}!
} else {
record := DBStoreSessions{
session_id: sid
created_at: time.now()
data: json.encode(val)
}
sql store.db {
insert record into DBStoreSessions
}!
}
}
+58
View File
@@ -0,0 +1,58 @@
module sessions
import time
pub struct MemoryStoreSessions[T] {
pub mut:
created_at time.Time
data T
}
// MemoryStore stores sessions in a `map` in memory only.
pub struct MemoryStore[T] {
pub mut:
data map[string]MemoryStoreSessions[T]
}
// get data from all sessions.
pub fn (mut store MemoryStore[T]) all() ![]T {
return store.data.values().map(it.data)
}
// get session for session id `sid`. The session can be `max_age` old.
// `max_age` will be ignored when set to `0`
pub fn (mut store MemoryStore[T]) get(sid string, max_age time.Duration) !T {
if record := store.data[sid] {
// session is expired
if max_age != 0 && record.created_at.add(max_age) < time.now() {
store.destroy(sid)!
return error('session is expired')
}
return record.data
} else {
return error('session does not exist')
}
}
// destroy data for session id `sid`.
pub fn (mut store MemoryStore[T]) destroy(sid string) ! {
store.data.delete(sid)
}
// clear all sessions.
pub fn (mut store MemoryStore[T]) clear() ! {
store.data.clear()
}
// set session data for session id `sid`.
pub fn (mut store MemoryStore[T]) set(sid string, val T) ! {
if sid in store.data {
store.data[sid].data = val
} else {
store.data[sid] = MemoryStoreSessions[T]{
created_at: time.now()
data: val
}
}
}
+197
View File
@@ -0,0 +1,197 @@
module sessions
import crypto.sha256
import crypto.hmac
import encoding.base64
import net.http
import rand
import time
const session_id_length = 32
// new_session_id creates and returns a random session id and its signed version.
// You can directly use the signed version as a cookie value
pub fn new_session_id(secret []u8) (string, string) {
sid := rand.hex(session_id_length)
hashed := hmac.new(secret, sid.bytes(), sha256.sum, sha256.block_size)
// separate session id and hmac with a `.`
return sid, '${sid}.${base64.url_encode(hashed)}'
}
// verify_session_id verifies the signed session id with `secret`.
// This function returns the session id and if the session id is valid.
pub fn verify_session_id(raw_sid string, secret []u8) (string, bool) {
parts := raw_sid.split('.')
if parts.len != 2 {
return '', false
}
sid := parts[0]
actual_hmac := base64.url_decode(parts[1])
new_hmac := hmac.new(secret, sid.bytes(), sha256.sum, sha256.block_size)
// use `hmac.equal` to prevent leaking timing information
return sid, hmac.equal(actual_hmac, new_hmac)
}
// CurrentSession contains the session data during a request.
// If you use veb you could embed it on your Context struct to have easy access to the session id and data.
// Usage example:
// ```v
// struct Context {
// veb.Context
// sessions.CurrentSessions[User]
// }
// ```
pub struct CurrentSession[T] {
pub mut:
session_id string
session_data ?T
}
// CookieOptions contains the default settings for the cookie created in the `Sessions` struct.
pub struct CookieOptions {
pub:
cookie_name string = 'sid'
domain string
http_only bool = true
path string = '/'
same_site http.SameSite = .same_site_strict_mode
secure bool
}
// Sessions can be used to easily integrate sessions with veb.
// This struct contains the store that holds all session data it also provides
// an easy way to manage sessions in your veb app.
// Usage example:
// ```v
// pub struct App {
// pub mut:
// sessions &sessions.Sessions[User]
// }
// ```
@[heap]
pub struct Sessions[T] {
pub:
secret []u8 @[required]
cookie_options CookieOptions
// max age of session data and id, default is 30 days
max_age time.Duration = time.hour * 24 * 30
// set to true if you want to create a session if there isn't any data stored yet.
// Also called pre-sessions
save_uninitialized bool
pub mut:
store Store[T] @[required]
}
// set_session_id generates a new session id and set a Set-Cookie header on the response.
pub fn (mut s Sessions[T]) set_session_id[X](mut ctx X) string {
sid, signed := new_session_id(s.secret)
ctx.CurrentSession.session_id = sid
ctx.set_cookie(http.Cookie{
value: signed
max_age: int(s.max_age / time.second)
domain: s.cookie_options.domain
http_only: s.cookie_options.http_only
name: s.cookie_options.cookie_name
path: s.cookie_options.path
same_site: s.cookie_options.same_site
secure: s.cookie_options.secure
})
// indicate that the response should not be cached: we don't want the session id cookie
// to be cached by the browser, or any other agent
// https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.1.4
ctx.res.header.add(.cache_control, 'no-cache="Set-Cookie"')
return sid
}
// validate_session validates the current session, returns the session id and the validation status.
pub fn (mut s Sessions[T]) validate_session[X](ctx X) (string, bool) {
cookie := ctx.get_cookie(s.cookie_options.cookie_name) or { return '', false }
return verify_session_id(cookie, s.secret)
}
// get the data associated with the current session, if it exists.
pub fn (mut s Sessions[T]) get[X](ctx X) !T {
sid := s.get_session_id(ctx) or { return error('cannot find session id') }
return s.store.get(sid, s.max_age)!
}
// destroy the data for the current session.
pub fn (mut s Sessions[T]) destroy[X](mut ctx X) ! {
if sid := s.get_session_id(ctx) {
s.store.destroy(sid)!
ctx.session_data = none
}
}
// logout destroys the data for the current session and removes the session id Cookie.
pub fn (mut s Sessions[T]) logout[X](mut ctx X) ! {
s.destroy(mut ctx)!
ctx.set_cookie(http.Cookie{
name: s.cookie_options.cookie_name
value: ''
expires: time.unix(0)
})
}
// save `data` for the current session.
pub fn (mut s Sessions[T]) save[X](mut ctx X, data T) ! {
if sid := s.get_session_id(ctx) {
s.store.set(sid, data)!
ctx.CurrentSession.session_data = data
} else {
if s.save_uninitialized == false {
// no valid session id, but the user only wants to create a session
// when data is saved. So we create the session here
sid := s.set_session_id(mut ctx)
s.store.set(sid, data)!
ctx.CurrentSession.session_data = data
}
eprintln('[veb.sessions] error: trying to save data without a valid session!')
}
}
// resave saves `data` for the current session and reset the session id.
// You should use this function when the authentication or authorization status changes
// e.g. when a user signs in or switches between accounts/permissions.
// This function also destroys the data associated to the old session id.
pub fn (mut s Sessions[T]) resave[X](mut ctx X, data T) ! {
if sid := s.get_session_id(ctx) {
s.store.destroy(sid)!
}
s.save(mut ctx, data)
}
// get_session_id retrieves the current session id, if it is set.
// The HMAC signature is verified when extracting from cookies.
pub fn (s &Sessions[T]) get_session_id[X](ctx X) ?string {
// first check session id from `ctx`
sid_from_ctx := ctx.CurrentSession.session_id
if sid_from_ctx != '' {
return sid_from_ctx
} else if cookie := ctx.get_cookie(s.cookie_options.cookie_name) {
// check request headers for the session_id cookie and verify HMAC signature
sid, valid := verify_session_id(cookie, s.secret)
if valid {
return sid
}
return none
} else {
// check the Set-Cookie headers on the response for a session id
for cookie in ctx.res.cookies() {
if cookie.name == s.cookie_options.cookie_name {
return cookie.value
}
}
// No session id is set
return none
}
}
+21
View File
@@ -0,0 +1,21 @@
module sessions
import time
pub interface Store[T] {
mut:
// get the current session data if the id exists and if it's not expired
get(sid string, max_age time.Duration) !T
// destroy session data for `sid`
destroy(sid string) !
// set session data for `val`
set(sid string, val T) !
}
// get data from all sessions, optional to implement.
pub fn (mut s Store[T]) all[T]() ![]T {
return []T{}
}
// clear all session data, optional to implement.
pub fn (mut s Store[T]) clear[T]() ! {}
@@ -0,0 +1,99 @@
// vtest retry: 3
import db.sqlite
import x.json2 as json
import os
import time
import x.sessions
const max_age = time.second
const db_path = os.join_path(os.vtmp_dir(), 'x_sessions_db_test.db')
fn testsuite_begin() {
os.rm(db_path) or {}
}
fn testsuite_end() {
os.rm(db_path) or {}
}
pub struct User {
name string
age int
}
const default_user = User{
name: 'john'
age: 99
}
const default_user_encoded = json.encode(default_user)
fn test_store_set() {
mut db := get_connection()!
defer {
db.close() or {}
}
mut store := sessions.DBStore.create[User](db)!
store.set('a', default_user)!
mut rows := sql db {
select from sessions.DBStoreSessions
}!
assert rows.len == 1
// check if created at time is not empty
assert rows[0].created_at != time.Time{}
assert rows[0].data == default_user_encoded
first_created := rows[0].created_at
store.set('a', User{ age: 99 })!
rows = sql db {
select from sessions.DBStoreSessions
}!
assert rows.len == 1
assert rows[0].created_at == first_created
assert rows[0].data == json.encode(User{ age: 99 })
}
fn test_store_get() {
mut db := get_connection()!
defer {
db.close() or {}
}
mut store := sessions.DBStore.create[User](db)!
store.set('b', default_user)!
if data := store.get('b', max_age) {
assert data == default_user
} else {
$if !windows {
assert false, 'session data should not be none'
}
}
}
fn test_store_session_expired() {
mut db := get_connection()!
defer {
db.close() or {}
}
mut store := sessions.DBStore.create[User](db)!
store.set('c', default_user)!
time.sleep(2 * max_age)
if data := store.get('c', max_age) {
assert false, 'session should be expired!'
} else {
assert err.msg() == 'session is expired'
}
// verify that data is deleted
rows := sql db {
select from sessions.DBStoreSessions where session_id == 'c'
}!
assert rows.len == 0
}
fn get_connection() !sqlite.DB {
return sqlite.connect(db_path)!
}
@@ -0,0 +1,54 @@
import time
import x.sessions
const max_age = time.second
pub struct User {
name string
age int
}
const default_user = User{
name: 'john'
age: 99
}
fn test_store_set() {
mut store := sessions.MemoryStore[User]{}
store.set('a', default_user)!
// check if created at time is not empty
assert store.data['a'].created_at != time.Time{}
assert store.data['a'].data == default_user
first_created := store.data['a'].created_at
store.set('a', User{ age: 99 })!
assert store.data['a'].created_at == first_created
assert store.data['a'].data.age == 99
}
fn test_store_get() {
mut store := sessions.MemoryStore[User]{}
store.set('a', default_user)!
if data := store.get('a', max_age) {
assert data == default_user
} else {
assert false, 'session data should not be none'
}
}
fn test_store_session_expired() {
mut store := sessions.MemoryStore[User]{}
store.set('a', default_user)!
time.sleep(2 * max_age)
if data := store.get('a', max_age) {
assert false, 'session should be expired!'
} else {
assert err.msg() == 'session is expired'
}
assert store.data.len == 0
}
@@ -0,0 +1,194 @@
// vtest retry: 2
import net.http
import time
import x.sessions
import veb
import x.sessions.veb_middleware
const port = 13010
const localserver = 'http://127.0.0.1:${port}'
const exit_after = time.second * 10
const cookie_name = 'SESSION_ID'
const max_age = time.second * 2
pub struct User {
pub mut:
name string
age int
verified bool
}
const default_user = User{
name: 'casper'
age: 21
verified: false
}
pub struct Context {
veb.Context
sessions.CurrentSession[User]
}
pub struct App {
veb.Middleware[Context]
pub mut:
sessions &sessions.Sessions[User]
started chan bool
}
pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (app &App) session_data(mut ctx Context) veb.Result {
return ctx.text('${ctx.session_data}')
}
pub fn (app &App) protected(mut ctx Context) veb.Result {
if user := ctx.session_data {
return ctx.json(user)
} else {
ctx.res.set_status(.unauthorized)
return ctx.text('No session saved!')
}
}
pub fn (mut app App) save_session(mut ctx Context) veb.Result {
app.sessions.save(mut ctx, default_user) or { return ctx.server_error(err.msg()) }
return ctx.ok('')
}
pub fn (mut app App) update_session(mut ctx Context) veb.Result {
if mut user := ctx.session_data {
user.age++
app.sessions.save(mut ctx, user) or { return ctx.server_error(err.msg()) }
// sessions module should also update the context
if new_user := ctx.session_data {
assert new_user == user, 'session data is not updated on the context'
} else {
assert false, 'session data should not be none'
}
}
return ctx.ok('')
}
pub fn (mut app App) destroy_session(mut ctx Context) veb.Result {
app.sessions.destroy(mut ctx) or { return ctx.server_error(err.msg()) }
// sessions module should also update the context
assert ctx.session_data == none
return ctx.ok('')
}
fn testsuite_begin() {
spawn fn () {
time.sleep(exit_after)
assert false, 'timeout reached!'
exit(1)
}()
mut app := &App{
sessions: &sessions.Sessions[User]{
store: sessions.MemoryStore[User]{}
secret: 'secret'.bytes()
cookie_options: sessions.CookieOptions{
cookie_name: cookie_name
}
max_age: max_age
}
}
app.use(veb_middleware.create[User, Context](mut app.sessions))
spawn veb.run_at[App, Context](mut app, port: port, family: .ip, timeout_in_seconds: 2)
// app startup time
_ := <-app.started
wait_for_server()!
}
fn test_empty_session() {
x := http.get('${localserver}/session_data')!
assert x.body == 'Option(none)'
}
fn test_protected_without_session() {
x := http.get('${localserver}/protected')!
assert x.status() == .unauthorized
}
fn test_save_session() {
sid := get_session_id()!
x := make_request_with_session_id(.get, '/session_data', sid)!
// cast to `?User` since, session_data is of type `?T`
assert x.body == '${?User(default_user)}'
}
fn test_update_session() {
sid := get_session_id()!
mut x := make_request_with_session_id(.get, '/update_session', sid)!
x = make_request_with_session_id(.get, '/session_data', sid)!
mut updated_user := default_user
updated_user.age++
assert x.body == '${?User(updated_user)}'
}
fn test_destroy_session() {
sid := get_session_id()!
mut x := make_request_with_session_id(.get, '/session_data', sid)!
assert x.body != 'Option(none)'
x = make_request_with_session_id(.get, '/destroy_session', sid)!
assert x.status() == .ok
x = make_request_with_session_id(.get, '/session_data', sid)!
assert x.body == 'Option(none)'
}
fn test_session_expired() {
sid := get_session_id()!
mut x := make_request_with_session_id(.get, '/session_data', sid)!
assert x.body != 'Option(none)'
time.sleep(max_age)
x = make_request_with_session_id(.get, '/session_data', sid)!
assert x.body == 'Option(none)'
}
// Utility
fn get_session_id() !string {
x := http.get('${localserver}/save_session')!
return x.cookies()[0].value
}
fn make_request_with_session_id(method http.Method, path string, sid string) !http.Response {
return http.fetch(http.FetchConfig{
url: '${localserver}${path}'
method: method
cookies: {
cookie_name: sid
}
})
}
fn wait_for_server() ! {
for _ in 0 .. 40 {
response := http.get('${localserver}/session_data') or {
time.sleep(50 * time.millisecond)
continue
}
if response.status() == .ok {
return
}
time.sleep(50 * time.millisecond)
}
return error('session test server did not start in time')
}
@@ -0,0 +1,62 @@
import time
import x.sessions
const max_age = time.second
const secret = 'session_test'.bytes()
pub struct User {
name string
age int
}
const default_user = User{
name: 'john'
age: 99
}
fn test_session_id() {
unverified_sid, sid_with_hmac := sessions.new_session_id(secret)
verified_sid, valid := sessions.verify_session_id(sid_with_hmac, secret)
assert unverified_sid == verified_sid
assert valid
}
fn test_forged_signature_rejected() {
// Create a valid session ID
sid, _ := sessions.new_session_id(secret)
// Forge a cookie with valid session ID but invalid signature
forged_cookie := '${sid}.INVALID_SIGNATURE'
verified_sid, valid := sessions.verify_session_id(forged_cookie, secret)
// Forged signature must be rejected
assert !valid
assert verified_sid == sid
}
fn test_wrong_secret_rejected() {
// Create a session with one secret
_, signed_cookie := sessions.new_session_id(secret)
// Try to verify with a different secret
wrong_secret := 'wrong_secret'.bytes()
_, valid := sessions.verify_session_id(signed_cookie, wrong_secret)
// Must be rejected when using wrong secret
assert !valid
}
fn test_malformed_cookie_rejected() {
// Cookie without signature separator
_, valid1 := sessions.verify_session_id('just_a_session_id', secret)
assert !valid1
// Empty cookie
_, valid2 := sessions.verify_session_id('', secret)
assert !valid2
// Cookie with empty parts
_, valid3 := sessions.verify_session_id('.', secret)
assert !valid3
}
@@ -0,0 +1,33 @@
module veb_middleware
import x.sessions
import veb
// create can be used to add session middleware to your veb app to ensure
// a valid session always exists. If a valid session exists the session data will
// be loaded into `session_data`, else a new session id will be generated.
// You have to pass the Context type as the generic type.
pub fn create[T, X](mut s sessions.Sessions[T]) veb.MiddlewareOptions[X] {
return veb.MiddlewareOptions[X]{
handler: fn [mut s] [T, X](mut ctx X) bool {
// a session id is retrieved from the client, so it must be considered
// untrusted and has to be verified on every request
sid, valid := s.validate_session(ctx)
if !valid {
if s.save_uninitialized {
// invalid session id, so create a new one
s.set_session_id(mut ctx)
}
return true
}
ctx.CurrentSession.session_id = sid
if data := s.store.get(sid, s.max_age) {
ctx.CurrentSession.session_data = data
}
return true
}
}
}