Skip to content

Commit

Permalink
Merge pull request #14 from maccyber/get_cookies_from_redirected_resp…
Browse files Browse the repository at this point in the history
…onse

Handle set-cookie from a redirect response fixes #13
use `URL::hostname` instead of `URL::host` according to RFC ports don't matter
improve tests
prevent leaking sensitive headers to other domains after redirect
  • Loading branch information
jd1378 authored Dec 11, 2022
2 parents 9dd180a + f55f2a4 commit 96f2428
Show file tree
Hide file tree
Showing 4 changed files with 409 additions and 37 deletions.
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

0 comments on commit 96f2428

Please sign in to comment.