Standard library · Network

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:

  • AllowAll does not list PATCH. It allows GET, POST, PUT, DELETE and OPTIONS with the Content-Type and Authorization headers, 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).
  • Secure is a minimum. It sets nosniff, X-Frame-Options: DENY and X-XSS-Protection, a header current browsers ignore. It sets no Content-Security-Policy and no Strict-Transport-Security.
  • The last call wins. A header set twice with the same name keeps the last value, so NoCache followed by Cache leaves only max-age.
  • Log prints with println before the handler runs, so it shows no status; http.EnableAccessLog on 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.

net-http-middleware-cors.salam
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")
end
Outputpreflight: 204 access-control-allow-origin: * access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS access-control-allow-headers: Content-Type, Authorization get: 200 [{"id":1,"text":"buy bread"}] cache-control: no-store, no-cache, must-revalidate pragma: no-cache

AllowAll 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.

net-http-middleware-chain.salam
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
end
Output/account cache-control=no-store, no-cache, must-revalidate frame=DENY /logo.svg cache-control=max-age=86400 frame= /news.json cache-control=max-age=60 frame=

Log 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.

net-http-middleware-strict.salam
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
end
Outputx-frame-options: DENY content-security-policy: default-src 'self' strict-transport-security: max-age=31536000 referrer-policy: same-origin cache-control lines on /report: ["max-age=300"]

A 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.

net-http-middleware-own.salam
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") + "]"
end
Output201 {"item":"tea"} [https://shop.example.com] 415 {"error":"send JSON"} []

Send 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

JobFunction
Let pages on any origin call the APIAllowAll
Basic protection for HTML pagesSecure
Pages with private dataNoCache
Images, styles and scripts that rarely changeCache with a number of seconds
See requests while developingLog

Functions

Log

func Log(ctx: i64)
Prints [HTTP], the method and the path of the request to the standard output; no status, since it runs before the handler answers.
net-http-middleware-Headers.salam
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")
end
Output[HTTP] GET / access-control-allow-origin: * x-content-type-options: nosniff x-frame-options: DENY cache-control: no-store, no-cache, must-revalidate max-age=3600

AllowAll

func AllowAll(ctx: i64)
Opens CORS to every origin: 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)
Sets 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)
Tells the browser and any proxies not to keep the response: 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)
Sets 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