Merge pull request #5384 from pooja-bruno/move/common-cookie-file-in-buno-request-package

This commit is contained in:
Pooja
2025-08-21 21:15:35 +05:30
committed by GitHub
parent e71ee3eff5
commit 04d1e50f98
15 changed files with 86 additions and 94 deletions

View File

@@ -1,505 +0,0 @@
import { Cookie, CookieJar } from 'tough-cookie';
import each from 'lodash/each';
import moment from 'moment';
import { isPotentiallyTrustworthyOrigin } from '../utils';
const cookieJar = new CookieJar();
const addCookieToJar = (setCookieHeader: string, requestUrl: string): void => {
const cookie = Cookie.parse(setCookieHeader, { loose: true });
if (!cookie) return;
cookieJar.setCookieSync(cookie, requestUrl, {
ignoreError: true
});
};
const getCookiesForUrl = (url: string) => {
return cookieJar.getCookiesSync(url, {
secure: isPotentiallyTrustworthyOrigin(url),
} as any);
};
const getCookieStringForUrl = (url: string): string => {
const cookies = getCookiesForUrl(url);
if (!Array.isArray(cookies) || !cookies.length) return '';
const validCookies = cookies.filter((cookie: any) => !cookie.expires || (cookie.expires as any) > Date.now());
return validCookies.map((cookie) => cookie.cookieString()).join('; ');
};
const getDomainsWithCookies = (): Promise<Array<{ domain: string; cookies: Cookie[]; cookieString: string }>> => {
return new Promise((resolve, reject) => {
const domainCookieMap: Record<string, Cookie[]> = {};
(cookieJar as any).store.getAllCookies((err: Error, cookies: Cookie[]) => {
if (err) return reject(err);
cookies.forEach((cookie) => {
// Handle null domain by skipping the cookie
if (!cookie.domain) return;
if (!domainCookieMap[cookie.domain]) {
domainCookieMap[cookie.domain] = [cookie];
} else {
domainCookieMap[cookie.domain].push(cookie);
}
});
const domains = Object.keys(domainCookieMap);
const domainsWithCookies: Array<{ domain: string; cookies: Cookie[]; cookieString: string }> = [];
each(domains, (domain) => {
const cookiesForDomain = domainCookieMap[domain];
const validCookies = cookiesForDomain.filter((cookie: any) => !cookie.expires || (cookie.expires as any) > Date.now());
if (validCookies.length) {
domainsWithCookies.push({
domain,
cookies: validCookies,
cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ')
});
}
});
resolve(domainsWithCookies);
});
});
};
const deleteCookie = (domain: string, path: string, cookieKey: string): Promise<void> => {
return new Promise((resolve, reject) => {
(cookieJar as any).store.removeCookie(domain, path, cookieKey, (err: Error) => {
if (err) return reject(err);
resolve();
});
});
};
const deleteCookiesForDomain = (domain: string): Promise<void> => {
return new Promise((resolve, reject) => {
(cookieJar as any).store.removeCookies(domain, null, (err: Error) => {
if (err) return reject(err);
resolve();
});
});
};
const updateCookieObj = (cookieObj: any, oldCookie: Cookie) => {
return {
...cookieObj,
path: oldCookie.path,
key: oldCookie.key,
domain: oldCookie.domain,
expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
creation: oldCookie?.creation && moment(oldCookie.creation).isValid() ? new Date(oldCookie.creation) : new Date(),
lastAccessed:
oldCookie?.lastAccessed && moment(oldCookie.lastAccessed).isValid()
? new Date(oldCookie.lastAccessed)
: new Date()
} as any;
};
const createCookieObj = (cookieObj: any) => {
return {
...cookieObj,
path: cookieObj.path,
expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
creation: cookieObj?.creation && moment(cookieObj.creation).isValid() ? new Date(cookieObj.creation) : new Date(),
lastAccessed:
cookieObj?.lastAccessed && moment(cookieObj.lastAccessed).isValid()
? new Date(cookieObj.lastAccessed)
: new Date()
} as any;
};
const addCookieForDomain = (domain: string, cookieObj: any): Promise<void> => {
return new Promise((resolve, reject) => {
try {
const cookie = new Cookie(createCookieObj(cookieObj));
(cookieJar as any).store.putCookie(cookie, (err: Error) => {
if (err) return reject(err);
resolve();
});
} catch (err) {
reject(err);
}
});
};
const modifyCookieForDomain = (domain: string, oldCookieObj: any, cookieObj: any): Promise<void> => {
return new Promise((resolve, reject) => {
try {
const oldCookie = new Cookie(createCookieObj(oldCookieObj));
const newCookie = new Cookie(updateCookieObj(cookieObj, oldCookie));
(cookieJar as any).store.updateCookie(oldCookie, newCookie, (removeErr: Error) => {
if (removeErr) return reject(removeErr);
resolve();
});
} catch (err) {
reject(err);
}
});
};
const parseCookieString = (cookieStr: string): any | null => {
try {
const cookie = Cookie.parse(cookieStr);
if (!cookie) return null;
return {
...cookie,
expires: cookie.expires === 'Infinity' || (cookie.expires as any) === Infinity ? null : cookie.expires
};
} catch (err) {
throw err;
}
};
const createCookieString = (cookieObj: any): string => {
const cookie = new Cookie(createCookieObj(cookieObj));
let cookieString = cookie.toString(); // tough-cookie omits domain
// Manually append domain if cookie is hostOnly but we still want Domain flag
if (cookieObj.hostOnly && !cookieString.includes('Domain=')) {
cookieString += `; Domain=${cookieObj.domain}`;
}
return cookieString;
}
const saveCookies = (url: string, headers: any) => {
if (headers['set-cookie']) {
let setCookieHeaders = Array.isArray(headers['set-cookie'])
? headers['set-cookie']
: [headers['set-cookie']];
for (let setCookieHeader of setCookieHeaders) {
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
addCookieToJar(setCookieHeader, url);
}
}
}
};
const cookieJarWrapper = () => {
return {
// Get the full cookie object for the given URL & name.
getCookie: function (
url: string,
cookieName: string,
callback?: (err: Error | null | undefined, cookie?: Cookie | null) => void
) {
if (!url || !cookieName) {
const error = new Error('URL and cookie name are required');
if (callback) return callback(error);
return Promise.reject(error);
}
if (callback) {
// Callback mode
return cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {
if (err) return callback(err);
const cookieList = cookies || [];
const cookie = cookieList.find((c) => c.key === cookieName);
callback(null, cookie || null);
});
}
// Promise mode
return new Promise<Cookie | null>((resolve, reject) => {
cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {
if (err) return reject(err);
const cookieList = cookies || [];
const cookie = cookieList.find((c) => c.key === cookieName);
resolve(cookie || null);
});
});
},
// Get all cookies that would be sent to the given URL.
getCookies: function (url: string, callback?: (err: Error | null | undefined, cookies?: Cookie[]) => void) {
if (!url) {
const error = new Error('URL is required');
if (callback) return callback(error);
return Promise.reject(error);
}
if (callback) {
// Callback mode
return cookieJar.getCookies(url, callback as any);
}
// Promise mode
return new Promise<Cookie[]>((resolve, reject) => {
cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {
if (err) return reject(err);
resolve(cookies || []);
});
});
},
setCookie: function (
url: string,
nameOrCookieObj: string | Record<string, any>,
valueOrCallback?: string | ((err?: Error | undefined) => void),
maybeCallback?: (err?: Error | undefined) => void
) {
// Determine the callback
let callback: ((err?: Error | undefined) => void) | undefined;
if (typeof maybeCallback === 'function') {
callback = maybeCallback;
} else if (typeof valueOrCallback === 'function') {
callback = valueOrCallback as (err?: Error | undefined) => void;
}
const executeSetCookie = () => {
if (!url) throw new Error('URL is required');
// CASE 1: name/value pair provided
if (typeof nameOrCookieObj === 'string') {
const cookieName = nameOrCookieObj;
const cookieValue = typeof valueOrCallback === 'string' ? valueOrCallback : '';
if (!cookieName) throw new Error('Cookie name is required');
const cookie = new Cookie({
key: cookieName,
value: cookieValue,
domain: new URL(url).hostname,
});
cookieJar.setCookieSync(cookie, url, { ignoreError: true });
return;
}
// CASE 2: cookie object provided
if (typeof nameOrCookieObj === 'object' && nameOrCookieObj !== null) {
const obj = { ...(nameOrCookieObj as any) } as any;
if (!obj.key && obj.name) obj.key = obj.name;
if (!obj.key) throw new Error('cookieObject.key (name) is required');
const base = {
domain: new URL(url).hostname,
...obj,
} as any;
const processedCookie = createCookieObj(base);
const cookie = new Cookie(processedCookie);
cookieJar.setCookieSync(cookie, url, { ignoreError: true });
return;
}
// If we reach here, arguments were invalid
throw new Error('Invalid arguments passed to setCookie');
};
if (callback) {
// Callback mode
try {
executeSetCookie();
callback(undefined);
} catch (err) {
callback(err as Error);
}
return;
}
// Promise mode
return new Promise<void>((resolve, reject) => {
try {
executeSetCookie();
resolve();
} catch (err) {
reject(err);
}
});
},
setCookies: function (
url: string,
cookiesArray: any[],
callback?: (err?: Error | undefined) => void
) {
const executeSetCookies = () => {
if (!url) throw new Error('URL is required');
if (!Array.isArray(cookiesArray)) {
throw new Error('setCookies expects an array of cookie objects');
}
for (const cookieObject of cookiesArray) {
const obj = { ...(cookieObject as any) } as any;
if (!obj.key && obj.name) obj.key = obj.name;
if (!obj.key) throw new Error('cookieObject.key (name) is required');
const base = {
domain: new URL(url).hostname,
...obj
} as any;
const processedCookie = createCookieObj(base);
const cookie = new Cookie(processedCookie);
cookieJar.setCookieSync(cookie, url, { ignoreError: true });
}
};
if (callback) {
// Callback mode
try {
executeSetCookies();
callback(undefined);
} catch (err) {
callback(err as Error);
}
return;
}
// Promise mode
return new Promise<void>((resolve, reject) => {
try {
executeSetCookies();
resolve();
} catch (err) {
reject(err);
}
});
},
clear: function (callback?: (err?: Error | undefined) => void) {
if (callback) {
// Callback mode
return (cookieJar as any).store.removeAllCookies(callback);
}
// Promise mode
return new Promise<void>((resolve, reject) => {
(cookieJar as any).store.removeAllCookies((err?: Error) => {
if (err) reject(err);
else resolve();
});
});
},
deleteCookies: function (url: string, callback?: (err?: Error | undefined) => void) {
if (!url) {
const error = new Error('URL is required');
if (callback) return callback(error);
return Promise.reject(error);
}
if (callback) {
// Callback mode
return cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {
if (err) return callback(err);
const cookieList = cookies || [];
if (!cookieList.length) return callback(undefined);
let pending = cookieList.length;
const done = (removeErr?: Error) => {
if (removeErr) return callback(removeErr);
if (--pending === 0) {
callback(undefined);
}
};
cookieList.forEach((cookie) => {
(cookieJar as any).store.removeCookie(cookie.domain, cookie.path, cookie.key, done);
});
});
}
// Promise mode
return new Promise<void>((resolve, reject) => {
cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {
if (err) return reject(err);
const cookieList = cookies || [];
if (!cookieList.length) return resolve();
let pending = cookieList.length;
const done = (removeErr?: Error) => {
if (removeErr) return reject(removeErr);
if (--pending === 0) {
resolve();
}
};
cookieList.forEach((cookie) => {
(cookieJar as any).store.removeCookie(cookie.domain, cookie.path, cookie.key, done);
});
});
});
},
deleteCookie: function (url: string, cookieName: string, callback?: (err?: Error | undefined) => void) {
if (!url || !cookieName) {
const error = new Error('URL and cookie name are required');
if (callback) return callback(error);
return Promise.reject(error);
}
const executeDelete = (callback: (err?: Error) => void) => {
cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {
if (err) return callback(err);
// Filter cookies matching key
const cookieList = cookies || [];
const matchingCookies = cookieList.filter((c) => c.key === cookieName);
if (!matchingCookies.length) return callback(undefined);
const urlPath = new URL(url).pathname || '/';
// Prioritise a cookie whose path exactly matches the URL path
let cookieToDelete = matchingCookies.find((c) => c.path === urlPath);
// If not found, fall back to the first matching cookie (most specific path first)
if (!cookieToDelete) {
// tough-cookie sorts cookies by path length desc, preserve that order
cookieToDelete = matchingCookies[0];
}
(cookieJar as any).store.removeCookie(
cookieToDelete.domain,
cookieToDelete.path,
cookieToDelete.key,
callback
);
});
};
if (callback) {
// Callback mode
return executeDelete(callback);
}
// Promise mode
return new Promise<void>((resolve, reject) => {
executeDelete((err?: Error) => {
if (err) reject(err);
else resolve();
});
});
}
} as const;
};
const cookiesModule = {
cookieJar,
addCookieToJar,
getCookiesForUrl,
getCookieStringForUrl,
getDomainsWithCookies,
deleteCookie,
deleteCookiesForDomain,
addCookieForDomain,
modifyCookieForDomain,
parseCookieString,
createCookieString,
updateCookieObj,
createCookieObj,
jar: cookieJarWrapper,
saveCookies
};
export default cookiesModule;

