104 lines
2.8 KiB
TypeScript
104 lines
2.8 KiB
TypeScript
interface Cookie {
|
|
name: string;
|
|
value: string;
|
|
domain: string;
|
|
path: string;
|
|
secure?: boolean;
|
|
httpOnly?: boolean;
|
|
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
}
|
|
|
|
export class CookieConfiguration {
|
|
private readonly cookie: Cookie;
|
|
private readonly targetUrl: URL;
|
|
|
|
constructor(cookie: Cookie, targetUrl: string) {
|
|
this.cookie = cookie;
|
|
try {
|
|
this.targetUrl = new URL(targetUrl);
|
|
} catch (error) {
|
|
throw new Error(`Invalid target URL: ${targetUrl}`);
|
|
}
|
|
|
|
this.validate();
|
|
}
|
|
|
|
private validate(): void {
|
|
if (!this.isValidDomain()) {
|
|
throw new Error(
|
|
`Domain mismatch: Cookie domain "${this.cookie.domain}" is invalid for target "${this.targetUrl.hostname}"`
|
|
);
|
|
}
|
|
|
|
if (!this.isValidPath()) {
|
|
throw new Error(
|
|
`Path not valid: Cookie path "${this.cookie.path}" is invalid for target path "${this.targetUrl.pathname}"`
|
|
);
|
|
}
|
|
}
|
|
|
|
private isValidDomain(): boolean {
|
|
const targetHost = this.targetUrl.hostname;
|
|
const cookieDomain = this.cookie.domain;
|
|
|
|
// Empty domain is invalid
|
|
if (!cookieDomain) {
|
|
return false;
|
|
}
|
|
|
|
// Exact match
|
|
if (cookieDomain === targetHost) {
|
|
return true;
|
|
}
|
|
|
|
// Wildcard domain (e.g., ".iracing.com" matches "members-ng.iracing.com")
|
|
if (cookieDomain.startsWith('.')) {
|
|
const domainWithoutDot = cookieDomain.slice(1);
|
|
return targetHost === domainWithoutDot || targetHost.endsWith('.' + domainWithoutDot);
|
|
}
|
|
|
|
// Subdomain compatibility: Allow cookies from related subdomains if they share the same base domain
|
|
// Example: "members.iracing.com" → "members-ng.iracing.com" (both share "iracing.com")
|
|
if (this.isSameBaseDomain(cookieDomain, targetHost)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if two domains share the same base domain (last 2 parts)
|
|
* @example
|
|
* isSameBaseDomain('members.iracing.com', 'members-ng.iracing.com') // true
|
|
* isSameBaseDomain('example.com', 'iracing.com') // false
|
|
*/
|
|
private isSameBaseDomain(domain1: string, domain2: string): boolean {
|
|
const parts1 = domain1.split('.');
|
|
const parts2 = domain2.split('.');
|
|
|
|
// Need at least 2 parts (domain.tld) for valid comparison
|
|
if (parts1.length < 2 || parts2.length < 2) {
|
|
return false;
|
|
}
|
|
|
|
// Compare last 2 parts (e.g., "iracing.com")
|
|
const base1 = parts1.slice(-2).join('.');
|
|
const base2 = parts2.slice(-2).join('.');
|
|
|
|
return base1 === base2;
|
|
}
|
|
|
|
private isValidPath(): boolean {
|
|
// Empty path is invalid
|
|
if (!this.cookie.path) {
|
|
return false;
|
|
}
|
|
|
|
// Path must be prefix of target pathname
|
|
return this.targetUrl.pathname.startsWith(this.cookie.path);
|
|
}
|
|
|
|
getValidatedCookie(): Cookie {
|
|
return { ...this.cookie };
|
|
}
|
|
} |