123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597(* Ocsigen
* http://www.ocsigen.org
* Module eliommod_cookies.ml
* Copyright (C) 2007 Vincent Balat
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, with linking exception;
* either version 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*)openEliom_lib(** Cookie management *)openLwtincludeEliom_cookies_base(*****************************************************************************)letmake_new_session_id()=Ocsigen_lib.make_cryptographic_safe_string()^"H"typedate=floattypecookie={full_state_name:Eliom_common.full_state_name;expiry:dateoption;timeout:Eliom_common.timeout;session_group:Eliom_common.perssessgrpoption}modulePersistent_cookies=struct(* Another table, containing the session info for each cookie *)(* the table contains:
- the expiration date (by timeout), changed at each access to the table
(float option) None -> no expiration
- the timeout for the user (float option option) None -> see global config
Some None -> no timeout
*)(* It is lazy, because we must delay the creation of the table until
the initialization of eliom in case we use static linking with
sqlite backend ... *)moduleOcsipersist=Eliom_common.Ocsipersist.Functorial(* NOTE: Do not forget to change the version number when the internal format changes! *)letpersistent_cookie_table_version="_v5"(* v2 introduces session groups *)(* v3 introduces tab sessions *)(* v4 introduces group tables *)(* v5 removes secure scopes *)moduleCookies=Ocsipersist.Table(structletname="eliom_persist_cookies"^persistent_cookie_table_versionend)(Ocsipersist.Column.String)(Ocsipersist.Column.Marshal(structtypet=cookieend))let()=Eliom_common.Persistent_tables.add_functorial_table(moduleCookies)(* maps expiry dates to cookie IDs; may have superfluous entries, i.e cookies
that will not actually expire on the given date. *)moduleExpiry_dates=structincludeOcsipersist.Table(structletname="eliom_persist_cookies_expiry_dates"end)(Ocsipersist.Column.Float)(Ocsipersist.Column.String)letadd_cookieexpcookie=modify_optexp@@function|None->Somecookie|Somecookies_str->letcookies=String.split_on_char','cookies_strinifList.memcookiecookiesthenSomecookies_strelseSome(cookies_str^","^cookie)letremove_cookieexp_ocookie=exp_o|>Eliom_lib.Option.Lwt.iter@@funexp->modify_optexp@@function|None->None|Somecookies_str->letcookies=String.split_on_char','cookies_strinletcookies'=List.filter(func->c<>cookie)cookiesinifcookies'=[]thenNoneelseSome(String.concat","cookies')endletaddcookie({expiry;_}ascontent)=Eliom_lib.Option.Lwt.iter(funt->Expiry_dates.add_cookietcookie)expiry>>=fun_->Cookies.addcookiecontentletreplace_if_existscookie({expiry;_}ascontent)=Eliom_lib.Option.Lwt.iter(funt->Expiry_dates.add_cookietcookie)expiry>>=fun_->Cookies.replace_if_existscookiecontentletgarbage_collect~sectiongc_cookie=letnow=Unix.time()inExpiry_dates.iter~lt:now@@fundatecookies_str->letcookies=String.split_on_char','cookies_strinletcookies_log=String.concat","@@List.mapEliom_common.Hashed_cookies.sha256cookiesinLwt_log.ign_info_f~section"potentially expired cookies %.0f: %s"datecookies_log;Lwt_list.iter_sgc_cookiecookies>>=fun_->Expiry_dates.removedateend(*****************************************************************************)(* cookie manipulation *)(** look in table to find if the session cookies sent by the browser
correspond to existing (and not closed) sessions *)letget_cookie_infonowsitedataservice_cookiesdata_cookiespersistent_cookiessecure_cookies:'aEliom_common.cookie_info*'blist=(* get info about service session cookies: *)letf_servservice_cookies=Eliom_common.Full_state_name_table.fold(funnamevalue(oktable,failedlist)->trylethvalue=Eliom_common.Hashed_cookies.hashvalueinlet{Eliom_common.Service_cookie.session_table;expiry;timeout;session_group;session_group_node;_}=Eliom_common.SessionCookies.findsitedata.Eliom_common.session_services(Eliom_common.Hashed_cookies.to_stringhvalue)inEliommod_sessiongroups.Serv.upsession_group_node;match!expirywith|Sometwhent<now->(* session expired by timeout *)Eliommod_sessiongroups.Serv.removesession_group_node;(Eliom_common.Full_state_name_table.addname(Somevalue(* value sent by the browser *),refEliom_common.SCData_session_expired(* ask the browser
to remove the cookie *))oktable,name::failedlist)|_->(Eliom_common.Full_state_name_table.addname(Somevalue(* value sent by the browser *),ref(Eliom_common.SC{Eliom_common.sc_hvalue=hvalue(* value *);Eliom_common.sc_set_value=None;Eliom_common.sc_table=refsession_table;Eliom_common.sc_timeout=timeout;Eliom_common.sc_exp=expiry;Eliom_common.sc_cookie_exp=refEliom_common.CENothing(* cookie expiration date to send
to the browser.
We don't change it *);Eliom_common.sc_session_group=session_group;Eliom_common.sc_session_group_node=session_group_node}))oktable,failedlist)withNot_found->(Eliom_common.Full_state_name_table.addname(Somevalue(* value sent by the browser *),refEliom_common.SCData_session_expired(* ask the browser
to remove the cookie *))oktable,name::failedlist))service_cookies(Eliom_common.Full_state_name_table.empty,[])in(* get info about "in memory" data session cookies: *)letf_datadata_cookies=Eliom_common.Full_state_name_table.map(funvalue->lazy(trylethvalue=Eliom_common.Hashed_cookies.hashvalueinlet{Eliom_common.Data_cookie.expiry;timeout;session_group;session_group_node;_}=Eliom_common.SessionCookies.findsitedata.Eliom_common.session_data(Eliom_common.Hashed_cookies.to_stringhvalue)inEliommod_sessiongroups.Serv.upsession_group_node;match!expirywith|Sometwhent<now->(* session expired by timeout *)Eliommod_sessiongroups.Data.removesession_group_node;(Somevalue(* value sent by the browser *),refEliom_common.SCData_session_expired(* ask the browser
to remove the cookie *))|_->(Somevalue(* value sent by the browser *),ref(Eliom_common.SC{Eliom_common.dc_hvalue=hvalue(* value *);Eliom_common.dc_set_value=None;Eliom_common.dc_timeout=timeout(* user timeout ref *);Eliom_common.dc_exp=expiry(* expiration date
(server side) *);Eliom_common.dc_cookie_exp=refEliom_common.CENothing(* cookie expiration date to send
to the browser.
We don't change it *);Eliom_common.dc_session_group=session_group;Eliom_common.dc_session_group_node=session_group_node}))withNot_found->(Somevalue(* value sent by the browser *),refEliom_common.SCData_session_expired(* ask the browser
to remove the cookie *))))data_cookiesin(* *** get info about persistent session cookies: *)letf_perspersistent_cookies=Eliom_common.Full_state_name_table.map(funvalue->lazy(catch(fun()->lethvalue=Eliom_common.Hashed_cookies.hashvalueinlethvalue_string=Eliom_common.Hashed_cookies.to_stringhvalueinPersistent_cookies.Cookies.find(Eliom_common.Hashed_cookies.to_stringhvalue)>>=fun{expiry=persexp;timeout=perstimeout;session_group=sessgrp;_}->Eliommod_sessiongroups.Pers.uphvalue_stringsessgrp>>=fun()->matchpersexpwith|Sometwhent<now->(* session expired by timeout *)Eliom_common.Persistent_tables.remove_key_from_all_tableshvalue_string>>=fun()->return(Some(value(* value at the beginning
of the request *),perstimeout(* user persistent timeout
at the beginning
of the request *),persexp(* expiration date (server)
at the beginning
of the request *),sessgrp(* session group at beginning *)),refEliom_common.SCData_session_expired(* ask the browser to
remove the cookie *))|_->return(Some(value(* value at the beginning
of the request *),perstimeout(* user persistent timeout
at the beginning
of the request *),persexp(* expiration date (server)
at the beginning
of the request *),sessgrp(* session group at beginning *)),ref(Eliom_common.SC{Eliom_common.pc_hvalue=hvalue(* value *);Eliom_common.pc_set_value=None;Eliom_common.pc_timeout=refperstimeout(* user persistent timeout ref *);Eliom_common.pc_cookie_exp=refEliom_common.CENothing(* persistent cookie expiration
date ref to send to the browser:
We don't change it *);Eliom_common.pc_session_group=refsessgrp})))(function|Not_found->return(Some(value(* value at the beginning
of the request *),Eliom_common.TGlobal(* user persistent timeout
at the beginning
of the request *),Some0.(* expiration date (server)
at the beginning
of the request *),None(* session group at beginning *)),refEliom_common.SCData_session_expired(* ask the browser
to remove the cookie *))|e->faile)))persistent_cookies(* the persistent cookies sent by the request *)inletservoktable,servfailedlist=f_servservice_cookiesinletdataoktable=f_datadata_cookiesinletpersoktable=f_perspersistent_cookiesinletsec,sservfailedlist=letsc,dc,pc=secure_cookiesinletservoktable,servfailedlist=f_servscinletdataoktable=f_datadcinletpersoktable=f_perspcin(refservoktable,refdataoktable,refpersoktable),servfailedlistin(((refservoktable,refdataoktable,refpersoktable),sec),sservfailedlist@servfailedlist)(*****************************************************************************)(* table cookie -> session table *)letnew_service_cookie_table():Eliom_common.tablesEliom_common.Service_cookie.table=Eliom_common.SessionCookies.create100letnew_data_cookie_table():Eliom_common.Data_cookie.table=Eliom_common.SessionCookies.create100(*****************************************************************************)(* Create the table of cookies to send to the browser or to unset *)(* (from cookie_info) *)letcompute_session_cookies_to_sendsitedata((service_cookie_info,data_cookie_info,pers_cookies_info),secure_ci)(endlist:Ocsigen_cookie_map.t)=letgetservvexp(old,newi)=return(letnewinfo=match!newiwith|Eliom_common.SCNo_data|Eliom_common.SCData_session_expired->None|Eliom_common.SCc->Some(c.Eliom_common.sc_hvalue,c.Eliom_common.sc_set_value,!(c.Eliom_common.sc_cookie_exp))inold,newinfo)inletgetdatavexpv=ifLazy.is_valvthenreturn(letold,newi=Lazy.forcevinletnewinfo=match!newiwith|Eliom_common.SCNo_data|Eliom_common.SCData_session_expired->None|Eliom_common.SCc->Some(c.Eliom_common.dc_hvalue,c.Eliom_common.dc_set_value,!(c.Eliom_common.dc_cookie_exp))inold,newinfo)elsefailNot_foundinletgetpersvexpv=ifLazy.is_valvthenLazy.forcev>>=fun(old,newi)->return(letoldinfo=matcholdwithNone->None|Some(v,_,_,_)->Somevinletnewinfo=match!newiwith|Eliom_common.SCNo_data|Eliom_common.SCData_session_expired->None|Eliom_common.SCc->Some(c.Eliom_common.pc_hvalue,c.Eliom_common.pc_set_value,!(c.Eliom_common.pc_cookie_exp))inoldinfo,newinfo)elsefailNot_foundinletch_exp=function|Eliom_common.CENothing|Eliom_common.CEBrowser->None|Eliom_common.CESomea->Someainletauxfcookiekindsecuretab2cooktab=cooktab>>=funcooktab->Eliom_common.Full_state_name_table.fold(funfull_st_namevaluebeg->beg>>=funbeg->catch(fun()->fvalue>>=fun(old,newc)->return(matchold,newcwith|None,None->beg|Some_,None->Ocsigen_cookie_map.add~path:(Eliom_common.get_site_dirsitedata)(Eliom_common.make_full_cookie_namecookiekindfull_st_name)OUnsetbeg(* the path is always site_dir because the cookie cannot
have been unset by a service outside
this site directory *)|_,Some(_,Somev,exp)->(* New value *)Ocsigen_cookie_map.add~path:(Eliom_common.get_site_dirsitedata)(Eliom_common.make_full_cookie_namecookiekindfull_st_name)(OSet(ch_expexp,v,secure))beg|Someoldv,Some(_,None,exp)->ifexp=Eliom_common.CENothingthenbegelseOcsigen_cookie_map.add~path:(Eliom_common.get_site_dirsitedata)(Eliom_common.make_full_cookie_namecookiekindfull_st_name)(OSet(ch_expexp,oldv,secure))beg|None,Some(_,None,_)->(* Should not happen *)beg))(functionNot_found->returnbeg|e->faile))tab2(returncooktab)inauxgetpersvexpEliom_common.persistentcookienamefalse!pers_cookies_info(auxgetdatavexpEliom_common.datacookienamefalse!data_cookie_info(auxgetservvexpEliom_common.servicecookienamefalse!service_cookie_info(letservice_cookie_info,data_cookie_info,pers_cookies_info=secure_ciinauxgetpersvexpEliom_common.persistentcookienametrue!pers_cookies_info(auxgetdatavexpEliom_common.datacookienametrue!data_cookie_info(auxgetservvexpEliom_common.servicecookienametrue!service_cookie_info(returnendlist))))))letcompute_cookies_to_send=compute_session_cookies_to_sendletcompute_new_ri_cookies'nowripathricookiescookies_set_by_page=Ocsigen_cookie_map.Map_path.fold(funcpathtcookies->ifUrl.is_prefix_skip_end_slash(Url.remove_slash_at_beginningcpath)(Url.remove_slash_at_beginningripath)thenOcsigen_cookie_map.Map_inner.fold(funnamevcookies->(*VVV We always keep secure cookies, event if the protocol is not secure,
because this function is for actions only. Is that right? *)matchvwith|OSet(Someexp,value,_secure)whenexp>now->Ocsigen_cookie_map.Map_inner.addnamevaluecookies|OSet(None,value,_secure)->Ocsigen_cookie_map.Map_inner.addnamevaluecookies|OSet(Someexp,_value,_secure)whenexp<=now->Ocsigen_cookie_map.Map_inner.removenamecookies|OUnset->Ocsigen_cookie_map.Map_inner.removenamecookies|_->cookies)tcookieselsecookies)cookies_set_by_pagericookies(** Compute new ri.ri_cookies value
from an old ri.ri_cookies and all_cookie_info
as if it had been sent by the browser *)letcompute_new_ri_cookies(now:float)(ripath:stringlist)(ricookies:stringOcsigen_cookie_map.Map_inner.t)((ci,secure_ci):Eliom_common.tablesEliom_common.cookie_info)(cookies_set_by_page:Ocsigen_cookie_map.t):stringOcsigen_cookie_map.Map_inner.tLwt.t=(* first we add cookies set by page: *)letric=compute_new_ri_cookies'nowripathricookiescookies_set_by_pagein(* then session cookies: *)letf_secure(service_cookie_info,data_cookie_info,pers_cookie_info)ric=letric=Eliom_common.Full_state_name_table.fold(fun({Eliom_common.user_scope=sc;_}asfull_st_name)(_,v)beg->letct=Eliom_common.cookie_level_of_user_scopescinifct=`Client_processthenbegelseletn=Eliom_common.make_full_cookie_nameEliom_common.servicecookienamefull_st_nameinmatch!vwith|Eliom_common.SCData_session_expired|Eliom_common.SCNo_data->Ocsigen_cookie_map.Map_inner.removenbeg|Eliom_common.SC{Eliom_common.sc_set_value=Somev;_}->Ocsigen_cookie_map.Map_inner.addnvbeg|Eliom_common.SC{Eliom_common.sc_set_value=None;_}->beg)!service_cookie_inforicinletric=Eliom_common.Full_state_name_table.fold(fun({Eliom_common.user_scope=sc;_}asfull_st_name)vbeg->letct=Eliom_common.cookie_level_of_user_scopescinifct=`Client_processthenbegelseletn=Eliom_common.make_full_cookie_nameEliom_common.datacookienamefull_st_nameinifLazy.is_valvthenlet_,v=Lazy.forcevinmatch!vwith|Eliom_common.SCData_session_expired|Eliom_common.SCNo_data->Ocsigen_cookie_map.Map_inner.removenbeg|Eliom_common.SC{Eliom_common.dc_set_value=Somev;_}->Ocsigen_cookie_map.Map_inner.addnvbeg|Eliom_common.SC{Eliom_common.dc_set_value=None;_}->begelsebeg)!data_cookie_inforicinletric=Eliom_common.Full_state_name_table.fold(fun({Eliom_common.user_scope=sc;_}asfull_st_name)vbeg->letct=Eliom_common.cookie_level_of_user_scopescinifct=`Client_processthenbegelseletn=Eliom_common.make_full_cookie_nameEliom_common.persistentcookienamefull_st_nameinbeg>>=funbeg->ifLazy.is_valvthenLazy.forcev>>=fun(_,v)->match!vwith|Eliom_common.SCData_session_expired|Eliom_common.SCNo_data->Lwt.return(Ocsigen_cookie_map.Map_inner.removenbeg)|Eliom_common.SC{Eliom_common.pc_set_value=Somev;_}->Lwt.return(Ocsigen_cookie_map.Map_inner.addnvbeg)|Eliom_common.SC{Eliom_common.pc_set_value=None;_}->Lwt.returnbegelsereturnbeg)!pers_cookie_info(Lwt.returnric)inricinffalseciric>>=funric->ftruesecure_ciric(*VVV We always keep secure cookies, even if the protocol is not secure,
because this function is for actions only. Is that right? *)