123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618openWebdav_configopenLwt.InfixopenWebmachine.RdmoduleXml=Webdav_xmlletaccess_src=Logs.Src.create"webdav.access"~doc:"HTTP server access log"moduleAccess_log=(valLogs.src_logaccess_src:Logs.LOG)letresponse_time_src=Logs.Src.create"webdav.time"~doc:"HTTP request response time"moduleTime_log=(valLogs.src_logresponse_time_src:Logs.LOG)letcreate~f=letdata:(string,int)Hashtbl.t=Hashtbl.create7in(funx->letkey=fxinletcur=matchHashtbl.find_optdatakeywith|None->0|Somex->xinHashtbl.replacedatakey(succcur)),(fun()->letdata,total=Hashtbl.fold(funkeyvalue(acc,total)->(Metrics.uintkeyvalue::acc),value+total)data([],0)inMetrics.uint"total"total::data)letcounter_metrics~fname=letopenMetricsinletdoc="Counter metrics"inletincr,get=create~finletdatathing=incrthing;Data.v(get())inSrc.v~doc~tags:Metrics.Tags.[]~datanamelethttp_status=letf=function|#Cohttp.Code.informational_status->"1xx"|#Cohttp.Code.success_status->"2xx"|#Cohttp.Code.redirection_status->"3xx"|#Cohttp.Code.client_error_status->"4xx"|#Cohttp.Code.server_error_status->"5xx"|`Codec->Printf.sprintf"%dxx"(c/100)inletsrc=counter_metrics~f"http_response"in(funr->Metrics.addsrc(funx->x)(fund->dr))letis_alphanum=function|'a'..'z'|'0'..'9'|'A'..'Z'->true|_->falseletsaneusername=username<>""&&Xml.for_allis_alphanumusernamemoduleHeaders=structletget_content_typeheaders=matchCohttp.Header.getheaders"Content-Type"with|None->"text/calendar"|Somex->xletget_authorizationheaders=Cohttp.Header.getheaders"Authorization"letget_userheaders=matchget_authorizationheaderswith|None->assertfalse|Somev->vletget_depthheaders=Cohttp.Header.getheaders"Depth"letis_user_agent_mozillaheaders=matchCohttp.Header.getheaders"User-Agent"with|None->false|Somex->(* Apple seems to use the regular expression 'Mozilla/.*Gecko.*' *)letre=Re.compile(Re.Perl.re"^Mozilla/.*Gecko.*")inRe.execprexletreplace_locationlocationheaders=Cohttp.Header.replaceheaders"Location"(Uri.to_string@@location)letreplace_etag_add_locationetaglocationheaders=letheaders'=Cohttp.Header.replaceheaders"Etag"etaginCohttp.Header.replaceheaders'"Location"(Uri.to_string@@location)letreplace_content_typecontent_typeheaders=Cohttp.Header.replaceheaders"Content-Type"content_typeletreplace_authorizationauthheaders=Cohttp.Header.replaceheaders"Authorization"authletreplace_last_modifiedlast_modifiedheaders=Cohttp.Header.replaceheaders"Last-Modified"last_modifiedletreplace_davdavheaders=Cohttp.Header.replaceheaders"DAV"davendletto_statusx=Cohttp.Code.code_of_status(x:>Cohttp.Code.status_code)moduletypeServer=sigvalrespond:?headers:Cohttp.Header.t->?flush:bool->status:Cohttp.Code.status_code->body:Cohttp_lwt.Body.t->unit->(Cohttp.Response.t*Cohttp_lwt.Body.t)Lwt.tendmoduleMake(R:Mirage_crypto_rng_mirage.S)(Clock:Mirage_clock.PCLOCK)(Fs:Webdav_fs.S)(S:Server)=structmoduleWmClock=structletnow()=letts=Clock.now_d_ps()inletspan=Ptime.Span.vtsinmatchPtime.Span.to_int_sspanwith|None->0|Someseconds->secondsendmoduleWm=Webmachine.Make(Lwt)(WmClock)moduleDav=Webdav_api.Make(R)(Clock)(Fs)classhandlerconfigfsnowgenerate_salt=object(self)inherit[Cohttp_lwt.Body.t]Wm.resourcemethodprivatewrite_componentrd=Cohttp_lwt.Body.to_stringrd.req_body>>=funbody->letpath=self#pathrdinletcontent_type=Headers.get_content_typerd.req_headersinletuser=Headers.get_userrd.req_headersinAccess_log.debug(funm->m"write_component path %s user %s body @.%s"pathuserbody);Dav.write_componentfsconfig~path(now())~content_type~data:body>>=function|Errore->Wm.respond(to_statuse)rd|Oketag->letlocation=Uri.with_pathconfig.hostpathinletrd'=with_resp_headers(Headers.replace_etag_add_locationetaglocation)rdinWm.continuetruerd'methodprivateread_calendarrd=letpath=self#pathrdinletis_mozilla=Headers.is_user_agent_mozillard.req_headersinAccess_log.debug(funm->m"read_calendar path %s is_mozilla %b"pathis_mozilla);Dav.readfs~path~is_mozilla>>=function|Errore->Wm.respond(to_statuse)rd|Ok(content_type,body)->letrd'=with_resp_headers(Headers.replace_content_typecontent_type)rdinWm.continue(`Stringbody)rd'method!allowed_methodsrd=Wm.continue[`GET;`HEAD;`PUT;`DELETE;`OPTIONS;`Other"PROPFIND";`Other"PROPPATCH";`Other"MKCOL";`Other"MKCALENDAR";`Other"REPORT";`Other"ACL"]rdmethod!known_methodsrd=Wm.continue[`GET;`HEAD;`PUT;`DELETE;`OPTIONS;`Other"PROPFIND";`Other"PROPPATCH";`Other"MKCOL";`Other"MKCALENDAR";`Other"REPORT";`Other"ACL"]rdmethod!charsets_providedrd=Wm.continue["utf-8",(funid->id)]rdmethod!resource_existsrd=Fs.existsfs(self#pathrd)>>=funv->Wm.continuevrdmethodcontent_types_providedrd=Wm.continue["text/xml",self#read_calendar;"text/calendar",self#read_calendar]rdmethodcontent_types_acceptedrd=Wm.continue["text/xml",self#write_component;"text/calendar",self#write_component]rdmethod!is_authorizedrd=(* TODO implement digest authentication! *)matchHeaders.get_authorizationrd.req_headerswith|None->Wm.continue(`Basic"calendar")rd|Somev->Dav.verify_auth_headerfsconfigv>>=function|Error(`Msgmsg)->Access_log.warn(funm->m"is_authorized failed with header value %s and message %s"vmsg);Wm.continue(`Basic"invalid authorization")rd|Error(`Unknown_user(name,password))->ifconfig.do_trust_on_first_usethenbeginifsanenamethenbeginletnow=now()inletsalt=generate_salt()inDav.make_userfsnowconfig~name~password~salt>>=function|Errore->Wm.respond(to_statuse)rd|Ok_principal->letrd'=with_req_headers(Headers.replace_authorizationname)rdinWm.continue`Authorizedrd'endelsebeginAccess_log.warn(funm->m"is_authorized failed with unknown invalid username %s"name);Wm.continue(`Basic"invalid authorization")rdendendelsebeginAccess_log.warn(funm->m"is_authorized failed with unknown username %s"name);Wm.continue(`Basic"invalid authorization")rdend|Okuser->letrd'=with_req_headers(Headers.replace_authorizationuser)rdinWm.continue`Authorizedrd'method!forbiddenrd=letpath=self#pathrdinletuser=Headers.get_userrd.req_headersinDav.access_granted_for_aclfsconfig~pathrd.meth~user>>=fungranted->Wm.continue(notgranted)rdmethod!process_propertyrd=letpath=self#pathrdinletuser=Headers.get_userrd.req_headersinletdepth=Headers.get_depthrd.req_headersinCohttp_lwt.Body.to_stringrd.req_body>>=funbody->Access_log.debug(funm->m"process_property verb %s path %s user %s body @.%s"(Cohttp.Code.string_of_methodrd.meth)pathuserbody);letdispatch_on_verb=matchrd.methwith|`Other"PROPFIND"->Dav.propfindfsconfig~path~user~depth~data:body|`Other"PROPPATCH"->Dav.proppatchfsconfig~path~user~data:body|_->assertfalseindispatch_on_verb>>=function|Error(`Forbiddenbody)->Wm.respond~body:(`Stringbody)(to_status`Forbidden)rd|Error(`Bad_requestase)->Wm.respond(to_statuse)rd|Error`Property_not_found->Wm.continue`Property_not_foundrd|Okbody->letrd'=with_resp_headers(Headers.replace_content_type"application/xml")rdinWm.continue`Multistatus{rd'withresp_body=`Stringbody}method!reportrd=letpath=self#pathrdinletuser=Headers.get_userrd.req_headersinCohttp_lwt.Body.to_stringrd.req_body>>=funbody->Access_log.debug(funm->m"report path %s user %s body @.%s"pathuserbody);Dav.reportfsconfig~path~user~data:body>>=function|Error(`Bad_requestase)->Wm.respond(to_statuse)rd|Okbody->letrd'=with_resp_headers(Headers.replace_content_type"application/xml")rdinWm.continue`Multistatus{rd'withresp_body=`Stringbody}(* required by webmachine API *)method!cannot_createrd=letxml=Xml.node~ns:Xml.dav_ns"error"[Xml.node~ns:Xml.dav_ns"resource-must-be-null"[]]inleterr=Xml.tree_to_stringxmlinletrd'={rdwithresp_body=`Stringerr}inWm.continue()rd'method!create_collectionrd=letpath=self#pathrdinletuser=Headers.get_userrd.req_headersinCohttp_lwt.Body.to_stringrd.req_body>>=funbody->Access_log.debug(funm->m"create_collection verb %s path %s user %s body @.%s"(Cohttp.Code.string_of_methodrd.meth)pathuserbody);Dav.mkcolfs~pathconfig~userrd.meth(now())~data:body>>=function|Error(`Bad_requestase)->Wm.respond(to_statuse)rd|Error(`Forbiddenbody)->Wm.continue`Forbidden{rdwithresp_body=`Stringbody}|Error`Conflict->Wm.continue`Conflictrd|Ok_->Wm.continue`Createdrdmethod!delete_resourcerd=letpath=self#pathrdinAccess_log.debug(funm->m"delete_resource path %s"path);Dav.deletefs~path(now())>>=fundeleted->Wm.continuedeletedrdmethod!last_modifiedrd=letpath=self#pathrdinDav.last_modifiedfs~path>>=funlm->Wm.continuelmrdmethod!generate_etagrd=letpath=self#pathrdin(Dav.last_modifiedfs~path>|=function|None->rd|Somelm->with_resp_headers(Headers.replace_last_modifiedlm)rd)>>=funrd'->Dav.compute_etagfs~path>>=funetag->Wm.continueetagrd'method!finish_requestrd=letrd'=ifrd.meth=`OPTIONSthen(* access-control, access-control, calendar-access, calendar-schedule, calendar-auto-schedule,
calendar-availability, inbox-availability, calendar-proxy, calendarserver-private-events,
calendarserver-private-comments, calendarserver-sharing, calendarserver-sharing-no-scheduling,
calendarserver-group-sharee, calendar-query-extended, calendar-default-alarms,
calendar-managed-attachments, calendarserver-partstat-changes, calendarserver-group-attendee,
calendar-no-timezone, calendarserver-recurrence-split, addressbook, addressbook, extended-mkcol,
calendarserver-principal-property-search, calendarserver-principal-search, calendarserver-home-sync *)with_resp_headers(Headers.replace_dav"1, extended-mkcol, calendar-access")rdelserdinWm.continue()rd'methodprivatepathrd=Uri.path(rd.uri)endclassredirectconfig=object(self)inherit[Cohttp_lwt.Body.t]Wm.resourcemethod!allowed_methodsrd=Wm.continue[`GET;`Other"PROPFIND"]rdmethod!known_methods=self#allowed_methodsmethodcontent_types_providedrd=Wm.continue[("*/*",self#redirect)]rdmethodcontent_types_acceptedrd=Wm.continue[]rdmethod!process_propertyrd=letrd'=redirect(Uri.to_string@@Uri.with_pathconfig.hostconfig.calendars)rdinWm.respond301rd'methodprivateredirectrd=letrd'=redirect(Uri.to_string@@Uri.with_pathconfig.hostconfig.calendars)rdinWm.respond301rd'end(* TODO force create user, uses delete user *)classuserconfigfsnowgenerate_salt=object(self)inherit[Cohttp_lwt.Body.t]Wm.resourcemethodprivaterequested_userrd=matchWebmachine.Rd.lookup_path_info"id"rdwith|None->Error`Bad_request|Somex->ifnot(sanex)thenError`Bad_requestelseOkxmethodprivaterequested_passwordrd=matchUri.get_query_paramrd.uri"password"with|None->Error`Bad_request|Somex->Okxmethod!allowed_methodsrd=Wm.continue[`PUT;`OPTIONS;`DELETE]rdmethod!known_methodsrd=Wm.continue[`PUT;`OPTIONS;`DELETE]rdmethodprivatecreate_userrd=matchself#requested_userrd,self#requested_passwordrdwith|Error_,_|_,Error_->Wm.respond(to_status`Bad_request)rd|Okname,Okpassword->letnow=now()inletsalt=generate_salt()inDav.make_userfsnowconfig~name~password~salt>>=function|Errore->Wm.respond(to_statuse)rd|Okprincipal_url->letrd'=with_resp_headers(Headers.replace_locationprincipal_url)rdinWm.continuetruerd'(* TODO? allow a user to delete themselves *)(* TODO? soft-delete: "mark as deleted" *)method!delete_resourcerd=matchself#requested_userrdwith|Error`Bad_request->Wm.respond(to_status`Bad_request)rd|Okname->Dav.delete_userfsconfigname>>=function|Errore->Wm.respond(to_statuse)rd|Ok()->Wm.continuetruerdmethod!is_conflictrd=matchself#requested_userrdwith|Error`Bad_request->Wm.respond(to_status`Bad_request)rd|Okname->Fs.dir_existsfs(`Dir[config.principals;name])>>=funuser_exists->(* TODO this is a hack to overload PUT /user for creation of new users and password changes *)matchuser_exists,self#requested_passwordrdwith|true,Oknew_pass->beginletsalt=generate_salt()inDav.change_user_passwordfsconfig~name~password:new_pass~salt>>=function|Ok()->Wm.respond(to_status`OK)rd|Errore->Wm.respond(to_statuse)rdend|_,_->Wm.continueuser_existsrdmethodcontent_types_providedrd=Wm.continue[("*/*",Wm.continue`Empty)]rdmethodcontent_types_acceptedrd=Wm.continue[("application/octet-stream",self#create_user)]rdmethod!is_authorizedrd=(* TODO implement digest authentication! *)matchHeaders.get_authorizationrd.req_headerswith|None->Wm.continue(`Basic"calendar")rd|Somev->Dav.verify_auth_headerfsconfigv>>=function|Error(`Msgmsg)->Access_log.warn(funm->m"is_authorized failed with header value %s and message %s"vmsg);Wm.continue(`Basic"invalid authorization")rd|Error(`Unknown_user(name,_))->Access_log.warn(funm->m"is_authorized failed with unknown user %s"name);Wm.continue(`Basic"invalid authorization")rd|Okuser->letrd'=with_req_headers(Headers.replace_authorizationuser)rdinWm.continue`Authorizedrd'method!forbiddenrd=letuser=Headers.get_userrd.req_headersinmatchself#requested_userrdwith|Error`Bad_request->Wm.respond(to_status`Bad_request)rd|Okrequested_user->Dav.access_granted_for_aclfsconfig~path:(config.principals^"/"^requested_user)rd.meth~user>>=funprincipals_granted->Wm.continue(notprincipals_granted)rdendclassgroupconfigfsnow=object(self)inherit[Cohttp_lwt.Body.t]Wm.resourcemethodprivaterequested_grouprd=matchWebmachine.Rd.lookup_path_info"id"rdwith|None->Error`Bad_request|Somex->ifnot(sanex)thenError`Bad_requestelseOkxmethodprivaterequested_membersrd=matchUri.get_query_paramrd.uri"members"with|None->Ok[]|Somex->letmembers=String.split_on_char','xinifList.for_allsanemembersthenOkmemberselseError`Bad_requestmethod!allowed_methodsrd=Wm.continue[`PUT;`OPTIONS;`DELETE]rdmethod!known_methodsrd=Wm.continue[`PUT;`OPTIONS;`DELETE]rdmethodprivatecreate_grouprd=matchself#requested_grouprd,self#requested_membersrdwith|Error_,_|_,Error_->Wm.respond(to_status`Bad_request)rd|Okname,Okmembers->letnow=now()inDav.make_groupfsnowconfignamemembers>>=function|Errore->Wm.respond(to_statuse)rd|Okprincipal_url->letrd'=with_resp_headers(Headers.replace_locationprincipal_url)rdinWm.continuetruerd'method!delete_resourcerd=matchself#requested_grouprdwith|Error`Bad_request->Wm.respond(to_status`Bad_request)rd|Okname->Dav.delete_groupfsconfigname>>=function|Errore->Wm.respond(to_statuse)rd|Ok()->Wm.continuetruerdmethod!is_conflictrd=matchself#requested_grouprdwith|Error`Bad_request->Wm.respond(to_status`Bad_request)rd|Okname->Fs.dir_existsfs(`Dir[config.principals;name])>>=fungroup_exists->matchgroup_exists,self#requested_membersrdwith|true,Okmembers->beginDav.replace_group_membersfsconfignamemembers>>=function|Ok()->Wm.respond(to_status`OK)rd|Errore->Wm.respond(to_statuse)rdend|_->Wm.continuegroup_existsrdmethodcontent_types_providedrd=Wm.continue[("*/*",Wm.continue`Empty)]rdmethodcontent_types_acceptedrd=Wm.continue[("application/octet-stream",self#create_group)]rdmethod!is_authorizedrd=(* TODO implement digest authentication! *)matchHeaders.get_authorizationrd.req_headerswith|None->Wm.continue(`Basic"calendar")rd|Somev->Dav.verify_auth_headerfsconfigv>>=function|Error(`Msgmsg)->Access_log.warn(funm->m"is_authorized failed with header value %s and message %s"vmsg);Wm.continue(`Basic"invalid authorization")rd|Error(`Unknown_user(name,_))->Access_log.warn(funm->m"is_authorized failed with unknown user %s"name);Wm.continue(`Basic"invalid authorization")rd|Okuser->letrd'=with_req_headers(Headers.replace_authorizationuser)rdinWm.continue`Authorizedrd'method!forbiddenrd=letuser=Headers.get_userrd.req_headersinmatchself#requested_grouprdwith|Error`Bad_request->Wm.respond(to_status`Bad_request)rd|Okrequested_group->Dav.access_granted_for_aclfsconfig~path:(config.principals^"/"^requested_group)rd.meth~user>>=funprincipals_granted->Wm.continue(notprincipals_granted)rdendclassgroup_usersconfigfs=object(self)inherit[Cohttp_lwt.Body.t]Wm.resourcemethodprivaterequested_grouprd=matchWebmachine.Rd.lookup_path_info"group_id"rdwith|None->Error`Bad_request|Somex->ifnot(sanex)thenError`Bad_requestelseOkxmethodprivaterequested_memberrd=matchWebmachine.Rd.lookup_path_info"user_id"rdwith|None->Error`Bad_request|Somex->ifnot(sanex)thenError`Bad_requestelseOkxmethod!allowed_methodsrd=Wm.continue[`PUT;`OPTIONS;`DELETE]rdmethod!known_methodsrd=Wm.continue[`PUT;`OPTIONS;`DELETE]rdmethodprivateadd_group_memberrd=matchself#requested_grouprd,self#requested_memberrdwith|Error_,_|_,Error_->Wm.respond(to_status`Bad_request)rd|Okgroup,Okmember->Dav.enrollfsconfig~group~member>>=function|Ok()->Wm.continuetruerd|Errore->Wm.respond(to_statuse)rdmethod!delete_resourcerd=matchself#requested_grouprd,self#requested_memberrdwith|Error`Bad_request,_|_,Error`Bad_request->Wm.respond(to_status`Bad_request)rd|Okgroup,Okmember->Dav.resignfsconfig~group~member>>=function|Ok()->Wm.continuetruerd|Errore->Wm.respond(to_statuse)rdmethodcontent_types_providedrd=Wm.continue[("*/*",Wm.continue`Empty)]rdmethodcontent_types_acceptedrd=Wm.continue[("application/octet-stream",self#add_group_member)]rdmethod!is_conflictrd=Wm.continuefalserdmethod!is_authorizedrd=(* TODO implement digest authentication! *)matchHeaders.get_authorizationrd.req_headerswith|None->Wm.continue(`Basic"calendar")rd|Somev->Dav.verify_auth_headerfsconfigv>>=function|Error(`Msgmsg)->Access_log.warn(funm->m"is_authorized failed with header value %s and message %s"vmsg);Wm.continue(`Basic"invalid authorization")rd|Error(`Unknown_user(name,_))->Access_log.warn(funm->m"is_authorized failed with unknown user %s"name);Wm.continue(`Basic"invalid authorization")rd|Okuser->letrd'=with_req_headers(Headers.replace_authorizationuser)rdinWm.continue`Authorizedrd'method!forbiddenrd=letuser=Headers.get_userrd.req_headersinmatchself#requested_grouprdwith|Error`Bad_request->Wm.respond(to_status`Bad_request)rd|Okrequested_group->Dav.access_granted_for_aclfsconfig~path:(config.principals^"/"^requested_group)rd.meth~user>>=funprincipals_granted->Wm.continue(notprincipals_granted)rdend(* the route table *)letroutesconfigfsnowgenerate_salt=[("/.well-known/caldav",fun()->newredirectconfig);("/users/:id",fun()->newuserconfigfsnowgenerate_salt);("/groups/:group_id/users/:user_id",fun()->newgroup_usersconfigfs);("/groups/:id",fun()->newgroupconfigfsnow);("/"^config.principals,fun()->newhandlerconfigfsnowgenerate_salt);("/"^config.principals^"/*",fun()->newhandlerconfigfsnowgenerate_salt);("/"^config.calendars,fun()->newhandlerconfigfsnowgenerate_salt);("/"^config.calendars^"/*",fun()->newhandlerconfigfsnowgenerate_salt);]letdispatchconfigfsrequestbody=(* Perform route dispatch. If [None] is returned, then the URI path did not
* match any of the route patterns. In this case the server should return a
* 404 [`Not_found]. *)letnow()=Ptime.v(Clock.now_d_ps())inletstart=now()inAccess_log.debug(funm->m"request %s %s"(Cohttp.Code.string_of_method(Cohttp.Request.methrequest))(Cohttp.Request.resourcerequest));Access_log.debug(funm->m"request headers %s"(Cohttp.Header.to_string(Cohttp.Request.headersrequest)));Wm.dispatch'(routesconfigfsnowDav.generate_salt)~body~request>|=beginfunction|None->(`Not_found,Cohttp.Header.init(),`String"Not found",[])|Someresult->resultend>>=fun(status,headers,body,_path)->letstop=now()inletdiff=Ptime.diffstopstartinAccess_log.debug(funm->m"response %d response time %a"(Cohttp.Code.code_of_statusstatus)Ptime.Span.ppdiff);(* Access_log.debug (fun m -> m "%s %s path: %s"
(Cohttp.Code.string_of_method (Cohttp.Request.meth request))
(Uri.path (Cohttp.Request.uri request))
(String.concat ", " path)) ; *)Time_log.debug(funm->m"%s\t%s\t%d\t%f"(Cohttp.Code.string_of_method(Cohttp.Request.methrequest))(Cohttp.Request.resourcerequest)(Cohttp.Code.code_of_statusstatus)(Ptime.Span.to_float_sdiff));(* Access_log.debug (fun m -> m "body: %s"
(match body with `String s -> s | `Empty -> "empty" | _ -> "unknown") ) ; *)(* Finally, send the response to the client *)http_statusstatus;S.respond~headers~body~status()end