View File

@@ -1,6 +1,5 @@
export { mockDataFunctions } from './utils/faker-functions';
export { default as interpolate } from './interpolate';
export { default as isRequestTagsIncluded } from './tags';
export { default as cookies } from './cookies';
export * as utils from './utils';

View File

@@ -2,8 +2,4 @@ export {
encodeUrl,
parseQueryParams,
buildQueryString,
} from './url';
export {
isPotentiallyTrustworthyOrigin
} from './url/validation';
} from './url';

View File

@@ -1,133 +0,0 @@
import { isPotentiallyTrustworthyOrigin } from './validation';
describe('isPotentiallyTrustworthyOrigin', () => {
describe('secure schemes', () => {
it('should return true for HTTPS URLs', () => {
expect(isPotentiallyTrustworthyOrigin('https://example.com')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('https://api.github.com/v1/users')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('https://localhost:3000')).toBe(true);
});
it('should return true for WSS URLs', () => {
expect(isPotentiallyTrustworthyOrigin('wss://example.com')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('wss://localhost:8080/ws')).toBe(true);
});
it('should return true for file URLs', () => {
expect(isPotentiallyTrustworthyOrigin('file:///path/to/file.html')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('file://localhost/path/to/file.html')).toBe(true);
});
});
describe('insecure schemes', () => {
it('should return false for HTTP URLs with non-localhost domains', () => {
expect(isPotentiallyTrustworthyOrigin('http://example.com')).toBe(false);
expect(isPotentiallyTrustworthyOrigin('http://api.github.com')).toBe(false);
});
it('should return false for WS URLs with non-localhost domains', () => {
expect(isPotentiallyTrustworthyOrigin('ws://example.com')).toBe(false);
expect(isPotentiallyTrustworthyOrigin('ws://api.github.com')).toBe(false);
});
it('should return false for other schemes', () => {
expect(isPotentiallyTrustworthyOrigin('ftp://example.com')).toBe(false);
expect(isPotentiallyTrustworthyOrigin('ssh://example.com')).toBe(false);
});
it('should return true for HTTP/WS URLs with localhost (localhost is always trustworthy)', () => {
expect(isPotentiallyTrustworthyOrigin('http://localhost')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('ws://localhost')).toBe(true);
});
});
describe('loopback addresses', () => {
describe('IPv4 loopback', () => {
it('should return true for 127.0.0.1', () => {
expect(isPotentiallyTrustworthyOrigin('http://127.0.0.1')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://127.0.0.1:3000')).toBe(true);
});
it('should return true for other 127.x.x.x addresses', () => {
expect(isPotentiallyTrustworthyOrigin('http://127.0.0.0')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://127.255.255.255')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://127.1.2.3')).toBe(true);
});
it('should return false for non-loopback IPv4 addresses', () => {
expect(isPotentiallyTrustworthyOrigin('http://192.168.1.1')).toBe(false);
expect(isPotentiallyTrustworthyOrigin('http://10.0.0.1')).toBe(false);
expect(isPotentiallyTrustworthyOrigin('http://172.16.0.1')).toBe(false);
expect(isPotentiallyTrustworthyOrigin('http://8.8.8.8')).toBe(false);
});
});
describe('IPv6 loopback', () => {
it('should return true for ::1', () => {
expect(isPotentiallyTrustworthyOrigin('http://[::1]')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://[::1]:3000')).toBe(true);
});
it('should return false for non-loopback IPv6 addresses', () => {
expect(isPotentiallyTrustworthyOrigin('http://[2001:db8::1]')).toBe(false);
expect(isPotentiallyTrustworthyOrigin('http://[fe80::1]')).toBe(false);
});
});
});
describe('localhost hostnames', () => {
it('should return true for localhost and *.localhost domains', () => {
expect(isPotentiallyTrustworthyOrigin('http://localhost')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://localhost:3000')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://app.localhost')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://api.localhost')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://sub.domain.localhost')).toBe(true);
});
it('should handle case insensitive localhost', () => {
expect(isPotentiallyTrustworthyOrigin('http://LOCALHOST')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://LocalHost')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://APP.LOCALHOST')).toBe(true);
});
it('should return false for non-localhost domains', () => {
expect(isPotentiallyTrustworthyOrigin('http://api.example.com')).toBe(false);
expect(isPotentiallyTrustworthyOrigin('http://localhost.example.com')).toBe(false);
});
});
describe('edge cases', () => {
it('should handle trailing dots in hostnames', () => {
expect(isPotentiallyTrustworthyOrigin('http://localhost.')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://app.localhost.')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://example.com.')).toBe(false);
});
it('should handle URLs with query parameters and fragments', () => {
expect(isPotentiallyTrustworthyOrigin('https://example.com/path?query=value#fragment')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://localhost/path?query=value#fragment')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://api.example.com/path?query=value#fragment')).toBe(false);
});
it('should handle URLs with authentication', () => {
expect(isPotentiallyTrustworthyOrigin('https://user:pass@example.com')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://user:pass@localhost')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('http://user:pass@api.example.com')).toBe(false);
});
});
describe('mixed scenarios', () => {
it('should prioritize secure schemes over hostname checks', () => {
// Even though example.com is not localhost, HTTPS makes it trustworthy
expect(isPotentiallyTrustworthyOrigin('https://example.com')).toBe(true);
// Even though 192.168.1.1 is not loopback, HTTPS makes it trustworthy
expect(isPotentiallyTrustworthyOrigin('https://192.168.1.1')).toBe(true);
});
it('should handle localhost with different schemes', () => {
expect(isPotentiallyTrustworthyOrigin('https://localhost')).toBe(true);
expect(isPotentiallyTrustworthyOrigin('wss://localhost')).toBe(true);
});
});
});

View File

@@ -1,67 +0,0 @@
import { isIPv4, isIPv6, isIP } from 'is-ip';
const hostNoBrackets = (host: string): string => {
if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) {
return host.substring(1, host.length - 1);
}
return host;
};
const isLoopbackV4 = (address: string): boolean => {
const octets = address.split('.');
if (octets.length !== 4 || parseInt(octets[0], 10) !== 127) {
return false;
}
return octets.every((octet) => {
const n = parseInt(octet, 10);
return !Number.isNaN(n) && n >= 0 && n <= 255;
});
};
const isLoopbackV6 = (address: string): boolean => address === '::1';
const isIpLoopback = (address: string): boolean => {
if (isIPv4(address)) {
return isLoopbackV4(address);
}
if (isIPv6(address)) {
return isLoopbackV6(address);
}
return false;
};
const isNormalizedLocalhostTLD = (host: string): boolean => host.toLowerCase().endsWith('.localhost');
const isLocalHostname = (host: string): boolean => {
return host.toLowerCase() === 'localhost' || isNormalizedLocalhostTLD(host);
};
/**
* Mirrors Chrome / Secure Contexts spec for "potentially trustworthy origins".
*/
const isPotentiallyTrustworthyOrigin = (urlString: string): boolean => {
let url: URL;
try {
url = new URL(urlString);
} catch {
return false; // invalid URL or opaque origin
}
const scheme = url.protocol.replace(':', '').toLowerCase();
const hostname = hostNoBrackets(url.hostname).replace(/\.+$/, '');
// Secure schemes
if (scheme === 'https' || scheme === 'wss' || scheme === 'file') {
return true;
}
// IP literals
if (isIP(hostname)) {
return isIpLoopback(hostname);
}
// localhost / *.localhost
return isLocalHostname(hostname);
};
export { isPotentiallyTrustworthyOrigin };