http.ml 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. (*
  2. * _ _ ____ _
  3. * _| || |_/ ___| ___ _ __ _ __ ___ | |
  4. * |_ .. _\___ \ / _ \ '_ \| '_ \ / _ \| |
  5. * |_ _|___) | __/ |_) | |_) | (_) |_|
  6. * |_||_| |____/ \___| .__/| .__/ \___/(_)
  7. * |_| |_|
  8. *
  9. * Personal Social Web.
  10. *
  11. * Copyright (C) The #Seppo contributors. All rights reserved.
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU General Public License as published by
  15. * the Free Software Foundation, either version 3 of the License, or
  16. * (at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. *)
  26. let ( let* ) = Result.bind
  27. let ( let*% ) r f : ('b,'e) Lwt_result.t =
  28. (* https://discuss.ocaml.org/t/idiomatic-let-result-bind-and-lwt-bind/12554?u=mro *)
  29. match r with
  30. | Error _ as e -> Lwt.return e (* similar to Result.map_error but without unwrapping *)
  31. | Ok v -> f v
  32. let pp_status ppf status = Format.pp_print_string ppf (status |> Cohttp.Code.string_of_status)
  33. let reso ~base url =
  34. Uri.resolve "https" base url
  35. (** subtract the base from path, so as Uri.resolve "" base x = path *)
  36. let relpa base path =
  37. let rec f = function
  38. | _ :: [], p -> p
  39. | bh :: bt, ph :: pt when String.equal bh ph -> f (bt,pt)
  40. | _ -> []
  41. in
  42. let is_sep = Astring.Char.equal '/' in
  43. let ba = base |> Astring.String.fields ~is_sep
  44. and pa = path |> Astring.String.fields ~is_sep in
  45. f (ba,pa) |> Astring.String.concat ~sep:"/"
  46. let abs_to_rel ~base url =
  47. match url |> Uri.host with
  48. | None -> url
  49. | Some _ as ho ->
  50. let url = if Option.equal String.equal (Uri.host base) ho
  51. then Uri.with_host url None
  52. else url in
  53. let url = if Option.equal String.equal (Uri.scheme base) (Uri.scheme url)
  54. then Uri.with_scheme url None
  55. else url in
  56. let url = if Option.equal Int.equal (Uri.port base) (Uri.port url)
  57. then Uri.with_port url None
  58. else url in
  59. let url = Uri.with_path url (relpa (Uri.path base) (Uri.path url)) in
  60. url
  61. (* https://tools.ietf.org/html/rfc2616/#section-3.3.1
  62. https://tools.ietf.org/html/rfc1123#page-55
  63. https://tools.ietf.org/html/rfc822#section-5.1
  64. *)
  65. let to_rfc1123 (time : Ptime.t) =
  66. (* MIT License, Copyright 2021 Anton Bachin
  67. https://github.com/aantron/dream/blob/master/src/pure/formats.ml#L51 *)
  68. let weekday =
  69. match Ptime.weekday time with
  70. | `Sun -> "Sun"
  71. | `Mon -> "Mon"
  72. | `Tue -> "Tue"
  73. | `Wed -> "Wed"
  74. | `Thu -> "Thu"
  75. | `Fri -> "Fri"
  76. | `Sat -> "Sat"
  77. in
  78. let (y, m, d), ((hh, mm, ss), _tz_offset_s) = Ptime.to_date_time time in
  79. let month =
  80. match m with
  81. | 1 -> "Jan"
  82. | 2 -> "Feb"
  83. | 3 -> "Mar"
  84. | 4 -> "Apr"
  85. | 5 -> "May"
  86. | 6 -> "Jun"
  87. | 7 -> "Jul"
  88. | 8 -> "Aug"
  89. | 9 -> "Sep"
  90. | 10 -> "Oct"
  91. | 11 -> "Nov"
  92. | 12 -> "Dec"
  93. | _ -> failwith "Month < 1 or > 12 not allowed"
  94. in
  95. (* [Ptime.to_date_time] docs give range 0..60 for [ss], accounting for
  96. leap seconds. However, RFC 6265 §5.1.1 states:
  97. 5. Abort these steps and fail to parse the cookie-date if:
  98. * the second-value is greater than 59.
  99. (Note that leap seconds cannot be represented in this syntax.)
  100. See https://tools.ietf.org/html/rfc6265#section-5.1.1.
  101. Even though [Ptime.to_date_time] time does not return leap seconds, in
  102. case I misunderstood the gmtime API, of system differences, or future
  103. refactoring, make sure no leap seconds creep into the output. *)
  104. Printf.sprintf "%s, %02i %s %04i %02i:%02i:%02i GMT" weekday d month y hh mm
  105. (min 59 ss)
  106. module Mime = struct
  107. module C = As2_vocab.Constants.ContentType
  108. let _app_act_json= C._app_act_json
  109. let app_jlda = C.app_jlda
  110. let app_jrd = C.app_jrd
  111. let app_atom_xml = C.app_atom_xml
  112. let app_form_url = "application/x-www-form-urlencoded"
  113. let app_json = C.app_json
  114. let img_jpeg = "image/jpeg"
  115. let text_css = "text/css; charset=utf8"
  116. let text_html = "text/html; charset=utf8"
  117. let text_plain = "text/plain; charset=utf8"
  118. let text_xml = "text/xml"
  119. let text_xsl = "text/xsl"
  120. let is_app_json m =
  121. _app_act_json |> String.equal m
  122. || app_json |> String.equal m
  123. end
  124. module H = struct
  125. (** a http header field: key,value *)
  126. type t = string * string
  127. let add' h (n, v) = Cohttp.Header.add h n v
  128. let accept ct :t= ("Accept", ct)
  129. let acc_app_json :t= accept Mime.app_json
  130. let acc_app_jrd :t= accept Mime.app_jrd
  131. let acc_app_jlda :t= accept Mime.app_jlda
  132. let agent :t= ("User-Agent", St.seppo_s)
  133. let content_type ct :t= ("Content-Type", ct)
  134. let ct_jlda :t= content_type Mime.app_jlda
  135. let ct_html :t= content_type Mime.text_html
  136. let ct_json :t= content_type Mime.app_json
  137. let ct_plain :t= content_type Mime.text_plain
  138. let ct_xml :t= content_type Mime.text_xml
  139. let content_length cl :t= ("Content-Length", cl |> string_of_int)
  140. let location url :t= ("Location", url |> Uri.to_string)
  141. let retry_after t :t= ("Retry-After", t |> to_rfc1123)
  142. let set_cookie v :t= ("Set-Cookie", v)
  143. let max_age _ :t= assert false (* set via webserver config *)
  144. let x_request_id u :t= ("X-Request-Id", Uuidm.to_string u)
  145. end
  146. module R = Cgi.Response
  147. type t = R.t
  148. type t' = (t,t) result
  149. (** enable railway programming, error for exit. *)
  150. (** See also https://github.com/aantron/dream/blob/master/src/pure/status.ml
  151. RFC1945 demands absolute uris https://www.rfc-editor.org/rfc/rfc1945#section-10.11 *)
  152. let s302' ?(header = []) url :t= (`Found, [ H.ct_plain; H.location url ] @ header, R.nobody)
  153. let s302 ?(header = []) url = Error (s302' ~header url)
  154. let s400' ?(body = R.nobody) ?(mime = H.ct_plain) () :t= (`Bad_request, [ mime ], body)
  155. let s400 ?(body = R.nobody) ?(mime = H.ct_plain) () = Error (s400' ~body ~mime ())
  156. let s401 :t' = Error (`Unauthorized, [ H.ct_plain ], R.nobody)
  157. let s403' :t = (`Forbidden, [ H.ct_plain ], R.nobody)
  158. let s403 = Error s403'
  159. let s404 :t'= Error (`Not_found, [ H.ct_plain ], R.nobody)
  160. let s405 :t'= Error (`Method_not_allowed, [ H.ct_plain ], R.nobody)
  161. let s411' :t = (`Length_required, [ H.ct_plain ], R.nobody)
  162. let s411 = Error s411'
  163. let s413 = Error (`Code 413, [ H.ct_plain ], R.nobody) (* Payload too large *)
  164. (* https://stackoverflow.com/a/42171674/349514 *)
  165. let s422' :t= (`Unprocessable_entity, [ H.ct_plain ], R.nobody)
  166. let s422 = Error s422'
  167. let s422x :t'= Error (`Unprocessable_entity, [ H.ct_xml ], R.nobody)
  168. (* https://tools.ietf.org/html/rfc6585#section-4
  169. Retry-After https://tools.ietf.org/html/rfc2616#section-14.37
  170. HTTP-date https://tools.ietf.org/html/rfc1123
  171. https://github.com/inhabitedtype/ocaml-webmachine/blob/master/lib/rfc1123.ml
  172. *)
  173. let s429_t tm = Error (`Too_many_requests, [ H.ct_plain; H.retry_after tm ], ("429: Too many requests." |> R.body))
  174. let s500' :t= (`Internal_server_error, [ H.ct_plain ], R.nobody) (** HTTP 500 Internal Server error and empty body (camel) *)
  175. let s500 :t'= Error s500' (** HTTP 500 Internal Server error and empty body (camel) *)
  176. let s501 :t'= Error (`Not_implemented, [ H.ct_plain ], R.nobody)
  177. let s502' ~(body : out_channel -> unit) ?(mime = H.ct_plain) () : t= (`Bad_gateway, [ mime ], body)
  178. let s502 ~body ?(mime = H.ct_plain) () :t'= Error (s502' ~body ~mime ())
  179. let err500 ?(error = s500') ?(level = Logs.Error) msg e : t =
  180. Logr.msg level (fun m -> m "%s: %s" msg e);
  181. error
  182. (** Send a clob as is and 200 Ok *)
  183. let clob_send _ mime clob : t' =
  184. Ok (`OK, [
  185. H.content_type mime;
  186. clob |> String.length |> H.content_length
  187. ], clob |> Cgi.Response.body)
  188. (*
  189. * https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
  190. * see also https://github.com/Gopiandcode/http_sig_ocaml/blob/254d464c16025e189ceb20190710fe50e9bd8d2b/http_sig.ml#L50
  191. *
  192. * Another list of k-v-pairs but in idiosyncratic encoding. Different from Cookie.
  193. *)
  194. module Signature = struct
  195. (* https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 *)
  196. module P = struct
  197. open Tyre
  198. (*
  199. let _htab = char '\t'
  200. (* https://stackoverflow.com/a/52336696/349514 *)
  201. let _vchar = pcre {|[!-~]|}
  202. let _sp = char ' '
  203. (* https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 *)
  204. let _tchar = pcre {|[!#$%&'*+-.^_`|~0-9a-zA-Z]|}
  205. let _obs_text = pcre {|€-ÿ|} (* %x80-FF *)
  206. *)
  207. (* https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 *)
  208. let token = pcre {|[!#$%&'*+-.^_`|~0-9a-zA-Z]+|} (* rep1 tchar *)
  209. let qdtext = pcre {|[\t !#-\[\]-~€-ÿ]|}
  210. (* htab (* HTAB *)
  211. <|> sp (* SP *)
  212. <|> char '!' (* %x21 *)
  213. <|> pcre {|[#-\[]|} (* %x23-5B *)
  214. <|> pcre {|[\]-~]|} (* %x5D-7E *)
  215. <|> obs_text
  216. *)
  217. let dquote = char '"'
  218. let quoted_pair = char '\\' *> pcre {|[\t !-~€-ÿ]|} (* (htab <|> sp <|> vchar <|> obs_text) *)
  219. let quoted_string =
  220. conv
  221. (fun x ->
  222. let buf = Buffer.create 100 in
  223. x
  224. |> Seq.fold_left (fun bu u ->
  225. (match u with
  226. | `Left ch
  227. | `Right ch -> ch)
  228. |> Buffer.add_string bu; bu) buf
  229. |> Buffer.contents)
  230. (fun x ->
  231. x
  232. |> String.to_seq
  233. |> Seq.map (fun c ->
  234. let s = Astring.String.of_char c in
  235. if c == '"' (* quote more? *)
  236. then `Right s
  237. else `Left s))
  238. (dquote *> (rep (qdtext <|> quoted_pair)) <* dquote)
  239. let ows = pcre {|[ \t]*|}
  240. let bws = ows
  241. (* https://datatracker.ietf.org/doc/html/rfc7235#section-2.1 *)
  242. let auth_param =
  243. conv
  244. (function
  245. | (t,`Left x)
  246. | (t,`Right x) -> t,x)
  247. (fun (t,s) ->
  248. (* TODO make s a token (`Left) if possible *)
  249. (t,`Right s))
  250. (token <* bws <* char '=' <* bws <&> (token <|> quoted_string))
  251. let list_auth_param =
  252. (* implement production 'credentials' at https://datatracker.ietf.org/doc/html/rfc7235#appendix-C *)
  253. let sep = bws *> char ',' <* bws in
  254. start *> separated_list ~sep auth_param <* stop
  255. (* https://gabriel.radanne.net/papers/tyre/tyre_paper.pdf#page=9 *)
  256. let list_auth_param' = compile list_auth_param
  257. end
  258. (** https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-4.1 *)
  259. let decode = Tyre.exec P.list_auth_param'
  260. (** the header value without escaping e.g. = or "" *)
  261. let encode =
  262. (*
  263. |> List.fold_left (fun init (k,v) -> Printf.sprintf {|%s="%s"|} k v :: init) []
  264. |> Astring.String.concat ~sep:"," in
  265. *)
  266. Tyre.eval P.list_auth_param
  267. (** add (request-target) iff request given *)
  268. let to_sign_string0 ~request h : string =
  269. let h = h |> Cohttp.Header.to_frames in
  270. (match request with
  271. | None -> h
  272. | Some (meth,uri) ->
  273. let s = Printf.sprintf "(request-target): %s %s"
  274. (meth |> Cohttp.Code.string_of_method |> String.lowercase_ascii)
  275. (uri |> Uri.path_and_query) in
  276. s :: h)
  277. |> Astring.String.concat ~sep:"\n"
  278. (**
  279. - key_id
  280. - pk
  281. - now *)
  282. type t_key = Uri.t * X509.Private_key.t * Ptime.t
  283. let mkey id pk t : t_key = (id,pk,t)
  284. (** build the string to sign *)
  285. let to_sign_string'
  286. (meth : Cohttp.Code.meth)
  287. (targ : Uri.t)
  288. (hdrs : (string * string) list) =
  289. let n,s = [],[] in
  290. let n,s = match hdrs |> List.assoc_opt "digest" with
  291. | None -> n,s
  292. | Some d -> "digest" :: n,("digest",d) :: s in
  293. let n = "(request-target)" :: "host" :: "date" :: n in
  294. let s = ("(request-target)",Printf.sprintf "%s %s"
  295. (meth |> Cohttp.Code.string_of_method |> Astring.String.map Astring.Char.Ascii.lowercase)
  296. (targ |> Uri.path_and_query))
  297. :: ("host",targ |> Uri.host |> Option.get)
  298. :: ("date",hdrs |> List.assoc "date")
  299. :: s in
  300. n,s
  301. let to_sign_string meth targ hdrs =
  302. let n,l = to_sign_string' meth targ hdrs in
  303. n |> Astring.String.concat ~sep:" "
  304. ,
  305. l |> Cohttp.Header.of_list
  306. |> Cohttp.Header.to_frames
  307. |> Astring.String.concat ~sep:"\n"
  308. (**
  309. HTTP signature according https://tools.ietf.org/id/draft-cavage-http-signatures-12.html#rfc.appendix.C
  310. *)
  311. module RSA_SHA256 = struct
  312. let hash = `SHA256
  313. and scheme = `RSA_PKCS1
  314. let name = "rsa-sha256"
  315. and sign = X509.Private_key.sign hash ~scheme
  316. and verify = X509.Public_key.verify hash ~scheme
  317. end
  318. (**
  319. HTTP signature according https://tools.ietf.org/id/draft-cavage-http-signatures-12.html#rfc.appendix.C
  320. *)
  321. module HS2019 = struct
  322. let hash = `SHA512
  323. and scheme = `RSA_PSS
  324. let name = "hs2019"
  325. and sign = X509.Private_key.sign hash ~scheme
  326. and verify = X509.Public_key.verify hash ~scheme
  327. end
  328. let add
  329. (priv : X509.Private_key.t)
  330. (meth : Cohttp.Code.meth)
  331. (targ : Uri.t)
  332. (hdrs : (string * string) list) =
  333. assert (hdrs |> List.assoc_opt "date" |> Option.is_some);
  334. assert (targ |> Uri.host |> Option.is_some);
  335. assert (hdrs |> List.assoc_opt "host" |> Option.is_some);
  336. assert (hdrs |> List.assoc "host" |> Astring.String.equal (targ |> Uri.host_with_default ~default:""));
  337. let n,s = to_sign_string meth targ hdrs in
  338. (* build the signature header value *)
  339. match RSA_SHA256.(name,(sign priv (`Message (s) ))) with
  340. | _,(Error _ as e) -> e
  341. | alg,Ok si ->
  342. let v = [
  343. "algorithm",alg;
  344. "headers" ,n;
  345. "signature", si |> Base64.encode_string;
  346. ]
  347. |> encode in
  348. Ok ( hdrs @ ["signature",v] )
  349. end
  350. (** Create headers including a signature for a POST request.
  351. https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/#http-signatures
  352. https://socialhub.activitypub.rocks/t/help-needed-http-signatures/2458
  353. https://tools.ietf.org/id/draft-cavage-http-signatures-12.html
  354. HTTP signature according https://tools.ietf.org/id/draft-cavage-http-signatures-12.html#rfc.appendix.C
  355. https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-10.html#name-creating-a-signature
  356. Digest http://tools.ietf.org/html/rfc3230#section-4.3.2
  357. https://docs.joinmastodon.org/spec/security/#http
  358. https://w3id.org/security#publicKey
  359. https://w3id.org/security/v1
  360. NOT: https://datatracker.ietf.org/doc/draft-ietf-httpbis-message-signatures/
  361. *)
  362. let signed_headers (key_id,pk,date : Signature.t_key) dige uri =
  363. let open Cohttp in
  364. let hdr = (
  365. ("host", uri |> Uri.host |> Option.value ~default:"-") ::
  366. ("date", date |> to_rfc1123) ::
  367. match dige with
  368. | None -> []
  369. | Some dige -> ("digest", dige) :: []
  370. ) in
  371. let meth,lst = match dige with
  372. | None -> `GET, ""
  373. | Some _ -> `POST," digest"
  374. in
  375. (*
  376. let _n,tx_ = Signature.to_sign_string2 meth uri hdr in
  377. let tx_ = tx_ |> Cohttp.Header.of_list |> Cohttp.Header.to_frames |> Astring.String.concat ~sep:"\n" in
  378. assert (tx_ |> String.equal tx');
  379. *)
  380. let tx = hdr
  381. |> Cohttp.Header.of_list
  382. |> Signature.to_sign_string0 ~request:(Some (meth,uri)) in
  383. let sgna =
  384. Signature.RSA_SHA256.sign
  385. pk
  386. (`Message tx)
  387. |> Result.get_ok
  388. |> Base64.encode_exn
  389. in
  390. ["keyId", key_id |> Uri.to_string ;
  391. "algorithm", Signature.RSA_SHA256.name ;
  392. "headers", "(request-target) host date" ^ lst ;
  393. "signature", sgna ;
  394. ]
  395. |> Signature.encode
  396. (*
  397. Printf.sprintf (* must be symmetric to Signature.decode *)
  398. "keyId=\"%s\",\
  399. algorithm=\"%s\",\
  400. headers=\"(request-target) host date%s\",\
  401. signature=\"%s\""
  402. (key_id |> Uri.to_string)
  403. algo
  404. lst
  405. (sgna |> Cstruct.to_string |> Base64.encode_exn)
  406. *)
  407. |> Header.add (hdr |> Header.of_list) "signature"
  408. (* https://github.com/mirage/ocaml-cohttp#dealing-with-timeouts *)
  409. let timeout ~seconds ~f =
  410. try%lwt
  411. Lwt.pick
  412. [
  413. Lwt.map Result.ok (f ()) ;
  414. Lwt.map (fun () -> Error "Timeout") (Lwt_unix.sleep seconds);
  415. ]
  416. with
  417. | Failure s -> Lwt.return (Error s)
  418. (* don't care about maximum redirects but rather enforce a timeout *)
  419. let get
  420. ?(key = None)
  421. ?(seconds = 5.0)
  422. ?(uuid_gen = () |> Random.State.make_self_init |> Uuidm.v4_gen)
  423. ?(headers = Cohttp.Header.init())
  424. uri =
  425. let t0 = Sys.time () in
  426. let uuid = () |> uuid_gen in
  427. let headers = H.agent |> H.add' headers in
  428. let headers = uuid |> H.x_request_id |> H.add' headers in
  429. (* based on https://github.com/mirage/ocaml-cohttp#dealing-with-redirects *)
  430. let rec get_follow uri =
  431. let headers = match key with
  432. | None -> headers
  433. | Some key ->
  434. Cohttp.Header.(signed_headers key None uri |> to_list |> add_list headers) in
  435. let%lwt r = Cohttp_lwt_unix.Client.get ~headers uri in
  436. follow_redirect ~base:uri r
  437. and follow_redirect ~base (response, body) =
  438. let sta = response.status in
  439. Logr.debug (fun m -> m "%s.%s %a %a" "Http" "get.follow_redirect" Uuidm.pp uuid pp_status sta);
  440. match sta with
  441. | #Cohttp.Code.redirection_status ->
  442. (* should we ignore the status and just use location if present? *)
  443. ( match "location" |> Cohttp.Header.get (Cohttp.Response.headers response) with
  444. | Some loc ->
  445. Logr.debug (fun m -> m "%s.%s HTTP %a location: %s" "Http" "get.follow_redirect" pp_status sta loc);
  446. (* The unconsumed body would leak memory *)
  447. let%lwt _ = Cohttp_lwt.Body.drain_body body in
  448. loc
  449. |> Uri.of_string
  450. |> reso ~base
  451. |> get_follow
  452. | None ->
  453. Logr.warn (fun m -> m "%s.%s missing location header %a" "Http" "get.follow_redirect" Uri.pp_hum base);
  454. Lwt.return (response, body) )
  455. | _ ->
  456. (* here the http header signature validation could be done.
  457. But not for now: https://seppo.mro.name/issues/23 *)
  458. Logr.debug (fun m -> m "%s.%s %a %a" "Http" "get" Uuidm.pp uuid Cohttp.Response.pp_hum response);
  459. Lwt.return (response, body)
  460. and f () = get_follow uri in
  461. Logr.debug (fun m -> m "%s.%s %a %a" "Http" "get" Uri.pp uri Cohttp.Header.pp_hum headers);
  462. let r = timeout ~seconds ~f in
  463. Logr.info (fun m -> m "%s.%s %a dt=%.3fs localhost -> %a" "Http" "get" Uuidm.pp uuid (Sys.time() -. t0) Uri.pp uri);
  464. r
  465. let post
  466. ?(seconds = 5.0)
  467. ?(uuid_gen = () |> Random.State.make_self_init |> Uuidm.v4_gen)
  468. ~headers
  469. body
  470. uri : 'a Lwt.t =
  471. let t0 = Sys.time () in
  472. let uuid = () |> uuid_gen in
  473. let headers = uuid |> H.x_request_id |> H.add' headers in
  474. let headers = H.agent |> H.add' headers in
  475. let headers = body |> String.length |> H.content_length |> H.add' headers in
  476. let f () = Cohttp_lwt_unix.Client.post ~body:(`String body) ~headers uri
  477. (* here the http header signature validation could be done.
  478. But no for now: https://seppo.mro.name/issues/23 *)
  479. in
  480. let r = timeout ~seconds ~f in
  481. Logr.info (fun m -> m "%s.%s %a dt=%.3fs localhost -> %a" "Http" "post" Uuidm.pp uuid (Sys.time() -. t0) Uri.pp uri);
  482. Logr.debug (fun m -> m "%s.%s\n%s%s" "Http" "post" (headers |> Cohttp.Header.to_string) body);
  483. r
  484. let get_jsonv
  485. ?(key = None)
  486. ?(seconds = 5.0)
  487. ?(headers = [ H.acc_app_jlda ] |> Cohttp.Header.of_list)
  488. fkt
  489. uri =
  490. Logr.debug (fun m -> m "%s.%s %a" "Http" "get_jsonv" Uri.pp uri);
  491. let err fmt msg =
  492. Error (Printf.sprintf fmt msg) in
  493. let%lwt p = get ~key ~seconds ~headers uri in
  494. match p with
  495. | Error _ as e -> Lwt.return e
  496. | Ok (resp, body) ->
  497. match resp.status with
  498. | #Cohttp.Code.success_status as sta ->
  499. Logr.debug (fun m -> m "%s.%s get %a %a" "Http" "get_jsonv" Uri.pp uri pp_status sta);
  500. let%lwt body = body |> Cohttp_lwt.Body.to_string in
  501. (* doesn't validate the digest https://seppo.mro.name/issues/23 *)
  502. (try
  503. (resp, body |> Ezjsonm.value_from_string)
  504. |> fkt
  505. with
  506. | Ezjsonm.Parse_error (_,msg) ->
  507. err "parsing json: '%s'" msg
  508. | e ->
  509. err "parsing json: '%s'" (e |> Printexc.to_string) )
  510. |> Lwt.return
  511. | sta -> err "Gateway error: %s" (sta |> Cohttp.Code.string_of_status)
  512. |> Lwt.return
  513. let err400 k =
  514. (`Bad_request, [ H.ct_plain ], ("required input missing: " ^ k) |> R.body)
  515. (** Extract one required parameter from a get query
  516. pq: typically Cgi.Request.path_and_query *)
  517. let par1 ?(err = err400) pq k0 =
  518. let* v0 = k0 |> Uri.get_query_param pq |> Option.to_result ~none:(err k0) in
  519. Ok v0
  520. (** Extract two required parameters from a get query
  521. pq: typically Cgi.Request.path_and_query *)
  522. let par2 ?(err = err400) pq (k0,k1) =
  523. let* v0 = k0 |> Uri.get_query_param pq |> Option.to_result ~none:(err k0) in
  524. let* v1 = k1 |> Uri.get_query_param pq |> Option.to_result ~none:(err k1) in
  525. Ok (v0,v1)
  526. (** run a value through a function *)
  527. let f1 ?(f = Uri.of_string) v0 =
  528. Ok (v0 |> f)
  529. (** run a tuple's values through a function *)
  530. let f2 ?(f = Uri.of_string) (v0,v1) =
  531. Ok (v0 |> f,v1 |> f)