Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle set-cookie from a redirect response fixes #13 #14

Merged
merged 13 commits into from
Dec 11, 2022
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ jobs:
- name: Check runtime issues
run: deno run --reload mod.ts
- name: Run tests
run: deno test --allow-net=127.0.0.1
run: deno test --allow-net=127.0.0.1,localhost

10 changes: 6 additions & 4 deletions cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export class Cookie {

case "domain":
if (attrValue) {
const domain = parseURL(attrValue).host;
const domain = parseURL(attrValue).hostname;
if (domain) {
options.domain = domain;
}
Expand Down Expand Up @@ -356,8 +356,10 @@ export class Cookie {
}

if (this.domain) {
const host = urlObj.host; // 'host' includes port number, if specified
if (isSameDomainOrSubdomain(this.domain, host)) {
// according to rfc 6265 8.5. Weak Confidentiality,
// port should not matter, hence the usage of 'hostname' over 'host'
const hostname = urlObj.hostname; // 'host' includes port number, if specified, hostname does not
if (isSameDomainOrSubdomain(this.domain, hostname)) {
return true;
}
}
Expand All @@ -370,7 +372,7 @@ export class Cookie {
}

setDomain(url: string | Request | URL) {
this.domain = parseURL(url).host;
this.domain = parseURL(url).hostname;
}

setPath(url: string | Request | URL) {
Expand Down
119 changes: 103 additions & 16 deletions fetch_wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,136 @@
import { CookieJar } from "./cookie_jar.ts";

// Max 20 redirects is fetch default setting
const MAX_REDIRECT = 20;

export type WrapFetchOptions = {
/** your own fetch function. defaults to global fetch. This allows wrapping your fetch function multiple times. */
fetch?: typeof fetch;
/** The cookie jar to use when wrapping fetch. Will create a new one if not provided. */
cookieJar?: CookieJar;
};

type FetchParameters = Parameters<typeof fetch>;
interface ExtendedRequestInit extends RequestInit {
redirectCount?: number;
}

const redirectStatus = new Set([301, 302, 303, 307, 308]);

function isRedirect(status: number): boolean {
return redirectStatus.has(status);
}

// Credit <https://github.com/node-fetch/node-fetch/blob/5e78af3ba7555fa1e466e804b2e51c5b687ac1a2/src/utils/is.js#L68>.
function isDomainOrSubdomain(destination: string, original: string): boolean {
const orig = new URL(original).hostname;
const dest = new URL(destination).hostname;

return orig === dest || orig.endsWith(`.${dest}`);
}

export function wrapFetch(options?: WrapFetchOptions): typeof fetch {
const { cookieJar = new CookieJar(), fetch = globalThis.fetch } = options ||
{};

async function wrappedFetch(
input: FetchParameters[0],
init?: FetchParameters[1],
) {
input: RequestInfo | URL,
init?: ExtendedRequestInit,
): Promise<Response> {
// let fetch handle the error
if (!input) {
return await fetch(input);
}
const cookieString = cookieJar.getCookieString(input);

let interceptedInit: RequestInit;
if (init) {
interceptedInit = init;
} else if (input instanceof Request) {
interceptedInit = input;
} else {
interceptedInit = {};
let originalRedirectOption: ExtendedRequestInit["redirect"];
const originalRequestUrl: string = (input as Request).url ||
input.toString();

if (input instanceof Request) {
originalRedirectOption = input.redirect;
}
if (init?.redirect) {
originalRedirectOption = init?.redirect;
}

if (!(interceptedInit.headers instanceof Headers)) {
interceptedInit.headers = new Headers(interceptedInit.headers || {});
const interceptedInit: ExtendedRequestInit = {
...init,
redirect: "manual",
};

const reqHeaders = new Headers((input as Request).headers || {});

if (init?.headers) {
new Headers(init.headers).forEach((value, key) => {
reqHeaders.set(key, value);
});
}
interceptedInit.headers.set("cookie", cookieString);

const response = await fetch(input, interceptedInit);
reqHeaders.set("cookie", cookieString);
reqHeaders.delete("cookie2"); // Remove cookie2 if it exists, It's deprecated

interceptedInit.headers = reqHeaders;

const response = await fetch(input, interceptedInit as RequestInit);

response.headers.forEach((value, key) => {
if (key.toLowerCase() === "set-cookie") {
cookieJar.setCookie(value, response.url);
}
});
return response;

const redirectCount = interceptedInit.redirectCount ?? 0;
const redirectUrl = response.headers.get("location");

// Do this check here to allow tail recursion of redirect.
if (redirectCount > 0) {
Object.defineProperty(response, "redirected", { value: true });
}

if (
// Return if response is not redirect
!isRedirect(response.status) ||
// or location is not set
!redirectUrl ||
// or if it's the first request and request.redirect is set to 'manual'
(redirectCount === 0 && originalRedirectOption === "manual")
) {
return response;
}

if (originalRedirectOption === "error") {
await response.body?.cancel();
throw new TypeError(
`URI requested responded with a redirect and redirect mode is set to error: ${response.url}`,
);
}

// If maximum redirects are reached throw error
if (redirectCount >= MAX_REDIRECT) {
await response.body?.cancel();
throw new TypeError(
`Reached maximum redirect of ${MAX_REDIRECT} for URL: ${response.url}`,
);
}

await response.body?.cancel();

interceptedInit.redirectCount = redirectCount + 1;

const filteredHeaders = new Headers(interceptedInit.headers);

// Do not forward sensitive headers to third-party domains.
if (!isDomainOrSubdomain(originalRequestUrl, redirectUrl)) {
for (
const name of ["authorization", "www-authenticate"] // cookie headers are handled differently
) {
filteredHeaders.delete(name);
}
}

interceptedInit.headers = filteredHeaders;

return await wrappedFetch(redirectUrl, interceptedInit as RequestInit);
}

return wrappedFetch;
Expand Down
Loading