kopia lustrzana https://github.com/Tldraw/Tldraw
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 maintainbilitypull/3192/head
rodzic
d96389418a
commit
952fe910d3
|
@ -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, too—and hey, if we decide to offer a hosted thing, then that's another stor
|
||||
risky for us and for them, too—and 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">
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
Ładowanie…
Reference in New Issue