main.bend

emerging-ezhttp@0.16.0.0 · 287 lines · 0 definitions

1# ezhttp: HTTP/1.1 client and server for Bend 2, with auth, cookie and CORS helpers.
2#
3# Shared message types, BYO JSON via encode/decode. TLS for the client stays
4# at the wire/runtime layer.
5import Base
6import emerging-ezhttp@0.16.0.0/src/url.bend as Url
7import emerging-ezhttp@0.16.0.0/src/pct.bend as Pct
8import emerging-ezhttp@0.16.0.0/src/http.bend as Http
9import emerging-ezhttp@0.16.0.0/src/body.bend as Body
10import emerging-ezhttp@0.16.0.0/src/form.bend as Form
11import emerging-ezhttp@0.16.0.0/src/client.bend as Client
12import emerging-ezhttp@0.16.0.0/src/server.bend as Server
13import emerging-ezhttp@0.16.0.0/src/cookie.bend as Cookie
14import emerging-ezhttp@0.16.0.0/src/cache.bend as Cache
15import emerging-ezhttp@0.16.0.0/src/cors.bend as Cors
16import emerging-ezhttp@0.16.0.0/src/html.bend as Html
17
18# URL parse re-export
19def parse_url(url: String) -> Url.Loc:
20  Url.parse(url)
21
22# percent-encode a path (RFC 3986 §2.1), as Python's urllib.parse.quote(text):
23# unreserved characters and "/" stay, every other character becomes %XX per
24# UTF-8 byte, upper-case hex (SPEC.md HTTP-URL)
25def quote_path(text: String) -> String:
26  Pct.quote(True{}, text)
27
28# percent-encode a path segment, query key or value (RFC 3986 §2.1), as
29# urllib.parse.quote(text, safe=""): like quote_path, but "/" becomes %2F
30def quote_component(text: String) -> String:
31  Pct.quote(False{}, text)
32
33# response parse re-export
34def parse_response(raw: String) -> Http.Reply:
35  Http.parse(raw)
36
37# a response as the client reads it for a request with this method: interim
38# 1xx heads skipped, and no body for HEAD or a 1xx, 204 or 304 status,
39# whatever Content-Length says (SPEC.md HTTP-CLI-9)
40def parse_response_to(method: String, raw: String) -> Http.Reply:
41  Http.parse.to(method, raw)
42
43# the reason phrase of a response's status line (RFC 9112 §4), read from the
44# head parse_response_to stops at; empty when there is none (SPEC.md
45# HTTP-CLI-11)
46def reason_phrase(raw: String) -> String:
47  Http.phrase.to(raw)
48
49# request parse re-export (server inbound)
50def parse_request(raw: String) -> Http.Request:
51  Http.ask(raw)
52
53# format a response message (server outbound)
54def format_response(status: U32, headers: List<&2, Http.Header>, body: String)
55  -> String:
56  Http.respond(status, headers, body)
57
58# empty request body
59def empty_body() -> Body.Body:
60  Body.body.empty()
61
62# UTF-8 text request body
63def text_body(text: String) -> Body.Body:
64  Body.body.text(text)
65
66# octet-list request body
67def octets_body(bs: List<&2, U32>) -> Body.Body:
68  Body.body.octets(bs)
69
70# a multipart/form-data text field (RFC 7578)
71def form_field(name: String, value: String) -> Form.Part:
72  Form.form.field(name, value)
73
74# a multipart/form-data file part: field name, filename, content type and the
75# file's bytes (each 0..255, as File.read_bytes gives them)
76def form_file(name: String, filename: String, ctype: String,
77  bytes: List<&2, U32>) -> Form.Part:
78  Form.form.file(name, filename, ctype, bytes)
79
80# a multipart/form-data body (RFC 7578) with this boundary: the Content-Type
81# field value to send and an octet body, or why the boundary cannot be used
82# (SPEC.md HTTP-FORM)
83def form_body(boundary: String, parts: List<&2, Form.Part>) -> Form.Form:
84  Form.form.body(boundary, parts)
85
86# BYO JSON (or any codec): encode a value with the caller's to_text
87def encode(~A: Data, ~to_text: A -> String, value: A) -> Body.Body:
88  Body.body.encode(A, to_text, value)
89
90# BYO JSON (or any codec): decode body text with the caller's from_text
91def decode(
92  ~A: Data,
93  ~from_text: String -> Maybe<&2, A>,
94  text: String
95) -> Maybe<&2, A>:
96  Body.body.decode(A, from_text, text)
97
98# Bearer token header helper
99def bearer(token: String) -> Http.Header:
100  Client.client.bearer(token)
101
102# Basic credentials header (RFC 7617)
103def basic(user: String, password: String) -> Http.Header:
104  Client.client.basic(user, password)
105
106# Set-Cookie header from a parsed cookie (RFC 6265)
107def set_cookie(cookie: Cookie.Cookie) -> Http.Header:
108  Cookie.cookie.set(cookie)
109
110# parse one Set-Cookie field value
111def parse_cookie(line: String) -> Maybe<&2, Cookie.Cookie>:
112  Cookie.cookie.parse(line)
113
114# Cookie request header for a list of cookies
115def cookie_header(cs: List<&2, Cookie.Cookie>) -> Http.Header:
116  Cookie.cookie.request(cs)
117
118# parse a Cache-Control field value (RFC 9111)
119def cache_control(value: String) -> Cache.Cc:
120  Cache.cache.parse(value)
121
122# freshness of a Cache-Control value at an age in seconds
123def fresh(value: String, age: Nat) -> Bool:
124  Cache.cache.fresh(Cache.cache.parse(value), age)
125
126# CORS preflight or simple headers on a handler reply
127def cors_reply(cfg: Cors.Cfg, req: Http.Request, reply: Http.Reply) -> Http.Reply:
128  Cors.cors.on(cfg, req, reply)
129
130# escape text for HTML as markupsafe and Jinja's autoescape do: & < > " ' as
131# &amp; &lt; &gt; &#34; &#39;, every other character as itself
132def html_escape(text: String) -> String:
133  Html.escape(text)
134
135# GET with no extra headers
136def http.get(url: String) -> IO(Client.Response):
137  Client.client.get(url, [])
138
139# GET with headers
140def http.get_with(url: String, headers: List<&2, Http.Header>)
141  -> IO(Client.Response):
142  Client.client.get(url, headers)
143
144# GET with headers and a timeout in milliseconds that bounds connecting,
145# sending and receiving; with `follow`, redirects are followed (GET only, at
146# most 10 hops, see src/redirect.bend)
147def http.get_opts(
148  url: String,
149  headers: List<&2, Http.Header>,
150  timeout_ms: U32,
151  follow: Bool
152) -> IO(Client.Response):
153  Client.client.get.opts(url, headers, timeout_ms, follow)
154
155# a request with any method, headers, a body and a timeout in milliseconds
156# that bounds connecting, sending and receiving; with `follow`, a GET follows
157# redirects (at most 10 hops, see src/redirect.bend) and every other method
158# answers a redirect as its response
159def http.request_opts(
160  method: String,
161  url: String,
162  headers: List<&2, Http.Header>,
163  body: Body.Body,
164  timeout_ms: U32,
165  follow: Bool
166) -> IO(Client.Response):
167  Client.client.request.opts(method, url, headers, body, timeout_ms, follow)
168
169# POST with headers, a body and a timeout in milliseconds; redirects are not
170# followed
171def http.post_opts(
172  url: String,
173  headers: List<&2, Http.Header>,
174  body: Body.Body,
175  timeout_ms: U32
176) -> IO(Client.Response):
177  http.request_opts("POST", url, headers, body, timeout_ms, False{})
178
179# PUT with headers, a body and a timeout in milliseconds; redirects are not
180# followed
181def http.put_opts(
182  url: String,
183  headers: List<&2, Http.Header>,
184  body: Body.Body,
185  timeout_ms: U32
186) -> IO(Client.Response):
187  http.request_opts("PUT", url, headers, body, timeout_ms, False{})
188
189# DELETE with headers, a body and a timeout in milliseconds; redirects are
190# not followed
191def http.delete_opts(
192  url: String,
193  headers: List<&2, Http.Header>,
194  body: Body.Body,
195  timeout_ms: U32
196) -> IO(Client.Response):
197  http.request_opts("DELETE", url, headers, body, timeout_ms, False{})
198
199# whether a response is an exchange that ran out of time
200def http.timed_out(r: Client.Response) -> Bool:
201  Client.client.timed_out(r)
202
203# HEAD with no extra headers
204def http.head(url: String) -> IO(Client.Response):
205  Client.client.head(url, [])
206
207# HEAD with headers
208def http.head_with(url: String, headers: List<&2, Http.Header>)
209  -> IO(Client.Response):
210  Client.client.head(url, headers)
211
212# POST with a body and no extra headers
213def http.post(url: String, body: Body.Body) -> IO(Client.Response):
214  Client.client.post(url, [], body)
215
216# POST with headers and a body
217def http.post_with(
218  url: String,
219  headers: List<&2, Http.Header>,
220  body: Body.Body
221) -> IO(Client.Response):
222  Client.client.post(url, headers, body)
223
224# PUT with a body and no extra headers
225def http.put(url: String, body: Body.Body) -> IO(Client.Response):
226  Client.client.put(url, [], body)
227
228# PUT with headers and a body
229def http.put_with(url: String, headers: List<&2, Http.Header>, body: Body.Body)
230  -> IO(Client.Response):
231  Client.client.put(url, headers, body)
232
233# DELETE with a body and no extra headers
234def http.delete(url: String, body: Body.Body) -> IO(Client.Response):
235  Client.client.delete(url, [], body)
236
237# DELETE with headers and a body
238def http.delete_with(
239  url: String,
240  headers: List<&2, Http.Header>,
241  body: Body.Body
242) -> IO(Client.Response):
243  Client.client.delete(url, headers, body)
244
245# OPTIONS with no extra headers
246def http.options(url: String) -> IO(Client.Response):
247  Client.client.options(url, [])
248
249# OPTIONS with headers
250def http.options_with(url: String, headers: List<&2, Http.Header>)
251  -> IO(Client.Response):
252  Client.client.options(url, headers)
253
254# listen on host:port and serve up to `limit` connections
255def http.serve(
256  ~handle: Http.Request -> Http.Reply,
257  host: String,
258  port: U32,
259  limit: U32
260) -> IO(Unit):
261  Server.server.serve(handle, host, port, limit)
262
263# listen on host:port and serve up to `limit` connections with a handler that
264# does IO; a failed connection is logged and the next one is served
265def http.serve_io(
266  ~handle: Http.Request -> IO(Http.Reply),
267  host: String,
268  port: U32,
269  limit: U32
270) -> IO(Unit):
271  Server.server.serve_io(handle, host, port, limit)
272
273# listen on host:port and serve with an IO handler until the process stops
274@unsafe def http.serve_forever(
275  ~handle: Http.Request -> IO(Http.Reply),
276  host: String,
277  port: U32
278) -> IO(Unit):
279  Server.server.serve_forever(handle, host, port)
280
281# listen on host:port, serve one connection, then stop
282def http.serve_once(
283  ~handle: Http.Request -> Http.Reply,
284  host: String,
285  port: U32
286) -> IO(Unit):
287  Server.server.once(handle, host, port)