net/http/middleware
The usual security, cache and CORS headers, and request logging, in one call.
import net.http.middleware
Five small helpers for net/http handlers: AllowAll for CORS, Secure for basic security headers, NoCache and Cache for caching, and Log for a line per request. Each takes the context id, as the handler does, and only sets response headers (or prints), so you call them on the first lines of a handler.
There is no middleware chain in Salam and no way to wrap a handler from outside. A middleware is an ordinary function you call with the context id, and the headers it sets go out with whatever the handler writes after it. That keeps the order visible in the handler itself. For logins use net/http/session, and for checking input net/http/validate.
What to know before you rely on them:
AllowAlldoes not listPATCH. It allows GET, POST, PUT, DELETE and OPTIONS with theContent-TypeandAuthorizationheaders, for every origin. A browser refuses a PATCH from another origin, and*cannot be used with cookies; write your own rule for those (last section).Secureis a minimum. It setsnosniff,X-Frame-Options: DENYandX-XSS-Protection, a header current browsers ignore. It sets noContent-Security-Policyand noStrict-Transport-Security.- The last call wins. A header set twice with the same name keeps the last value, so
NoCachefollowed byCacheleaves onlymax-age. Logprints withprintlnbefore the handler runs, so it shows no status;http.EnableAccessLogon the server logs the status too.
CORS for an API used from a browser
A page on another origin can only call your API when the response allows it. For anything other than a simple GET the browser first sends an OPTIONS request, the preflight, and checks the Access-Control-Allow-* headers on the answer. Register an OPTIONS route that calls AllowAll and answers 204 with no body, and call AllowAll in the real handler too.
import net.http
import net.http.middleware
import net.router
func preflight(ctx: i64):
middleware.AllowAll(ctx)
http.SetStatus(ctx, 204)
http.Text(ctx, "")
end
func notes(ctx: i64):
middleware.AllowAll(ctx)
middleware.NoCache(ctx)
http.Json(ctx, `[{"id":1,"text":"buy bread"}]`)
end
func main:
mut r := router.NewRouter()
router.Options(r, "/api/notes", preflight)
router.Get(r, "/api/notes", notes)
mut s := http.NewServer(19765)
http.Use(s, r)
_ := http.RunBackground(s)
url := "http://127.0.0.1:19765/api/notes"
mut asked := http.NewHeaders()
asked.insert("Origin", "https://app.example.com")
asked.insert("Access-Control-Request-Method", "GET")
p := http.OptionsWith(url, asked)
println "preflight:", http.Status(p)
each name in ["access-control-allow-origin", "access-control-allow-methods", "access-control-allow-headers"]:
println " " + name + ": " + http.GetHeader(p, name)
end
g := http.GetWith(url, asked)
println "get:", http.Status(g), http.Body(g)
println " cache-control: " + http.GetHeader(g, "cache-control")
println " pragma: " + http.GetHeader(g, "pragma")
endAllowAll allows every origin, the methods GET, POST, PUT, DELETE and OPTIONS (not PATCH) and the Content-Type and Authorization headers. That suits a public, read only API. An API that uses cookies or other headers needs its own rule, as the last section shows.
One helper per kind of response
Instead of repeating the same calls at the top of every handler, group them in a function for each kind of response: private pages that must not be cached and must not appear in a frame, and public files that browsers may keep. Secure adds nosniff, X-Frame-Options: DENY and the XSS header; NoCache forbids caching; Cache allows it for the given number of seconds.
import net.http
import net.http.middleware
import net.router
func page(ctx: i64):
middleware.Secure(ctx)
middleware.NoCache(ctx)
end
func asset(ctx: i64, seconds: int):
middleware.Cache(ctx, seconds)
end
func account(ctx: i64):
page(ctx)
http.Html(ctx, "<h1>Your account</h1>")
end
func logo(ctx: i64):
asset(ctx, 86400)
http.Text(ctx, "logo bytes")
end
func news(ctx: i64):
asset(ctx, 60)
http.Json(ctx, `["release 0.5.5"]`)
end
func main:
mut r := router.NewRouter()
router.Get(r, "/account", account)
router.Get(r, "/logo.svg", logo)
router.Get(r, "/news.json", news)
mut s := http.NewServer(19766)
http.Use(s, r)
_ := http.RunBackground(s)
base := "http://127.0.0.1:19766"
each path in ["/account", "/logo.svg", "/news.json"]:
res := http.Get(base + path)
println path + " cache-control=" + http.GetHeader(res, "cache-control") + " frame=" + http.GetHeader(res, "x-frame-options")
end
endLog prints one line per request with the method and path, which helps while developing. In production, http.EnableAccessLog on the server logs every request with its status, without a call in each handler.
Stricter headers for pages with private data
For an invoice or an account page, build on Secure with the headers it leaves out: a Content-Security-Policy that only lets the page load files from its own origin, Strict-Transport-Security so browsers keep to HTTPS (send it only when the site is served over HTTPS), and a Referrer-Policy. The second route shows the "last call wins" rule: it calls NoCache and then Cache, and the answer carries a single Cache-Control with the later value.
import net.http
import net.http.middleware
import net.router
func strict(ctx: i64):
middleware.Secure(ctx)
http.SetHeader(ctx, "Content-Security-Policy", "default-src 'self'")
http.SetHeader(ctx, "Strict-Transport-Security", "max-age=31536000")
http.SetHeader(ctx, "Referrer-Policy", "same-origin")
end
func invoice(ctx: i64):
strict(ctx)
middleware.NoCache(ctx)
http.Html(ctx, "<h1>Invoice 1042</h1>")
end
func report(ctx: i64):
middleware.NoCache(ctx)
middleware.Cache(ctx, 300)
http.Text(ctx, "monthly report")
end
func main:
mut r := router.NewRouter()
router.Get(r, "/invoice", invoice)
router.Get(r, "/report", report)
mut s := http.NewServer(18641)
http.SetHost(s, "127.0.0.1")
http.Use(s, r)
if not http.RunBackground(s):
println "port 18641 is busy"
ret 1
end
a := http.Get("http://127.0.0.1:18641/invoice")
each name in ["x-frame-options", "content-security-policy", "strict-transport-security", "referrer-policy"]:
println name + ": " + http.GetHeader(a, name)
end
b := http.Get("http://127.0.0.1:18641/report")
println "cache-control lines on /report:", http.HeaderValues(b, "cache-control")
ret 0
endA policy of default-src 'self' also blocks inline <script> and style attributes, so test your pages with it before you turn it on.
Writing your own middleware
When the ready made headers are not right, write a function of the same shape. A function that only adds headers returns nothing. A function that may stop the request returns a bool: it writes the error response itself and returns false, and the handler returns at once. This example allows a single trusted origin and accepts only JSON bodies.
import net.http
import net.http.middleware
import net.router
func cors_for(ctx: i64, origin: str):
if http.Header(ctx, "Origin") == origin:
http.SetHeader(ctx, "Access-Control-Allow-Origin", origin)
http.SetHeader(ctx, "Vary", "Origin")
end
end
func needs_json(ctx: i64): bool:
if http.Header(ctx, "Content-Type").starts_with("application/json"):
ret true
end
http.SetStatus(ctx, 415)
http.Json(ctx, `{"error":"send JSON"}`)
ret false
end
func create(ctx: i64):
middleware.Secure(ctx)
cors_for(ctx, "https://shop.example.com")
if not needs_json(ctx): ret end
http.SetStatus(ctx, 201)
http.Json(ctx, http.Body(ctx))
end
func main:
mut r := router.NewRouter()
router.Post(r, "/orders", create)
mut s := http.NewServer(19767)
http.Use(s, r)
_ := http.RunBackground(s)
url := "http://127.0.0.1:19767/orders"
mut good := http.NewHeaders()
good.insert("Origin", "https://shop.example.com")
good.insert("Content-Type", "application/json")
a := http.PostWith(url, `{"item":"tea"}`, good)
println http.Status(a), http.Body(a), "[" + http.GetHeader(a, "access-control-allow-origin") + "]"
mut other := http.NewHeaders()
other.insert("Origin", "https://evil.example")
other.insert("Content-Type", "text/plain")
b := http.PostWith(url, "item=tea", other)
println http.Status(b), http.Body(b), "[" + http.GetHeader(b, "access-control-allow-origin") + "]"
endSend Vary: Origin whenever the answer depends on the Origin header, so that a shared cache does not hand one origin's answer to another.
Which function for which job
| Job | Function |
|---|---|
| Let pages on any origin call the API | AllowAll |
| Basic protection for HTML pages | Secure |
| Pages with private data | NoCache |
| Images, styles and scripts that rarely change | Cache with a number of seconds |
| See requests while developing | Log |
Functions
Log
func Log(ctx: i64)[HTTP], the method and the path of the request to the standard output; no status, since it runs before the handler answers.import net.http
import net.http.middleware
import net.router
func home(ctx: i64):
middleware.Log(ctx)
middleware.AllowAll(ctx)
middleware.Secure(ctx)
middleware.NoCache(ctx)
http.Text(ctx, "hello")
end
func logo(ctx: i64):
middleware.Cache(ctx, 3600)
http.Text(ctx, "png")
end
func main:
mut r := router.NewRouter()
router.Get(r, "/", home)
router.Get(r, "/logo.png", logo)
mut s := http.NewServer(19624)
http.Use(s, r)
_ := http.RunBackground(s)
a := http.Get("http://127.0.0.1:19624/")
each name in ["access-control-allow-origin", "x-content-type-options", "x-frame-options", "cache-control"]:
println name + ": " + http.GetHeader(a, name)
end
println http.GetHeader(http.Get("http://127.0.0.1:19624/logo.png"), "cache-control")
endAllowAll
func AllowAll(ctx: i64)Access-Control-Allow-Origin: *, the methods GET, POST, PUT, DELETE and OPTIONS, and the headers Content-Type and Authorization. PATCH is not included.The example for this item is shown with the ones above: net-http-middleware-Headers.salam
Secure
func Secure(ctx: i64)X-Content-Type-Options: nosniff, X-Frame-Options: DENY and X-XSS-Protection: 1; mode=block. No CSP or HSTS.The example for this item is shown with the ones above: net-http-middleware-Headers.salam
NoCache
func NoCache(ctx: i64)Cache-Control: no-store, no-cache, must-revalidate and Pragma: no-cache.The example for this item is shown with the ones above: net-http-middleware-Headers.salam
Cache
func Cache(ctx: i64, seconds: int)Cache-Control: max-age= with this many seconds, replacing an earlier Cache-Control.The example for this item is shown with the ones above: net-http-middleware-Headers.salam