Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"reporter-options": "configFile=.mocha-multi.json"
},
"devDependencies": {
"@adobe/eslint-config-helix": "3.0.16",
"@adobe/eslint-config-helix": "2.0.9",
"@adobe/fetch": "4.2.3",
"@cloudflare/workers-types": "4.20250510.0",
"@semantic-release/changelog": "6.0.3",
Expand Down
25 changes: 25 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,18 @@ async function handleImageRoute(req, env, ctx) {
});
}

/**
* Only paths under this prefix are served by the worker. www.aem.live's CDN
* routes exactly this (case-sensitive) prefix to the worker; everything else
* on www.aem.live is served by the aem.live origin directly.
*/
const RUM_PREFIX = '/tools/rum/';

/**
* Canonical public origin of the site the worker fronts.
*/
const CANONICAL_ORIGIN = 'https://www.aem.live';

/**
* Handle request
* @param {Request} request
Expand All @@ -440,6 +452,19 @@ async function handleImageRoute(req, env, ctx) {
const handleRequest = async (request, env, ctx) => {
const url = new URL(request.url);

// Do not serve a copy of the aem.live site on the worker's own hostnames
// (e.g. *.workers.dev), which gets flagged as phishing. Anything outside
// /tools/rum/ is redirected to the canonical site instead of proxied.
if (!url.pathname.startsWith(RUM_PREFIX)) {
return new Response('', {
status: 301,
headers: {
location: `${CANONICAL_ORIGIN}${url.pathname}${url.search}`,
'cache-control': 'public, max-age=3600',
},
});
}

if (url.pathname.startsWith('/tools/rum/_ogimage')) {
return handleImageRoute(request, env, ctx);
}
Expand Down
197 changes: 195 additions & 2 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,202 @@
*/

import assert from 'assert';
import worker from '../src/index.js';

const WORKER_HOST = 'https://rum-proxy-ci.adobeaem.workers.dev';
const ORIGIN_HOST = 'main--helix-website--adobe.aem.live';

const EXPLORER_HTML = '<html><head><title>RUM Explorer</title></head><body></body></html>';

