RFC 6265 Cookie / Set-Cookie parsing and building for Standard ML, plus
HMAC-signed (tamper-evident) cookies. Pure, I/O-free, deterministic, and built
test-first against the RFC 6265 / RFC 7231 examples. Dual-compiler:
MLton + Poly/ML.
Cookie.parseCookie— parse a requestCookie:header value ("a=1; b=2") into(name, value)pairs.Cookie.set_cookie— a record with the standardSet-Cookieattributes:Path,Domain,Max-Age,Expires,Secure,HttpOnly,SameSite.Cookie.build/Cookie.parseSetCookie— serialize / parse aSet-Cookievalue, round-tripping all attributes.Max-Ageis bounded to the signed 32-bit range, so an oversized value yieldsmaxAge = NONEidentically on MLton and Poly/ML rather than overflowing the defaultint.Cookie.httpDate— format an IMF-fixdate ("Sun, 06 Nov 1994 08:49:37 GMT", RFC 7231 §7.1.1.1) forExpires, derived from a sml-datetimedateplus a time of day.SignedCookie— cookie values signed with HMAC-SHA256 via sml-crypto; reads verify the signature in constant time and reject tampered or wrong-key tokens.
datatype same_site = Strict | Lax | None
type set_cookie =
{ name : string, value : string
, path : string option, domain : string option
, maxAge : int option, expires : string option
, secure : bool, httpOnly : bool, sameSite : same_site option }
val Cookie.cookie : string -> string -> set_cookie
val Cookie.parseCookie : string -> (string * string) list
val Cookie.build : set_cookie -> string
val Cookie.parseSetCookie : string -> set_cookie option
val Cookie.httpDate : DateTime.date -> int * int * int -> string
val SignedCookie.sign : { key : string, name : string, value : string } -> Cookie.set_cookie
val SignedCookie.read : { key : string, name : string } -> (string * string) list -> string option(* Build a hardened session cookie *)
val sc = { name = "SID", value = "31d4d96e407aad42"
, path = SOME "/", domain = SOME "example.com"
, maxAge = SOME 3600, expires = NONE
, secure = true, httpOnly = true, sameSite = SOME Cookie.Lax }
val header = Cookie.build sc
(* "SID=31d4d96e407aad42; Path=/; Domain=example.com; Max-Age=3600; Secure; HttpOnly; SameSite=Lax" *)
(* Signed cookie round-trip *)
val out = Cookie.build (SignedCookie.sign { key = "secret", name = "auth", value = "user-42" })
val pairs = Cookie.parseCookie out
val SOME "user-42" = SignedCookie.read { key = "secret", name = "auth" } pairsRunning examples/demo.sml with make example builds a
bare and a fully-attributed Set-Cookie, round-trips one through
parseSetCookie, formats an Expires IMF-fixdate, and signs/verifies a
cookie including a tamper-detection check (output is byte-identical under
MLton and Poly/ML):
sml-cookie demo
Bare cookie:
SID=31d4d96e407aad42
Fully-attributed cookie:
SID=31d4d96e407aad42; Path=/; Domain=example.com; Max-Age=3600; Secure; HttpOnly; SameSite=Lax
Round-trip via parseSetCookie:
name=SID value=31d4d96e407aad42 path=/ domain=example.com maxAge=3600 secure=true httpOnly=true sameSite=Lax
Expires formatted as an IMF-fixdate:
Sun, 06 Nov 1994 08:49:37 GMT
Signed cookie round-trip:
Set-Cookie: auth=dXNlci00Mg.jVma5oL9Qhc4WcDmpt6NjyYD99g8CHCHB4Jrjstc-Q0
verified value = user-42
Tamper detection:
tampered cookie rejected (NONE)
make test # MLton
make test-poly # Poly/ML
make all-tests # both27 deterministic checks, identical under MLton and Poly/ML.
package github.com/sjqtentacles/sml-cookie
require {
github.com/sjqtentacles/sml-http
github.com/sjqtentacles/sml-crypto
github.com/sjqtentacles/sml-datetime
}
Dependencies are also vendored under lib/github.com/sjqtentacles/ and
committed, so make needs no network.
lib/github.com/sjqtentacles/sml-cookie/
cookie.sig cookie.sml RFC 6265 parse/build + IMF-fixdate
signed.sig signed.sml HMAC-signed cookies (via sml-crypto)
sources.mlb sml-cookie.mlb
test/ Harness suite (27 checks)
MIT