Skip to content
Merged
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
63 changes: 41 additions & 22 deletions packages/all/test/cross-origin-iframe-packer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,35 +50,42 @@ interface IWindow extends Window {
}
type ExtraOptions = {
usePackFn?: boolean;
crossOrigin?: boolean;
};

async function injectRecordScript(
frame: puppeteer.Frame,
options?: ExtraOptions,
allowedOrigins?: string[],
) {
await frame.addScriptTag({
path: path.resolve(__dirname, '../dist/rrweb-all.umd.cjs'),
});
options = options || {};
await frame.evaluate((options) => {
(window as unknown as IWindow).snapshots = [];
const { record, pack } = (window as unknown as IWindow).rrweb;
const config: recordOptions<eventWithTime> = {
recordCrossOriginIframes: true,
recordCanvas: true,
emit(event) {
(window as unknown as IWindow).snapshots.push(event);
(window as unknown as IWindow).emit(event);
},
};
if (options.usePackFn) {
config.packFn = pack;
}
record(config);
}, options);
await frame.evaluate(
(options, allowedOrigins) => {
(window as unknown as IWindow).snapshots = [];
const { record, pack } = (window as unknown as IWindow).rrweb;
const config: recordOptions<eventWithTime> = {
recordCrossOriginIframes: !!allowedOrigins,
recordCanvas: true,
allowedIframeOrigins: allowedOrigins,
emit(event) {
(window as unknown as IWindow).snapshots.push(event);
(window as unknown as IWindow).emit(event);
},
};
if (options.usePackFn) {
config.packFn = pack;
}
record(config);
},
options,
allowedOrigins,
);

for (const child of frame.childFrames()) {
await injectRecordScript(child, options);
await injectRecordScript(child, options, allowedOrigins);
}
}

Expand All @@ -87,19 +94,24 @@ const setup = function (
content: string,
options?: ExtraOptions,
): ISuite {
const ctx = {} as ISuite;
const ctx = {} as ISuite & {
serverB: http.Server;
serverBURL: string;
};

beforeAll(async () => {
ctx.browser = await launchPuppeteer();
ctx.server = await startServer();
ctx.serverURL = getServerURL(ctx.server);
ctx.serverB = await startServer();
ctx.serverBURL = getServerURL(ctx.serverB);
});

beforeEach(async () => {
ctx.page = await ctx.browser.newPage();
await ctx.page.goto('about:blank');
await ctx.page.goto(`${ctx.serverURL}/html/blank.html`);
await ctx.page.setContent(
content.replace(/\{SERVER_URL\}/g, ctx.serverURL),
content.replace(/\{SERVER_URL\}/g, ctx.serverBURL),
);
ctx.events = [];
await ctx.page.exposeFunction('emit', (e: eventWithTime) => {
Expand All @@ -110,7 +122,10 @@ const setup = function (
});

ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
await injectRecordScript(ctx.page.mainFrame(), options);
const allowedOrigins = options?.crossOrigin
? [ctx.serverURL, ctx.serverBURL]
: undefined;
await injectRecordScript(ctx.page.mainFrame(), options, allowedOrigins);
});

afterEach(async () => {
Expand All @@ -120,6 +135,7 @@ const setup = function (
afterAll(async () => {
await ctx.browser.close();
ctx.server.close();
ctx.serverB.close();
});

return ctx;
Expand All @@ -137,7 +153,10 @@ describe('cross origin iframes & packer', function (this: ISuite) {
</body>
</html>
`;
const ctx = setup.call(this, content, { usePackFn: true });
const ctx = setup.call(this, content, {
usePackFn: true,
crossOrigin: true,
});

describe('should support packFn option in record()', () => {
it('', async () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/rrweb/src/record/cross-origin-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
function toOrigin(url: string): string | null {
try {
const origin = new URL(url).origin;
return origin !== 'null' ? origin : null;
} catch {
return null;
}
}

export function buildAllowedOriginSet(origins: string[]): ReadonlySet<string> {
if (!Array.isArray(origins) || origins.length === 0) {
throw new Error(
'[rrweb] allowedIframeOrigins must be a non-empty array of origin strings.',
);
}

const set = new Set<string>();
for (let i = 0; i < origins.length; i++) {
const entry = origins[i];
if (typeof entry !== 'string') {
throw new Error(
`[rrweb] allowedIframeOrigins[${i}] must be a string, got ${typeof entry}.`,
);
}
const origin = toOrigin(entry);
if (origin) {
set.add(origin);
}
}

return Object.freeze(set);
}
22 changes: 21 additions & 1 deletion packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
registerErrorHandler,
unregisterErrorHandler,
} from './error-handler';
import { buildAllowedOriginSet } from './cross-origin-utils';
import dom from '@rrweb/utils';

let wrappedEmit!: (e: eventWithoutTime, isCheckout?: boolean) => void;
Expand Down Expand Up @@ -93,6 +94,7 @@
recordDOM = true,
recordCanvas = false,
recordCrossOriginIframes = false,
allowedIframeOrigins,
recordAfter = options.recordAfter === 'DOMContentLoaded'
? options.recordAfter
: 'load',
Expand All @@ -107,6 +109,18 @@

registerErrorHandler(errorHandler);

let validatedOrigins: ReadonlySet<string> | undefined;
if (
recordCrossOriginIframes &&
allowedIframeOrigins &&
allowedIframeOrigins.length > 0
) {
validatedOrigins = buildAllowedOriginSet(allowedIframeOrigins);
if (validatedOrigins.size === 0) {
validatedOrigins = undefined;
}
}

const inEmittingFrame = recordCrossOriginIframes
? window.parent === window
: true;
Expand Down Expand Up @@ -231,7 +245,13 @@
origin: window.location.origin,
isCheckout,
};
window.parent.postMessage(message, '*');
if (validatedOrigins) {
for (const targetOrigin of validatedOrigins) {
window.parent.postMessage(message, targetOrigin);
}
} else {
window.parent.postMessage(message, '*');
}
}

if (e.type === EventType.FullSnapshot) {
Expand Down Expand Up @@ -562,7 +582,7 @@
plugins
?.filter((p) => p.observer)
?.map((p) => ({
observer: p.observer!,

Check warning on line 585 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Check and Report Upload

Forbidden non-null assertion

Check warning on line 585 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Check and Report Upload

Forbidden non-null assertion

Check warning on line 585 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/index.ts#L585

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
options: p.options,
callback: (payload: object) =>
wrappedEmit({
Expand All @@ -580,7 +600,7 @@

iframeManager.addLoadListener((iframeEl) => {
try {
const iframeDoc = iframeEl.contentDocument!;

Check warning on line 603 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Check and Report Upload

Forbidden non-null assertion

Check warning on line 603 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/index.ts#L603

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
const iframeHandler = observe(iframeDoc);
handlers.push(iframeHandler);

Expand Down
1 change: 1 addition & 0 deletions packages/rrweb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export type recordOptions<T> = {
recordDOM?: boolean;
recordCanvas?: boolean;
recordCrossOriginIframes?: boolean;
allowedIframeOrigins?: string[];
recordAfter?: 'DOMContentLoaded' | 'load';
userTriggeredOnInput?: boolean;
collectFonts?: boolean;
Expand Down
Loading
Loading