describe('index tests', () => {
it('should pass', () => {
assert.ok(true);
/** @type {typeof globalThis.fetch} */
let originalFetch;
/** @type {Array<string>} */
let fetched;
/** @type {(url: URL) => Response} */
let respond;

const ctx = { waitUntil: () => {} };

beforeEach(() => {
originalFetch = globalThis.fetch;
fetched = [];
respond = () => new Response('not found', { status: 404 });
globalThis.fetch = async (input) => {
const url = new URL(input instanceof Request ? input.url : String(input));
fetched.push(url.href);
return respond(url);
};
});

afterEach(() => {
globalThis.fetch = originalFetch;
});

describe('paths outside /tools/rum/', () => {
['/', '/docs/', '/developer/tutorial', '/tools/rum', '/tools/rumx', '/tools/', '/TOOLS/rum/explorer.html'].forEach((path) => {
it(`redirects ${path} to www.aem.live without calling the origin`, async () => {
const resp = await worker.fetch(new Request(`${WORKER_HOST}${path}`), {}, ctx);
assert.strictEqual(resp.status, 301);
assert.strictEqual(resp.headers.get('location'), `https://www.aem.live${path}`);
assert.strictEqual(await resp.text(), '');
assert.deepStrictEqual(fetched, []);
});
});

it('keeps path and query in the redirect', async () => {
const resp = await worker.fetch(new Request(`${WORKER_HOST}/docs/setup?a=1&b=two%20words`), {}, ctx);
assert.strictEqual(resp.status, 301);
assert.strictEqual(resp.headers.get('location'), 'https://www.aem.live/docs/setup?a=1&b=two%20words');
assert.deepStrictEqual(fetched, []);
});

it('does not serve any branded site content', async () => {
respond = () => new Response('<html><head><title>Adobe Experience Manager</title></head></html>', {
status: 200,
headers: { 'content-type': 'text/html' },
});
const resp = await worker.fetch(new Request(`${WORKER_HOST}/`), {}, ctx);
assert.strictEqual(resp.status, 301);
assert.ok(!(await resp.text()).includes('Adobe'));
assert.deepStrictEqual(fetched, []);
});
});

describe('/tools/rum/explorer.html', () => {
it('still proxies to the origin and adds og meta tags', async () => {
respond = () => new Response(EXPLORER_HTML, {
status: 200,
headers: { 'content-type': 'text/html' },
});
const resp = await worker.fetch(new Request(`${WORKER_HOST}/tools/rum/explorer.html?domain=www.example.com&view=week`), {}, ctx);
assert.strictEqual(resp.status, 200);
assert.strictEqual(fetched.length, 1);
const upstream = new URL(fetched[0]);
assert.strictEqual(upstream.hostname, ORIGIN_HOST);
assert.strictEqual(upstream.pathname, '/tools/rum/explorer.html');
const html = await resp.text();
assert.ok(html.includes('<meta property="og:site_name" content="RUM Explorer" />'));
assert.ok(html.includes('<meta property="og:title" content="RUM Data for www.example.com" />'));
assert.ok(html.includes('<meta property="og:description" content="Weekly RUM data for www.example.com" />'));
assert.ok(html.includes('https://www.aem.live/tools/rum/_ogimage?domain=www.example.com&amp;view=week')
|| html.includes('https://www.aem.live/tools/rum/_ogimage?domain=www.example.com&view=week'));
});

it('escapes parameters in og meta tags', async () => {
respond = () => new Response(EXPLORER_HTML, { status: 200 });
const resp = await worker.fetch(new Request(`${WORKER_HOST}/tools/rum/explorer.html?domain=${encodeURIComponent('"><script>x</script>')}`), {}, ctx);
const html = await resp.text();
assert.ok(!html.includes('<script>x</script>'));
assert.ok(html.includes('&quot;&gt;&lt;script&gt;x&lt;/script&gt;'));
});

it('passes through non-ok origin responses', async () => {
respond = () => new Response('gone', { status: 404 });
const resp = await worker.fetch(new Request(`${WORKER_HOST}/tools/rum/explorer.html`), {}, ctx);
assert.strictEqual(resp.status, 404);
assert.strictEqual(fetched.length, 1);
});
});

describe('other /tools/rum/ assets', () => {
it('are proxied from the origin unchanged', async () => {
respond = () => new Response('export default 1;', {
status: 200,
headers: { 'content-type': 'application/javascript' },
});
const resp = await worker.fetch(new Request(`${WORKER_HOST}/tools/rum/elements/list-facet.js?v=1`), {}, ctx);
assert.strictEqual(resp.status, 200);
assert.strictEqual(await resp.text(), 'export default 1;');
assert.strictEqual(fetched.length, 1);
const upstream = new URL(fetched[0]);
assert.strictEqual(upstream.hostname, ORIGIN_HOST);
assert.strictEqual(upstream.pathname, '/tools/rum/elements/list-facet.js');
assert.strictEqual(upstream.search, '?v=1');
});
});

describe('/tools/rum/_cors', () => {
it('returns 400 for a missing url', async () => {
const resp = await worker.fetch(new Request(`${WORKER_HOST}/tools/rum/_cors`), {}, ctx);
assert.strictEqual(resp.status, 400);
assert.strictEqual(resp.headers.get('x-error'), 'invalid url');
assert.deepStrictEqual(fetched, []);
});

it('returns 403 for an invalid domainkey', async () => {
const target = 'https://www.example.com/data.json';
const resp = await worker.fetch(new Request(`${WORKER_HOST}/tools/rum/_cors?url=${encodeURIComponent(target)}&domainkey=bad`), {}, ctx);
assert.strictEqual(resp.status, 403);
assert.strictEqual(resp.headers.get('x-error'), 'invalid domainkey');
assert.strictEqual(fetched.length, 1);
assert.ok(fetched[0].startsWith('https://bundles.aem.page/domains/www.example.com'));
});

it('proxies the target with CORS headers for a valid domainkey', async () => {
const target = 'https://www.example.com/data.json';
respond = (url) => {
if (url.hostname === 'bundles.aem.page') {
return new Response('{}', { status: 200 });
}
return new Response('{"ok":true}', {
status: 200,
headers: { 'content-type': 'application/json' },
});
};
const resp = await worker.fetch(new Request(`${WORKER_HOST}/tools/rum/_cors?url=${encodeURIComponent(target)}&domainkey=good`), {}, ctx);
assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.headers.get('access-control-allow-origin'), '*');
assert.strictEqual(await resp.text(), '{"ok":true}');
assert.deepStrictEqual(fetched.map((u) => new URL(u).hostname), ['bundles.aem.page', 'www.example.com']);
});
});

describe('/tools/rum/_ogimage', () => {
it('returns 400 for missing domain or view', async () => {
const resp = await worker.fetch(new Request(`${WORKER_HOST}/tools/rum/_ogimage`), {}, ctx);
assert.strictEqual(resp.status, 400);
assert.strictEqual(resp.headers.get('x-error'), 'missing domain or view');
assert.deepStrictEqual(fetched, []);
});

it('serves a stored image from the bucket', async () => {
const keys = [];
const env = {
IMAGE_BUCKET: {
head: async (key) => {
keys.push(key);
return { customMetadata: { state: 'loaded' } };
},
get: async () => ({
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
httpMetadata: { contentType: 'image/jpeg' },
}),
},
};
const resp = await worker.fetch(new Request(`${WORKER_HOST}/tools/rum/_ogimage?view=week&domain=www.example.com`), env, ctx);
assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.headers.get('content-type'), 'image/jpeg');
assert.deepStrictEqual(keys, ['images/www.example.com/week/domain=www.example.com&view=week']);
assert.deepStrictEqual(fetched, []);
});

it('redirects to the default image while a screenshot is pending', async () => {
const waited = [];
const env = {
IMAGE_BUCKET: {
head: async () => ({ customMetadata: { state: 'pending' } }),
},
};
const resp = await worker.fetch(
new Request(`${WORKER_HOST}/tools/rum/_ogimage?view=week&domain=www.example.com`),
env,
{ waitUntil: (p) => waited.push(p) },
);
assert.strictEqual(resp.status, 302);
assert.ok(resp.headers.get('location').startsWith('https://www.aem.live/default-social.png'));
assert.strictEqual(waited.length, 1);
await waited[0];
});
});
});
34 changes: 34 additions & 0 deletions test/post-deploy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,40 @@ describe('Post-Deploy Tests', () => {
});
});

