~/bend-docscommunity

cors.bend source

cors.bend on the hub · documented module

# ezhttp/cors: Fetch CORS response headers for a simple request and an OPTIONS# preflight. Origins are reflected, or `*`, and credentials never pair with `*`.# See https://fetch.spec.whatwg.org/#cors-protocol.import Baseimport ./http.bend as Http# allow-list for a server. `origins` of `["*"]` allows any origin.type Cfg is Data:  Cfg{    origins: List<&2, String>    methods: List<&2, String>    headers: List<&2, String>    credentials: Bool    max_age: String    expose: List<&2, String>  }# whether `*` is one of the allowed originsdef cors.star.step(hit: Bool, rest: Unit -> Bool) -> Bool:  match hit:    case True{}:      True{}    case False{}:      rest(Unit{})# walk origins for `*`def cors.star(os: List<&2, String>) -> Bool:  match os:    case []:      False{}    case h <> t:      cors.star.step(String.eq(h, "*"), _u => cors.star(t))# case-sensitive membershipdef cors.hit.step(hit: Bool, rest: Unit -> Bool) -> Bool:  match hit:    case True{}:      True{}    case False{}:      rest(Unit{})# whether the needle is in the listdef cors.hit(+needle: String, xs: List<&2, String>) -> Bool:  match xs:    case []:      False{}    case h <> t:      cors.hit.step(String.eq(h, needle), _u => cors.hit(needle, t))# lowercase a list of namesdef cors.lower(xs: List<&2, String>) -> List<&2, String>:  match xs:    case []:      []    case h <> t:      String.to_lower(h) <> cors.lower(t)# case-insensitive membership (header names, RFC 9110 §5.1)def cors.hit_ci(+needle: String, xs: List<&2, String>) -> Bool:  cors.hit(String.to_lower(needle), cors.lower(xs))# `*`, or an explicit origindef cors.allowed(+os: List<&2, String>, origin: String) -> Bool:  Bool.or(cors.star(os), cors.hit(origin, os))# `*` when every origin is allowed and credentials are off; else the origindef cors.acao.star(origin: String, star: Bool) -> String:  match star:    case True{}:      "*"    case False{}:      origin# credentials always reflect the origin (Fetch: `*` cannot be used)def cors.acao.cred(+os: List<&2, String>, origin: String, credentials: Bool)  -> String:  match credentials:    case True{}:      origin    case False{}:      cors.acao.star(origin, cors.star(os))# empty when the origin is not alloweddef cors.acao.ok(+os: List<&2, String>, credentials: Bool, origin: String,  ok: Bool) -> String:  match ok:    case False{}:      ""    case True{}:      cors.acao.cred(os, origin, credentials)# Access-Control-Allow-Origin value, or "" when the origin is refuseddef cors.acao(+os: List<&2, String>, credentials: Bool, +origin: String) -> String:  cors.acao.ok(os, credentials, origin, cors.allowed(os, origin))# true when ACAO is a specific origin and Vary: Origin should be setdef cors.vary(+acao: String) -> Bool:  Bool.and(Bool.not(String.eq(acao, "*")), Bool.not(String.eq(acao, "")))# every requested header name is allowed. Empty means none were requested.def cors.names.step(ok: Bool, rest: Unit -> Bool) -> Bool:  match ok:    case True{}:      rest(Unit{})    case False{}:      False{}# requested header names against the allow listdef cors.names(ps: List<&2, String>, +allow: List<&2, String>) -> Bool:  match ps:    case []:      True{}    case h <> t:      cors.names.step(cors.hit_ci(String.trim(h), allow), _u => cors.names(t, allow))# an empty request-header list asks for nothingdef cors.headers_empty(+requested: String, +allow: List<&2, String>, empty: Bool)  -> Bool:  match empty:    case True{}:      True{}    case False{}:      cors.names(String.split(requested, ','), allow)# Access-Control-Request-Headers, comma-separateddef cors.headers_ok(+requested: String, +allow: List<&2, String>) -> Bool:  cors.headers_empty(requested, allow, String.eq(requested, ""))# join with ", "def cors.join(xs: List<&2, String>) -> String:  String.join(xs, ", ")# append Vary when the origin is reflecteddef cors.vary_on(hs: List<&2, Http.Header>, _acao: String, vary: Bool)  -> List<&2, Http.Header>:  match vary:    case False{}:      hs    case True{}:      Http.H{"Vary", "Origin"} <> hs# append credentials when they are alloweddef cors.cred_on(hs: List<&2, Http.Header>, credentials: Bool)  -> List<&2, Http.Header>:  match credentials:    case False{}:      hs    case True{}:      Http.H{"Access-Control-Allow-Credentials", "true"} <> hs# preflight response headersdef cors.preflight_hs(+acao: String, methods: List<&2, String>,  headers: List<&2, String>, credentials: Bool, max_age: String)  -> List<&2, Http.Header>:  cors.cred_on(cors.vary_on([    Http.H{"Access-Control-Allow-Origin", acao},    Http.H{"Access-Control-Allow-Methods", cors.join(methods)},    Http.H{"Access-Control-Allow-Headers", cors.join(headers)},    Http.H{"Access-Control-Max-Age", max_age}  ], acao, cors.vary(acao)), credentials)# 400 when a requested header is not allowed; otherwise 200def cors.preflight.headers(acao: String, methods: List<&2, String>,  headers: List<&2, String>, credentials: Bool, max_age: String,  _req: String, ok: Bool) -> Http.Reply:  match ok:    case False{}:      Http.Reply{400, [], ""}    case True{}:      Http.Reply{200, cors.preflight_hs(acao, methods, headers, credentials,        max_age), ""}# 400 when the method is not alloweddef cors.preflight.method(acao: String, methods: List<&2, String>,  +headers: List<&2, String>, credentials: Bool, max_age: String,  +req_headers: String, _method: String, ok: Bool) -> Http.Reply:  match ok:    case False{}:      Http.Reply{400, [], ""}    case True{}:      cors.preflight.headers(acao, methods, headers, credentials, max_age,        req_headers, cors.headers_ok(req_headers, headers))# skip the method check when the origin was refuseddef cors.preflight.denied(acao: String, +methods: List<&2, String>,  headers: List<&2, String>, credentials: Bool, max_age: String,  req_headers: String, +method: String, denied: Bool) -> Http.Reply:  match denied:    case True{}:      Http.Reply{400, [], ""}    case False{}:      cors.preflight.method(acao, methods, headers, credentials, max_age,        req_headers, method, cors.hit(method, methods))# OPTIONS preflight (Fetch CORS). Refused origins and methods are 400.def cors.preflight(cfg: Cfg, origin: String, method: String, req_headers: String)  -> Http.Reply:  match cfg:    case Cfg{+origins, methods, headers, +credentials, max_age, expose}:      +acao = cors.acao(origins, credentials, origin)      cors.preflight.denied(acao, methods, headers, credentials, max_age,        req_headers, method, String.eq(acao, ""))# Access-Control-Expose-Headers when the list is not emptydef cors.expose_of(hs: List<&2, Http.Header>, expose: List<&2, String>,  empty: Bool) -> List<&2, Http.Header>:  match empty:    case True{}:      hs    case False{}:      Http.H{"Access-Control-Expose-Headers", cors.join(expose)} <> hs# Access-Control-Expose-Headers when the list is not emptydef cors.expose_on(hs: List<&2, Http.Header>, +expose: List<&2, String>)  -> List<&2, Http.Header>:  cors.expose_of(hs, expose, List.is_empty(&2, String, expose))# simple-response CORS headers (ACAO, optional credentials, expose, vary)def cors.simple_hs(+acao: String, credentials: Bool, expose: List<&2, String>)  -> List<&2, Http.Header>:  cors.cred_on(cors.vary_on(cors.expose_on([    Http.H{"Access-Control-Allow-Origin", acao}  ], expose), acao, cors.vary(acao)), credentials)# leave a reply alone when there is nothing to adddef cors.simple.skip(add: Bool, reply: Http.Reply, hs: List<&2, Http.Header>)  -> Http.Reply:  match add reply:    case False{} r:      r    case True{} Http.Torn{why}:      Http.Torn{why}    case True{} Http.Reply{status, headers, body}:      Http.Reply{status, List.append(&2, Http.Header, hs, headers), body}# append CORS headers when the origin is alloweddef cors.simple.add(reply: Http.Reply, acao: String, credentials: Bool,  expose: List<&2, String>, add: Bool) -> Http.Reply:  cors.simple.skip(add, reply, cors.simple_hs(acao, credentials, expose))# a non-preflight response. A missing or refused origin is unchanged.def cors.simple(cfg: Cfg, origin: String, reply: Http.Reply) -> Http.Reply:  match cfg:    case Cfg{+origins, methods, headers, +credentials, max_age, expose}:      +acao = cors.acao(origins, credentials, origin)      cors.simple.add(reply, acao, credentials, expose,        Bool.not(String.eq(acao, "")))# Origin request headerdef cors.origin(origin: String) -> Http.Header:  Http.H{"Origin", origin}# Access-Control-Request-Methoddef cors.request_method(method: String) -> Http.Header:  Http.H{"Access-Control-Request-Method", method}# Access-Control-Request-Headersdef cors.request_headers(names: String) -> Http.Header:  Http.H{"Access-Control-Request-Headers", names}# OPTIONS with Access-Control-Request-Method is a preflightdef cors.is_preflight.method(method: String, asked: String) -> Bool:  Bool.and(String.eq(method, "OPTIONS"), Bool.not(String.eq(asked, "")))# whether this request is a CORS preflightdef cors.is_preflight(req: Http.Request) -> Bool:  match req:    case Http.Bad{why}:      False{}    case Http.Request{method, target, headers, body}:      cors.is_preflight.method(method,        Http.headers.find(headers, "access-control-request-method"))# preflight from the request's CORS headersdef cors.preflight_req(cfg: Cfg, req: Http.Request) -> Http.Reply:  match req:    case Http.Bad{why}:      Http.Reply{400, [], why}    case Http.Request{method, target, +headers, body}:      cors.preflight(cfg, Http.headers.find(headers, "origin"),        Http.headers.find(headers, "access-control-request-method"),        Http.headers.find(headers, "access-control-request-headers"))# simple CORS from the request Origindef cors.simple_req(cfg: Cfg, req: Http.Request, reply: Http.Reply) -> Http.Reply:  match req:    case Http.Bad{why}:      reply    case Http.Request{method, target, headers, body}:      cors.simple(cfg, Http.headers.find(headers, "origin"), reply)# preflight, or CORS headers on the handler replydef cors.route(cfg: Cfg, req: Http.Request, +reply: Http.Reply, pre: Bool)  -> Http.Reply:  match pre:    case True{}:      cors.preflight_req(cfg, req)    case False{}:      cors.simple_req(cfg, req, reply)# answer a request: preflight or simple CORS on top of `reply`def cors.on(cfg: Cfg, +req: Http.Request, reply: Http.Reply) -> Http.Reply:  cors.route(cfg, req, reply, cors.is_preflight(req))