feat: implement CookieManager for enhanced cookie handling

This commit is contained in:
Flummi 2025-03-19 09:55:21 +01:00
parent e197050aa2
commit a8a8701739
4 changed files with 180 additions and 41 deletions

74
dist/cookieManager.js vendored Normal file
View File

@ -0,0 +1,74 @@
import fs from "fs";
export default class CookieManager {
cookies = {};
parse(headers, domain) {
if (!this.cookies[domain])
this.cookies[domain] = {};
headers?.forEach(header => {
const [cookiePart, ...attributes] = header.split(';').map(part => part.trim());
const [name, value] = cookiePart.split('=');
const cookie = { value: value || "", domain };
attributes.forEach(attr => {
const [key, val] = attr.split('=');
const lowerKey = key.toLowerCase();
switch (lowerKey) {
case 'path':
cookie.path = val;
break;
case 'expires':
cookie.expires = val ? new Date(val) : undefined;
break;
case 'max-age':
cookie.maxAge = parseInt(val, 10);
break;
case 'httponly':
cookie.httpOnly = true;
break;
case 'secure':
cookie.secure = true;
break;
case 'samesite':
cookie.sameSite = val;
break;
}
});
Object.assign(this.cookies[domain], { [name]: cookie });
});
}
format(domain) {
this.cleanupExpiredCookies();
return Object.entries(this.cookies[domain] || {})
.map(([key, value]) => `${key}=${value.value}`)
.join('; ');
}
getCookies(domain) {
this.cleanupExpiredCookies();
return this.cookies[domain] || {};
}
cleanupExpiredCookies() {
const now = new Date();
Object.keys(this.cookies).forEach(domain => {
Object.keys(this.cookies[domain]).forEach(key => {
if (this.cookies[domain][key].expires && this.cookies[domain][key].expires < now)
delete this.cookies[domain][key];
});
});
}
saveToFile(filePath) {
this.cleanupExpiredCookies();
fs.writeFileSync(filePath, JSON.stringify(this.cookies, null, 2), "utf8");
}
loadFromFile(filePath) {
if (!fs.existsSync(filePath))
return console.warn(`The file ${filePath} does not exist.`);
const fileContent = fs.readFileSync(filePath, "utf8");
const loadedCookies = JSON.parse(fileContent);
Object.keys(loadedCookies).forEach(domain => {
Object.keys(loadedCookies[domain]).forEach(cookieName => {
const cookie = loadedCookies[domain][cookieName];
cookie.expires = cookie.expires ? new Date(cookie.expires) : undefined;
});
});
this.cookies = loadedCookies;
}
}

23
dist/index.js vendored
View File

