iframe protector: track referrer and refactor a bit for maintability (#3534)

- adds `document.referrer` to tracking to get a sense of who's embedding
us
- refactors the hardcoded strings for maintainbility
pull/3192/head
Mime Čuvalo 2024-04-23 11:04:39 +01:00 zatwierdzone przez GitHub
rodzic d96389418a
commit 952fe910d3
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
10 zmienionych plików z 75 dodań i 50 usunięć

Wyświetl plik

@ -8,7 +8,7 @@ import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
/*
If we're in an iframe, we need to figure out whether we're on a whitelisted host (e.g. tldraw itself)
or a not-allowed host (e.g. someone else's website). Some websites embed tldraw in iframes and this is kinda
risky for us and for them, tooand hey, if we decide to offer a hosted thing, then that's another stor
risky for us and for them, tooand hey, if we decide to offer a hosted thing, then that's another story.
Figuring this out is a little tricky because the same code here is going to run on:
- the website as a top window (tldraw-top)
@ -27,8 +27,30 @@ and we should show an annoying messsage.
If we're not in an iframe, we don't need to do anything.
*/
export const ROOM_CONTEXT = {
PUBLIC_MULTIPLAYER: 'public-multiplayer',
PUBLIC_READONLY: 'public-readonly',
PUBLIC_SNAPSHOT: 'public-snapshot',
HISTORY_SNAPSHOT: 'history-snapshot',
HISTORY: 'history',
LOCAL: 'local',
} as const
type $ROOM_CONTEXT = (typeof ROOM_CONTEXT)[keyof typeof ROOM_CONTEXT]
const EMBEDDED_STATE = {
IFRAME_UNKNOWN: 'iframe-unknown',
IFRAME_NOT_ALLOWED: 'iframe-not-allowed',
NOT_IFRAME: 'not-iframe',
IFRAME_OK: 'iframe-ok',
} as const
type $EMBEDDED_STATE = (typeof EMBEDDED_STATE)[keyof typeof EMBEDDED_STATE]
// Which routes do we allow to be embedded in tldraw.com itself?
const WHITELIST_CONTEXT = ['public-multiplayer', 'public-readonly', 'public-snapshot']
const WHITELIST_CONTEXT: $ROOM_CONTEXT[] = [
ROOM_CONTEXT.PUBLIC_MULTIPLAYER,
ROOM_CONTEXT.PUBLIC_READONLY,
ROOM_CONTEXT.PUBLIC_SNAPSHOT,
]
const EXPECTED_QUESTION = 'are we cool?'
const EXPECTED_RESPONSE = 'yes' + version
@ -38,18 +60,12 @@ export function IFrameProtector({
children,
}: {
slug: string
context:
| 'public-multiplayer'
| 'public-readonly'
| 'public-snapshot'
| 'history-snapshot'
| 'history'
| 'local'
context: $ROOM_CONTEXT
children: ReactNode
}) {
const [embeddedState, setEmbeddedState] = useState<
'iframe-unknown' | 'iframe-not-allowed' | 'not-iframe' | 'iframe-ok'
>(isInIframe() ? 'iframe-unknown' : 'not-iframe')
const [embeddedState, setEmbeddedState] = useState<$EMBEDDED_STATE>(
isInIframe() ? EMBEDDED_STATE.IFRAME_UNKNOWN : EMBEDDED_STATE.NOT_IFRAME
)
const url = useUrl()
@ -73,24 +89,33 @@ export function IFrameProtector({
if (event.data === EXPECTED_RESPONSE) {
// todo: check the origin?
setEmbeddedState('iframe-ok')
setEmbeddedState(EMBEDDED_STATE.IFRAME_OK)
clearTimeout(timeout)
}
}
window.addEventListener('message', handleMessageEvent, false)
if (embeddedState === 'iframe-unknown') {
if (embeddedState === EMBEDDED_STATE.IFRAME_UNKNOWN) {
// We iframe embeddings on multiplayer or readonly
if (WHITELIST_CONTEXT.includes(context)) {
window.parent.postMessage(EXPECTED_QUESTION, '*') // todo: send to a specific origin?
timeout = setTimeout(() => {
setEmbeddedState('iframe-not-allowed')
trackAnalyticsEvent('connect_to_room_in_iframe', { slug, context })
setEmbeddedState(EMBEDDED_STATE.IFRAME_NOT_ALLOWED)
const referrer = document.referrer
const ancestorOrigins = JSON.stringify(
Object.values(window.location.ancestorOrigins || {})
)
trackAnalyticsEvent('connect_to_room_in_iframe', {
slug,
context,
referrer,
ancestorOrigins,
})
}, 1000)
} else {
// We don't allow iframe embeddings on other routes
setEmbeddedState('iframe-not-allowed')
setEmbeddedState(EMBEDDED_STATE.IFRAME_NOT_ALLOWED)
}
}
@ -100,12 +125,12 @@ export function IFrameProtector({
}
}, [embeddedState, slug, context])
if (embeddedState === 'iframe-unknown') {
if (embeddedState === EMBEDDED_STATE.IFRAME_UNKNOWN) {
// We're in an iframe, but we don't know if it's a tldraw iframe
return <LoadingScreen>Loading in an iframe...</LoadingScreen>
return <LoadingScreen>Loading in an iframe</LoadingScreen>
}
if (embeddedState === 'iframe-not-allowed') {
if (embeddedState === EMBEDDED_STATE.IFRAME_NOT_ALLOWED) {
// We're in an iframe and its not one of ours
return (
<div className="tldraw__editor tl-container">

Wyświetl plik

@ -2,7 +2,7 @@ import { RoomSnapshot } from '@tldraw/tlsync'
import '../../styles/globals.css'
import { BoardHistorySnapshot } from '../components/BoardHistorySnapshot/BoardHistorySnapshot'
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { defineLoader } from '../utils/defineLoader'
const { loader, useData } = defineLoader(async (args) => {
@ -38,7 +38,7 @@ export function Component() {
const { data, roomId, timestamp } = result
return (
<IFrameProtector slug={roomId} context="history-snapshot">
<IFrameProtector slug={roomId} context={ROOM_CONTEXT.HISTORY_SNAPSHOT}>
<BoardHistorySnapshot data={data} roomId={roomId} timestamp={timestamp} />
</IFrameProtector>
)

Wyświetl plik

@ -1,6 +1,6 @@
import { BoardHistoryLog } from '../components/BoardHistoryLog/BoardHistoryLog'
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { defineLoader } from '../utils/defineLoader'
const { loader, useData } = defineLoader(async (args) => {
@ -33,7 +33,7 @@ export function Component() {
/>
)
return (
<IFrameProtector slug={data.boardId} context="history">
<IFrameProtector slug={data.boardId} context={ROOM_CONTEXT.HISTORY}>
<BoardHistoryLog data={data.data} />
</IFrameProtector>
)

Wyświetl plik

@ -1,13 +1,13 @@
import { ROOM_OPEN_MODE } from '@tldraw/dotcom-shared'
import { useParams } from 'react-router-dom'
import '../../styles/globals.css'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { MultiplayerEditor } from '../components/MultiplayerEditor'
export function Component() {
const id = useParams()['roomId'] as string
return (
<IFrameProtector slug={id} context="public-multiplayer">
<IFrameProtector slug={id} context={ROOM_CONTEXT.PUBLIC_MULTIPLAYER}>
<MultiplayerEditor roomOpenMode={ROOM_OPEN_MODE.READ_WRITE} roomSlug={id} />
</IFrameProtector>
)

Wyświetl plik

@ -1,13 +1,13 @@
import { ROOM_OPEN_MODE } from '@tldraw/dotcom-shared'
import { useParams } from 'react-router-dom'
import '../../styles/globals.css'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { MultiplayerEditor } from '../components/MultiplayerEditor'
export function Component() {
const id = useParams()['roomId'] as string
return (
<IFrameProtector slug={id} context="public-readonly">
<IFrameProtector slug={id} context={ROOM_CONTEXT.PUBLIC_READONLY}>
<MultiplayerEditor roomOpenMode={ROOM_OPEN_MODE.READ_ONLY_LEGACY} roomSlug={id} />
</IFrameProtector>
)

Wyświetl plik

@ -1,13 +1,13 @@
import { ROOM_OPEN_MODE } from '@tldraw/dotcom-shared'
import { useParams } from 'react-router-dom'
import '../../styles/globals.css'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { MultiplayerEditor } from '../components/MultiplayerEditor'
export function Component() {
const id = useParams()['roomId'] as string
return (
<IFrameProtector slug={id} context="public-readonly">
<IFrameProtector slug={id} context={ROOM_CONTEXT.PUBLIC_READONLY}>
<MultiplayerEditor roomOpenMode={ROOM_OPEN_MODE.READ_ONLY} roomSlug={id} />
</IFrameProtector>
)

Wyświetl plik

@ -1,6 +1,6 @@
import { SerializedSchema, TLRecord } from 'tldraw'
import '../../styles/globals.css'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { SnapshotsEditor } from '../components/SnapshotsEditor'
import { defineLoader } from '../utils/defineLoader'
@ -23,7 +23,7 @@ export function Component() {
if (!result) throw Error('Room not found')
const { roomId, records, schema } = result
return (
<IFrameProtector slug={roomId} context="public-snapshot">
<IFrameProtector slug={roomId} context={ROOM_CONTEXT.PUBLIC_SNAPSHOT}>
<SnapshotsEditor records={records} schema={schema} />
</IFrameProtector>
)

Wyświetl plik

@ -1,10 +1,10 @@
import '../../styles/globals.css'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { LocalEditor } from '../components/LocalEditor'
export function Component() {
return (
<IFrameProtector slug="home" context="local">
<IFrameProtector slug="home" context={ROOM_CONTEXT.LOCAL}>
<LocalEditor />
</IFrameProtector>
)

Wyświetl plik

@ -409,7 +409,7 @@ export const DefaultQuickActions: NamedExoticComponent<TLUiQuickActionsProps>;
export function DefaultQuickActionsContent(): JSX_2.Element | undefined;
// @public (undocumented)
export const defaultShapeTools: (typeof ArrowShapeTool | typeof DrawShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[];
export const defaultShapeTools: (typeof ArrowShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof HighlightShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[];
// @public (undocumented)
export const defaultShapeUtils: TLAnyShapeUtilConstructor[];
@ -455,7 +455,7 @@ export function downsizeImage(blob: Blob, width: number, height: number, opts?:
// @public (undocumented)
export class DrawShapeTool extends StateNode {
// (undocumented)
static children: () => (typeof Drawing | typeof Idle_2)[];
static children: () => (typeof Drawing | typeof Idle_3)[];
// (undocumented)
static id: string;
// (undocumented)
@ -678,7 +678,7 @@ export function FrameToolbarItem(): JSX_2.Element;
// @public (undocumented)
export class GeoShapeTool extends StateNode {
// (undocumented)
static children: () => (typeof Idle_3 | typeof Pointing_2)[];
static children: () => (typeof Idle_2 | typeof Pointing_2)[];
// (undocumented)
static id: string;
// (undocumented)
@ -875,7 +875,7 @@ export function HexagonToolbarItem(): JSX_2.Element;
// @public (undocumented)
export class HighlightShapeTool extends StateNode {
// (undocumented)
static children: () => (typeof Drawing | typeof Idle_2)[];
static children: () => (typeof Drawing | typeof Idle_3)[];
// (undocumented)
static id: string;
// (undocumented)

Wyświetl plik

@ -3829,15 +3829,6 @@
"kind": "Content",
"text": " | typeof "
},
{
"kind": "Reference",
"text": "DrawShapeTool",
"canonicalReference": "tldraw!DrawShapeTool:class"
},
{
"kind": "Content",
"text": " | typeof "
},
{
"kind": "Reference",
"text": "FrameShapeTool",
@ -3856,6 +3847,15 @@
"kind": "Content",
"text": " | typeof "
},
{
"kind": "Reference",
"text": "HighlightShapeTool",
"canonicalReference": "tldraw!HighlightShapeTool:class"
},
{
"kind": "Content",
"text": " | typeof "
},
{
"kind": "Reference",
"text": "LineShapeTool",
@ -4490,7 +4490,7 @@
{
"kind": "Reference",
"text": "Idle",
"canonicalReference": "tldraw!~Idle_2:class"
"canonicalReference": "tldraw!~Idle_3:class"
},
{
"kind": "Content",
@ -7891,7 +7891,7 @@
{
"kind": "Reference",
"text": "Idle",
"canonicalReference": "tldraw!~Idle_3:class"
"canonicalReference": "tldraw!~Idle_2:class"
},
{
"kind": "Content",
@ -9721,7 +9721,7 @@
{
"kind": "Reference",
"text": "Idle",
"canonicalReference": "tldraw!~Idle_2:class"
"canonicalReference": "tldraw!~Idle_3:class"
},
{
"kind": "Content",