feat: implement CookieManager for enhanced cookie handling
This commit is contained in:
		
							
								
								
									
										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"),
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user