1 # ezhttp/src/echo: the Cookie request header a client sends back after 2 # Set-Cookie fields, reduced to names and values (RFC 6265 §5.2 parsing, 3 # §5.3 step 11 replacement, §5.4 serialization). No attribute is read: no 4 # Domain, Path, Expires, Max-Age, Secure or HttpOnly, so nothing expires and 5 # every cookie goes back. Pure. SPEC.md HTTP-CK lists what is guaranteed. 6 import Base 7 import ./http.bend as Http 8 import ./qs.bend as Qs 9 10 # whitespace as RFC 6265 §5.2 strips it: SP or HTAB (RFC 5234 WSP) 11 def wsp(+c: Char) -> Bool: 12 Bool.or(Char.is_eq(c, ' '), Char.is_eq(c, '\t')) 13 14 # one character of the leading strip: dropped, and the strip goes on, when 15 # it is WSP 16 def wsp.lead.if(t: String, +h: Char, space: Bool, rest: Unit -> String) 17 -> String: 18 match space: 19 case True{}: 20 rest(Unit{}) 21 case False{}: 22 SCon{h, t} 23 24 # a string without its leading WSP 25 def wsp.lead(s: String) -> String: 26 match s: 27 case SNil{}: 28 SNil{} 29 case SCon{+h, +t}: 30 wsp.lead.if(t, h, wsp(h), _u => wsp.lead(t)) 31 32 # a string without its leading and trailing WSP 33 def wsp.trim(s: String) -> String: 34 String.reverse(wsp.lead(String.reverse(wsp.lead(s)))) 35 36 # a pair whose name is known: ignored when the name is empty (§5.2 step 5) 37 def pair.named(+name: String, value: String, empty: Bool) -> Maybe<&2, Qs.Kv()>: 38 match empty: 39 case True{}: 40 None{} 41 case False{}: 42 Some{(name, value)} 43 44 # the name-value-pair cut at its first `=`: none without one (§5.2 step 2), 45 # else the name before it and the value after it, each without leading and 46 # trailing WSP (steps 3, 4) 47 def pair.cut(m: Maybe<&2, Qs.Kv()>) -> Maybe<&2, Qs.Kv()>: 48 match m: 49 case None{}: 50 None{} 51 case Some{(before, after)}: 52 +name = wsp.trim(before) 53 pair.named(name, wsp.trim(after), String.is_empty(name)) 54 55 # the cookie one Set-Cookie field value sets: its name-value-pair is the text 56 # before its first `;`, all of it without one (§5.2 step 1) 57 def pair(line: String) -> Maybe<&2, Qs.Kv()>: 58 pair.cut(Qs.cut(Http.first(String.split(line, ';')))) 59 60 # one stored cookie, once it is known whether it has the new one's name: 61 # that one takes the new value in its place, any other is kept and the rest 62 # of the store looked at 63 def put.at(key: String, old: String, value: String, t: List<&2, Qs.Kv()>, 64 hit: Bool, rest: Unit -> List<&2, Qs.Kv()>) -> List<&2, Qs.Kv()>: 65 match hit: 66 case True{}: 67 (key, value) <> t 68 case False{}: 69 (key, old) <> rest(Unit{}) 70 71 # a cookie put in a store: a stored one of the same name (compared exactly) 72 # keeps its place and takes the new value (§5.3 step 11.3 keeps its 73 # creation-time, which orders it, §5.4 step 2), else it goes after the others 74 def put(jar: List<&2, Qs.Kv()>, +name: String, +value: String) 75 -> List<&2, Qs.Kv()>: 76 match jar: 77 case []: 78 [(name, value)] 79 case (+key, old) <> +t: 80 put.at(key, old, value, t, String.eq(key, name), 81 _u => put(t, name, value)) 82 83 # a store after one Set-Cookie field value: unchanged when it sets nothing 84 def add(jar: List<&2, Qs.Kv()>, m: Maybe<&2, Qs.Kv()>) -> List<&2, Qs.Kv()>: 85 match m: 86 case None{}: 87 jar 88 case Some{(name, value)}: 89 put(jar, name, value) 90 91 # a store after Set-Cookie field values, in the order they came 92 def store(lines: List<&2, String>, jar: List<&2, Qs.Kv()>) 93 -> List<&2, Qs.Kv()>: 94 match lines: 95 case []: 96 jar 97 case l <> t: 98 store(t, add(jar, pair(l))) 99 100 # one cookie in the Cookie header: its name, `=`, its value (§5.4 step 4.1) 101 def one(name: String, value: String) -> String: 102 name ++ "=" ++ value 103 104 # the cookies after the first, each after `; ` (§5.4 step 4.2) 105 def more(jar: List<&2, Qs.Kv()>) -> String: 106 match jar: 107 case []: 108 "" 109 case (name, value) <> t: 110 "; " ++ one(name, value) ++ more(t) 111 112 # a store as the Cookie header's value, empty for an empty store 113 def line(jar: List<&2, Qs.Kv()>) -> String: 114 match jar: 115 case []: 116 "" 117 case (name, value) <> t: 118 one(name, value) ++ more(t) 119 120 # the Cookie header value to send after these Set-Cookie field values 121 def echo(lines: List<&2, String>) -> String: 122 line(store(lines, [])) 123 124 # one header field: its value in front of the rest when it is Set-Cookie 125 def values.put(value: String, rest: List<&2, String>, hit: Bool) 126 -> List<&2, String>: 127 match hit: 128 case True{}: 129 value <> rest 130 case False{}: 131 rest 132 133 # the values of the Set-Cookie fields among response headers, in order (field 134 # names compared case-insensitively, RFC 9110 §5.1) 135 def values(hs: List<&2, Http.Header>) -> List<&2, String>: 136 match hs: 137 case []: 138 [] 139 case Http.H{name, value} <> t: 140 values.put(value, values(t), String.eq(String.to_lower(name), 141 "set-cookie"))