// On the worker's own hostname (*.workers.dev) the aem.live site must not be
// served; only /tools/rum/ is handled by the worker. On www.aem.live, these
// paths are served by the aem.live origin directly, not by this worker.
(ENVIRONMENT === 'production' ? describe.skip : describe)('No site proxy outside /tools/rum/', () => {
it('redirects /docs/ to www.aem.live instead of serving the site', async () => {
const response = await fetch(`https://${TEST_DOMAIN}/docs/?x=1`, { redirect: 'manual' });
assert.strictEqual(response.status, 301, 'Should return 301');
assert.strictEqual(response.headers.get('location'), 'https://www.aem.live/docs/?x=1');
const body = await response.text();
assert.ok(!body.includes('<html'), 'Should not contain an HTML page');
});

it('does not serve the aem.live home page on /', async () => {
const response = await fetch(`https://${TEST_DOMAIN}/`, { redirect: 'manual' });
// 301 from the worker, or 403 if Cloudflare's phishing interstitial
// still answers the root URL before the worker runs.
assert.ok(
[301, 403].includes(response.status),
`Should return 301 or 403, got ${response.status}`,
);
if (response.status === 301) {
assert.strictEqual(response.headers.get('location'), 'https://www.aem.live/');
}
const body = await response.text();
assert.ok(!body.includes('Adobe Experience Manager'), 'Should not serve the aem.live page');
});

it('still serves /tools/rum/_cors from the worker', async () => {
const response = await fetch(`https://${TEST_DOMAIN}/tools/rum/_cors`, { redirect: 'manual' });
assert.strictEqual(response.status, 400);
assert.strictEqual(response.headers.get('x-error'), 'invalid url');
});
});

// Keep the original simple test for backwards compatibility
it('passes', async () => {
assert.ok(true);
Expand Down
Loading