Skip to content

Commit ae46b44

Browse files
authored
Parsing of request cookies and Cookie class for Set-Cookie (#33)
2 parents 058e632 + eb07e62 commit ae46b44

File tree

4 files changed

+200
-0
lines changed

4 files changed

+200
-0
lines changed

src/Cookie.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* A cookie that the server wants to ask the client to set.
3+
*/
4+
class Cookie implements Cookie.CookieOptions {
5+
/**
6+
* ```abnf
7+
*
8+
* token = 1*<any CHAR except CTLs or separators>
9+
* separators = "(" | ")" | "<" | ">" | "@"
10+
* | "," | ";" | ":" | "\" | <">
11+
* | "/" | "[" | "]" | "?" | "="
12+
* | "{" | "}" | SP | HT
13+
* ```
14+
* @see {@link https://www.rfc-editor.org/rfc/rfc2616.html#section-2.2|RFC 2616, Section 2.2}
15+
*/
16+
private static readonly TOKEN = /^[^\x00-\x1F\x7F\x20\x09\x28\x29\x3C\x3E\x40\x2C\x3B\x3A\x5C\x22\x2F\x5B\x5D\x3F\x3D\x7B\x7D]+$/;
17+
18+
/**
19+
* ```abnf
20+
* cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
21+
* ; US-ASCII characters excluding CTLs,
22+
* ; whitespace DQUOTE, comma, semicolon,
23+
* ; and backslash
24+
* ```
25+
* @see {@link https://httpwg.org/specs/rfc6265.html#sane-set-cookie-syntax|RFC 6265, Section 4.1.1}
26+
*/
27+
private static readonly COOKIE_OCTET = /^[^\x00-\x1F\x7F\x20\x09\x22\x2C\x3B\x5C]+$/;
28+
29+
public readonly domain?: string;
30+
public readonly expires?: Date;
31+
public readonly httpOnly: boolean;
32+
public readonly maxAge?: number;
33+
public readonly partitioned: boolean;
34+
public readonly path?: string;
35+
public readonly sameSite?: Cookie.SameSite;
36+
public readonly secure: boolean;
37+
38+
/**
39+
* The name of this cookie.
40+
*/
41+
public readonly name: string;
42+
43+
/**
44+
* The value of this cookie.
45+
*/
46+
public readonly value: string;
47+
48+
/**
49+
* @param name The name of this cookie.
50+
* @param value The value of this cookie.
51+
* @param options Cookie options.
52+
*/
53+
public constructor(name: string, value: string, options?: Partial<Cookie.CookieOptions>) {
54+
if (!Cookie.TOKEN.test(name))
55+
throw new SyntaxError(`Cookie name "${name}" is not a valid "token" as per RFC 2616, Section 2.2.`);
56+
if (!Cookie.COOKIE_OCTET.test(value))
57+
throw new SyntaxError(`In cookie "${name}", value "${value}" is not a valid "*cookie-octet" as per RFC 6265, Section 4.1.1.`);
58+
this.name = name;
59+
this.value = value;
60+
this.domain = options?.domain;
61+
this.expires = options?.expires;
62+
this.httpOnly = options?.httpOnly ?? false;
63+
this.maxAge = options?.maxAge;
64+
this.partitioned = options?.partitioned ?? false;
65+
this.path = options?.path;
66+
this.sameSite = options?.sameSite;
67+
this.secure = options?.secure ?? false;
68+
}
69+
70+
public serialise() {
71+
return [
72+
[this.name, this.value].join("="),
73+
this.domain !== undefined ? ["Domain", this.domain].join("=") : null,
74+
this.expires !== undefined ? ["Expires", this.expires.toUTCString()].join("=") : null,
75+
this.httpOnly ? "HttpOnly" : null,
76+
this.maxAge !== undefined ? ["Max-Age", this.maxAge].join("=") : null,
77+
this.partitioned ? "Partitioned" : null,
78+
this.path !== undefined ? ["Path", this.path].join("=") : null,
79+
this.sameSite !== undefined ? ["SameSite", this.sameSite].join("=") : null,
80+
this.secure ? "Secure" : null
81+
].filter(p => p !== null).join("; ");
82+
}
83+
}
84+
85+
namespace Cookie {
86+
export const enum SameSite {
87+
/**
88+
* Send the cookie only for requests originating from the same
89+
* {@link https://developer.mozilla.org/en-US/docs/Glossary/Site|site} that set the cookie.
90+
*/
91+
STRICT = "Strict",
92+
93+
/**
94+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#lax|Cookie
95+
* `SameSite=Lax`}
96+
*/
97+
LAX = "Lax",
98+
99+
/**
100+
* Send the cookie with both cross-site and same-site requests. The `Secure` attribute must also be set when
101+
* using this value.
102+
*/
103+
NONE = "None"
104+
}
105+
106+
export interface CookieOptions {
107+
/**
108+
* Defines the host to which the cookie will be sent.
109+
* @see {@link
110+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#domaindomain-value|Cookie
111+
* `Domain=<domain-value>`}
112+
*/
113+
domain?: string;
114+
115+
/**
116+
* Indicates the maximum lifetime of the cookie.
117+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#expiresdate|Cookie
118+
* `Expires=<date>`}
119+
*/
120+
expires?: Date;
121+
122+
/**
123+
* Forbids JavaScript from accessing the cookie.
124+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#httponly|Cookie
125+
* `HttpOnly`}
126+
*/
127+
httpOnly: boolean;
128+
129+
/**
130+
* Indicates the number of seconds until the cookie expires.
131+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#max-agenumber|Cookie
132+
* `Max-Age=<number>`}
133+
*/
134+
maxAge?: number;
135+
136+
/**
137+
* Indicates that the cookie should be stored using partitioned storage.
138+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#partitioned|Cookie
139+
* `Partitioned`}
140+
*/
141+
partitioned: boolean;
142+
143+
/**
144+
* Indicates the path that must exist in the requested URL for the browser to send the `Cookie` header.
145+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#pathpath-value|Cookie
146+
* `Path=<path-value>`}
147+
*/
148+
path?: string;
149+
150+
/**
151+
* Controls whether or not a cookie is sent with cross-site requests.
152+
* @see {@link
153+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#samesitesamesite-value|Cookie
154+
* `SameSite=<samesite-value>`}
155+
*/
156+
sameSite?: Cookie.SameSite;
157+
158+
/**
159+
* Indicates that the cookie is sent to the server only when a request is made with the `https:` scheme (except on
160+
* localhost), and therefore, is more resistant
161+
* to {@link https://developer.mozilla.org/en-US/docs/Glossary/MitM|man-in-the-middle} attacks.
162+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#secure|Cookie
163+
* `Secure`}
164+
*/
165+
secure: boolean;
166+
}
167+
}
168+
169+
export {Cookie};

src/Request.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export class Request {
3232
*/
3333
public readonly ip: IPv4 | IPv6;
3434

35+
/**
36+
* The parsed request cookies from the {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cookie|Cookie} request header.
37+
*/
38+
public readonly cookies: ReadonlyMap<string, string>;
39+
3540
/**
3641
* Construct a new Request.
3742
* @param method See {@link Request#method}.
@@ -52,6 +57,22 @@ export class Request {
5257
this.headers = headers;
5358
this.bodyStream = bodyStream;
5459
this.ip = ip;
60+
61+
this.cookies = new Map(
62+
this.headers.get("cookie")
63+
?.split("; ")
64+
.map(cookie => {
65+
const separatorIndex = cookie.indexOf("=");
66+
if (separatorIndex < 1)
67+
return null;
68+
const name = cookie.substring(0, separatorIndex);
69+
const value = cookie.substring(separatorIndex + 1);
70+
if (value.startsWith("\"") && value.endsWith("\""))
71+
return [name, value.substring(1, value.length - 1)];
72+
return [name, value];
73+
})
74+
.filter((cookie): cookie is [string, string] => cookie !== null)
75+
)
5576
}
5677

5778
/**

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* Auto-generated by generateIndices.sh */
2+
export * from "./Cookie.js";
23
export * from "./Request.js";
34
export * from "./Server.js";
45
export * from "./ServerErrorRegistry.js";

src/response/Response.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import http from "node:http";
2+
import {Cookie} from "../Cookie.js";
23
import {Request} from "../Request.js";
34
import {Server} from "../Server.js";
45

@@ -26,6 +27,14 @@ export abstract class Response {
2627
this.headers = new Headers(headers);
2728
}
2829

30+
/**
31+
* Set a response cookie.
32+
* @param cookie The cookie to set.
33+
*/
34+
public setCookie(cookie: Cookie) {
35+
this.headers.append("set-cookie", cookie.serialise());
36+
}
37+
2938
/**
3039
* @internal
3140
*/

0 commit comments

Comments
 (0)