node-fetch/test/referrer.js

553 lines
19 KiB
JavaScript

import chai from 'chai';
import fetch, {Request, Headers} from '../src/index.js';
import {
DEFAULT_REFERRER_POLICY, ReferrerPolicy, stripURLForUseAsAReferrer, validateReferrerPolicy,
isOriginPotentiallyTrustworthy, isUrlPotentiallyTrustworthy, determineRequestsReferrer,
parseReferrerPolicyFromHeader
} from '../src/utils/referrer.js';
import TestServer from './utils/server.js';
const {expect} = chai;
describe('fetch() with referrer and referrerPolicy', () => {
const local = new TestServer();
let base;
before(async () => {
await local.start();
base = `http://${local.hostname}:${local.port}/`;
});
after(async () => {
return local.stop();
});
it('should send request without a referrer by default', () => {
return fetch(`${base}inspect`).then(res => res.json()).then(res => {
expect(res.headers.referer).to.be.undefined;
});
});
it('should send request with a referrer', () => {
return fetch(`${base}inspect`, {
referrer: base,
referrerPolicy: 'unsafe-url'
}).then(res => res.json()).then(res => {
expect(res.headers.referer).to.equal(base);
});
});
it('should send request with referrerPolicy strict-origin-when-cross-origin by default', () => {
return Promise.all([
fetch(`${base}inspect`, {
referrer: base
}).then(res => res.json()).then(res => {
expect(res.headers.referer).to.equal(base);
}),
fetch(`${base}inspect`, {
referrer: 'https://example.com'
}).then(res => res.json()).then(res => {
expect(res.headers.referer).to.be.undefined;
})
]);
});
it('should send request with a referrer and respect redirected referrer-policy', () => {
return Promise.all([
fetch(`${base}redirect/referrer-policy`, {
referrer: base
}).then(res => res.json()).then(res => {
expect(res.headers.referer).to.equal(base);
}),
fetch(`${base}redirect/referrer-policy`, {
referrer: 'https://example.com'
}).then(res => res.json()).then(res => {
expect(res.headers.referer).to.be.undefined;
}),
fetch(`${base}redirect/referrer-policy`, {
referrer: 'https://example.com',
referrerPolicy: 'unsafe-url'
}).then(res => res.json()).then(res => {
expect(res.headers.referer).to.equal('https://example.com/');
}),
fetch(`${base}redirect/referrer-policy/same-origin`, {
referrer: 'https://example.com',
referrerPolicy: 'unsafe-url'
}).then(res => res.json()).then(res => {
expect(res.headers.referer).to.undefined;
})
]);
});
});
describe('Request constructor', () => {
describe('referrer', () => {
it('should leave referrer undefined by default', () => {
const req = new Request('http://example.com');
expect(req.referrer).to.be.undefined;
});
it('should accept empty string referrer as no-referrer', () => {
const referrer = '';
const req = new Request('http://example.com', {referrer});
expect(req.referrer).to.equal(referrer);
});
it('should accept about:client referrer as client', () => {
const referrer = 'about:client';
const req = new Request('http://example.com', {referrer});
expect(req.referrer).to.equal(referrer);
});
it('should accept about://client referrer as client', () => {
const req = new Request('http://example.com', {referrer: 'about://client'});
expect(req.referrer).to.equal('about:client');
});
it('should accept a string URL referrer', () => {
const referrer = 'http://example.com/';
const req = new Request('http://example.com', {referrer});
expect(req.referrer).to.equal(referrer);
});
it('should accept a URL referrer', () => {
const referrer = new URL('http://example.com');
const req = new Request('http://example.com', {referrer});
expect(req.referrer).to.equal(referrer.toString());
});
it('should accept a referrer from input', () => {
const referrer = 'http://example.com/';
const req = new Request(new Request('http://example.com', {referrer}));
expect(req.referrer).to.equal(referrer.toString());
});
it('should throw a TypeError for an invalid URL', () => {
expect(() => {
const req = new Request('http://example.com', {referrer: 'foobar'});
expect.fail(req);
}).to.throw(TypeError, /Invalid URL/);
});
});
describe('referrerPolicy', () => {
it('should default refererPolicy to empty string', () => {
const req = new Request('http://example.com');
expect(req.referrerPolicy).to.equal('');
});
it('should accept refererPolicy', () => {
const referrerPolicy = 'unsafe-url';
const req = new Request('http://example.com', {referrerPolicy});
expect(req.referrerPolicy).to.equal(referrerPolicy);
});
it('should accept referrerPolicy from input', () => {
const referrerPolicy = 'unsafe-url';
const req = new Request(new Request('http://example.com', {referrerPolicy}));
expect(req.referrerPolicy).to.equal(referrerPolicy);
});
it('should throw a TypeError for an invalid referrerPolicy', () => {
expect(() => {
const req = new Request('http://example.com', {referrerPolicy: 'foobar'});
expect.fail(req);
}).to.throw(TypeError, 'Invalid referrerPolicy: foobar');
});
});
});
describe('utils/referrer', () => {
it('default policy should be strict-origin-when-cross-origin', () => {
expect(DEFAULT_REFERRER_POLICY).to.equal('strict-origin-when-cross-origin');
});
describe('stripURLForUseAsAReferrer', () => {
it('should return no-referrer for null/undefined URL', () => {
expect(stripURLForUseAsAReferrer(undefined)).to.equal('no-referrer');
expect(stripURLForUseAsAReferrer(null)).to.equal('no-referrer');
});
it('should return no-referrer for about:, blob:, and data: URLs', () => {
expect(stripURLForUseAsAReferrer('about:client')).to.equal('no-referrer');
expect(stripURLForUseAsAReferrer('blob:theblog')).to.equal('no-referrer');
expect(stripURLForUseAsAReferrer('data:,thedata')).to.equal('no-referrer');
});
it('should strip the username, password, and hash', () => {
const urlStr = 'http://foo:[email protected]/foo?q=search#theanchor';
expect(stripURLForUseAsAReferrer(urlStr).toString())
.to.equal('http://example.com/foo?q=search');
});
it('should strip the pathname and query when origin-only', () => {
const urlStr = 'http://foo:[email protected]/foo?q=search#theanchor';
expect(stripURLForUseAsAReferrer(urlStr, true).toString())
.to.equal('http://example.com/');
});
});
describe('validateReferrerPolicy', () => {
it('should return the referrer policy', () => {
for (const referrerPolicy of ReferrerPolicy) {
expect(validateReferrerPolicy(referrerPolicy)).to.equal(referrerPolicy);
}
});
it('should throw a TypeError for invalid referrer policies', () => {
expect(validateReferrerPolicy.bind(null, undefined))
.to.throw(TypeError, 'Invalid referrerPolicy: undefined');
expect(validateReferrerPolicy.bind(null, null))
.to.throw(TypeError, 'Invalid referrerPolicy: null');
expect(validateReferrerPolicy.bind(null, false))
.to.throw(TypeError, 'Invalid referrerPolicy: false');
expect(validateReferrerPolicy.bind(null, 0))
.to.throw(TypeError, 'Invalid referrerPolicy: 0');
expect(validateReferrerPolicy.bind(null, 'always'))
.to.throw(TypeError, 'Invalid referrerPolicy: always');
});
});
const testIsOriginPotentiallyTrustworthyStatements = func => {
it('should be potentially trustworthy for HTTPS and WSS URLs', () => {
expect(func(new URL('https://example.com'))).to.be.true;
expect(func(new URL('wss://example.com'))).to.be.true;
});
it('should be potentially trustworthy for loopback IP address URLs', () => {
expect(func(new URL('http://127.0.0.1'))).to.be.true;
expect(func(new URL('http://127.1.2.3'))).to.be.true;
expect(func(new URL('ws://[::1]'))).to.be.true;
});
it('should not be potentially trustworthy for "localhost" URLs', () => {
expect(func(new URL('http://localhost'))).to.be.false;
});
it('should be potentially trustworthy for file: URLs', () => {
expect(func(new URL('file://foo/bar'))).to.be.true;
});
it('should not be potentially trustworthy for all other origins', () => {
expect(func(new URL('http://example.com'))).to.be.false;
expect(func(new URL('ws://example.com'))).to.be.false;
});
};
describe('isOriginPotentiallyTrustworthy', () => {
testIsOriginPotentiallyTrustworthyStatements(isOriginPotentiallyTrustworthy);
});
describe('isUrlPotentiallyTrustworthy', () => {
it('should be potentially trustworthy for about:blank and about:srcdoc', () => {
expect(isUrlPotentiallyTrustworthy(new URL('about:blank'))).to.be.true;
expect(isUrlPotentiallyTrustworthy(new URL('about:srcdoc'))).to.be.true;
});
it('should be potentially trustworthy for data: URLs', () => {
expect(isUrlPotentiallyTrustworthy(new URL('data:,thedata'))).to.be.true;
});
it('should be potentially trustworthy for blob: and filesystem: URLs', () => {
expect(isUrlPotentiallyTrustworthy(new URL('blob:theblob'))).to.be.true;
expect(isUrlPotentiallyTrustworthy(new URL('filesystem:thefilesystem'))).to.be.true;
});
testIsOriginPotentiallyTrustworthyStatements(isUrlPotentiallyTrustworthy);
});
describe('determineRequestsReferrer', () => {
it('should return null for no-referrer or empty referrerPolicy', () => {
expect(determineRequestsReferrer({referrer: 'no-referrer'})).to.be.null;
expect(determineRequestsReferrer({referrerPolicy: ''})).to.be.null;
});
it('should return no-referrer for about:client', () => {
expect(determineRequestsReferrer({
referrer: 'about:client',
referrerPolicy: DEFAULT_REFERRER_POLICY
})).to.equal('no-referrer');
});
it('should return just the origin for URLs over 4096 characters', () => {
expect(determineRequestsReferrer({
url: 'http://foo:[email protected]/foo?q=search#theanchor',
referrer: `http://example.com/${'0'.repeat(4096)}`,
referrerPolicy: DEFAULT_REFERRER_POLICY
}).toString()).to.equal('http://example.com/');
});
it('should alter the referrer URL by callback', () => {
expect(determineRequestsReferrer({
url: 'http://foo:[email protected]/foo?q=search#theanchor',
referrer: 'http://foo:[email protected]/foo?q=search#theanchor',
referrerPolicy: 'unsafe-url'
}, {
referrerURLCallback: referrerURL => {
return new URL(referrerURL.toString().replace(/^http:/, 'myprotocol:'));
}
}).toString()).to.equal('myprotocol://example.com/foo?q=search');
});
it('should alter the referrer origin by callback', () => {
expect(determineRequestsReferrer({
url: 'http://foo:[email protected]/foo?q=search#theanchor',
referrer: 'http://foo:[email protected]/foo?q=search#theanchor',
referrerPolicy: 'origin'
}, {
referrerOriginCallback: referrerOrigin => {
return new URL(referrerOrigin.toString().replace(/^http:/, 'myprotocol:'));
}
}).toString()).to.equal('myprotocol://example.com/');
});
it('should throw a TypeError for an invalid policy', () => {
expect(() => {
determineRequestsReferrer({
url: 'http://foo:[email protected]/foo?q=search#theanchor',
referrer: 'http://foo:[email protected]/foo?q=search#theanchor',
referrerPolicy: 'always'
});
}).to.throw(TypeError, 'Invalid referrerPolicy: always');
});
const referrerPolicyTestLabel = ({currentURLTrust, referrerURLTrust, sameOrigin}) => {
if (currentURLTrust === null && referrerURLTrust === null && sameOrigin === null) {
return 'Always';
}
const result = [];
if (currentURLTrust !== null) {
result.push(`Current URL is ${currentURLTrust ? '' : 'not '}potentially trustworthy`);
}
if (referrerURLTrust !== null) {
result.push(`Referrer URL is ${referrerURLTrust ? '' : 'not '}potentially trustworthy`);
}
if (sameOrigin !== null) {
result.push(`Current URL & Referrer URL do ${sameOrigin ? '' : 'not '}have same origin`);
}
return result.join(', ');
};
const referrerPolicyTests = (referrerPolicy, matrix) => {
describe(`Referrer policy: ${referrerPolicy}`, () => {
for (const {currentURLTrust, referrerURLTrust, sameOrigin, result} of matrix) {
describe(referrerPolicyTestLabel({currentURLTrust, referrerURLTrust, sameOrigin}), () => {
const requests = [];
if (sameOrigin === true || sameOrigin === null) {
requests.push({
referrerPolicy,
url: 'http://foo:[email protected]/foo?q=search#theanchor',
referrer: 'http://foo:[email protected]/foo?q=search#theanchor'
});
}
if (sameOrigin === false || sameOrigin === null) {
requests.push({
referrerPolicy,
url: 'http://foo:[email protected]/foo?q=search#theanchor',
referrer: 'http://foo:[email protected]/foo?q=search#theanchor'
});
}
let requestsLength = requests.length;
switch (currentURLTrust) {
case null:
for (let i = 0; i < requestsLength; i++) {
const req = requests[i];
requests.push({...req, url: req.url.replace(/^http:/, 'https:')});
}
break;
case true:
for (let i = 0; i < requestsLength; i++) {
const req = requests[i];
req.url = req.url.replace(/^http:/, 'https:');
}
break;
case false:
// nothing to do, default is not potentially trustworthy
break;
default:
throw new TypeError(`Invalid currentURLTrust condition: ${currentURLTrust}`);
}
requestsLength = requests.length;
switch (referrerURLTrust) {
case null:
for (let i = 0; i < requestsLength; i++) {
const req = requests[i];
if (sameOrigin) {
if (req.url.startsWith('https:')) {
requests.splice(i, 1);
} else {
continue;
}
}
requests.push({...req, referrer: req.referrer.replace(/^http:/, 'https:')});
}
break;
case true:
for (let i = 0; i < requestsLength; i++) {
const req = requests[i];
req.referrer = req.referrer.replace(/^http:/, 'https:');
}
break;
case false:
// nothing to do, default is not potentially trustworthy
break;
default:
throw new TypeError(`Invalid referrerURLTrust condition: ${referrerURLTrust}`);
}
it('should have tests', () => {
expect(requests).to.not.be.empty;
});
for (const req of requests) {
it(`should return ${result} for url: ${req.url}, referrer: ${req.referrer}`, () => {
if (result === 'no-referrer') {
return expect(determineRequestsReferrer(req).toString())
.to.equal('no-referrer');
}
if (result === 'referrer-origin') {
const referrerOrigih = stripURLForUseAsAReferrer(req.referrer, true);
return expect(determineRequestsReferrer(req).toString())
.to.equal(referrerOrigih.toString());
}
if (result === 'referrer-url') {
const referrerURL = stripURLForUseAsAReferrer(req.referrer);
return expect(determineRequestsReferrer(req).toString())
.to.equal(referrerURL.toString());
}
throw new TypeError(`Invalid result: ${result}`);
});
}
});
}
});
};
// 3.1 no-referrer
referrerPolicyTests('no-referrer', [
{currentURLTrust: null, referrerURLTrust: null, sameOrigin: null, result: 'no-referrer'}
]);
// 3.2 no-referrer-when-downgrade
referrerPolicyTests('no-referrer-when-downgrade', [
{currentURLTrust: false, referrerURLTrust: true, sameOrigin: null, result: 'no-referrer'},
{currentURLTrust: null, referrerURLTrust: false, sameOrigin: null, result: 'referrer-url'},
{currentURLTrust: true, referrerURLTrust: true, sameOrigin: null, result: 'referrer-url'}
]);
// 3.3 same-origin
referrerPolicyTests('same-origin', [
{currentURLTrust: null, referrerURLTrust: null, sameOrigin: false, result: 'no-referrer'},
{currentURLTrust: null, referrerURLTrust: null, sameOrigin: true, result: 'referrer-url'}
]);
// 3.4 origin
referrerPolicyTests('origin', [
{currentURLTrust: null, referrerURLTrust: null, sameOrigin: null, result: 'referrer-origin'}
]);
// 3.5 strict-origin
referrerPolicyTests('strict-origin', [
{currentURLTrust: false, referrerURLTrust: true, sameOrigin: null, result: 'no-referrer'},
{currentURLTrust: null, referrerURLTrust: false, sameOrigin: null, result: 'referrer-origin'},
{currentURLTrust: true, referrerURLTrust: true, sameOrigin: null, result: 'referrer-origin'}
]);
// 3.6 origin-when-cross-origin
referrerPolicyTests('origin-when-cross-origin', [
{currentURLTrust: null, referrerURLTrust: null, sameOrigin: false, result: 'referrer-origin'},
{currentURLTrust: null, referrerURLTrust: null, sameOrigin: true, result: 'referrer-url'}
]);
// 3.7 strict-origin-when-cross-origin
referrerPolicyTests('strict-origin-when-cross-origin', [
{currentURLTrust: false, referrerURLTrust: true, sameOrigin: false, result: 'no-referrer'},
{currentURLTrust: null, referrerURLTrust: false, sameOrigin: false,
result: 'referrer-origin'},
{currentURLTrust: true, referrerURLTrust: true, sameOrigin: false, result: 'referrer-origin'},
{currentURLTrust: null, referrerURLTrust: null, sameOrigin: true, result: 'referrer-url'}
]);
// 3.8 unsafe-url
referrerPolicyTests('unsafe-url', [
{currentURLTrust: null, referrerURLTrust: null, sameOrigin: null, result: 'referrer-url'}
]);
});
describe('parseReferrerPolicyFromHeader', () => {
it('should return an empty string when no referrer policy is found', () => {
expect(parseReferrerPolicyFromHeader(new Headers())).to.equal('');
expect(parseReferrerPolicyFromHeader(
new Headers([['Referrer-Policy', '']])
)).to.equal('');
});
it('should return the last valid referrer policy', () => {
expect(parseReferrerPolicyFromHeader(
new Headers([['Referrer-Policy', 'no-referrer']])
)).to.equal('no-referrer');
expect(parseReferrerPolicyFromHeader(
new Headers([['Referrer-Policy', 'no-referrer unsafe-url']])
)).to.equal('unsafe-url');
expect(parseReferrerPolicyFromHeader(
new Headers([['Referrer-Policy', 'foo no-referrer bar']])
)).to.equal('no-referrer');
expect(parseReferrerPolicyFromHeader(
new Headers([['Referrer-Policy', 'foo no-referrer unsafe-url bar']])
)).to.equal('unsafe-url');
});
it('should use all Referrer-Policy headers', () => {
expect(parseReferrerPolicyFromHeader(
new Headers([
['Referrer-Policy', 'no-referrer'],
['Referrer-Policy', '']
])
)).to.equal('no-referrer');
expect(parseReferrerPolicyFromHeader(
new Headers([
['Referrer-Policy', 'no-referrer'],
['Referrer-Policy', 'unsafe-url']
])
)).to.equal('unsafe-url');
expect(parseReferrerPolicyFromHeader(
new Headers([
['Referrer-Policy', 'no-referrer foo'],
['Referrer-Policy', 'bar unsafe-url wow']
])
)).to.equal('unsafe-url');
expect(parseReferrerPolicyFromHeader(
new Headers([
['Referrer-Policy', 'no-referrer unsafe-url'],
['Referrer-Policy', 'foo bar']
])
)).to.equal('unsafe-url');
});
});
});