Sessions and server side state

Summary

Eliom references let you store data on the server for one user (session data). The interface is very similar to regular OCaml references, with extra parameters ?scope (session, client process, group of sessions) and ?persistent (for keeping the values across server restarts, in the Ocsipersist database). For example if you create an Eliom reference of scope session, its value will be different for each session (one session = one browser process).

To create an Eliom reference containing initial value None:

let myref = Eliom.Reference.Volatile.eref ~scope None

Where ~scope may be (amongst others):

  • Eliom.Common.default_session_scope if you want to store server side data for one browser (one session),
  • Eliom.Common.default_process_scope if you want to store server side data for one tab of a browser (one client process),
  • Eliom.Common.request_scope if you want to store server side data during one request,
  • Eliom.Common.default_group_scope if you want to store server side data for a group of sessions (for example all browsers belonging to the same user -- see below).

Setting this reference, during a request:

...
Eliom.Reference.Volatile.set myref (Some "user data")

Getting the value of this reference (different for each user):

  Eliom.Reference.Volatile.get myref

Most of the time, what we want is to store data for one user, not for one browser instance. To do that, we use scope "group of sessions". A session is attached to a group of session by calling function Eliom.State.set_volatile_data_session_group (for non-persistent groups). The name of the group may be for example the user id. This will automatically create a session (and set a cookie) if needed.

Example:

let open_session login password =
  let%lwt b = check_password login password in
  if b then begin
    Eliom.State.set_volatile_data_session_group
      ~scope:Eliom.Common.default_session_scope
      (Int64.to_string (get_userid login));
    Lwt.return_unit
  end
  else
    ...

Introduction

Different scopes for data and services

The server-side state of an application refers to server-side data that can be shared by all clients, or that can be specific to a limited scope, such as:

  • the site,
  • a session,
  • a group of sessions (for example all sessions of the same user),
  • a client-side process (when programming an Eliom app), or
  • the current request.

For a given scope, server-side state consists of:

  • services registered with this specific scope, and
  • data encapsulated within an Eliom reference created with this specific scope.
  • Bus, channels, etc. (actually implemented using services and Eliom references)

States disappear either when the associated scope is explicitly discarded, or after a timeout.

Scope and cookies

From a technical point of view, sessions and groups of sessions are implemented automatically within Eliom by asking the browser to send a session identifier in a cookie. Client-side processes also send an identifier with each request, using a kind of "client-side process-cookie".

It is possible to create different scopes of the same level if you want several states for the same application (advanced use). Each scope uses its own cookies, and you can discard data for a single scope.

Secure cookies

States can be secure or not. Secure means that the state data or service will be associated to a secure cookie, that is a cookie that is sent by the browser only if the protocol is HTTPS. Use secure states to access confidential data, that you do not want to send through HTTP.

Three kinds of states

Because services are functions, which cannot be saved, there are three kinds of states (for each scope):

  • volatile data states,
  • volatile service states,
  • persistent data states.

Volatile states will not survive after relaunching the server. There is no persistent service state. Be very careful if you use both persistent state data and service state, as your session may become inconsistent. (Use service states only for volatile services, like coservices with timeouts.)

Be very careful where you store your data. To avoid shutting down the server, note that it is possible to ask the server to dynamically load new versions of your site (see command reload).

Scopes, cookies and sessions

Basics

Eliom uses a notion of scopes to restrict the visibility of server-side data to a set of clients. Together with Eliom references, this is a very convenient way to implement a session mechanism for your Web site.

There are two categories of scopes:

Within the user scopes, Eliom distinguishes three scopes that differ with respect to how Eliom associates clients and data.

User scopes are organised in a hierarchy: client processes belong to a session, and sessions belong to a group of sessions.

Data and dynamic services with a user scope can be discarded explicitly or via a timeout. See sections Closing session and Timeouts and session duration for more information.

If you want to handle multiple sessions for the same site (e.g. several different data sessions that could be created and discarded independently), you can create new user scopes that will use different cookies. See section Hierarchies of scopes for more information.

Creating sessions and scopes

Automatic session creation

Eliom automatically creates a session (and sets the corresponding cookie on the client) when you first modify an Eliom reference of scope session, when you register a service with this scope or when you enter a session group.

