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 { URL } from "url";
|
||||||
import querystring from "querystring";
|
import querystring from "querystring";
|
||||||
import zlib from "zlib";
|
import zlib from "zlib";
|
||||||
|
import CookieManager from './cookieManager.js';
|
||||||
|
export const cookieManager = new CookieManager();
|
||||||
class fetch {
|
class fetch {
|
||||||
cookies = {};
|
|
||||||
constructor() { }
|
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) {
|
decompress(data, encoding) {
|
||||||
return encoding === "br" ? zlib.brotliDecompressSync(data) :
|
return encoding === "br" ? zlib.brotliDecompressSync(data) :
|
||||||
encoding === "gzip" ? zlib.gunzipSync(data) :
|
encoding === "gzip" ? zlib.gunzipSync(data) :
|
||||||
@ -62,14 +49,14 @@ class fetch {
|
|||||||
headers: {
|
headers: {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
...(body ? { "Content-Length": Buffer.byteLength(body) } : {}),
|
...(body ? { "Content-Length": Buffer.byteLength(body) } : {}),
|
||||||
"Cookie": this.formatCookies(),
|
"Cookie": cookieManager.format(hostname),
|
||||||
"Accept-Encoding": "br, gzip, deflate",
|
"Accept-Encoding": "br, gzip, deflate",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const requester = protocol === "https:" ? https : http;
|
const requester = protocol === "https:" ? https : http;
|
||||||
const req = requester.request(requestOptions, res => {
|
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) {
|
if (options.followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
req.destroy();
|
req.destroy();
|
||||||
if (redirectCount >= (options.maxRedirects || 5))
|
if (redirectCount >= (options.maxRedirects || 5))
|
||||||
@ -82,7 +69,7 @@ class fetch {
|
|||||||
return resolve({
|
return resolve({
|
||||||
body: res,
|
body: res,
|
||||||
headers: res.headers,
|
headers: res.headers,
|
||||||
cookies: this.cookies,
|
cookies: cookieManager.getCookies(hostname),
|
||||||
text: () => this.readData(res, "text"),
|
text: () => this.readData(res, "text"),
|
||||||
json: () => this.readData(res, "json"),
|
json: () => this.readData(res, "json"),
|
||||||
buffer: () => this.readData(res, "buffer"),
|
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 { URL } from "url";
|
||||||
import querystring from "querystring";
|
import querystring from "querystring";
|
||||||
import zlib from "zlib";
|
import zlib from "zlib";
|
||||||
|
import CookieManager, { Cookies } from './cookieManager.js';
|
||||||
|
|
||||||
|
export const cookieManager = new CookieManager();
|
||||||
|
|
||||||
class fetch {
|
class fetch {
|
||||||
private cookies: Record<string, string> = {};
|
|
||||||
|
|
||||||
constructor() { }
|
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(
|
private decompress(
|
||||||
data: Buffer,
|
data: Buffer,
|
||||||
encoding: string | undefined
|
encoding: string | undefined
|
||||||
@ -75,7 +59,7 @@ class fetch {
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
body: http.IncomingMessage;
|
body: http.IncomingMessage;
|
||||||
headers: http.IncomingHttpHeaders;
|
headers: http.IncomingHttpHeaders;
|
||||||
cookies: Record<string, string>;
|
cookies: Cookies;
|
||||||
text: () => Promise<string>;
|
text: () => Promise<string>;
|
||||||
json: () => Promise<JSON>;
|
json: () => Promise<JSON>;
|
||||||
buffer: () => Promise<Buffer>;
|
buffer: () => Promise<Buffer>;
|
||||||
@ -97,7 +81,7 @@ class fetch {
|
|||||||
headers: {
|
headers: {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
...(body ? { "Content-Length": Buffer.byteLength(body) } : {}),
|
...(body ? { "Content-Length": Buffer.byteLength(body) } : {}),
|
||||||
"Cookie": this.formatCookies(),
|
"Cookie": cookieManager.format(hostname),
|
||||||
"Accept-Encoding": "br, gzip, deflate",
|
"Accept-Encoding": "br, gzip, deflate",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -105,7 +89,7 @@ class fetch {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const requester = protocol === "https:" ? https : http;
|
const requester = protocol === "https:" ? https : http;
|
||||||
const req = requester.request(requestOptions, res => {
|
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) {
|
if(options.followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
req.destroy();
|
req.destroy();
|
||||||
if(redirectCount >= (options.maxRedirects || 5))
|
if(redirectCount >= (options.maxRedirects || 5))
|
||||||
@ -120,7 +104,7 @@ class fetch {
|
|||||||
return resolve({
|
return resolve({
|
||||||
body: res,
|
body: res,
|
||||||
headers: res.headers,
|
headers: res.headers,
|
||||||
cookies: this.cookies,
|
cookies: cookieManager.getCookies(hostname),
|
||||||
text: () => this.readData<string>(res, "text"),
|
text: () => this.readData<string>(res, "text"),
|
||||||
json: () => this.readData<JSON>(res, "json"),
|
json: () => this.readData<JSON>(res, "json"),
|
||||||
buffer: () => this.readData<Buffer>(res, "buffer"),
|
buffer: () => this.readData<Buffer>(res, "buffer"),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user