3. More details on services and page generation
You now know all Eliom's main concepts. In that part, we'll give more details on some aspects that have been seen before:
- The different types of output for services
- Timeouts and error handling
- Persistence of sessions
- Advanced forms
Static parts ¶
Fully static pages
The staticmod extension allows to associate
to your site a static directory
where you can put all the static (non generated) parts of your
web-site (for examples images and stylesheets).
See the default config file ocsigen.conf to
learn how to do that.
A predefined service can be used to make links to static files.
Get it using
(static_dir sp).
That service takes as string parameter the name of the file.
For example
Eliom.a (static_dir ~sp) sp [pcdata "download image"] "ocsigen8-100x30.png"
creates this link: download image
It is now also possible to handle static pages with Eliom, using Eliom_predefmod.Files (see later).
Other kinds of pages ¶
Sending portions of pages
The Eliom_predefmod.Blocks module allows to register services that send portions of pages, of any type that may be contained directly in a <body> tag (blocks of xhtml DTD). It is useful to create AJAX pages (i.e. pages using the XMLHttpRequest Javascript object). Note that the service returns a list of blocks.
let divpage = Eliom_predefmod.Blocks.register_new_service ~path:["div"] ~get_params:unit (fun sp () () -> return [div [h2 [pcdata "Hallo"]; p [pcdata "Blablablabla"] ]])
The Eliom_predefmod.SubXhtml module allows to create other modules for registering portions of pages of other types. For example, Eliom_predefmod.Blocks is defined by:
module Blocks = SubXhtml(struct type content = Xhtmltypes.body_content end)
Redirections
The Eliom_predefmod.Redirection module allows to register HTTP redirections.
[New in 1.1.0. For 1.0.0, please see module Eliom_predefmod.Redirections.]
If a request is done towards such a service, the server asks the browser
to retry with another URL.
Such services return a GET service without parameter at all. Example:
let redir1 = Eliom_predefmod.Redirection.register_new_service ~options:`Temporary ~path:["redir"] ~get_params:Eliom_parameters.unit (fun sp () () -> Lwt.return coucou)
If you want to give parameters to such services, use Eliom_services.preapply (see also later in the tutorial). Example:
let redir = Eliom_predefmod.Redirection.register_new_service ~options:`Temporary ~path:["redir"] ~get_params:(int "o") (fun sp o () -> Lwt.return (Eliom_services.preapply coucou_params (o,(22,"ee"))))
The options parameter may be either `Temporary or `Permanent.
Note that the cost of a redirection is one more query and one more answer.
Sending files ¶
You may want to register a service that will send files. To do that, use the Eliom_predefmod.Files module. Example:
let sendfile = Files.register_new_service ~path:["sendfile"] ~get_params:unit (fun _ () () -> return "filename")
Other example, with suffix URL:
let sendfile2 = Files.register_new_service ~path:["files"] ~get_params:(suffix (all_suffix "filename")) (fun _ s () -> return ("//path//"^(Ocsigen_lib.string_of_url_path ~encode:false s)))
The extension Staticmod is another way to handle static files (see the default configuration file for more information).
Registering services that decide what they want to send
You may want to register a service that will send, for instance, sometimes an xhtml page, sometimes a file, sometimes something else. To do that, use the Eliom_predefmod.Any module, together with the send function of the module you want to use. Example:
let send_any = Eliom_predefmod.Any.register_new_service ~path:["sendany"] ~get_params:(string "type") (fun sp s () -> if s = "valid" then Eliom_predefmod.Xhtml.send sp (html (head (title (pcdata "")) []) (body [p [pcdata "This page has been statically typechecked.\n If you change the parameter in the URL you will get an unchecked text page"]])) else Eliom_predefmod.HtmlText.send sp "<html><body><p>It is not a valid page. Put type=\"valid\" in the URL to get a typechecked page.</p></body></html>" )
See a valid page, and a non valid page.
You may also use Eliom_predefmod.Any to send cookies or to choose a different charset than the default (default charset is set in configuration file) for the page you send. To do that use the optional parameters ?cookies and ?charset of the send function.
Cookies
A simplest way to set your own cookies on the client is to use functions like Eliom_predefmod.Xhtml.Cookies.register instead of Eliom_predefmod.Xhtml.register. The function you register returns a pair containing the page (as usual) and a list of cookies, of type Eliom_services.cookie defined by:
type cookie = | Set of string list option * float option * string * string * bool | Unset of string list option * string
[New in 1.1.0] For version 1.0.0, the type cookie was slightly different (no secure cookies).
The string list option is a the path for which you want to set/unset the cookie (relative to the main directory of your site, defined in the configuration file). None means for all your site.
The float option is a the expiration date (Unix timestamp, in seconds since the epoch). None means that the cookie will expire when the browser will be closed.
If the bool is true and the protocol is https, the server will ask the browser to send the cookie only through secure connections.
You can access the cookies sent by the browser using Eliom_sessions.get_cookies.
Example:
let cookiename = "mycookie" let cookies = new_service ["cookies"] unit () let _ = Cookies.register cookies (fun sp () () -> return ((html (head (title (pcdata "")) []) (body [p [pcdata (try "cookie value: "^ (Ocsigen_lib.String_Table.find cookiename (Eliom_sessions.get_cookies sp)) with _ -> "<cookie not set>"); br (); a cookies sp [pcdata "send other cookie"] ()]])), [Eliom_services.Set (None, None, cookiename, string_of_int (Random.int 100), false)]))
Persistence of sessions ¶
Tables of sessions (for data or services) are kept in memory, and thus will disappear if you close the server process. To solve this problem, Ocsigen allows to reload the modules of your configuration file without shutting down the server. Another solution provided by Eliom is to save session data on hard drive.
Updating sites without shutting down the server
To reload the modules of the configuration file without stoping the server, use /etc/init.d/ocsigen reload for most of the distributions, or do it manually using:
echo reload > /var/run/ocsigen_command
Only modules loaded inside <site> or <library> will be reloaded. Module loaded using <extension> will not.
Have a look at the logs to see if all went well during the reload. If something went wrong, old services may still be reachable.
Note that coservices created with the old modules or URLs that have not been masked by new ones will still reachable after the update.
During the reload, some information of the configuration file will not be re-read (for example port numbers, user and group, etc.).
Persistent data
Eliom allows to use more persistent data, using the module Ocsipersist. (Ocsipersist is needed in eliom.cma, thus you need to dynlink it in the configuration file before Eliom). There are currently two implementations of Ocsipersist: ocsipersist-dbm.cma (uses the DBM database) and ocsipersist-sqlite.cma (uses the SQLite database, and depends on sqlite3.cma).
These modules allow to:
- Create persistent references (still present after restarting the server),
- Create persistent association tables,
- Set persistent session data (using set_persistent_data, see below).
Note that persistent data are serialized on hard drive using OCaml's Marshal module:
- It is not possible to serialize closures or services (as we are using dynamic linking).
- If you ever change the type of serialised data, don't forget to delete the database file! Or if you really want to keep it, and you know what you are doing, you can use the sqlite client to manually update the table or a program to create a new sqlite or dbm table for the new type.
Suppose for example that you use get/set_persistent_data (see below) to store a (int, string) tuple with the user's login credentials. At this point you stop the server, and change the code such that get/set_persistent_data now to store a (int, string, string). Now recompile and restart the server. If by any chance a client with an old cookie reconnects, you get a segfault on the server, because of the type change in the data stored in the DB backend ...
Persistent references
Ocsipersist allows to create persistent references. Here is an example of page with a persistent counter:
let mystore = Ocsipersist.open_store "eliomexamplestore2" let count2 = let next = let cthr = Ocsipersist.make_persistent mystore "countpage" 0 in let mutex = Lwt_mutex.create () in (fun () -> cthr >>= (fun c -> Lwt_mutex.lock mutex >>= fun () -> Ocsipersist.get c >>= (fun oldc -> let newc = oldc + 1 in Ocsipersist.set c newc >>= (fun () -> Lwt_mutex.unlock mutex; return newc)))) in register_new_service ~path:["count2"] ~get_params:unit (fun _ () () -> next () >>= (fun n -> return (html (head (title (pcdata "counter")) []) (body [p [pcdata (string_of_int n)]]))))
Persistent tables
Ocsipersist also allows to create very basic persistent tables. Use them if you don't need complex requests on your tables. Otherwise use a database such as PostgreSQL or MySQL. Here are the interface you can use:
type 'value table val open_table : string -> 'value table val find : 'value table -> string -> 'value Lwt.t val add : 'value table -> string -> 'value -> unit Lwt.t val remove : 'value table -> string -> unit Lwt.t
As you can see, all these function are cooperative.
Persistent session data
Eliom also implements persistent session tables. You can use them instead of memory tables if you don't need to register closures.
The following example is a new version of our site with users, with persistent connections. (login_box, disconnect_box and disconnect_action are the same as before).
(************************************************************) (************ Connection of users, version 4 ****************) (**************** (persistent sessions) *********************) (************************************************************) let my_persistent_table = create_persistent_table "eliom_example_table" (* -------------------------------------------------------- *) (* We create one main service and two (POST) actions *) (* (for connection and disconnection) *) let persist_session_example = Eliom_services.new_service ~path:["persist"] ~get_params:unit () let persist_session_connect_action = Eliom_services.new_post_coservice' ~name:"connect4" ~post_params:(string "login") () (* disconnect_action, login_box and disconnect_box have been defined in the section about actions *) (* ----------------------------------------------------------- *) (* Handler for "persist_session_example" service (main page): *) let persist_session_example_handler sp () () = Eliom_sessions.get_persistent_session_data ~table:my_persistent_table ~sp () >>= fun sessdat -> return (html (head (title (pcdata "")) []) (body (match sessdat with | Eliom_sessions.Data name -> [p [pcdata ("Hello "^name); br ()]; disconnect_box sp "Close session"] | Eliom_sessions.Data_session_expired -> [login_box sp true persist_session_connect_action; p [em [pcdata "The only user is 'toto'."]]] | Eliom_sessions.No_data -> [login_box sp false persist_session_connect_action; p [em [pcdata "The only user is 'toto'."]]] ))) (* ----------------------------------------------------------- *) (* Handler for persist_session_connect_action (user logs in): *) let persist_session_connect_action_handler sp () login = Eliom_sessions.close_session ~sp () >>= fun () -> if login = "toto" (* Check user and password :-) *) then Eliom_sessions.set_persistent_session_data ~table:my_persistent_table ~sp login else (return ()) (* -------------------------------------------------------- *) (* Registration of main services: *) let () = Eliom_predefmod.Xhtml.register ~service:persist_session_example persist_session_example_handler; Eliom_predefmod.Action.register ~service:persist_session_connect_action persist_session_connect_action_handler
As it is not possible to serialize closures, there is no persistent session service table. Be very carefull if you use both persistent session data tables and service session tables, as your session may become inconsistent (use the session service table only for volatile services, like coservices with timeouts).
Other concepts ¶
Pre-applied services ¶
Services or coservices with GET parameters can be preapplied to obtain a service without parameters. Example:
let preappl = Eliom_services.preapply coucou_params (3,(4,"cinq"))
It is not possible to register something on a preapplied service, but you can use them in links or as fallbacks for coservices.
Void coservices [New in 1.1.0] ¶
Eliom_services.void_coservice' is a special non-attached action, with special behaviour: it has no parameter at all, even non-attached parameters. Use it if you want to make a link to the current page without non-attached parameters. It is almost equivalent to a POST non-attached coservice without POST parameters, on which you register an action that does nothing, but you can use it with <a> links, not only forms. Example:
Eliom_duce.Xhtml.a ~service:Eliom_services.void_coservice' ~sp {{ "cancel" }} ()
There is also Eliom_services.https_void_coservice' (same, but forces use of HTTPS), Eliom_services.void_hidden_coservice', and Eliom_services.https_void_hidden_coservice'. "Hidden" means that they keep GET non attached parameters.
Giving information to fallbacks ¶
The function Eliom_sessions.get_link_too_old returns true if the coservice called has not been found. In that case, the current service is the fallback.
The function Eliom_sessions.get_expired_service_sessions returns returns the list of names of service sessions expired for the current request.
It is also possible to send other information to fallback, about what succeeded before they were called. Put this information in the request cache. The request cache is a polymorphic table returned by Eliom_sessions.get_request_cache. See the module Polytables to understand how to use it. You may also want to use this table to cache some data during the duration of a request.
Here is a new version of the example of session with actions, using the polymorphic request data table:
(************************************************************) (************ Connection of users, version 6 ****************) (************************************************************) (* -------------------------------------------------------- *) (* We create one main service and two (POST) actions *) (* (for connection and disconnection) *) let connect_example6 = Eliom_services.new_service ~path:["action2"] ~get_params:unit () let connect_action = Eliom_services.new_post_coservice' ~name:"connect6" ~post_params:(string "login") () (* new disconnect action and box: *) let disconnect_action = Eliom_predefmod.Action.register_new_post_coservice' ~name:"disconnect6" ~post_params:Eliom_parameters.unit (fun sp () () -> Eliom_sessions.close_session ~sp ()) let disconnect_box sp s = Eliom_predefmod.Xhtml.post_form disconnect_action sp (fun _ -> [p [Eliom_predefmod.Xhtml.string_input ~input_type:`Submit ~value:s ()]]) () let bad_user_key = Polytables.make_key () let get_bad_user table = try Polytables.get ~table ~key:bad_user_key with Not_found -> false (* -------------------------------------------------------- *) (* new login box: *) let login_box sp session_expired action = Eliom_predefmod.Xhtml.post_form action sp (fun loginname -> let l = [pcdata "login: "; string_input ~input_type:`Text ~name:loginname ()] in [p (if get_bad_user (Eliom_sessions.get_request_cache sp) then (pcdata "Wrong user")::(br ())::l else if session_expired then (pcdata "Session expired")::(br ())::l else l) ]) () (* -------------------------------------------------------- *) (* Handler for the "connect_example6" service (main page): *) let connect_example6_handler sp () () = let group = Eliom_sessions.get_volatile_data_session_group ~sp () in return (html (head (title (pcdata "")) []) (body (match group with | Eliom_sessions.Data name -> [p [pcdata ("Hello "^name); br ()]; disconnect_box sp "Close session"] | Eliom_sessions.Data_session_expired -> [login_box sp true connect_action; p [em [pcdata "The only user is 'toto'."]]] | Eliom_sessions.No_data -> [login_box sp false connect_action; p [em [pcdata "The only user is 'toto'."]]] ))) (* -------------------------------------------------------- *) (* New handler for connect_action (user logs in): *) let connect_action_handler sp () login = Eliom_sessions.close_session ~sp () >>= fun () -> if login = "toto" (* Check user and password :-) *) then begin Eliom_sessions.set_volatile_data_session_group ~set_max:4 ~sp login; return () end else begin Polytables.set (Eliom_sessions.get_request_cache sp) bad_user_key true; return () end (* -------------------------------------------------------- *) (* Registration of main services: *) let () = Eliom_predefmod.Xhtml.register ~service:connect_example6 connect_example6_handler; Eliom_predefmod.Action.register ~service:connect_action connect_action_handler
If the actions raises an exception (with Lwt.fail), the server will send an error 500 (like for any other service). Think about catching the exceptions and put them in the list if they correspond to usual cases you want to handle while generating the page after the action.
Disposable coservices
It is possible to set a limit to the number of uses of (attached or non-attached) coservices. Just give the maximum number of uses with the optional ?max_use parameter while creating your coservices. Example
let disposable = new_service ["disposable"] unit () let _ = register disposable (fun sp () () -> let disp_coservice = new_coservice ~max_use:2 ~fallback:disposable ~get_params:unit () in register_for_session sp disp_coservice (fun sp () () -> return (html (head (title (pcdata "")) []) (body [p [pcdata "I am a disposable coservice"; br (); a disp_coservice sp [pcdata "Try me once again"] ()]])) ); return (html (head (title (pcdata "")) []) (body [p [(if Eliom_sessions.get_link_too_old sp then pcdata "Your link was outdated. I am the fallback. I just created a new disposable coservice. You can use it only twice." else pcdata "I just created a disposable coservice. You can use it only twice."); br (); a disp_coservice sp [pcdata "Try it!"] ()]])))
[[site:tuto/disposa using:
Eliom_sessions.set_global_volatile_timeout ~sp (Some 7200.)
You can change that value for one user only using:
Eliom_sessions.set_volatile_session_timeout ~sp (Some 7200.)
Note that there is also a possibility to change the default value for Eliom in the configuration file like this:
<extension findlib-package="ocsigen.ext.eliom"> <volatiletimeout value="7200"/> </extension>
means no timeout.
Warning: that default may be overriden by each site using Eliom_sessions.set_global_volatile_timeout or Eliom_sessions.set_default_volatile_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 (Eliom_sessions.get_config function, see below).
Timeout for coservices
It is also possible to put timeouts on coservices using the optional parameter ?timeout of functions new_coservice, new_coservice', etc. Note that session coservices cannot survive after the end of the session. Use this if you don't want your coservice to be available during all the session duration. For example if your coservice is here to show the results of a search, you probably want it to be available only for a short time. The following example shows a coservice with timeout registered in the session table.
let timeout = new_service ["timeout"] unit () let _ = let page sp () () = let timeoutcoserv = register_new_coservice_for_session ~sp ~fallback:timeout ~get_params:unit ~timeout:5. (fun _ _ _ -> return (html (head (title (pcdata "Coservices with timeouts")) []) (body [p [pcdata "I am a coservice with timeout."; br (); pcdata "Try to reload the page!"; br (); pcdata "I will disappear after 5 seconds of inactivity." ]; ]))) in return (html (head (title (pcdata "Coservices with timeouts")) []) (body [p [pcdata "I just created a coservice with 5 seconds timeout."; br (); a timeoutcoserv sp [pcdata "Try it"] (); ]; ])) in register timeout page
Registering coservices in public table during session
If you want to register coservices in the public table during a session, (that is, after the initialisation phase of your module), you must add the optional sp parameter to the register function. Remember that using register without sp is possible only during initialisation!
We recommend to put a timeout on such coservices, otherwise, they will be available until the end of the server process, and it will not be possible to re-create them when the server is relaunched.
The following example is a translation of the previous one using the public table:
let publiccoduringsess = new_service ["publiccoduringsess"] unit () let _ = let page sp () () = let timeoutcoserv = register_new_coservice ~sp ~fallback:publiccoduringsess ~get_params:unit ~timeout:5. (fun _ _ _ -> return (html (head (title (pcdata "Coservices with timeouts")) []) (body [p [pcdata "I am a public coservice with timeout."; br (); pcdata "I will disappear after 5 seconds of inactivity." ]; ]))) in return (html (head (title (pcdata "Public coservices with timeouts")) []) (body [p [pcdata "I just created a public coservice with 5 seconds timeout."; br (); a timeoutcoserv sp [pcdata "Try it"] (); ]; ])) in register publiccoduringsess page
Defining an exception handler for the whole site
When an exception is raised during the generation of a page, or when the page has not been found or has wrong parameters, an HTTP error 500 or 404 is sent to the client. You may want to catch these exceptions to print your own error page. Do this using Eliom_services.set_exn_handler. Here is the handler used by this tutorial:
let _ = Eliom_services.set_exn_handler (fun sp e -> match e with | Eliom_common.Eliom_404 -> Eliom_predefmod.Xhtml.send ~code:404 ~sp (html (head (title (pcdata "")) []) (body [h1 [pcdata "Eliom tutorial"]; p [pcdata "Page not found"]])) | Eliom_common.Eliom_Wrong_parameter -> Eliom_predefmod.Xhtml.send ~sp (html (head (title (pcdata "")) []) (body [h1 [pcdata "Eliom tutorial"]; p [pcdata "Wrong parameters"]])) | e -> fail e)
Giving configuration options to your sites
You can add your own options in the configuration file for your Web site. For example:
<eliom module="//path_to///yourmodule.cmo"> <youroptions> ... </eliom>
Use Eliom_sessions.get_config during the initialization of your module to get the data between <eliom> and </eliom>. Warning: parsing these data is very basic for now. That feature will be improved in the future.
More about sessions - session names
By default, Eliom is using three cookies :
- One for session services,
- one for volatile session data,
- one for persistent session data.
They correspond to three different sessions (opened only if needed). Eliom_sessions.close_session closes all three sessions, but you may want to desynchronize the three sessions by using Eliom_sessions.close_persistent_session (persistent session), Eliom_sessions.close_service_session (session services), or Eliom_sessions.close_data_session (volatile data session). There is also Eliom_sessions.close_volatile_session for both volatile data session and session services. The module Eliom_sessions also contains functions for setting timeouts or expiration dates for cookies for each kind of session.
If you need more sessions (for example several different data sessions) for the same site, you can give a name to your sessions by giving the optional parameter ?session_name to functions like Eliom_sessions.close_data_session, Eliom_mkreg.ELIOMREGSIG1.register_for_session, or Eliom_sessions.get_volatile_session_data. Note that this tutorial has been implemented using this feature, even if it has been hidden for the sake of simplicity. That's how the different examples of sessions in this tutorial are independant.
Secure services [New in 1.1.0]
You may want to impose HTTPS for some of your services. To do that, use the optional parameter https:true while creating your service.
It is also possible to require http or https while creating a link or a form (using the optional parameter https:true). But it is never possible to make an http link towards an https service, even if you request it.
Warning: if the protocol needs to be changed (from http to https or vice versa), Eliom will generate absolute URLs. The host name and port numbers are guessed from the IP and the configuration by default, but it is recommended to specify them in the configuration file. For example:
<host hostfilter="*.org" defaulthostname="www.mywebsite.org" defaulthttpport="8080" defaulthttpsport="4433"> ... </host>
Secure sessions [New in 1.1.0]
For security reasons, Eliom does not use the same cookies in https and http. Secure sessions are using secure cookies (i.e. Ocsigen will ask the browsers to send the cookie only if the protocol is HTTPS). Thus it is not possible to access secure session if the user is using http. If the user is using https, Eliom will save data and services in secure session. But it is possible to access unsecure session data and to register unsecure session services using the optional parameter secure:false when calling functions like Eliom_sessions.set_volatile_session_data, Eliom_sessions.get_persistent_session_data, Eliom_predefmod.Xhtml.register_for_session, etc.
Non localized parameters[New in 1.3.0]
Non localized parameters are GET or POST parameters that are not taken into account by Eliom for choosing the service. They have a special prefix. Use this if you want some information to be available or not, through parameters, for all of your services.
let my_nl_params = Eliom_parameters.make_non_localized_parameters ~prefix:"tutoeliom" ~name:"mynlparams" (Eliom_parameters.int "a" ** Eliom_parameters.string "s") let nlparams = register_new_service ~path:["nlparams"] ~get_params:(int "i") (fun sp i () -> Lwt.return (html (head (title (pcdata "")) []) (body [p [pcdata "i = "; strong [pcdata (string_of_int i)]]; (match Eliom_parameters.get_non_localized_get_parameters sp my_nl_params with | None -> p [pcdata "I do not have my non localized parameters"] | Some (a, s) -> p [pcdata "I have my non localized parameters, "; pcdata ("with values a = "^string_of_int a^ " and s = "^s^".")] )])) )
To create a link or a form with non-localized parameters, use the optional parameter nl_params of functions Eliom_predefmod.XHTMLFORMSSIG.a, Eliom_predefmod.XHTMLFORMSSIG.get_form or Eliom_predefmod.XHTMLFORMSSIG.post_form. Example:
let tonlparams = register_new_service ~path:["nlparams"] ~get_params:unit (fun sp i () -> Lwt.return (html (head (title (pcdata "")) []) (body [p [a ~service:nlparams ~sp [pcdata "without nl params"] 4]; p [a ~service:nlparams ~sp ~nl_params:(Eliom_parameters.add_nl_parameter Eliom_parameters.empty_nl_params_set my_nl_params (22, "oh") ) [pcdata "with nl params"] 5]; get_form ~service:nlparams ~sp ~nl_params:(Eliom_parameters.add_nl_parameter Eliom_parameters.empty_nl_params_set my_nl_params (22, "oh") ) (fun iname -> [p [pcdata "form with hidden nl params"; Eliom_predefmod.Xhtml.int_input ~input_type:`Text ~name:iname (); Eliom_predefmod.Xhtml.string_input ~input_type:`Submit ~value:"Send" ()]]); get_form ~service:nlparams ~sp (fun iname -> let (aname, sname) = Eliom_parameters.get_nl_params_names my_nl_params in [p [pcdata "form with nl params fiels"; Eliom_predefmod.Xhtml.int_input ~input_type:`Text ~name:iname (); Eliom_predefmod.Xhtml.int_input ~input_type:`Text ~name:aname (); Eliom_predefmod.Xhtml.string_input ~input_type:`Text ~name:sname (); Eliom_predefmod.Xhtml.string_input ~input_type:`Submit ~value:"Send" ()]]); ])) )
It is also possible to create a new service by adding the non localized parameters to an existing service:
let nlparams_with_nlp = Eliom_services.add_non_localized_get_parameters my_nl_params nlparams
Then create your link as usual, for example: a nlparams_with_nlp sp [pcdata "Try it"] (22, (11, "aa")). Try it.
[New in 0.99.5] Session groups ¶
The idea is complementary to that of the "session name". While the optional session_name parameter allows for a single session to have multiple buckets of data associated with it, a session_group parameter (also optional) allow multiple sessions to be referenced together. For most uses, the session group is the user name. It allows to implement features like "close all sessions" for one user (even those opened on other browsers), or to limit the number of sessions one user may open at the same time.
Session groups have been suggested by Dario Teixeira and introduced in Eliom 0.99.5. Dario explains: Consider the following scenario: a user logs in from home using a "Remember me on this computer" feature, which sets a (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é, she also sets the "Remember me" option. Back home she realises her mistake, and wishes to do a "global logout", ie, closing all existing sessions associated with her user name.
It is highly recommended to use session groups! If you do not use them, the number of session is limitated by IP address, which can be a problem for example if the server is behind a reverse proxy.
(************************************************************) (************ Connection of users, version 5 ****************) (************************************************************) (* -------------------------------------------------------- *) (* We create one main service and two (POST) actions *) (* (for connection and disconnection) *) let connect_example5 = Eliom_services.new_service ~path:["groups"] ~get_params:Eliom_parameters.unit () let connect_action = Eliom_services.new_post_coservice' ~name:"connect5" ~post_params:(Eliom_parameters.string "login") () (* As the handler is very simple, we register it now: *) let disconnect_action = Eliom_predefmod.Action.register_new_post_coservice' ~name:"disconnect5" ~post_params:Eliom_parameters.unit (fun sp () () -> Eliom_sessions.close_session ~sp ()) (* -------------------------------------------------------- *) (* login ang logout boxes: *) let disconnect_box sp s = Eliom_predefmod.Xhtml.post_form disconnect_action sp (fun _ -> [p [Eliom_predefmod.Xhtml.string_input ~input_type:`Submit ~value:s ()]]) () let login_box sp = Eliom_predefmod.Xhtml.post_form connect_action sp (fun loginname -> [p (let l = [pcdata "login: "; Eliom_predefmod.Xhtml.string_input ~input_type:`Text ~name:loginname ()] in l) ]) () (* -------------------------------------------------------- *) (* Handler for the "connect_example5" service (main page): *) let connect_example5_handler sp () () = let sessdat = Eliom_sessions.get_volatile_data_session_group ~sp () in return (html (head (title (pcdata "")) []) (body (match sessdat with | Eliom_sessions.Data name -> [p [pcdata ("Hello "^name); br ()]; disconnect_box sp "Close session"] | Eliom_sessions.Data_session_expired | Eliom_sessions.No_data -> [login_box sp] ))) (* -------------------------------------------------------- *) (* Handler for connect_action (user logs in): *) let connect_action_handler sp () login = Eliom_sessions.close_session ~sp () >>= fun () -> Eliom_sessions.set_volatile_data_session_group ~set_max:4 ~sp login; return () (* -------------------------------------------------------- *) (* Registration of main services: *) let () = Eliom_predefmod.Xhtml.register ~service:connect_example5 connect_example5_handler; Eliom_predefmod.Action.register ~service:connect_action connect_action_handler
Note that in this case, we do not need a session table any more, because our session table was containing only the user name, and the user name is now the session group. (But if we need to save more data, we still need a session table).
As we will see later, there are three kinds of sessions (services, volatile data and persistent data). It is highly recommended to set a group for each of them!
[New in 1.3.0]CSRF-safe services ¶
Eliom implements a protection against CSRF attacks.
What is CSRF?
CSRF means Cross Site Request Forgery. Here is an explanation from Wikipedia:
For example, one user, Bob, might be browsing a chat forum where another user, Mallory, has posted a message. Suppose that Mallory has crafted an HTML image element that references a script on Bob's bank's website (rather than an image file), e.g., <img src="http://bank.example/withdraw?account=bob&amount=1000000&for=mallory"> If Bob's bank keeps his authentication information in a cookie, and if the cookie hasn't expired, then the attempt by Bob's browser to load the image will submit the withdrawal form with his cookie, thus authorizing a transaction without Bob's approval.
Solution with Eliom <= 1.2
There is an easy way to protect a service from such attacks with Eliom 1.2: just create a new anonymous coservice with timeout each time you display the form. Thus, a new token will be created for each form and no service will answer if you do not send it (or more precisely the fallback will do).
CSRF-safe services
In order to simplify this, Eliom add this possibility:
- When creating a new coservice, you can give the optional csrf_safe parameter
- If this parameter is true, actual registration of the service will be delayed and performed each time a form is created towards this coservice
Example:
let csrfsafe_example = Eliom_services.new_service ~path:["csrf"] ~get_params:Eliom_parameters.unit () let csrfsafe_example_post = Eliom_services.new_post_coservice ~csrf_safe:true ~csrf_session_name:"csrf" ~csrf_secure_session:true ~timeout:10. ~max_use:1 ~https:true ~fallback:csrfsafe_example ~post_params:Eliom_parameters.unit () let _ = let page sp () () = let l3 = Eliom_predefmod.Xhtml.post_form csrfsafe_example_post sp (fun _ -> [p [Eliom_predefmod.Xhtml.string_input ~input_type:`Submit ~value:"Click" ()]]) () in return (html (head (title (pcdata "CSRF safe service example")) []) (body [p [pcdata "A new coservice will be created each time this form is displayed"]; l3])) in Eliom_predefmod.Xhtml.register csrfsafe_example page; Eliom_predefmod.Xhtml.register csrfsafe_example_post (fun sp () () -> Lwt.return (html (head (title (pcdata "CSRF safe service")) []) (body [p [pcdata "This is a CSRF safe service"]])))
If you register in the global service table, the CSRF safe service will be available for everybody. But the actual (delayed) registration will take place in a session table, described by ?csrf_session_name and ?csrf_secure_session (corresponding to ?session_name and ?secure).
If you use register_for_session, the coservice will be available only for one session. The actual registration will take place in the same session table, described by ?csrf_session_name and ?csrf_secure_session. In that case, the parameters {{?session_name}}} and ?secure of register_for_session must be exactly the same.
Advanced forms and parameters ¶
This section shows more advanced use of page parameters and corresponding forms.
Parsing parameters using regular expressions
Eliom_parameters.regexp allows to parse page parameters using (Perl-compatible) regular expressions. We use the module Netstring_pcre, from OCamlnet. See the documentation about OCamlnet for more information. The following example shows a service that accepts only parameters values enclosed between [ and ]:
let r = Netstring_pcre.regexp "\\\\[(.*)\\\\]" let regexp = Eliom_predefmod.Xhtml.register_new_service ~path:["regexp"] ~get_params:(regexp r "$1" "myparam") (fun _ g () -> return (html (head (title (pcdata "")) []) (body [p [pcdata g]])))
Boolean checkboxes
Page may take parameter of type bool. A possible use of this type is in a form with boolean checkboxes, as in the example below:
(* Form with bool checkbox: *) let bool_params = register_new_service ~path:["bool"] ~get_params:(bool "case") (fun _ case () -> return << <html> <head><title></title></head> <body> <p> $pcdata (if case then "checked" else "not checked")$ </p> </body> </html> >>) let create_form_bool casename = <:xmllist< <p>check? $bool_checkbox ~name:casename ()$ <br/> $string_input ~input_type:`Submit ~value:"Click" ()$</p> >> let form_bool = register_new_service ["formbool"] unit (fun sp () () -> let f = get_form bool_params sp create_form_bool in return << <html> <head><title></title></head> <body> $f$ </body> </html> >>)
Important warning: As you can see, browsers do not send any value for unchecked boxes! An unchecked box is equivalent to no parameter at all! Thus it is not possible to distinguish between a service taking a boolean and a service taking no parameter at all (if they share the same URL). In Eliom services are tried in order of registration! The first matching service will answer.
Other types similar to bool:
- Eliom_parameters.opt (page taking an optional parameter),
- Eliom_parameters.sum (either a parameter or another).
See the interface here.
Type set
Page may take several parameters of the same name. It is useful when you want to create a form with a variable number of fields. To do that with Eliom, use the type Eliom_parameters.set. For example set int "val" means that the page will take zero, one or several parameters of name "val", all of type int. The function you register will receive the parameters in a list. Example:
let set = register_new_service ~path:["set"] ~get_params:(set string "s") (fun _ l () -> let ll = List.map (fun s -> << <strong>$str:s$ </strong> >>) l in return << <html> <head><title></title></head> <body> <p> You sent: $list:ll$ </p> </body> </html> >>)
These parameters may come from several kinds of widgets in forms. Here is an example of a form with several checkboxes, all sharing the same name, but with different values:
(* form to set *) let setform = register_new_service ~path:["setform"] ~get_params:unit (fun sp () () -> return (html (head (title (pcdata "")) []) (body [h1 [pcdata "Set Form"]; get_form set sp (fun n -> [p [pcdata "Form to set: "; string_checkbox ~name:n ~value:"box1" (); string_checkbox ~name:n ~value:"box2" ~checked:true (); string_checkbox ~name:n ~value:"box3" (); string_checkbox ~name:n ~value:"box4" (); string_input ~input_type:`Submit ~value:"Click" ()]]) ])))
Once again, note that there is no difference between an empty set or no parameter at all. If you register a service without parameters and a service with a set of parameters on the same URL, the firstly registered service that matches will answer.
Select
Here is an example of a select box.
let select_example_result = register_new_service ~path:["select"] ~get_params:(string "s") (fun sp g () -> return (html (head (title (pcdata "")) []) (body [p [pcdata "You selected: "; strong [pcdata g]]]))) let create_select_form = (fun select_name -> [p [pcdata "Select something: "; Eliom_predefmod.Xhtml.string_select ~name:select_name (Eliom_predefmod.Xhtml.Option ([] (* attributes *), "Bob" (* value *), None (* Content, if different from value *), false (* not selected *))) (* first line *) [Eliom_predefmod.Xhtml.Option ([], "Marc", None, false); (Eliom_predefmod.Xhtml.Optgroup ([], "Girls", ([], "Karin", None, false), [([a_disabled `Disabled], "Juliette", None, false); ([], "Alice", None, true); ([], "Germaine", Some (pcdata "Bob's mother"), false)]))] ; Eliom_predefmod.Xhtml.string_input ~input_type:`Submit ~value:"Send" ()]]) let select_example = register_new_service ["select"] unit (fun sp () () -> let f = Eliom_predefmod.Xhtml.get_form select_example_result sp create_select_form in return (html (head (title (pcdata "")) []) (body [f])))
To do "multiple" select boxes, use functions like Eliom_predefmod.XHTMLFORMSSIG.string_multiple_select. As you can see in the type, the service must be declared with parameters of type Eliom_parameters.set.
Clickable images
Here is an example of clickable image. You receive the coordinates the user clicked on.
let coord = register_new_service ~path:["coord"] ~get_params:(coordinates "coord") (fun _ c () -> return << <html> <head><title></title></head> <body> <p> You clicked on coordinates: ($str:(string_of_int c.abscissa)$, $str:(string_of_int c.ordinate)$) </p> </body> </html> >>) (* form to image *) let imageform = register_new_service ~path:["imageform"] ~get_params:unit (fun sp () () -> return (html (head (title (pcdata "")) []) (body [h1 [pcdata "Image Form"]; get_form coord sp (fun n -> [p [image_input ~src:(make_uri ~service:(static_dir sp) ~sp ["ocsigen5.png"]) ~name:n ()]]) ])))
You may also send a value with the coordinates:
let coord2 = register_new_service ~path:["coord2"] ~get_params:(int_coordinates "coord") (fun _ (i, c) () -> return << <html> <head><title></title></head> <body> <p> You clicked on coordinates: ($str:(string_of_int c.abscissa)$, $str:(string_of_int c.ordinate)$) </p> </body> </html> >>) (* form to image *) let imageform2 = register_new_service ~path:["imageform2"] ~get_params:unit (fun sp () () -> return (html (head (title (pcdata "")) []) (body [h1 [pcdata "Image Form"]; get_form coord2 sp (fun n -> [p [int_image_input ~src:(make_uri ~service:(static_dir sp) ~sp ["ocsigen5.png"]) ~name:n ~value:3 ()]]) ])))
Type list
Another way (than Eliom_parameters.set) to do variable length forms is to use indexed lists (using Eliom_parameters.list). The use of that feature is a bit more complex than set and still experimental. Here is an example of service taking an indexed list as parameter:
(* lists *) let coucou_list = register_new_service ~path:["coucou"] ~get_params:(list "a" (string "str")) (fun _ l () -> let ll = List.map (fun s -> << <strong>$str:s$</strong> >>) l in return << <html> <head><title></title></head> <body> <p> You sent: $list:ll$ </p> </body> </html> >>)
Here is an example of link towards this service: coucou?a.str[0]=toto&a.str[1]=titi.
Warning: As for sets or bools, if a request has no parameter, it will be considered as the empty list. Services are tried in order of registration.
As you see, the names of each list element is built from the name of the list, the name of the list element, and an index. To spare you creating yourself these names, Eliom provides you an iterator to create them.
(* Form with list: *) let create_listform f = (* Here, f.it is an iterator like List.map, but it must be applied to a function taking 2 arguments (unlike 1 in map), the first one being the name of the parameter, and the second one the element of list. The last parameter of f.it is the code that must be appended at the end of the list created *) f.it (fun stringname v init -> <:xmllist< <p>Write the value for $str:v$: $string_input ~input_type:`Text ~name:stringname ()$ </p> >>@init) ["one";"two";"three";"four"] <:xmllist< <p>$string_input ~input_type:`Submit ~value:"Click" ()$</p> >> let listform = register_new_service ["listform"] unit (fun sp () () -> let f = get_form coucou_list sp create_listform in return << <html> <head><title></title></head> <body> $f$ </body> </html> >>)
Important warning: As we have seen in the section about boolean (or optional) parameters, it is not possible to distinguish between a boolean with value "false", and no parameter at all. This causes problems if you create a list of boolean or optional values, as it is not possible to know the length of the list. In that case, Eliom always takes the shortest possible list.
Forms and suffixes
Service with "suffix" URLs have an equivalent version with usual parameters, allowing to create forms towards such services. Example:
(* Form for service with suffix: *) let create_suffixform ((suff, endsuff),i) = <:xmllist< <p>Write the suffix: $int_input ~input_type:`Text ~name:suff ()$ <br/> Write a string: $user_type_input (Ocsigen_lib.string_of_url_path ~encode:false) ~input_type:`Text ~name:endsuff () $ <br/> Write an int: $int_input ~input_type:`Text ~name:i ()$ <br/> $string_input ~input_type:`Submit ~value:"Click" ()$</p> >> let suffixform = register_new_service ["suffixform"] unit (fun sp () () -> let f = get_form isuffix sp create_suffixform in return << <html> <head><title></title></head> <body> $f$ </body> </html> >>)
Uploading files
The Eliom_parameters.file parameter type allows to send files in your request. The service gets something of type Ocsigen_extensions.file_info. You can extract information using this using these functions (from Eliom_sessions):
val get_tmp_filename : Ocsigen_extensions.file_info -> string val get_filesize : Ocsigen_extensions.file_info -> int64 val get_original_filename : Ocsigen_extensions.file_info -> string
Eliom_sessions.get_tmp_filename allows to know the actual name of the uploaded file on the hard drive. Eliom_sessions.get_original_filename gives the original filename.
To make possible the upload of files, you must configure a directory for uploaded files in Ocsigen's configuration file. For example:
<uploaddir>/tmp</uploaddir>
Files are kept in this directory only during the request. Then they are automatically cancelled. Thus your services must copy them somewhere else themselves if they want to keep them. In the following example, we create a new hard link to the file to keep it (the destination must be on the same partition of the disk).
let upload = new_service ~path:["upload"] ~get_params:unit () let upload2 = register_new_post_service ~fallback:upload ~post_params:(file "file") (fun _ () file -> let to_display = let newname = "/tmp/thefile" in (try Unix.unlink newname; with _ -> ()); Unix.link (Eliom_sessions.get_tmp_filename file) newname; let fd_in = open_in newname in try let line = input_line fd_in in close_in fd_in; line (*end*) with End_of_file -> close_in fd_in; "vide" in return (html (head (title (pcdata "Upload")) []) (body [h1 [pcdata to_display]]))) let uploadform = register upload (fun sp () () -> let f = (post_form upload2 sp (fun file -> [p [file_input ~name:file (); br (); string_input ~input_type:`Submit ~value:"Send" () ]]) ()) in return (html (head (title (pcdata "form")) []) (body [f])))
Try it (warning: uploading on ocsigen.org is forbidden).
Predefined constructs ¶
Images, CSS, Javascript
To include an image, simply use the function XHTML.M.img:
img ~alt:"Ocsigen" ~src:(Eliom_predefmod.Xhtml.make_uri ~service:senddoc ~sp ["ocsigen1024.jpg"]) ()
The function Eliom_predefmod.Xhtml.make_uri creates the relative URL string from current URL (in sp) (see above) to the URL of the image in the static directory configured in the configuration file.
To simplify the creation of <link> tags for CSS or <script> tags for Javascript, use the following functions:
css_link ~uri:(make_uri ~service:(static_dir sp) ~sp ["style.css"]) ()
js_script ~uri:(make_uri ~service:(static_dir sp) ~sp ["funs.js"]) ()
Basic menus
To make a menu on your web page, you can use the function Eliom_tools.menu. First, define your menu like this:
let mymenu current sp = Eliom_tools.menu ~classe:["menuprincipal"] (home, <:xmllist< Home >>) [ (infos, <:xmllist< More info >>); (tutorial, <:xmllist< Documentation >>) ] current sp
Here, home, infos, and tutorial are your three pages (generated for example by Eliom_services.new_service).
Then mymenu service:home sp will generate the following code:
<ul class="menu menuprincipal"> <li class="current first">Home </li> <li><a href="infos">More info</a> </li> <li class="last"><a href="tutorial">Documentation</a> </li> </ul>
Personalise it in your CSS style-sheet.
Eliom_tools.menu takes a list of services without GET parameters. If you want one of the link to contains GET parameters, pre-apply the service.
How to make a menu entry with GET parameters?
Preapply your service.
Hierarchical menus
(* Hierarchical menu *) open Eliom_tools_common open Eliom_tools let hier1 = new_service ~path:["hier1"] ~get_params:unit () let hier2 = new_service ~path:["hier2"] ~get_params:unit () let hier3 = new_service ~path:["hier3"] ~get_params:unit () let hier4 = new_service ~path:["hier4"] ~get_params:unit () let hier5 = new_service ~path:["hier5"] ~get_params:unit () let hier6 = new_service ~path:["hier6"] ~get_params:unit () let hier7 = new_service ~path:["hier7"] ~get_params:unit () let hier8 = new_service ~path:["hier8"] ~get_params:unit () let hier9 = new_service ~path:["hier9"] ~get_params:unit () let hier10 = new_service ~path:["hier10"] ~get_params:unit () let mymenu = ( (Main_page hier1), [([pcdata "page 1"], Site_tree (Main_page hier1, [])); ([pcdata "page 2"], Site_tree (Main_page hier2, [])); ([pcdata "submenu 4"], Site_tree (Default_page hier4, [([pcdata "submenu 3"], Site_tree (Not_clickable, [([pcdata "page 3"], Site_tree (Main_page hier3, [])); ([pcdata "page 4"], Site_tree (Main_page hier4, [])); ([pcdata "page 5"], Site_tree (Main_page hier5, []))] ) ); ([pcdata "page 6"], Site_tree (Main_page hier6, []))] ) ); ([pcdata "page 7"], Site_tree (Main_page hier7, [])); ([pcdata "disabled"], Disabled); ([pcdata "submenu 8"], Site_tree (Main_page hier8, [([pcdata "page 9"], Site_tree (Main_page hier9, [])); ([pcdata "page 10"], Site_tree (Main_page hier10, []))] ) ) ] ) let f i s sp () () = return (html (head (title (pcdata "")) ((style ~contenttype:"text/css" [cdata_style "a {color: red;}\n\n li.eliomtools_current > a {color: blue;}\n\n .breadthmenu li {\n\n display: inline;\n\n padding: 0px 1em;\n\n margin: 0px;\n\n border-right: solid 1px black;}\n\n .breadthmenu li.eliomtools_last {border: none;}\n\n "]):: structure_links mymenu ~service:s ~sp) ) (body [h1 [pcdata ("Page "^string_of_int i)]; h2 [pcdata "Depth first, whole tree:"]; div (hierarchical_menu_depth_first ~whole_tree:true mymenu ~service:s ~sp); h2 [pcdata "Depth first, only current submenu:"]; div (hierarchical_menu_depth_first mymenu ~service:s ~sp); h2 [pcdata "Breadth first:"]; div (hierarchical_menu_breadth_first ~classe:["breadthmenu"] mymenu ~service:s ~sp )])) let _ = register hier1 (f 1 hier1); register hier2 (f 2 hier2); register hier3 (f 3 hier3); register hier4 (f 4 hier4); register hier5 (f 5 hier5); register hier6 (f 6 hier6); register hier7 (f 7 hier7); register hier8 (f 8 hier8); register hier9 (f 9 hier9); register hier10 (f 10 hier10)
Configuration files ¶
Here are Eliom's options you can use in configuration files.
Timeouts
Timeouts for sessions can be set either inside tag <extension findlib-package="ocsigen.ext.eliom"/> (default value for all sites), or inside a <eliom/> tag (default for one site).
Timeouts can also be modified programmatically using functions like Eliom_sessions.set_global_volatile_timeout, but by default these functions will not override configuration files. (see module Eliom_sessions for other functions). Thus, a website can set its own defaults and the user can still override them from the configuration file. If you want to set a timeout programmatically even if it has been modified in a configuration file, use the optional parameter ~override_configfile:true.
Timeouts can be set either for all session names, for one precise session name, or for the default session name. To do that programmatically, use the optional parameter ~session_name. To do that in configuration file, use the optional attribute sessionname (where an empty string value means default session name). If this attribute is absent, the timeout will affect all sessions for which no other default has been set. The sessionname attribute exists only inside an <eliom/> tag (and not inside <extension findlib-package="ocsigen.ext.eliom"/>).
- <volatiletimeout value="30" [sessionname=""]/> The default timeout for volatile (in memory) sessions. value="infinity" means that the session will never finish. Note that each eliom module may set its own default, that will override this one.
- <persistenttimeout value="7200"/> Idem for persistent session data
- <datatimeout value="30" [sessionname=""]/> Like <timeout>, but for in memory data sessions only (not service sessions).
- <servicetimeout value="30" [sessionname=""]/> Like <timeout>, but for service sessions only (not in memory data sessions).
Garbage collector of sessions and services
These options can appear inside tag <extension findlib-package="ocsigen.ext.eliom"/>. For now, it cannot be set for each site independently (tell us if you need that).
- <sessiongcfrequency value="30"/> Time between two garbage collections of sessions, in seconds (default 3600) "infinity" means no GC of session.
- <persistentsessiongcfrequency value="86400"/> Time between two garbage collections of persistent sessions, in seconds (default 86400.) "infinity" means no GC of session.
- <servicesessiongcfrequency value="3600"/> Like <sessiongcfrequency>, but for service sessions only
- <datasessiongcfrequency value="3600"/> Like <sessiongcfrequency>, but for "in memory data" sessions only
Limiting the number of sessions or coservices
To prevent from denial of service, Eliom limits the number of sessions and the number of dynamic coservices. Without these limitations, it would be possible for an attacker to open repeatedly lots of sessions, or creating new services (for example CSRF safe coservices can create lots of coservices when you reload repeatedly a page). When the limit is reached, it is still possible to open new sessions or create new services, but the oldest session or service will disappear (the one that has not been used for the longest time).
Limiting sessions
First of all, there is a limitation of the number of sessions in a session group. The typical use of this is when an user opens several sessions from several computers. All the sessions belong to the same group (the group name is usually the user name). The limit is usually small (5 sessions per group by default). This limit is implemented for all kinds of sessions (service session, volatile and persistent data sessions). For persistent sessions, the implementation is not very efficient for very large limits.
It is highly recommended to use session groups when possible.
If you can't use session groups, the number of sessions is limitated by sub network for volatile sessions (service sessions and data sessions). The limitation is larger (default 1 million). The limit must be large enough, for example if the server is behind a reverse proxy, all incoming requests will come from the same IP address. Limiting by sub network instead of by IP address prevents attacks even if the attacker has a whole sub network available. The default mask for sub networks is /16 for IPv4 and /56 for IPv6.
Some figures: If 1 session takes 1000 bytes (data + tables etc), 1 million sessions take 1 GB. If somebody opens 1000 sessions per second, then it will take 1000 s (16 minutes) to reach 1000000. It means that regular users will have their sessions closed after 16 minutes of inactivity if they share their sub network with someone doing an attack (or if the server is behind a proxy).
For persistent sessions, there is no limitation per sub network for now. 1 billion sessions take 1 TB. If somebody opens 1000 sessions per second, then it will take 1 million s (16000 minutes = 266 h = 11 days) to reach 1TB.
Limiting services
The number of anonymous coservices is limited by session or by sub network if the service is registered in the global table. Default values: 1000 for one session, and 500000 for one subnet.
Note that there is no limitation of named coservices or regular services. It is not a good practice to allow the creation of too much services of this kinds dynamically.
How to set limits
The limits and the subnet mask can be set programmatically by each module (for example to adapt the values to the size of session data) or in the configuration file (for example to adapt the values to the size of memory or network configuration) (see module Eliom_sessions). By default, functions like Eliom_sessions.set_default_max_volatile_sessions_per_group will not override a value set in the configuration file (but if you use ~override_configfile:true). Thus, a website can set its own defaults and the user can still override them from the configuration file.
The configuration file options can be set either inside the tag <extension findlib-package="ocsigen.ext.eliom"/> (global configuration), or inside the <eliom/> tag (configuration for each site). But the limits always are for one site (that is: a global limit value of 10 means 10 for each Eliom site).
The syntax is:
- <maxvolatilesessionspergroup value="10"/>
- <maxservicesessionspergroup value="10"/>
- <maxdatasessionspergroup value="10"/>
- <maxpersistentsessionspergroup value="10"/>
- <maxvolatilesessionspersubnet value="500000"/>
- <maxservicesessionspersubnet value="500000"/>
- <maxdatasessionspersubnet value="500000"/>
- <maxanonymouscoservicespersession value="1000"/>
- <maxanonymouscoservicespersubnet value="500000"/>
- <ipv4subnetmask value="255.255.128.0"/>
- <ipv6subnetmask value="ff:ff:ff:ff:ff:ff::"/>
Miscellaneous ¶
Several Ocaml modules for one site
If your site consists of several modules, you can load them consecutively from the configuration file using <eliommodule> (same syntax as <eliom>, the difference being that <eliommodule> does not generate any page). In that case, only the position of the <eliom> tag will be taken into account for generating the page using Eliom. Note that there can be only one <eliom> tag for each <site> (or <host>).
Advanced use: create an extension for the server that access Eliom's data
If you want an Ocsigen extension with access to Eliom's data (for example if you want an extension that will register some services), you can use the function Eliom_extensions.register_eliom_extension to register the function that will generate the Ocsigen_extensions.answer from sp.
Static linking of Eliom modules
From version 1.2, it is possible to link extensions and Eliom modules statically (See here). Obviously, for Eliom modules, service registration and options setting must be delayed until the configuration file is read. To create a statically linkable Eliom module, use the function Eliom_services.register_eliom_module. It takes as parameters the name of the module and the initialization function, that will be called when the module is initialized in the configuration file. That function will register services (and possibly call Eliom_sessions.get_config if the module has configuration options). To initialize the module from the configuration file, use the syntax:
<eliommodule name="name"> ... </eliommodule>
(or <eliom name="name"> ... </eliom>)
which is equivalent to:
<eliommodule module="name.cmxs"> ... </eliommodule>
(or <eliom module="name.cmxs"> ... </eliom>) with the exception that it does not load the module using Dynlink, but calls the initialization function.
Examples ¶
Writing a forum
As an example, we will now write a small forum. Our forum has a main page, summarising all the messages and a page for each message. All the functions to access the database and print the result are left to the reader. We only want to show the structure of the site. Suppose you have written a function news_headers_list_box that writes the beginning of messages, and message_box that write a full message.
(* All the services: *) let main_page = new_service ~path:[""] ~get_params:unit () let news_page = new_service ["msg"] (int "num") () (* Construction of pages *) let home sp () () = page sp [h1 [pcdata "Mon site"]; news_headers_list_box sp anonymoususer news_page] let print_news_page sp i () = page sp [h1 [pcdata "Info"]; message_box i anonymoususer] (* Services registration *) let _ = register ~service:main_page home let _ = register ~service:news_page print_news_page
Now the same example with a login box on each page. We now have two versions of each page: connected and not connected. We need two actions (for connection and disconnection). Suppose we have the functions login_box, connected_box, and connect.
(* All the services: *) let main_page = new_service ~path:[""] ~get_params:unit () let news_page = new_service ["msg"] (int "num") () let connect_action = new_post_coservice' ~post_params:(string "login" ** string "password") (* Construction of pages *) let home sp () () = match get_volatile_session_data ~table:my_table ~sp () with | Eliom_sessions.Data_session_expired | Eliom_sessions.No_data -> page sp [h1 [pcdata "My site"]; login_box sp connect_action; news_headers_list_box sp anonymoususer news_page] | Eliom_sessions.Data user -> page sp [h1 [pcdata "Mon site"]; text_box "Bonjour !"; connected_box sp user disconnect_action; news_headers_list_box sp user news_page] let print_news_page sp i () = match get_volatile_session_data ~table:my_table ~sp () with | Eliom_sessions.Data_session_expired | Eliom_sessions.No_data -> page sp [h1 [pcdata "Info"]; login_box sp connect_action; message_box i anonymoususer] | Eliom_sessions.Data user -> page sp [h1 [pcdata "Info"]; connected_box sp user disconnect_action; message_box i user] (* Services registration *) let _ = register ~service:main_page home let _ = register ~service:news_page print_news_page let launch_session sp user = set_volatile_session_data my_table sp user let _ = Eliom_predefmod.Action.register ~action:connect_action (fun h (login, password) -> launch_session sp (connect login password); return [])
Miniwiki
Ocsigen's source code contains an example of Wiki written with Eliom by Janne Hellsten. It is called Miniwiki.
