Skip to content

Commit

Permalink
fix(parseIsomorphicRequest): bypassing cookies properly (#1155)
Browse files Browse the repository at this point in the history
* fix(parseIsomorphicRequest): bypassing cookies properly

* fix(parseIsomorphicRequest): add 'same-origin' as fallback value to credentials

* chore(parseIsomorphicRequest): add coments and improve the test codes

* fix(setRequestCookies): prevent overriding existing cookies

Co-authored-by: Artem Zakharchenko <kettanaito@gmail.com>
  • Loading branch information
hrsh7th and kettanaito committed Mar 10, 2022
1 parent c3cd80a commit 755bc9d
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 34 deletions.
94 changes: 94 additions & 0 deletions src/utils/request/parseIsomorphicRequest.test.ts
@@ -0,0 +1,94 @@
/**
* @jest-environment jsdom
*/
import { Headers } from 'headers-polyfill/lib'
import { parseIsomorphicRequest } from './parseIsomorphicRequest'
import { createIsomorphicRequest } from '../../../test/support/utils'
import { MockedRequest } from '../../handlers/RequestHandler'

test('parses an isomorphic request', () => {
const request = parseIsomorphicRequest(
createIsomorphicRequest({
id: 'request-1',
method: 'POST',
url: new URL('https://example.com/resource'),
headers: new Headers({
'Content-Type': 'application/json',
Cookie: 'token=abc-123',
}),
body: JSON.stringify({
id: 'user-1',
}),
}),
)

expect(request).toStrictEqual<MockedRequest>({
id: 'request-1',
method: 'POST',
url: new URL('https://example.com/resource'),
headers: new Headers({
'Content-Type': 'application/json',
Cookie: 'token=abc-123',
}),
cookies: {
token: 'abc-123',
},
destination: 'document',
credentials: 'same-origin',
referrer: '',
referrerPolicy: 'no-referrer',
mode: 'cors',
keepalive: false,
integrity: '',
redirect: 'manual',
cache: 'default',
bodyUsed: false,
body: {
id: 'user-1',
},
})
})

test('preserves cookies for request with "same-origin" credentials', () => {
const request = parseIsomorphicRequest(
createIsomorphicRequest({
credentials: 'same-origin',
headers: new Headers({
Cookie: 'foo=bar',
}),
}),
)

expect(request).toMatchObject<Partial<MockedRequest>>({
credentials: 'same-origin',
cookies: { foo: 'bar' },
})
})

test('preserves cookies for request with "include" credentials', () => {
const request = parseIsomorphicRequest(
createIsomorphicRequest({
credentials: 'include',
headers: new Headers({
Cookie: 'foo=bar',
}),
}),
)

expect(request).toMatchObject<Partial<MockedRequest>>({
credentials: 'include',
cookies: { foo: 'bar' },
})
})

test('sets request "credentials" to "same-origin" when no credentials were provided', () => {
const request = parseIsomorphicRequest(
createIsomorphicRequest({
credentials: undefined,
}),
)

expect(request).toMatchObject<Partial<MockedRequest>>({
credentials: 'same-origin',
})
})
20 changes: 1 addition & 19 deletions src/utils/request/parseIsomorphicRequest.ts
@@ -1,4 +1,3 @@
import * as cookieUtils from 'cookie'
import { IsomorphicRequest } from '@mswjs/interceptors'
import { MockedRequest } from '../../handlers/RequestHandler'
import { parseBody } from './parseBody'
Expand All @@ -15,6 +14,7 @@ export function parseIsomorphicRequest(
url: request.url,
method: request.method,
body: parseBody(request.body, request.headers),
credentials: request.credentials || 'same-origin',
headers: request.headers,
cookies: {},
redirect: 'manual',
Expand All @@ -26,28 +26,10 @@ export function parseIsomorphicRequest(
integrity: '',
destination: 'document',
bodyUsed: false,
credentials: 'same-origin',
}

// Set mocked request cookies from the `cookie` header of the original request.
// No need to take `credentials` into account, because in Node.js requests are intercepted
// _after_ they happen. Request issuer should have already taken care of sending relevant cookies.
// Unlike browser, where interception is on the worker level, _before_ the request happens.
const requestCookiesString = request.headers.get('cookie')

// Attach all the cookies from the virtual cookie store.
setRequestCookies(mockedRequest)

const requestCookies = requestCookiesString
? cookieUtils.parse(requestCookiesString)
: {}

// Merge both direct request cookies and the cookies inherited
// from other same-origin requests in the cookie store.
mockedRequest.cookies = {
...mockedRequest.cookies,
...requestCookies,
}

return mockedRequest
}
120 changes: 120 additions & 0 deletions src/utils/request/setRequestCookies.test.ts
@@ -0,0 +1,120 @@
/**
* @jest-environment jsdom
*/
import { Headers } from 'headers-polyfill/lib'
import { setRequestCookies } from './setRequestCookies'
import { createMockedRequest } from '../../../test/support/utils'

beforeAll(() => {
document.cookie = ''
})

afterEach(() => {
document.cookie = ''
})

test('preserves request cookies when there are no document cookies to infer', () => {
const request = createMockedRequest({
headers: new Headers({
Cookie: 'token=abc-123',
}),
})
setRequestCookies(request)

expect(request.cookies).toEqual({
token: 'abc-123',
})
expect(request.headers.get('cookie')).toEqual('token=abc-123')
})

test('infers document cookies for request with "same-origin" credentials', () => {
// Emulate cookies already present on the document.
document.cookie = 'documentCookie=yes'

const request = createMockedRequest({
// Make the request to the same origin as the document location.
url: new URL('/resource', location.href),
headers: new Headers({
Cookie: 'token=abc-123',
}),
credentials: 'same-origin',
})
setRequestCookies(request)

expect(request.headers.get('cookie')).toEqual(
'token=abc-123, documentCookie=yes',
)
expect(request.cookies).toEqual({
// Cookies present in the document must be forwarded.
documentCookie: 'yes',
token: 'abc-123',
})
})

test('does not infer document cookies for request with "same-origin" credentials made to extraneous origin', () => {
// Emulate cookies already present on the document.
document.cookie = 'documentCookie=yes'

const request = createMockedRequest({
// Make the request to a different origin as the document location.
url: new URL('/resource', 'https://example.com'),
headers: new Headers({
Cookie: 'token=abc-123',
}),
credentials: 'same-origin',
})
setRequestCookies(request)

expect(request.headers.get('cookie')).toEqual('token=abc-123')
expect(request.cookies).toEqual({
token: 'abc-123',
})
})

test('infers document cookies for request with "include" credentials', () => {
// Emulate cookies already present on the document.
document.cookie = 'documentCookie=yes'

const request = createMockedRequest({
// Make the request to a different origin as the document location.
url: new URL('/resource', location.href),
headers: new Headers({
Cookie: 'token=abc-123',
}),
credentials: 'include',
})
setRequestCookies(request)

expect(request.headers.get('cookie')).toEqual(
'token=abc-123, documentCookie=yes',
)
expect(request.cookies).toEqual({
// Cookies present in the document must be forwarded regardless.
documentCookie: 'yes',
token: 'abc-123',
})
})

test('infers document cookies for request with "include" credentials made to extraneous origin', () => {
// Emulate cookies already present on the document.
document.cookie = 'documentCookie=yes'

const request = createMockedRequest({
// Make the request to a different origin as the document location.
url: new URL('/resource', 'https://example.com'),
headers: new Headers({
Cookie: 'token=abc-123',
}),
credentials: 'include',
})
setRequestCookies(request)

expect(request.headers.get('cookie')).toEqual(
'token=abc-123, documentCookie=yes',
)
expect(request.cookies).toEqual({
// Cookies present in the document must be forwarded regardless.
documentCookie: 'yes',
token: 'abc-123',
})
})
60 changes: 45 additions & 15 deletions src/utils/request/setRequestCookies.ts
@@ -1,23 +1,53 @@
import * as cookieUtils from 'cookie'
import { store } from '@mswjs/cookies'
import { MockedRequest } from '../../handlers/RequestHandler'
import { getRequestCookies } from './getRequestCookies'

export function setRequestCookies(request: MockedRequest) {
/**
* Sets relevant cookies on the request.
* Request cookies are taken from the following sources:
* - Immediate (own) request cookies (those in the "Cookie" request header);
* - From the `document.cookie` based on the request's `credentials` value;
* - From the internal cookie store that persists/hydrates cookies in Node.js
*/
export function setRequestCookies(request: MockedRequest): void {
// Set mocked request cookies from the `cookie` header of the original request.
// No need to take `credentials` into account, because in Node.js requests are intercepted
// _after_ they happen. Request issuer should have already taken care of sending relevant cookies.
// Unlike browser, where interception is on the worker level, _before_ the request happens.
const requestCookiesString = request.headers.get('cookie')

store.hydrate()
request.cookies = {
...getRequestCookies(request),
...Array.from(
store.get({ ...request, url: request.url.toString() })?.entries(),
).reduce(
(cookies, [name, { value }]) => Object.assign(cookies, { [name]: value }),
{},
),

const cookiesFromStore = Array.from(
store.get({ ...request, url: request.url.toString() })?.entries(),
).reduce((cookies, [name, { value }]) => {
return Object.assign(cookies, { [name.trim()]: value })
}, {})

const cookiesFromDocument = getRequestCookies(request)

const forwardedCookies = {
...cookiesFromDocument,
...cookiesFromStore,
}

request.headers.set(
'cookie',
Object.entries(request.cookies)
.map(([name, value]) => `${name}=${value}`)
.join('; '),
)
// Ensure the persisted (document) cookies are propagated to the request.
// Propagated the cookies persisted in the Cookuie Store to the request headers.
// This forwards relevant request cookies based on the request's credentials.
for (const [name, value] of Object.entries(forwardedCookies)) {
request.headers.append('cookie', `${name}=${value}`)
}

const ownCookies = requestCookiesString
? cookieUtils.parse(requestCookiesString)
: {}

console.log({ cookiesFromDocument, cookiesFromStore, ownCookies, request })

request.cookies = {
...request.cookies,
...forwardedCookies,
...ownCookies,
}
}
14 changes: 14 additions & 0 deletions test/support/utils.ts
Expand Up @@ -3,6 +3,7 @@ import { Headers } from 'headers-polyfill'
import { MockedRequest } from './../../src'
import { uuidv4 } from '../../src/utils/internal/uuidv4'
import { ChildProcess } from 'child_process'
import { IsomorphicRequest } from '@mswjs/interceptors'

export function sleep(duration: number) {
return new Promise((resolve) => {
Expand Down Expand Up @@ -40,6 +41,19 @@ export function createMockedRequest(
}
}

export function createIsomorphicRequest(
initialValues: Partial<IsomorphicRequest> = {},
): IsomorphicRequest {
return {
id: uuidv4(),
method: 'GET',
url: new URL('/', location.href),
headers: new Headers(),
credentials: 'same-origin',
...initialValues,
}
}

export function promisifyChildProcess(
child: ChildProcess,
options?: { pipeStdio: boolean },
Expand Down

0 comments on commit 755bc9d

Please sign in to comment.