Read forms, cookies and forwarded clients
A handler for std.http.server gets the raw request.
Three modules read the common parts of it safely:
std.http.body reads forms, JSON and
uploads within limits, std.http.cookie
reads and writes cookies, and
std.http.proxy finds the client
behind a reverse proxy.
import std.bytes
import std.http
import std.http.body
import std.http.cookie
import std.http.proxy
import std.http.server
import std.net
import std.time
from std.http.body import FormLimits
from std.http.proxy import Forwarding, Proxies
from std.http.server import Exchange
from std.net import TcpStream
from std.sync import CancellationToken
pub enum Route:
Login
Me
pub class Site:
pub forms: FormLimits
pub proxies: Proxies
pub def handle(self, route: &Route, exchange: &mut Exchange[TcpStream]) -> Result[(), AllocError | std.http.HttpError | std.bytes.BytesError | std.http.cookie.CookieError | std.http.proxy.ProxyError | std.net.NetError]:
match route:
case Route.Login:
match std.http.body.read_form(exchange, &self.forms):
case Ok(fields):
match std.http.body.find(&fields, "name"):
case Some(index):
name = &fields[index].value
mut head = std.http.response(200, "OK")?
std.http.cookie.add(&mut head, &std.http.cookie.set_cookie("user", name)?)?
head.add("Content-Type", "text/plain")?
mut text = f"welcome {name}"?
match std.http.body.find(&fields, "city"):
case Some(city):
text = f"{text} from {fields[city].value}"?
case Nothing:
pass
exchange.respond(head, &std.bytes.from_text(&text)?)?
case Nothing:
exchange.send_text(400, "name is required")?
case Err(error):
std.http.body.reject(exchange, &error)?
case Route.Me:
cookies = std.http.cookie.cookies(&exchange.request.headers, 32)?
client = std.http.proxy.client_of(exchange, &self.proxies)?
address = std.net.format_ip(client.address)?
mut who = "nobody"
match std.http.cookie.find(&cookies, "user"):
case Some(index):
who = cookies[index].value
case Nothing:
pass
exchange.send_text(200, &f"{address} is {who}"?)?
Ok(())
def fetch(port: u16, request: &str) -> Result[str, AllocError | IoError | std.bytes.BytesError | std.net.NetError | RangeError]:
never = CancellationToken()?
deadline = std.time.deadline_after(&std.time.monotonic_now(), &std.time.seconds(5u32))?
mut stream = std.net.connect(&std.net.loopback_v4(port), &deadline, &never)?
data = std.bytes.from_text(request)?
stream.write_all_before(&data, &deadline, &never)?
mut reply = std.bytes.empty()?
mut buffer = std.bytes.empty()?
buffer.resize(512, 0u8)?
while True:
count = stream.read_before(&mut buffer, 0, 512, &deadline, &never)?
if count == 0:
break
reply.extend(&buffer[:count])?
text = reply.to_text()?
lines = text.split("\r\n")?
mut parts = [lines[0]]?
for line in lines:
if line.startswith("Set-Cookie"):
parts.append(line)?
parts.append(text.split("\r\n\r\n")?[1].strip()?)?
Ok(" | ".join(&parts)?)
def client(port: u16, stop: CancellationToken) -> Result[(), AllocError | IoError | RangeError | std.bytes.BytesError | std.net.NetError]:
with stop.cancel_on_exit():
form = "POST /login HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 28\r\nConnection: close\r\n\r\nname=Ada&city=S%C3%A3o+Paulo"
print(fetch(port, form)?)?
print(fetch(port, "POST /login HTTP/1.1\r\nHost: localhost\r\nContent-Type: text/plain\r\nContent-Length: 3\r\nConnection: close\r\n\r\nAda")?)?
print(fetch(port, "GET /me HTTP/1.1\r\nHost: localhost\r\nCookie: theme=dark; user=Ada\r\nX-Forwarded-For: 198.51.100.4, 203.0.113.7\r\nConnection: close\r\n\r\n")?)?
Ok(())
type ProxySetupError = std.http.server.ServerError | std.http.proxy.ProxyError | std.net.NetError
def main() -> Result[(), SpawnError[()] | AllocError | ProxySetupError | IoError | RangeError | std.bytes.BytesError | std.net.NetError]:
mut routes = std.http.server.router[Route]()?
routes.add("POST", "/login", Route.Login)?
routes.add("GET", "/me", Route.Me)?
mut proxies = std.http.proxy.proxies(Forwarding.XForwardedFor)?
proxies.trust("127.0.0.1")?
site = Site(std.http.body.form_limits(), proxies)
listener = std.net.listen(&std.net.loopback_v4(0u16), 16)?
port = listener.local_address()?.port
stop = CancellationToken()?
with spawn(client, port, stop.share()).drop_job()? as task:
std.http.server.serve(listener, &routes, &site, &std.http.server.config(), &stop)?
task.join()?
Ok(())
HTTP/1.1 200 OK | Set-Cookie: user=Ada; Path=/; Secure; HttpOnly; SameSite=Lax | welcome Ada from São Paulo
HTTP/1.1 415 Unsupported Media Type | Unsupported Media Type
HTTP/1.1 200 OK | 203.0.113.7 is Ada
read_form checks that the request says it carries a form, refuses a body
over forms.max_bytes (64 KiB by default) or with more than
forms.max_fields fields, and decodes each name=value pair: + is a
space and %C3%A3 is the UTF-8 encoding of ã. It returns the pairs in order; find gives the
index of the first with a name. When it fails, reject answers with the
status that fits: 415 for the wrong type, 413 for too much, 400 for a
malformed body.
set_cookie checks the name and value and gives the cookie safe defaults:
Secure, so the browser sends it only over HTTPS, HttpOnly, so page
scripts cannot read it, and SameSite=Lax, so other sites cannot send it
with a form post. No value can add an attribute or another field: a name
with a space, such as Ada L, is an error rather than a broken header, so
encode free text before storing it in a cookie.
cookies reads every Cookie field. It skips pairs that are not valid
cookies instead of failing, because the field carries the cookies of every
application on the host.
The server sees its peer, 127.0.0.1. When a reverse proxy forwards
requests, the peer is the proxy and the client’s address is in
X-Forwarded-For, which anyone can write. client_of believes the header
only because the program trusts 127.0.0.1, and reads it from the right:
203.0.113.7 is the first address that is not a trusted proxy, so it is the
client. 198.51.100.4 came from the client itself and is ignored. With no
trust call, the header is ignored and the client is the peer.