Skip to content

Commit

Permalink
feat: [#1553] Adds setting disableSameOriginPolicy, to make it possib…
Browse files Browse the repository at this point in the history
…le to bypass the same-origin policy (CORS) (#1554)

* feat: [1553] Add disableCrossOriginPolicy setting to disable cors in the browser

* chore: [#1553] Adds unit tests and changes name on setting for disabling same-origin policy

* chore: [#1553] Adds unit tests and changes name on setting for disabling same-origin policy

---------

Co-authored-by: David Ortner <[email protected]>
  • Loading branch information
OlaviSau and capricorn86 authored Nov 5, 2024
1 parent a78cd8f commit 1625d40
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 24 deletions.
4 changes: 4 additions & 0 deletions packages/happy-dom/src/browser/BrowserSettingsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export default class BrowserSettingsFactory {
...DefaultBrowserSettings.timer,
...settings?.timer
},
fetch: {
...DefaultBrowserSettings.fetch,
...settings?.fetch
},
device: {
...DefaultBrowserSettings.device,
...settings?.device
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-dom/src/browser/DefaultBrowserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export default <IBrowserSettings>{
maxIntervalIterations: -1,
preventTimerLoops: false
},
fetch: {
disableSameOriginPolicy: false
},
navigation: {
disableMainFrameNavigation: false,
disableChildFrameNavigation: false,
Expand Down
16 changes: 15 additions & 1 deletion packages/happy-dom/src/browser/types/IBrowserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,28 @@ export default interface IBrowserSettings {
/** Handle disabled resource loading as success */
handleDisabledFileLoadingAsSuccess: boolean;

/** Settings for timers */
/**
* Settings for timers
*/
timer: {
maxTimeout: number;
maxIntervalTime: number;
maxIntervalIterations: number;
preventTimerLoops: boolean;
};

/**
* Settings for fetch
*/
fetch: {
/**
* Disables same-origin policy (CORS)
*
* @see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
*/
disableSameOriginPolicy: boolean;
};

/**
* Disables error capturing.
*
Expand Down
12 changes: 12 additions & 0 deletions packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ export default interface IOptionalBrowserSettings {
maxIntervalIterations?: number;
};

/**
* Settings for fetch
*/
fetch?: {
/**
* Disables same-origin policy (CORS)
*
* @see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
*/
disableSameOriginPolicy?: boolean;
};

/**
* Disables error capturing.
*
Expand Down
25 changes: 16 additions & 9 deletions packages/happy-dom/src/fetch/Fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default class Fetch {
private request: Request;
private redirectCount = 0;
private disableCache: boolean;
private disableCrossOriginPolicy: boolean;
private disableSameOriginPolicy: boolean;
#browserFrame: IBrowserFrame;
#window: BrowserWindow;
#unfilteredHeaders: Headers | null = null;
Expand All @@ -69,7 +69,7 @@ export default class Fetch {
* @param [options.redirectCount] Redirect count.
* @param [options.contentType] Content Type.
* @param [options.disableCache] Disables the use of cached responses. It will still store the response in the cache.
* @param [options.disableCrossOriginPolicy] Disables the Cross-Origin policy.
* @param [options.disableSameOriginPolicy] Disables the Same-Origin policy.
* @param [options.unfilteredHeaders] Unfiltered headers - necessary for preflight requests.
*/
constructor(options: {
Expand All @@ -80,7 +80,7 @@ export default class Fetch {
redirectCount?: number;
contentType?: string;
disableCache?: boolean;
disableCrossOriginPolicy?: boolean;
disableSameOriginPolicy?: boolean;
unfilteredHeaders?: Headers;
}) {
this.#browserFrame = options.browserFrame;
Expand All @@ -95,7 +95,10 @@ export default class Fetch {
}
this.redirectCount = options.redirectCount ?? 0;
this.disableCache = options.disableCache ?? false;
this.disableCrossOriginPolicy = options.disableCrossOriginPolicy ?? false;
this.disableSameOriginPolicy =
options.disableSameOriginPolicy ??
this.#browserFrame.page.context.browser.settings.fetch.disableSameOriginPolicy ??
false;
}

/**
Expand Down Expand Up @@ -128,7 +131,11 @@ export default class Fetch {
this.#window.location.protocol === 'https:'
) {
throw new this.#window.DOMException(
`Mixed Content: The page at '${this.#window.location.href}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${this.request.url}'. This request has been blocked; the content must be served over HTTPS.`,
`Mixed Content: The page at '${
this.#window.location.href
}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${
this.request.url
}'. This request has been blocked; the content must be served over HTTPS.`,
DOMExceptionNameEnum.securityError
);
}
Expand All @@ -141,7 +148,7 @@ export default class Fetch {
}
}

if (!this.disableCrossOriginPolicy) {
if (!this.disableSameOriginPolicy) {
const compliesWithCrossOriginPolicy = await this.compliesWithCrossOriginPolicy();

if (!compliesWithCrossOriginPolicy) {
Expand Down Expand Up @@ -192,7 +199,7 @@ export default class Fetch {
url: this.request.url,
init: { headers, method: cachedResponse.request.method },
disableCache: true,
disableCrossOriginPolicy: true
disableSameOriginPolicy: true
});

if (cachedResponse.etag || !cachedResponse.staleWhileRevalidate) {
Expand Down Expand Up @@ -251,7 +258,7 @@ export default class Fetch {
*/
private async compliesWithCrossOriginPolicy(): Promise<boolean> {
if (
this.disableCrossOriginPolicy ||
this.disableSameOriginPolicy ||
!FetchCORSUtility.isCORS(this.#window.location.href, this.request[PropertySymbol.url])
) {
return true;
Expand Down Expand Up @@ -303,7 +310,7 @@ export default class Fetch {
url: this.request.url,
init: { method: 'OPTIONS' },
disableCache: true,
disableCrossOriginPolicy: true,
disableSameOriginPolicy: true,
unfilteredHeaders: corsHeaders
});

Expand Down
4 changes: 2 additions & 2 deletions packages/happy-dom/src/fetch/ResourceFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default class ResourceFetch {
browserFrame: this.#browserFrame,
window: this.window,
url,
disableCrossOriginPolicy: true
disableSameOriginPolicy: true
});
const response = await fetch.send();

Expand All @@ -60,7 +60,7 @@ export default class ResourceFetch {
browserFrame: this.#browserFrame,
window: this.window,
url,
disableCrossOriginPolicy: true
disableSameOriginPolicy: true
});

const response = fetch.send();
Expand Down
25 changes: 16 additions & 9 deletions packages/happy-dom/src/fetch/SyncFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default class SyncFetch {
private request: Request;
private redirectCount = 0;
private disableCache: boolean;
private disableCrossOriginPolicy: boolean;
private disableSameOriginPolicy: boolean;
#browserFrame: IBrowserFrame;
#window: BrowserWindow;
#unfilteredHeaders: Headers | null = null;
Expand All @@ -54,7 +54,7 @@ export default class SyncFetch {
* @param [options.redirectCount] Redirect count.
* @param [options.contentType] Content Type.
* @param [options.disableCache] Disables the use of cached responses. It will still store the response in the cache.
* @param [options.disableCrossOriginPolicy] Disables the Cross-Origin policy.
* @param [options.disableSameOriginPolicy] Disables the Same-Origin policy.
* @param [options.unfilteredHeaders] Unfiltered headers - necessary for preflight requests.
*/
constructor(options: {
Expand All @@ -65,7 +65,7 @@ export default class SyncFetch {
redirectCount?: number;
contentType?: string;
disableCache?: boolean;
disableCrossOriginPolicy?: boolean;
disableSameOriginPolicy?: boolean;
unfilteredHeaders?: Headers;
}) {
this.#browserFrame = options.browserFrame;
Expand All @@ -80,7 +80,10 @@ export default class SyncFetch {
}
this.redirectCount = options.redirectCount ?? 0;
this.disableCache = options.disableCache ?? false;
this.disableCrossOriginPolicy = options.disableCrossOriginPolicy ?? false;
this.disableSameOriginPolicy =
options.disableSameOriginPolicy ??
this.#browserFrame.page.context.browser.settings.fetch.disableSameOriginPolicy ??
false;
}

/**
Expand Down Expand Up @@ -118,7 +121,11 @@ export default class SyncFetch {
this.#window.location.protocol === 'https:'
) {
throw new this.#window.DOMException(
`Mixed Content: The page at '${this.#window.location.href}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${this.request.url}'. This request has been blocked; the content must be served over HTTPS.`,
`Mixed Content: The page at '${
this.#window.location.href
}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${
this.request.url
}'. This request has been blocked; the content must be served over HTTPS.`,
DOMExceptionNameEnum.securityError
);
}
Expand Down Expand Up @@ -177,7 +184,7 @@ export default class SyncFetch {
url: this.request.url,
init: { headers, method: cachedResponse.request.method },
disableCache: true,
disableCrossOriginPolicy: true
disableSameOriginPolicy: true
});

const validateResponse = <ISyncResponse>fetch.send();
Expand All @@ -199,7 +206,7 @@ export default class SyncFetch {
url: this.request.url,
init: { headers, method: cachedResponse.request.method },
disableCache: true,
disableCrossOriginPolicy: true
disableSameOriginPolicy: true
});
fetch.send().then((response) => {
response.buffer().then((body: Buffer) => {
Expand Down Expand Up @@ -236,7 +243,7 @@ export default class SyncFetch {
*/
private compliesWithCrossOriginPolicy(): boolean {
if (
this.disableCrossOriginPolicy ||
this.disableSameOriginPolicy ||
!FetchCORSUtility.isCORS(this.#window.location.href, this.request[PropertySymbol.url])
) {
return true;
Expand Down Expand Up @@ -288,7 +295,7 @@ export default class SyncFetch {
url: this.request.url,
init: { method: 'OPTIONS' },
disableCache: true,
disableCrossOriginPolicy: true,
disableSameOriginPolicy: true,
unfilteredHeaders: corsHeaders
});

Expand Down
64 changes: 64 additions & 0 deletions packages/happy-dom/test/fetch/Fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,70 @@ describe('Fetch', () => {
});
});

it('Allows cross-origin request if "Browser.settings.fetch.disableSameOriginPolicy" is set to "true".', async () => {
const originURL = 'http://localhost:8080';
const window = new Window({ url: originURL });
const url = 'http://other.origin.com/some/path';

window.happyDOM.settings.fetch.disableSameOriginPolicy = true;

let requestedUrl: string | null = null;
let postRequestHeaders: { [k: string]: string } | null = null;
let optionsRequestHeaders: { [k: string]: string } | null = null;

mockModule('http', {
request: (url, options) => {
requestedUrl = url;
if (options.method === 'OPTIONS') {
optionsRequestHeaders = options.headers;
} else if (options.method === 'POST') {
postRequestHeaders = options.headers;
}

return {
end: () => {},
on: (event: string, callback: (response: HTTP.IncomingMessage) => void) => {
if (event === 'response') {
async function* generate(): AsyncGenerator<string> {}

const response = <HTTP.IncomingMessage>Stream.Readable.from(generate());

response.headers = {};
response.rawHeaders = [];

callback(response);
}
},
setTimeout: () => {}
};
}
});

await window.fetch(url, {
method: 'POST',
body: '{"foo": "bar"}',
headers: {
'X-Custom-Header': 'yes',
'Content-Type': 'application/json'
}
});

expect(requestedUrl).toBe(url);
expect(optionsRequestHeaders).toBeNull();

expect(postRequestHeaders).toEqual({
Accept: '*/*',
Connection: 'close',
'Content-Type': 'application/json',
'Content-Length': '14',
'User-Agent': window.navigator.userAgent,
'Accept-Encoding': 'gzip, deflate, br',
Origin: originURL,
Referer: originURL + '/',
'X-Custom-Header': 'yes'
});
});

for (const httpCode of [301, 302, 303, 307, 308]) {
for (const method of ['GET', 'POST', 'PATCH']) {
it(`Should follow ${method} request redirect code ${httpCode}.`, async () => {
Expand Down
Loading

0 comments on commit 1625d40

Please sign in to comment.