feat: implement CookieManager for enhanced cookie handling
This commit is contained in:
parent
e197050aa2
commit
a8a8701739
74
dist/cookieManager.js
vendored
Normal file
74
dist/cookieManager.js
vendored
Normal 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
23
dist/index.js
vendored
@ -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
94
src/cookieManager.ts
Normal 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;
|
||||
}
|
||||
}
|
30
src/index.ts
30
src/index.ts
@ -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"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user