/******************************************************************************/
/* */
/* HTTP.Cookie.cls - The RexxHTTP cookie class */
/* =========================================== */
/* */
/* This file is part of the RexxHTTP package */
/* [See https://rexx.epbcn.com/rexxhttp/] */
/* */
/* Copyright (c) 2006-2026 Josep Maria Blasco <josep.maria.blasco@epbcn.com> */
/* */
/* License: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0) */
/* */
/* Version history: */
/* */
/* Date Version Details */
/* -------- ------- --------------------------------------------------------- */
/* 20061102 0.1 First version */
/* 20170301 0.2 Drop support for mod_rexx, OS/2, IIS, rsp compilers */
/* Change the way request~message works -- see 'unknown'. */
/* By default, it looks in the environment variable pool. */
/* 20260611 1.0 Global refactor, switch to Apache license */
/* */
/******************************************************************************/
.environment~HTTP.Cookie = .HTTP.Cookie
::Class HTTP.Cookie Public
--------------------------------------------------------------------------------
-- INIT method --
-- --
-- INIT(name, value) --
-- --
-- name must conform to RFC6265: ASCII non-control characters, none of the --
-- separators ()<>@,;:\"/[]?={} space tab, and may not begin with "$". --
-- A leading __Host- or __Secure- prefix is recognised and enforced at --
-- makestring time (see CHECKPREFIX). --
-- value any string. No encoding is performed here; the caller is responsible --
-- for percent-encoding if the value carries reserved characters. --
--------------------------------------------------------------------------------
::Method init
Expose name value path domain max_age secure httponly samesite cachedstring
Use Strict Arg name, value = ""
-- Validate the name against the RFC6265 token rules.
self~validatename(name)
-- Same control-character rule as the name (see rejectcontrolchars): a
-- cookie value with an unencoded CR/LF is as capable of splitting the
-- Set-Cookie header as an unvalidated response header (audit-core-
-- 20260725.md ยง2.1). The class comment above says encoding is the
-- caller's job; this does not encode anything, it only refuses the one
-- byte range that can corrupt the response, at the line that set it.
self~rejectcontrolchars("Cookie value", value)
-- Defaults: a session cookie (no Max-Age), not secure, not HttpOnly,
-- no Path, no Domain, no SameSite emitted.
path = ""
domain = ""
max_age = -1 -- negative => session cookie, attribute omitted
secure = 0
httponly = 0
samesite = "" -- "" => attribute omitted; else Strict/Lax/None
-- Memoised Set-Cookie value. .nil => not yet computed (or invalidated by
-- a setter). makestring populates it; any setter resets it to .nil.
cachedstring = .nil
Return
--------------------------------------------------------------------------------
-- VALIDATENAME --
-- Enforces the RFC6265 cookie-name token rules. Raises a syntax error --
-- on the first violation. --
--------------------------------------------------------------------------------
::Method validatename Private
Use Strict Arg n
If n == "" Then Error("Cookie name may not be the null string")
If n~left(1) == "$" Then
Error("Cookie name may not begin with '$'")
-- Separators forbidden by RFC6265, plus control chars and whitespace.
-- Both the double-quote (22x) and the backslash (5Cx) are among them;
-- built via x2c to keep the literal below unambiguous.
separators = "()<>@,;:" || "22"x || "/[]?={}" || "5C"x || " " || "09"x
Do i = 1 To n~length
c = n~substr(i,1)
If c~c2d < 32 | c~c2d == 127 Then
Error("Cookie name contains a control character")
If c~c2d > 127 Then
Error("Cookie name contains a non-ASCII character")
If separators~pos(c) > 0 Then
Error("Cookie name contains the forbidden character '"c"'")
End
Return
--------------------------------------------------------------------------------
-- REJECTCONTROLCHARS (Private) --
-- Fail at the origin -- the setter/init call that received the bad value -- --
-- not centrally at makestring, so the traceback points at the rexxlet line --
-- that set it. Same test as validatename's control-character check, applied --
-- to value/path/domain, which validatename does not cover. --
--------------------------------------------------------------------------------
::Method rejectcontrolchars Private
Use Strict Arg label, s
Do i = 1 To s~length
c = s~substr(i,1)
If c~c2d < 32 | c~c2d == 127 Then
Error(label "contains a control character (0x"c~c2x") at position" i)
End
Return
::Method name
Expose name
Return name
::Method value
Expose value
Return value
::Method "value="
Expose value cachedstring
Use Strict Arg value
self~rejectcontrolchars("Cookie value", value)
cachedstring = .nil
Return
::Method path
Expose path
Return path
::Method "path="
Expose path cachedstring
Use Strict Arg path
self~rejectcontrolchars("Cookie path", path)
cachedstring = .nil
Return
::Method domain
Expose domain
Return domain
::Method "domain="
Expose domain cachedstring
Use Strict Arg domain
self~rejectcontrolchars("Cookie domain", domain)
cachedstring = .nil
Return
--------------------------------------------------------------------------------
-- MAX_AGE / MAX_AGE= --
-- A whole number of seconds. Negative => session cookie (attribute omitted). --
-- Zero => the cookie is deleted by the user agent. --
--------------------------------------------------------------------------------
::Method max_age
Expose max_age
Return max_age
::Method "max_age="
Expose max_age cachedstring
Use Strict Arg seconds
If \seconds~datatype("Whole") Then
Error("Max-Age must be a whole number of seconds")
max_age = seconds
cachedstring = .nil
Return
--------------------------------------------------------------------------------
-- SECURE / SECURE= --
--------------------------------------------------------------------------------
::Method secure
Expose secure
Return secure
::Method "secure="
Expose secure cachedstring
Use Strict Arg val
If val \== 0, val \== 1 Then Error("Secure must be 0 or 1")
secure = val
cachedstring = .nil
Return
--------------------------------------------------------------------------------
-- HTTPONLY / HTTPONLY= --
--------------------------------------------------------------------------------
::Method httponly
Expose httponly
Return httponly
::Method "httponly="
Expose httponly cachedstring
Use Strict Arg val
If val \== 0, val \== 1 Then Error("HttpOnly must be 0 or 1")
httponly = val
cachedstring = .nil
Return
--------------------------------------------------------------------------------
-- SAMESITE / SAMESITE= --
-- Accepts Strict, Lax, None (case-insensitive); stored canonicalised. --
-- "" clears the attribute. The Secure-with-None rule is enforced at --
-- makestring time, not here, so attributes may be set in any order. --
--------------------------------------------------------------------------------
::Method samesite
Expose samesite
Return samesite
::Method "samesite="
Expose samesite cachedstring
Use Strict Arg val
Select Case val~upper
When "" Then samesite = ""
When "STRICT" Then samesite = "Strict"
When "LAX" Then samesite = "Lax"
When "NONE" Then samesite = "None"
Otherwise Error("SameSite must be Strict, Lax or None")
End
cachedstring = .nil
--------------------------------------------------------------------------------
-- CHECKCONSISTENCY (Private) --
-- Strict cross-attribute validation, run from makestring. Raises on the --
-- first violation so a rexxlet never emits a Set-Cookie the browser would --
-- silently discard. --
--------------------------------------------------------------------------------
::Method checkconsistency Private
Expose name path domain secure samesite
-- SameSite=None requires Secure (RFC6265bis; browsers reject otherwise).
If samesite == "None", secure \== 1 Then
Error("SameSite=None requires Secure")
-- __Secure- prefix: must be Secure. Prefix match is case-insensitive,
-- mirroring browser behaviour (__SeCuRe- is treated as __Secure-).
If name~left(9)~upper == "__SECURE-" Then Do
If secure \== 1 Then
Error("__Secure- prefixed cookies require Secure")
End
-- __Host- prefix: must be Secure, Path=/, and no Domain.
If name~left(7)~upper == "__HOST-" Then Do
If secure \== 1 Then
Error("__Host- prefixed cookies require Secure")
If domain \== "" Then
Error("__Host- prefixed cookies may not set Domain")
If path \== "/" Then
Error("__Host- prefixed cookies require Path=/")
End
--------------------------------------------------------------------------------
-- MAKESTRING --
-- Returns the value for a Set-Cookie header. Runs strict consistency checks --
-- first. Attribute order follows common practice: name=value first, then --
-- Max-Age, Domain, Path, SameSite, then the valueless flags Secure/HttpOnly. --
--------------------------------------------------------------------------------
::Method makestring
Expose name value path domain max_age secure httponly samesite cachedstring
-- Return the memoised value if nothing changed since it was computed.
If cachedstring \== .nil Then Return cachedstring
self~checkconsistency
s = name"="value
If max_age >= 0 Then s ||= "; Max-Age="max_age
If domain \== "" Then s ||= "; Domain="domain
If path \== "" Then s ||= "; Path="path
If samesite \== "" Then s ||= "; SameSite="samesite
If secure == 1 Then s ||= "; Secure"
If httponly == 1 Then s ||= "; HttpOnly"
cachedstring = s
Return s
--------------------------------------------------------------------------------
-- Error --
--------------------------------------------------------------------------------
::Routine Error
Raise syntax 93.900 array (Arg(1))