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. 5 import Base 6 import ./src/url.bend as Url 7 import ./src/http.bend as Http 8 import ./src/body.bend as Body 9 import ./src/client.bend as Client 10 import ./src/server.bend as Server 11 import ./src/cookie.bend as Cookie 12 import ./src/cache.bend as Cache 13 import ./src/cors.bend as Cors 14 15 # URL parse re-export 16 def parse_url(url: String) -> Url.Loc: 17 Url.parse(url) 18 19 # response parse re-export 20 def parse_response(raw: String) -> Http.Reply: 21 Http.parse(raw) 22 23 # request parse re-export (server inbound) 24 def parse_request(raw: String) -> Http.Request: 25 Http.ask(raw) 26 27 # format a response message (server outbound) 28 def format_response(status: U32, headers: List<&2, Http.Header>, body: String) 29 -> String: 30 Http.respond(status, headers, body) 31 32 # empty request body 33 def empty_body() -> Body.Body: 34 Body.body.empty() 35 36 # UTF-8 text request body 37 def text_body(s: String) -> Body.Body: 38 Body.body.text(s) 39 40 # octet-list request body 41 def octets_body(bs: List<&2, U32>) -> Body.Body: 42 Body.body.octets(bs) 43 44 # BYO JSON (or any codec): encode a value with the caller's to_text 45 def encode(~A: Data, ~to_text: A -> String, value: A) -> Body.Body: 46 Body.body.encode(A, to_text, value) 47 48 # BYO JSON (or any codec): decode body text with the caller's from_text 49 def decode( 50 ~A: Data, 51 ~from_text: String -> Maybe<&2, A>, 52 s: String 53 ) -> Maybe<&2, A>: 54 Body.body.decode(A, from_text, s) 55 56 # Bearer token header helper 57 def bearer(token: String) -> Http.Header: 58 Client.client.bearer(token) 59 60 # Basic credentials header (RFC 7617) 61 def basic(user: String, password: String) -> Http.Header: 62 Client.client.basic(user, password) 63 64 # Set-Cookie header from a parsed cookie (RFC 6265) 65 def set_cookie(c: Cookie.Cookie) -> Http.Header: 66 Cookie.cookie.set(c) 67 68 # parse one Set-Cookie field value 69 def parse_cookie(line: String) -> Maybe<&2, Cookie.Cookie>: 70 Cookie.cookie.parse(line) 71 72 # Cookie request header for a list of cookies 73 def cookie_header(cs: List<&2, Cookie.Cookie>) -> Http.Header: 74 Cookie.cookie.request(cs) 75 76 # parse a Cache-Control field value (RFC 9111) 77 def cache_control(value: String) -> Cache.Cc: 78 Cache.cache.parse(value) 79 80 # freshness of a Cache-Control value at an age in seconds 81 def fresh(value: String, age: Nat) -> Bool: 82 Cache.cache.fresh(Cache.cache.parse(value), age) 83 84 # CORS preflight or simple headers on a handler reply 85 def cors_reply(cfg: Cors.Cfg, req: Http.Request, reply: Http.Reply) -> Http.Reply: 86 Cors.cors.on(cfg, req, reply) 87 88 # GET with no extra headers 89 def http.get(url: String) -> IO(Client.Response): 90 Client.client.get(url, []) 91 92 # GET with headers 93 def http.get_with(url: String, headers: List<&2, Http.Header>) 94 -> IO(Client.Response): 95 Client.client.get(url, headers) 96 97 # HEAD with no extra headers 98 def http.head(url: String) -> IO(Client.Response): 99 Client.client.head(url, []) 100 101 # HEAD with headers 102 def http.head_with(url: String, headers: List<&2, Http.Header>) 103 -> IO(Client.Response): 104 Client.client.head(url, headers) 105 106 # POST with a body and no extra headers 107 def http.post(url: String, body: Body.Body) -> IO(Client.Response): 108 Client.client.post(url, [], body) 109 110 # POST with headers and a body 111 def http.post_with( 112 url: String, 113 headers: List<&2, Http.Header>, 114 body: Body.Body 115 ) -> IO(Client.Response): 116 Client.client.post(url, headers, body) 117 118 # PUT with a body and no extra headers 119 def http.put(url: String, body: Body.Body) -> IO(Client.Response): 120 Client.client.put(url, [], body) 121 122 # PUT with headers and a body 123 def http.put_with(url: String, headers: List<&2, Http.Header>, body: Body.Body) 124 -> IO(Client.Response): 125 Client.client.put(url, headers, body) 126 127 # DELETE with a body and no extra headers 128 def http.delete(url: String, body: Body.Body) -> IO(Client.Response): 129 Client.client.delete(url, [], body) 130 131 # DELETE with headers and a body 132 def http.delete_with( 133 url: String, 134 headers: List<&2, Http.Header>, 135 body: Body.Body 136 ) -> IO(Client.Response): 137 Client.client.delete(url, headers, body) 138 139 # OPTIONS with no extra headers 140 def http.options(url: String) -> IO(Client.Response): 141 Client.client.options(url, []) 142 143 # OPTIONS with headers 144 def http.options_with(url: String, headers: List<&2, Http.Header>) 145 -> IO(Client.Response): 146 Client.client.options(url, headers) 147 148 # listen on host:port and serve up to `limit` connections 149 def http.serve( 150 ~handle: Http.Request -> Http.Reply, 151 host: String, 152 port: U32, 153 limit: U32 154 ) -> IO(Unit): 155 Server.server.serve(handle, host, port, limit) 156 157 # listen on host:port and serve up to `limit` connections with a handler that 158 # does IO; a failed connection is logged and the next one is served 159 def http.serve_io( 160 ~handle: Http.Request -> IO(Http.Reply), 161 host: String, 162 port: U32, 163 limit: U32 164 ) -> IO(Unit): 165 Server.server.serve_io(handle, host, port, limit) 166 167 # listen on host:port and serve with an IO handler until the process stops 168 @unsafe def http.serve_forever( 169 ~handle: Http.Request -> IO(Http.Reply), 170 host: String, 171 port: U32 172 ) -> IO(Unit): 173 Server.server.serve_forever(handle, host, port) 174 175 # listen on host:port, serve one connection, then stop 176 def http.serve_once( 177 ~handle: Http.Request -> Http.Reply, 178 host: String, 179 port: U32 180 ) -> IO(Unit): 181 Server.server.once(handle, host, port)