@ -3,23 +3,10 @@ import https from "https";
import { URL } from "url";
import querystring from "querystring";
import zlib from "zlib";
import CookieManager from './cookieManager.js';
export const cookieManager = new CookieManager();
class fetch {
cookies = {};
constructor() { }
parseCookies(cookieHeader) {
if (!cookieHeader)
return;
cookieHeader.split(";").forEach(cookie => {
const [key, value] = cookie.split("=");
if (key && value)
this.cookies[key.trim()] = value.trim();
});
}
formatCookies() {
return Object.entries(this.cookies)
.map(([key, value]) => `${key}=${value}`)
.join("; ");
}
decompress(data, encoding) {
return encoding === "br" ? zlib.brotliDecompressSync(data) :
encoding === "gzip" ? zlib.gunzipSync(data) :
@ -62,14 +49,14 @@ class fetch {
headers: {
...options.headers,
...(body ? { "Content-Length": Buffer.byteLength(body) } : {}),
"Cookie": this.formatCookies(),
"Cookie": cookieManager.format(hostname),
"Accept-Encoding": "br, gzip, deflate",
}
};
return new Promise((resolve, reject) => {
const requester = protocol === "https:" ? https : http;
const req = requester.request(requestOptions, res => {
this.parseCookies(res.headers["set-cookie"]?.join("; "));
cookieManager.parse(res.headers["set-cookie"], hostname);
if (options.followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
req.destroy();
if (redirectCount >= (options.maxRedirects || 5))
@ -82,7 +69,7 @@ class fetch {
return resolve({
body: res,
headers: res.headers,
cookies: this.cookies,
cookies: cookieManager.getCookies(hostname),
text: () => this.readData(res, "text"),
json: () => this.readData(res, "json"),
buffer: () => this.readData(res, "buffer"),

94
src/cookieManager.ts Normal file
View File

@ -0,0 +1,94 @@
import fs from "fs";
export type Cookie = {
value: string;
domain?: string;
path?: string;
expires?: Date;
maxAge?: number;
httpOnly?: boolean;
secure?: boolean;
sameSite?: string;
}
export type Cookies = Record<string, Cookie>;
export default class CookieManager {
private cookies: Record<string, Cookies> = {};
parse(
headers: string[] | undefined,
domain: string
): void {
/* todo
* key -> domain | use "domain" only if cookie does not have one
* key -> expires | ???
*/
if(!this.cookies[domain])
this.cookies[domain] = {};
headers?.forEach(header => {
const [cookiePart, ...attributes] = header.split(';').map(part => part.trim());
const [name, value] = cookiePart.split('=');
const cookie: Cookie = { value: value || "", domain };
attributes.forEach(attr => {
const [key, val] = attr.split('=');
const lowerKey = key.toLowerCase();
switch(lowerKey) {
case 'path': cookie.path = val; break;
case 'expires': cookie.expires = val ? new Date(val) : undefined; break;
case 'max-age': cookie.maxAge = parseInt(val, 10); break;
case 'httponly': cookie.httpOnly = true; break;
case 'secure': cookie.secure = true; break;
case 'samesite': cookie.sameSite = val; break;
}
});
Object.assign(this.cookies[domain], { [name]: cookie });
});
}
format(domain: string): string {
this.cleanupExpiredCookies();
return Object.entries(this.cookies[domain] || {})
.map(([key, value]) => `${key}=${value.value}`)
.join('; ');
}
getCookies(domain: string): Cookies {
this.cleanupExpiredCookies();
return this.cookies[domain] || {};
}
cleanupExpiredCookies(): void {
const now = new Date();
Object.keys(this.cookies).forEach(domain => {
Object.keys(this.cookies[domain]).forEach(key => {
if(this.cookies[domain][key].expires && this.cookies[domain][key].expires < now)
delete this.cookies[domain][key];
});
});
}
saveToFile(filePath: string): void {
this.cleanupExpiredCookies();
fs.writeFileSync(filePath, JSON.stringify(this.cookies, null, 2), "utf8");
}
loadFromFile(filePath: string): void {
if(!fs.existsSync(filePath))
return console.warn(`The file ${filePath} does not exist.`);
const fileContent = fs.readFileSync(filePath, "utf8");
const loadedCookies: Record<string, Cookies> = JSON.parse(fileContent);
Object.keys(loadedCookies).forEach(domain => {
Object.keys(loadedCookies[domain]).forEach(cookieName => {
const cookie = loadedCookies[domain][cookieName];
cookie.expires = cookie.expires ? new Date(cookie.expires) : undefined;
});
});
this.cookies = loadedCookies;
}
}

View File

@ -3,29 +3,13 @@ import https from "https";
import { URL } from "url";
import querystring from "querystring";
import zlib from "zlib";
import CookieManager, { Cookies } from './cookieManager.js';
export const cookieManager = new CookieManager();
class fetch {
private cookies: Record<string, string> = {};
constructor() { }
private parseCookies(cookieHeader: string | undefined): void {
if(!cookieHeader)
return;
cookieHeader.split(";").forEach(cookie => {
const [key, value] = cookie.split("=");
if(key && value)
this.cookies[key.trim()] = value.trim();
});
}
private formatCookies(): string {
return Object.entries(this.cookies)
.map(([key, value]) => `${key}=${value}`)
.join("; ");
}
private decompress(
data: Buffer,
encoding: string | undefined
@ -75,7 +59,7 @@ class fetch {
): Promise<{
body: http.IncomingMessage;
headers: http.IncomingHttpHeaders;
cookies: Record<string, string>;
cookies: Cookies;
text: () => Promise<string>;
json: () => Promise<JSON>;
buffer: () => Promise<Buffer>;
@ -97,7 +81,7 @@ class fetch {
headers: {
...options.headers,
...(body ? { "Content-Length": Buffer.byteLength(body) } : {}),
"Cookie": this.formatCookies(),
"Cookie": cookieManager.format(hostname),
"Accept-Encoding": "br, gzip, deflate",
}
};
@ -105,7 +89,7 @@ class fetch {
return new Promise((resolve, reject) => {
const requester = protocol === "https:" ? https : http;
const req = requester.request(requestOptions, res => {
this.parseCookies(res.headers["set-cookie"]?.join("; "));
cookieManager.parse(res.headers["set-cookie"], hostname);
if(options.followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
req.destroy();
if(redirectCount >= (options.maxRedirects || 5))
@ -120,7 +104,7 @@ class fetch {
return resolve({
body: res,
headers: res.headers,
cookies: this.cookies,
cookies: cookieManager.getCookies(hostname),
text: () => this.readData<string>(res, "text"),
json: () => this.readData<JSON>(res, "json"),
buffer: () => this.readData<Buffer>(res, "buffer"),