mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 14:35:03 +00:00
Merge pull request #5384 from pooja-bruno/move/common-cookie-file-in-buno-request-package
This commit is contained in:
@@ -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;
|
||||
@@ -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';
|
||||
@@ -2,8 +2,4 @@ export {
|
||||
encodeUrl,
|
||||
parseQueryParams,
|
||||
buildQueryString,
|
||||
} from './url';
|
||||
|
||||
export {
|
||||
isPotentiallyTrustworthyOrigin
|
||||
} from './url/validation';
|
||||
} from './url';
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user