src/cors.bend
emerging-ezhttp@0.21.0.0 · 303 lines · 40 definitions
1
2
3
4import Base
5import emerging-ezhttp@0.21.0.0/src/http.bend as Http
6
7
8type Cfg is Data:
9 Cfg{
10 origins: List<&2, String>
11 methods: List<&2, String>
12 headers: List<&2, String>
13 credentials: Bool
14 max_age: String
15 expose: List<&2, String>
16 }
17
18
19def cors.star.step(hit: Bool, rest: Unit -> Bool) -> Bool:
20 match hit:
21 case True{}:
22 True{}
23 case False{}:
24 rest(Unit{})
25
26
27def cors.star(os: List<&2, String>) -> Bool:
28 match os:
29 case []:
30 False{}
31 case h <> t:
32 cors.star.step(String.eq(h, "*"), _u => cors.star(t))
33
34
35def cors.hit.step(hit: Bool, rest: Unit -> Bool) -> Bool:
36 match hit:
37 case True{}:
38 True{}
39 case False{}:
40 rest(Unit{})
41
42
43def cors.hit(+needle: String, xs: List<&2, String>) -> Bool:
44 match xs:
45 case []:
46 False{}
47 case h <> t:
48 cors.hit.step(String.eq(h, needle), _u => cors.hit(needle, t))
49
50
51def cors.lower(xs: List<&2, String>) -> List<&2, String>:
52 match xs:
53 case []:
54 []
55 case h <> t:
56 String.to_lower(h) <> cors.lower(t)
57
58
59def cors.hit_ci(+needle: String, xs: List<&2, String>) -> Bool:
60 cors.hit(String.to_lower(needle), cors.lower(xs))
61
62
63def cors.allowed(+os: List<&2, String>, origin: String) -> Bool:
64 Bool.or(cors.star(os), cors.hit(origin, os))
65
66
67def cors.acao.star(origin: String, star: Bool) -> String:
68 match star:
69 case True{}:
70 "*"
71 case False{}:
72 origin
73
74
75def cors.acao.cred(+os: List<&2, String>, origin: String, credentials: Bool)
76 -> String:
77 match credentials:
78 case True{}:
79 origin
80 case False{}:
81 cors.acao.star(origin, cors.star(os))
82
83
84def cors.acao.ok(+os: List<&2, String>, credentials: Bool, origin: String,
85 ok: Bool) -> String:
86 match ok:
87 case False{}:
88 ""
89 case True{}:
90 cors.acao.cred(os, origin, credentials)
91
92
93def cors.acao(+os: List<&2, String>, credentials: Bool, +origin: String) -> String:
94 cors.acao.ok(os, credentials, origin, cors.allowed(os, origin))
95
96
97def cors.vary(+acao: String) -> Bool:
98 Bool.and(Bool.not(String.eq(acao, "*")), Bool.not(String.eq(acao, "")))
99
100
101def cors.names.step(ok: Bool, rest: Unit -> Bool) -> Bool:
102 match ok:
103 case True{}:
104 rest(Unit{})
105 case False{}:
106 False{}
107
108
109def cors.names(ps: List<&2, String>, +allow: List<&2, String>) -> Bool:
110 match ps:
111 case []:
112 True{}
113 case h <> t:
114 cors.names.step(cors.hit_ci(String.trim(h), allow), _u => cors.names(t, allow))
115
116
117def cors.headers_empty(+requested: String, +allow: List<&2, String>, empty: Bool)
118 -> Bool:
119 match empty:
120 case True{}:
121 True{}
122 case False{}:
123 cors.names(String.split(requested, ','), allow)
124
125
126def cors.headers_ok(+requested: String, +allow: List<&2, String>) -> Bool:
127 cors.headers_empty(requested, allow, String.eq(requested, ""))
128
129
130def cors.join(xs: List<&2, String>) -> String:
131 String.join(xs, ", ")
132
133
134def cors.vary_on(hs: List<&2, Http.Header>, _acao: String, vary: Bool)
135 -> List<&2, Http.Header>:
136 match vary:
137 case False{}:
138 hs
139 case True{}:
140 Http.H{"Vary", "Origin"} <> hs
141
142
143def cors.cred_on(hs: List<&2, Http.Header>, credentials: Bool)
144 -> List<&2, Http.Header>:
145 match credentials:
146 case False{}:
147 hs
148 case True{}:
149 Http.H{"Access-Control-Allow-Credentials", "true"} <> hs
150
151
152def cors.preflight_hs(+acao: String, methods: List<&2, String>,
153 headers: List<&2, String>, credentials: Bool, max_age: String)
154 -> List<&2, Http.Header>:
155 cors.cred_on(cors.vary_on([
156 Http.H{"Access-Control-Allow-Origin", acao},
157 Http.H{"Access-Control-Allow-Methods", cors.join(methods)},
158 Http.H{"Access-Control-Allow-Headers", cors.join(headers)},
159 Http.H{"Access-Control-Max-Age", max_age}
160 ], acao, cors.vary(acao)), credentials)
161
162
163def cors.preflight.headers(acao: String, methods: List<&2, String>,
164 headers: List<&2, String>, credentials: Bool, max_age: String,
165 _req: String, ok: Bool) -> Http.Reply:
166 match ok:
167 case False{}:
168 Http.Reply{400, [], ""}
169 case True{}:
170 Http.Reply{200, cors.preflight_hs(acao, methods, headers, credentials,
171 max_age), ""}
172
173
174def cors.preflight.method(acao: String, methods: List<&2, String>,
175 +headers: List<&2, String>, credentials: Bool, max_age: String,
176 +req_headers: String, _method: String, ok: Bool) -> Http.Reply:
177 match ok:
178 case False{}:
179 Http.Reply{400, [], ""}
180 case True{}:
181 cors.preflight.headers(acao, methods, headers, credentials, max_age,
182 req_headers, cors.headers_ok(req_headers, headers))
183
184
185def cors.preflight.denied(acao: String, +methods: List<&2, String>,
186 headers: List<&2, String>, credentials: Bool, max_age: String,
187 req_headers: String, +method: String, denied: Bool) -> Http.Reply:
188 match denied:
189 case True{}:
190 Http.Reply{400, [], ""}
191 case False{}:
192 cors.preflight.method(acao, methods, headers, credentials, max_age,
193 req_headers, method, cors.hit(method, methods))
194
195
196def cors.preflight(cfg: Cfg, origin: String, method: String, req_headers: String)
197 -> Http.Reply:
198 match cfg:
199 case Cfg{+origins, methods, headers, +credentials, max_age, expose}:
200 +acao = cors.acao(origins, credentials, origin)
201 cors.preflight.denied(acao, methods, headers, credentials, max_age,
202 req_headers, method, String.eq(acao, ""))
203
204
205def cors.expose_of(hs: List<&2, Http.Header>, expose: List<&2, String>,
206 empty: Bool) -> List<&2, Http.Header>:
207 match empty:
208 case True{}:
209 hs
210 case False{}:
211 Http.H{"Access-Control-Expose-Headers", cors.join(expose)} <> hs
212
213
214def cors.expose_on(hs: List<&2, Http.Header>, +expose: List<&2, String>)
215 -> List<&2, Http.Header>:
216 cors.expose_of(hs, expose, List.is_empty(&2, String, expose))
217
218
219def cors.simple_hs(+acao: String, credentials: Bool, expose: List<&2, String>)
220 -> List<&2, Http.Header>:
221 cors.cred_on(cors.vary_on(cors.expose_on([
222 Http.H{"Access-Control-Allow-Origin", acao}
223 ], expose), acao, cors.vary(acao)), credentials)
224
225
226def cors.simple.skip(add: Bool, reply: Http.Reply, hs: List<&2, Http.Header>)
227 -> Http.Reply:
228 match add reply:
229 case False{} r:
230 r
231 case True{} Http.Torn{why}:
232 Http.Torn{why}
233 case True{} Http.Reply{status, headers, body}:
234 Http.Reply{status, List.append(&2, Http.Header, hs, headers), body}
235
236
237def cors.simple.add(reply: Http.Reply, acao: String, credentials: Bool,
238 expose: List<&2, String>, add: Bool) -> Http.Reply:
239 cors.simple.skip(add, reply, cors.simple_hs(acao, credentials, expose))
240
241
242def cors.simple(cfg: Cfg, origin: String, reply: Http.Reply) -> Http.Reply:
243 match cfg:
244 case Cfg{+origins, methods, headers, +credentials, max_age, expose}:
245 +acao = cors.acao(origins, credentials, origin)
246 cors.simple.add(reply, acao, credentials, expose,
247 Bool.not(String.eq(acao, "")))
248
249
250def cors.origin(origin: String) -> Http.Header:
251 Http.H{"Origin", origin}
252
253
254def cors.request_method(method: String) -> Http.Header:
255 Http.H{"Access-Control-Request-Method", method}
256
257
258def cors.request_headers(names: String) -> Http.Header:
259 Http.H{"Access-Control-Request-Headers", names}
260
261
262def cors.is_preflight.method(method: String, asked: String) -> Bool:
263 Bool.and(String.eq(method, "OPTIONS"), Bool.not(String.eq(asked, "")))
264
265
266def cors.is_preflight(req: Http.Request) -> Bool:
267 match req:
268 case Http.Bad{why}:
269 False{}
270 case Http.Request{method, target, headers, body}:
271 cors.is_preflight.method(method,
272 Http.headers.find(headers, "access-control-request-method"))
273
274
275def cors.preflight_req(cfg: Cfg, req: Http.Request) -> Http.Reply:
276 match req:
277 case Http.Bad{why}:
278 Http.Reply{400, [], why}
279 case Http.Request{method, target, +headers, body}:
280 cors.preflight(cfg, Http.headers.find(headers, "origin"),
281 Http.headers.find(headers, "access-control-request-method"),
282 Http.headers.find(headers, "access-control-request-headers"))
283
284
285def cors.simple_req(cfg: Cfg, req: Http.Request, reply: Http.Reply) -> Http.Reply:
286 match req:
287 case Http.Bad{why}:
288 reply
289 case Http.Request{method, target, headers, body}:
290 cors.simple(cfg, Http.headers.find(headers, "origin"), reply)
291
292
293def cors.route(cfg: Cfg, req: Http.Request, +reply: Http.Reply, pre: Bool)
294 -> Http.Reply:
295 match pre:
296 case True{}:
297 cors.preflight_req(cfg, req)
298 case False{}:
299 cors.simple_req(cfg, req, reply)
300
301
302def cors.on(cfg: Cfg, +req: Http.Request, reply: Http.Reply) -> Http.Reply:
303 cors.route(cfg, req, reply, cors.is_preflight(req))