net/http/session
User sessions over a cookie: keeping data between requests, and a CSRF token.
import net.http.session
A session keeps each visitor's data on the server and gives the browser nothing but a random id in an HttpOnly cookie. Use it for logins, carts and form tokens on a site built with net/http. When the client is not a browser, or several servers must accept the same login without sharing memory, a signed token from jwt is usually the better fit.
The flow in a handler: Start finds the visitor's session from the cookie or makes a new one (and sets the cookie), you read and change values with Value and SetValue, and Save writes the session back. Values are strings. A session expires after max_age seconds without a request; every request that finds it starts the clock again.
Things people get wrong:
- Forgetting
Save.Startreturns a copy; changes reach the store only when you save it. - Keeping request strings.
Param,Formand the like are freed when the handler returns, so copy them withstr.Clonebefore you store them (see the cart). - Not calling
Regenerateafter login, which leaves an id an attacker may have planted in the browser valid. - Expecting the sessions to survive. They live in this process's memory: a restart logs everyone out and two server processes do not share them. Two requests from one visitor at the same moment each save their own copy, and the later
Savewins.
Log in and log out
The usual shape: the login handler checks the password, calls Regenerate so the user gets a fresh id, stores who they are with SetValue and redirects. Every protected page calls Start and checks HasValue. Logging out calls End, which deletes the session on the server and clears the cookie.
import net.http
import net.http.session
import net.router
func login(ctx: i64):
if http.Form(ctx, "user") != "sara" or http.Form(ctx, "password") != "open sesame":
http.SetStatus(ctx, 401)
http.Text(ctx, "wrong user or password")
ret
end
mut s := session.Start(ctx)
session.Regenerate(ctx, s)
session.SetValue(s, "user", "sara")
session.Save(s)
http.Redirect(ctx, "/me")
end
func me(ctx: i64):
s := session.Start(ctx)
if not session.HasValue(s, "user"):
http.SetStatus(ctx, 401)
http.Text(ctx, "please log in")
ret
end
http.Text(ctx, "hello " + session.Value(s, "user"))
end
func logout(ctx: i64):
session.End(ctx)
http.Text(ctx, "logged out")
end
func main:
mut r := router.NewRouter()
router.Post(r, "/login", login)
router.Get(r, "/me", me)
router.Post(r, "/logout", logout)
mut srv := http.NewServer(19770)
http.Use(srv, r)
_ := http.RunBackground(srv)
base := "http://127.0.0.1:19770"
mut browser := http.NewClient()
browser.SetHeader("Content-Type", "application/x-www-form-urlencoded")
println http.Body(browser.Get(base + "/me"))
bad := browser.Post(base + "/login", "user=sara&password=guess")
println http.Status(bad), http.Body(bad)
ok := browser.Post(base + "/login", "user=sara&password=open+sesame")
println http.Status(ok), http.GetHeader(ok, "location")
println http.Body(browser.Get(base + "/me"))
println http.Body(browser.Post(base + "/logout", ""))
println http.Body(browser.Get(base + "/me"))
endhttp.NewClient keeps cookies between requests the way a browser does, which is why the second /me sees the login. A plain http.Get sends no cookies and always looks like a new visitor.
Protect forms with a CSRF token
The browser sends the session cookie with every request to your site, even one started by a form on another site. A CSRF token stops that: CSRFToken stores a random token in the session, you put it in a hidden field, and the handler that receives the form checks it with CSRFVerify. A forged form does not know the token and is refused.
import net.http
import net.http.session
import net.router
func form(ctx: i64):
mut s := session.Start(ctx)
token := session.CSRFToken(s)
session.Save(s)
http.Html(ctx, `<form method="post" action="/email"><input type="hidden" name="csrf" value="` + token + `"><input name="email"></form>`)
end
func change_email(ctx: i64):
s := session.Start(ctx)
if not session.CSRFVerify(s, http.Form(ctx, "csrf")):
http.SetStatus(ctx, 403)
http.Text(ctx, "the form has expired, reload the page")
ret
end
http.Text(ctx, "email changed to " + http.Form(ctx, "email"))
end
func token_in(html: str): str:
start := html.find(`value="`) + 7
rest := html.substr(start, html.len() - start)
ret rest.substr(0, rest.find(`"`))
end
func main:
mut r := router.NewRouter()
router.Get(r, "/email", form)
router.Post(r, "/email", change_email)
mut srv := http.NewServer(19771)
http.Use(srv, r)
_ := http.RunBackground(srv)
url := "http://127.0.0.1:19771/email"
mut browser := http.NewClient()
browser.SetHeader("Content-Type", "application/x-www-form-urlencoded")
token := token_in(http.Body(browser.Get(url)))
println "token length:", token.len()
forged := browser.Post(url, "email=evil%40example.com")
println http.Status(forged), http.Body(forged)
real := browser.Post(url, "csrf=" + token + "&email=sara%40example.com")
println http.Status(real), http.Body(real)
endCall Save after CSRFToken, because the first call creates the token in your copy of the session.
Keep data across requests: a shopping cart
Values are strings, so numbers go through conv.Atoi and back. Each visitor has a separate session, so two clients filling carts at the same time never see each other's items. ClearValues empties a session but keeps the user's id.
import net.http
import net.http.session
import net.router
import conv
import str
func add(ctx: i64):
item := str.Clone(http.Param(ctx, "item"))
mut s := session.Start(ctx)
mut count := 0
if session.HasValue(s, item):
count = conv.Atoi(session.Value(s, item))
end
session.SetValue(s, item, "" + (count + 1))
session.Save(s)
http.Text(ctx, "added " + item)
end
func cart(ctx: i64):
s := session.Start(ctx)
mut text := ""
each item in ["tea", "bread", "cheese"]:
if session.HasValue(s, item):
text = text + (text == "" ? "" : ", ") + item + " x" + session.Value(s, item)
end
end
http.Text(ctx, text == "" ? "the cart is empty" : text)
end
func empty(ctx: i64):
mut s := session.Start(ctx)
session.ClearValues(s)
session.Save(s)
http.Text(ctx, "emptied")
end
func main:
mut r := router.NewRouter()
router.Post(r, "/cart/:item", add)
router.Get(r, "/cart", cart)
router.Delete(r, "/cart", empty)
mut srv := http.NewServer(19772)
http.Use(srv, r)
_ := http.RunBackground(srv)
base := "http://127.0.0.1:19772/cart"
mut sara := http.NewClient()
mut reza := http.NewClient()
_ := sara.Post(base + "/tea", "")
_ := sara.Post(base + "/tea", "")
_ := sara.Post(base + "/bread", "")
_ := reza.Post(base + "/cheese", "")
println "sara:", http.Body(sara.Get(base))
println "reza:", http.Body(reza.Get(base))
println http.Body(sara.Delete(base))
println "sara:", http.Body(sara.Get(base))
println "reza:", http.Body(reza.Get(base))
endNote the str.Clone around Param. Values read from the request are freed when the handler returns, and a session outlives the request, so copy anything from the request before you use it as a key or value. Without the copy the cart keys point at freed memory and the cart looks empty.
Cookie settings and lifetime
Configure sets the cookie name, how long a session lives without activity, whether the cookie is sent over HTTPS only, and its SameSite mode. Call it once, before the server starts. On a site served over HTTPS, pass true for secure; Strict keeps the cookie off every request that starts on another site, and Lax, the default, still sends it when the user follows a link.
import net.http
import net.http.session
import net.router
import str
func hello(ctx: i64):
s := session.Start(ctx)
http.Text(ctx, "session " + (s.id.len() > 0))
end
func main:
session.Configure("shop_sid", 1800, true, "Strict")
mut r := router.NewRouter()
router.Get(r, "/", hello)
mut srv := http.NewServer(19773)
http.Use(srv, r)
_ := http.RunBackground(srv)
res := http.Get("http://127.0.0.1:19773/")
println http.Body(res)
line := http.SetCookieLines(res).get(0)
each part in str.Split(line, "; "):
if part.starts_with("shop_sid="):
println "shop_sid=<random id>"
else not part.starts_with("Expires="):
println part
end
end
mut s := session.New()
session.SetValue(s, "theme", "dark")
session.Save(s)
println "kept for", s.max_age, "seconds"
println "removed now:", session.CleanupExpired()
session.Destroy(s.id)
mut found := true
_ := session.Get(s.id, found)
println "after Destroy:", found
endThe cookie is always HttpOnly, so scripts in the page cannot read the id. Expired sessions are removed every hundred calls to New or Get; CleanupExpired does it at once and returns how many it removed. Sessions live in the memory of one process, so a restart logs everyone out, and two copies of the server behind a load balancer do not share them. For that, keep the data in a database such as db/redis.
Types
SessionData
struct SessionData| Name | Type | Description |
|---|---|---|
items | HashMap | The values. |
import net.http
import net.http.session
import net.router
import conv
func visit(ctx: i64):
mut s := session.Start(ctx)
mut count := 1
if session.HasValue(s, "visits"):
count = conv.Atoi(session.Value(s, "visits")) + 1
end
session.SetValue(s, "visits", "" + count)
session.Save(s)
http.Text(ctx, "visit " + count)
end
func login(ctx: i64):
mut s := session.Start(ctx)
session.Regenerate(ctx, s)
session.SetValue(s, "user", "ali")
token := session.CSRFToken(s)
session.Save(s)
http.Text(ctx, "" + session.CSRFVerify(s, token) + " " + session.CSRFVerify(s, "x"))
end
func logout(ctx: i64):
session.End(ctx)
http.Text(ctx, "goodbye")
end
func main:
session.Configure("sid", 3600, false, "Lax")
mut r := router.NewRouter()
router.Get(r, "/", visit)
router.Post(r, "/login", login)
router.Post(r, "/logout", logout)
mut srv := http.NewServer(19626)
http.Use(srv, r)
_ := http.RunBackground(srv)
mut c := http.NewClient()
base := "http://127.0.0.1:19626"
println http.Body(c.Get(base + "/"))
println http.Body(c.Get(base + "/"))
println http.Body(c.Post(base + "/login", ""))
println http.Body(c.Get(base + "/"))
println http.Body(c.Post(base + "/logout", ""))
println http.Body(c.Get(base + "/"))
mut s := session.New()
session.SetValue(s, "a", "1")
session.SetValue(s, "b", "2")
println session.DeleteValue(s, "a"), s.data.items.size()
session.Save(s)
mut found := false
back := session.Get(s.id, found)
println found, session.Value(back, "b"), back.max_age, s.id.len() > 0
session.ClearValues(s)
session.Destroy(s.id)
_ := session.Get(s.id, found)
println found, session.CleanupExpired() >= 0
println session.DefaultCookieName, session.DefaultMaxAge
endSession
struct Session| Name | Type | Description |
|---|---|---|
id | str = "" | The random id that sits in the cookie. |
data | SessionData | The session data. |
created_at | i64 = 0 | When it was made. |
last_seen | i64 = 0 | When it was last used. |
max_age | int = 0 | How many seconds the session lives without a request; 0 or less never expires. |
The example for this item is shown with the ones above: net-http-session-Session.salam
Constants and variables
DefaultCookieName
const DefaultCookieName: str = "salam_sid"The example for this item is shown with the ones above: net-http-session-Session.salam
DefaultMaxAge
const DefaultMaxAge: i32 = 86400The example for this item is shown with the ones above: net-http-session-Session.salam
Functions
Configure
func Configure(cookie_name: str, max_age_seconds: int, secure: bool, same_site: str)The example for this item is shown with the ones above: net-http-session-Session.salam
CleanupExpired
func CleanupExpired(): intThe example for this item is shown with the ones above: net-http-session-Session.salam
New
func New(): SessionThe example for this item is shown with the ones above: net-http-session-Session.salam
Get
func Get(id: str, ok &: bool): Sessionok is false when there is none or it has expired.The example for this item is shown with the ones above: net-http-session-Session.salam
Save
func Save(s: Session)The example for this item is shown with the ones above: net-http-session-Session.salam
Destroy
func Destroy(id: str)The example for this item is shown with the ones above: net-http-session-Session.salam
Value
func Value(s: Session, key: str): strThe example for this item is shown with the ones above: net-http-session-Session.salam
HasValue
func HasValue(s: Session, key: str): boolThe example for this item is shown with the ones above: net-http-session-Session.salam
SetValue
func SetValue(s &: Session, key: str, value: str)The example for this item is shown with the ones above: net-http-session-Session.salam
DeleteValue
func DeleteValue(s &: Session, key: str): boolThe example for this item is shown with the ones above: net-http-session-Session.salam
ClearValues
func ClearValues(s &: Session)The example for this item is shown with the ones above: net-http-session-Session.salam
Start
func Start(h: i64): SessionThe example for this item is shown with the ones above: net-http-session-Session.salam
End
func End(h: i64)The example for this item is shown with the ones above: net-http-session-Session.salam
Regenerate
func Regenerate(h: i64, s &: Session)The example for this item is shown with the ones above: net-http-session-Session.salam
CSRFToken
func CSRFToken(s &: Session): strThe example for this item is shown with the ones above: net-http-session-Session.salam
CSRFVerify
func CSRFVerify(s: Session, token: str): boolThe example for this item is shown with the ones above: net-http-session-Session.salam