Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Page
Library
Module
Module type
Parameter
Class
Class type
Source
vif_cookie.ml1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192(* Part of this code is based on the streaming project and Copyright (c) 2024 Anton Bachin <antonbachin@yahoo.com> SPDX-License-Identifier: ISC Copyright (c) 2024 Romain Calascibetta <romain.calascibetta@gmail.com> *) let src = Logs.Src.create "vif.cookie" module Log = (val Logs.src_log src : Logs.LOG) let key = String.lowercase_ascii key = "cookie" let str = match String.split_on_char '=' str with | [ k; v ] -> Some (String.trim k, String.trim v) | _ -> None let attribute str = match String.split_on_char '=' str with | [ a ] -> Some (String.trim a, None) | [ k; v ] -> Some (String.trim k, Some (String.trim v)) | _ -> None let str = let ( let* ) = Option.bind in match String.split_on_char ';' str with | [ c ] -> let* key, value = cookie c in Some { key; value; attributes= [] } | c :: vs -> let* key, value = cookie c in let attributes = List.map attribute vs in let attributes = List.filter_map Fun.id attributes in Some { key; value; attributes } | _ -> None let hdrs = let = List.filter (fun (k, _) -> is_cookie k) hdrs in let = List.map snd cookies in let = List.map cookie_of_string cookies in List.filter_map Fun.id cookies let without_prefix (is_host, is_secure) = match (is_host, is_secure) with | true, true | false, false -> cookie | true, false -> let key = cookie.key in let key = String.sub key 7 (String.length key - 7) in { key; value= cookie.value; attributes= cookie.attributes } | false, true -> let key = cookie.key in let key = String.sub key 9 (String.length key - 9) in { key; value= cookie.value; attributes= cookie.attributes } let filter_secure req0 = let is_secure = Option.is_some (Vif_request0.tls req0) || Vif_request0.on_localhost req0 in let fn ({ key; _ } as ) = let is_prefix_host = String.starts_with ~prefix:"__Host-" key in let is_prefix_secure = String.starts_with ~prefix:"__Secure-" key in let prefix = (is_prefix_host, is_prefix_secure) in let result = match (is_secure, is_prefix_host, is_prefix_secure) with | true, true, false -> Some cookie | true, false, true -> Some cookie | false, false, false -> Some cookie | _ -> None in Option.map (without_prefix prefix) result in List.filter_map fn cookies let to_key_values { key; value; attributes } = (key, (value, attributes)) let guard error fn = if fn () then Ok () else Error error let = `Invalid_encrypted_cookie type error = [ `Invalid_encrypted_cookie | `Msg of string | `Not_found ] let pp_error ppf = function | `Invalid_encrypted_cookie -> Fmt.string ppf "Invalid encrypted cookie" | `Not_found -> Fmt.string ppf "Cookie not found" | `Msg str -> Fmt.string ppf str let get ?(encrypted = true) ~name server req0 = let hdrs = Vif_request0.headers req0 in let = all_cookies hdrs in let = filter_secure req0 cookies in let = List.map to_key_values cookies in (* TODO(dinosaure): expiration. *) match List.assoc_opt name cookies with | None -> Error `Not_found | Some (value, _) when encrypted -> let ( let* ) = Result.bind in let alphabet = Base64.uri_safe_alphabet in let* value = Base64.decode ~pad:false ~alphabet value in let err = `Invalid_encrypted_cookie in let* () = guard err @@ fun () -> String.length value >= 14 in let* () = guard err @@ fun () -> value.[0] == '\x00' in let nonce = String.sub value 1 12 in let adata = "vif.cookie-" ^ name in let vdata = String.sub value 13 (String.length value - 13) in let key = Vif_server.cookie_key server in let value = Mirage_crypto.AES.GCM.authenticate_decrypt ~key ~nonce ~adata vdata in let* () = guard err @@ fun () -> Option.is_some value in Ok (Option.get value) | Some (value, _) -> Ok value type config = { expires: float option ; max_age: float option ; domain: [ `host ] Domain_name.t option ; path: bool ; secure: bool ; http_only: bool ; same_site: [ `Strict | `Lax | `None ] } let default_config = { expires= None ; max_age= None ; domain= None ; path= true ; secure= true ; http_only= true ; same_site= `Lax } let config ?expires ?max_age ?domain ?(path = true) ?(secure = true) ?(http_only = true) ?(same_site = `Lax) () = { expires; max_age; domain; path; secure; http_only; same_site } let cfg ~path name value = let expires = None in let max_age = None in let domain = Option.map (Fmt.str "Domain=%a" Domain_name.pp) cfg.domain in (* TODO(dinosaure): warn if cfg.domain != req0.tls.peer_name? *) let path = if cfg.path then Some (Fmt.str "Path=%s" path) else None in let secure = if cfg.secure then Some "Secure" else None in let http_only = if cfg.http_only then Some "HttpOnly" else None in let same_site = match cfg.same_site with | `Lax -> Some "SameSite=Lax" | `Strict -> Some "SameSite=Strict" | `None -> Some "SameSite=None" in let attributes = List.filter_map Fun.id [ expires; max_age; domain; path; secure; http_only; same_site ] in Fmt.str "%s=%s; %a" name value Fmt.(list ~sep:(any "; ") string) attributes let random len = Mirage_crypto_rng.generate len let ?(encrypt = true) ?(cfg = default_config) ?(path = "/") ~name server req0 value = let secure = Option.is_some (Vif_request0.tls req0) || Vif_request0.on_localhost req0 in let prefix = match (cfg.secure, cfg.domain, cfg.path, secure, path) with | true, None, true, true, "/" -> "__Host-" | true, _, _, true, _ -> "__Secure-" | _ -> "" in if encrypt then let key = Vif_server.cookie_key server in let nonce = random 12 in let adata = "vif.cookie-" ^ name in let value = Mirage_crypto.AES.GCM.authenticate_encrypt ~key ~nonce ~adata value in let alphabet = Base64.uri_safe_alphabet in let value = "\x00" ^ nonce ^ value in let value = Base64.encode_exn ~pad:false ~alphabet value in let value = set_cookie cfg ~path (prefix ^ name) value in Vif_response.add ~field:"set-cookie" value else let value = set_cookie cfg ~path name value in Vif_response.add ~field:"set-cookie" value let set ?encrypt ?cfg ?path ~name server req value = set ?encrypt ?cfg ?path ~name server req.Vif_request.request value