By default, Eliom is using three cookies for sessions (and session groups):

  • one for session services,
  • one for volatile session data,
  • one for persistent session data.

For client side processes, it uses the same three kinds of client side process cookies.

Hierarchies of scopes and multiple sessions (advanced use)

If you want to handle multiple sessions for the same site that can be created and discarded independently, you can create a new hierarchy of user scopes that will use different cookies, with the function Eliom.Common.create_scope_hierarchy.

let custom_session_hierarchy = Eliom.Common.create_scope_hierarchy "custom"
let custom_session = `Session custom_session_hierarchy

Then, the value custom_session can replace the usual Eliom.Common.default_session_scope for the ~scope parameter of the functions Eliom.Reference.eref, Eliom.Registration.Html.register, Eliom.State.discard, ...

(Same for `Client_process or `Session_group scope levels).

The function Eliom.Common.create_scope_hierarchy will prevent you from creating two scope hierarchies with the same name.

Example of use:

A typical use of hierarchies is to have one hierarchy for connected users (the default hierarchy), and one independent from connection. Use the first hierarchy to save user-related content. It will be discarded when the user logs out. Use the second hierarchy to save data corresponding to a tab or browser, (the user being connected or not), for example because this data must be available before the user logs in.

Closing sessions (and other states)

To discard a state, use the Eliom.State.discard function. It will remove all server-side services and data (persistent or not) for the given scope. Used with ~scope:Eliom.Common.default_session_scope, this will close a session. Used with ~scope:Eliom.Common.default_group_scope, this will close all sessions in the group.

It is also possible to selectively discard only services, persistent data, or volatile data (see the Eliom.State module). But this may be dangerous. Be very careful when doing this, as you are desynchronizing the three kinds of sessions.

Warnings:

  • It is a good idea to close the session when a user tries to connect, even if it is already connected.
  • You may also want to unset some request-scoped Eliom references when discarding a state,
  • If your state data contains opened file descriptors, they won't be closed by OCaml's garbage collector. Close them yourself! (for example using Gc.finalise).

Timeouts and session duration

The default timeout for volatile sessions (services and data) is one hour, and one day for persistent data sessions. Sessions will be automatically closed after this amount of time of inactivity from the user. You can change the timeout for your whole site using the function Eliom.State.set_global_volatile_state_timeout.

It is also possible to change the default value for Eliom through the configuration file, like this:

<extension findlib-package="eliom.server">
  <volatiletimeout value="7200"/>
</extension>

In the configuration files the value "infinity" means no timeout.

This default may be overridden by each site using Eliom.State.set_global_volatile_state_timeout (or, for all sites at once, Eliom.State.set_default_global_service_state_timeout and Eliom.State.set_default_global_volatile_data_state_timeout). If you want your user to be able to set the default in the configuration file for your site (between <site> and </site>), you must parse the configuration (using Eliom.Config.get_config function). You can also change the timeout for a specific user only with the following functions: Eliom.State.set_volatile_data_state_timeout. For more details, see the Eliom.State module's interface.

Secure session

By default, data and services saved in a session are available to requests using both HTTP and HTTPS. If you want to keep some state in a secure session that is visible only to a client accessing with the HTTPS protocol, you may provide the optional parameter ~secure:true when calling functions like Eliom.Reference.eref (or ~secure_session:true for Eliom.Registration.Html.register), etc.

The default can be set in the configuration file:

<extension findlib-package="eliom.server">
   <securecookies value="true" />
</extension>

This option can also be set inside a <eliom> tag, but be careful: in that case it will affect only the actions performed after setting this option, in the same site (and NOT the top-level instructions of the modules loaded before).

Secure sessions are using secure cookies, i.e., Ocsigen Server will ask the browsers to send the cookie only if the protocol is HTTPS. Thus it is not possible to access secure references and services if the user is using HTTP.

The server does not check the protocol currently used, neither to send nor to receive the cookies, which means that it will work even if your server is using HTTP behind a local proxy.

Session groups

Sharing data between a group of sessions

Session group is a kind of scope that allows sharing data or services between a set of sessions, typically all sessions for a given user. For example, using persistent Eliom references with scope Eliom.Common.default_group_scope is a convenient way to store data about a user without having to explicitly use an external database. (Persistent session group states are not discarded when all the sessions are closed).

A session group is identified by a name. The current session could be attached to a group of sessions using one of the following functions, depending on the nature of the data you want to share. They take the session group name as parameter:

A session can only be attached to one group at a time, but it is possible to create multiple sessions for a same client attached to different groups, see section Hierarchies of scopes for more information.

It's possible to fetch the current session group name of a session, if any, or to detach a session from a group. See the module Eliom.State for more information.

Services registered with scope Eliom.Common.default_group_scope are found only by the requests whose service session is in the group: attach the service session with Eliom.State.set_service_session_group, attaching the data sessions is not enough. Like volatile group data, these services are discarded when the last service session of the group is closed, for example when a user who has only one session disconnects. A service session without a group is its own group: the services it registers with the group scope go to its own table.

Limit the number of sessions within a group

The number of sessions in a group is limited: when a group is full, opening a session in it closes the session of the group that has been used least recently. This bounds the memory that one user can take by opening sessions, and a user who floods the server with sessions only closes their own, as each group has its own limit. Sessions that are not in a group are counted together, for the whole site, with a much larger limit; a flood of such sessions by one user closes the sessions without a group of the other users. That is why we recommend to always put the sessions of a user in a group, usually named after the user. The number of persistent sessions without a group is not limited.

By default, a group may hold 50 sessions of each kind (service sessions, volatile data sessions and persistent data sessions), and a browser session may hold 100 tab sessions. To change these limits:

Close all sessions of a group

Session groups allow the implementation of features like "close all sessions" for one user (even those opened on other browsers). Consider the following scenario: a user logs in from home using a "Remember me on this computer" feature, which sets an (almost) no-expiration cookie on his browser and session timeouts of infinity on the server. The user goes on vacation, and while logging from a cyber-café, he also sets the "Remember me" option. Back home he realises the mistake, and wishes to do a "global logout", i.e., closing all existing sessions associated with his user name.

For this kind of usage, it is highly recommended to set a group for each of the three kinds of states you use (services, volatile data and persistent data).

To close all sessions from a group, close the group.

See section Closing sessions for more information.

Eliom references

Principles

Eliom references (erefs) are a kind of references with a (possibly) limited scope. You declare a reference with an initial value and a scope. Then, when you change the associated value, it actually changes only for the scope you specified.

Eliom references can be persistent or not (that is: can survive after relaunching the server or not).

Eliom references are used for example:

  • to store session data, server side data for a client process, or user data (scope: session, client process, session group),
  • or to keep some information about the current request (scope: request), for example to give information to the service taking in charge the request after an action,
  • to implement persistent references (scope: global)
  • for caching functions (Eliom.Reference.eref_from_fun).

Non persistent global Eliom references are equivalent to regular OCaml references.

Eliom references are either created using the function Eliom.Reference.eref, that works like the usual OCaml ref function, but with at least one additional scope parameter. Or they may be created by the function Eliom.Reference.eref_from_fun: Its argument function is evaluated the first time the reference is accessed (through Eliom.Reference.get), within one scope or after the reference has been reset.

The Eliom.Reference module also defines functions to get the value, set, modify it (by applying a function to its content), and unset it, that is, reset it to its initial value.

Persistent references

Persistent references are Eliom references that survive a restart of the server. Their values are stored with Ocsipersist, like the data of persistent sessions. Ocsipersist has three backends: SQLite (package ocsipersist-sqlite), DBM (ocsipersist-dbm) and PostgreSQL (ocsipersist-pgsql). Your program must link one of them, and give it the location of the database (see Configuring the database). Since ocsipersist 3.2, there is no default location.

Persistent references are created by adding the ~persistent parameter to the Eliom.Reference.eref function calls. Since Eliom 13, the value of this parameter is a pair (name, codec): name is the name of the reference in the database, and codec is a Deriving_Json codec used to serialise the stored value:

type user_pref = { lang : string; theme : string } [@@deriving json]

let prefs =
  Eliom.Reference.eref
    ~persistent:("user_prefs", [%json: user_pref])
    ~scope:Eliom.Common.default_group_scope
    { lang = "en"; theme = "light" }

The type stored in the reference must be annotated with [@@deriving json] (or any equivalent way of producing a Deriving_Json.t value, e.g. Deriving_Json.convert for an abstract type, or one of the Deriving_Json.Json_xxx functors for parametric types).

Before Eliom 13, the value parameter of ~persistent was just the table name (a string), and Eliom used OCaml's unsafe Stdlib.Marshal module to serialise persistent data, which led to the following limitations:

  • On-disk values were unsafe to read across OCaml versions (Marshal gives no inter-version stability guarantee on certain types);
  • It was not possible to serialise closures or services (as we are using dynamic linking);
  • If you ever changed the type of serialised data, you had to also change the persistent reference name, or the server would crash while deserialising.

The Deriving_Json migration removes all of these limitations: serialised data is human-readable, stable across OCaml versions, and the [@@deriving json] annotation forces the codec to match the stored type at compile time.

Upgrading an existing site to Eliom 13 resets all persistent data. The JSON tables are given new names (a _json_ prefix for references and per-session/site state; new version suffixes for cookies and session groups), so the pre-13 Marshal tables are left orphaned and never read. Persistent references start again from their default value and users are logged out once. The old tables are kept in the database (they are not deleted), so their content can still be inspected or recovered by hand if needed. A stored value that cannot be deserialised is treated as an absent value rather than raising, so an incomplete migration never turns into a server error.

Beware that this reset concerns more than sessions: persistent references with the session-group scope are commonly keyed by user (the group name being the user id) and used to store durable per-user data such as preferences; those also restart from their default value, silently. If your application keeps data it cannot afford to lose in persistent references (group, site or global scope), export it before upgrading, with the Eliom 12 version of your application (for example into your SQL database), and re-import it through the new API afterwards. Reading the orphaned Marshal tables later remains possible in theory, but requires code built with the exact types the old application stored. Once the upgrade is validated, the old tables can be dropped from the storage backend to reclaim space.

Configuring the database

With a configuration file, load the configuration extension of your Ocsipersist backend before Eliom, and give it the location of the database. For SQLite, use ocsipersist-sqlite-config, whose <database> tag gives the database file (its directory must exist):

<extension findlib-package="ocsipersist-sqlite-config">
  <database file="/var/lib/mysite/ocsidb"/>
</extension>
<extension findlib-package="eliom.server"/>

For DBM, use ocsipersist-dbm-config, whose <store dir="..."/> tag gives the directory of the database. For PostgreSQL, use ocsipersist-pgsql-config, whose <database .../> tag gives the connection parameters.

Without a configuration file (when your program starts the server itself with Ocsigen.Server.start and Eliom.App.run), give the location with module Ocsipersist_settings before the server starts. For SQLite, link library ocsipersist-sqlite.settings and call:

let () = Ocsipersist_settings.set_db_file "/var/lib/mysite/ocsidb"

For DBM, link library ocsipersist-dbm.settings and call:

let () =
  Ocsipersist_settings.set_store "/var/lib/mysite/store";
  Ocsipersist.init ()

For PostgreSQL, link library ocsipersist-pgsql.settings, give the connection parameters with its functions (set_host, set_database...), then call Ocsipersist.init ().

Volatile references

The module Eliom.Reference.Volatile allows the creation of non-persistent Eliom references, which can then be used through a non-Lwt interface.

As Eliom.Reference.Volatile.eref is a subtype of Eliom.Reference.eref, a volatile reference eref may be used as (eref :> _ Eliom.Reference.eref) with the Lwt-interface of Eliom.Reference alike.

Accessing other states

Sometimes, it is useful to access other states. For example if you want to send a notification to another user, you may want to find the communication channel registered for this user. It can probably be found as an Eliom reference in the group corresponding to this user.

Use module Eliom.State.Ext to get the state corresponding to a group name. Use Eliom.State.Ext.iter_sub_states to iterate on all sessions in a group, or on all client processes in a session.

Use Eliom.Reference.Ext to access Eliom references belonging to another state.

Low-level cookies manipulation

Eliom references are used to store data on the server-side. It is also possible to ask the browser or the client-side process to record some piece of data and send it back to the server with each request.

This is implemented using the usual browser cookies for sessions (and a simulation of browser cookies by Eliom client side processes), with the function Eliom.State.set_cookie; the cookies sent by the client can be read back with Eliom.Request_info.get_cookies.