Skip to content

Commit

Permalink
feat(messaging): support nested iframes by tagging messages with dire…
Browse files Browse the repository at this point in the history
…ction

Cross-frame messages are now tagged as `HostToClient` or `ClientToHost` so that a frame running both
host and client code will not try to incorrectly process messages intended for the host code with
the client code and vice-versa.
  • Loading branch information
MikeBlandford committed Aug 3, 2020
1 parent 87f87a3 commit 00d2cbf
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 56 deletions.
20 changes: 16 additions & 4 deletions src/FrameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@ import {
ClientToHost,
validate as validateIncoming
} from './messages/ClientToHost';
import { validate as validateOutgoing } from './messages/HostToClient';
import { API_PROTOCOL, PartialMsg } from './messages/LabeledMsg';
import {
HostToClient,
validate as validateOutgoing
} from './messages/HostToClient';
import {
API_PROTOCOL,
applyHostProtocol,
PartialMsg
} from './messages/LabeledMsg';

/** @external */
const IFRAME_STYLE = `
Expand Down Expand Up @@ -140,10 +147,11 @@ class FrameManager {
*
* @param message The message to send.
*/
public sendToClient<T, V>(message: PartialMsg<T, V>) {
public sendToClient<T, V>(partialMsg: PartialMsg<T, V>) {
const clientOrigin = this._expectedClientOrigin();
if (this._iframe.contentWindow && clientOrigin) {
let validated = null;
const message = applyHostProtocol(partialMsg);
let validated: HostToClient;

try {
validated = validateOutgoing(message);
Expand Down Expand Up @@ -188,6 +196,10 @@ to bad data passed to a frame-router method.
private _handlePostMessage(handler: MessageHandler, event: MessageEvent) {
let validated = null;

if (event.data && event.data.direction === 'HostToClient') {
return;
}

try {
validated = validateIncoming(event.data);
} catch (e) {
Expand Down
15 changes: 12 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import {
HostToClient,
validate as validateIncoming
} from './messages/HostToClient';
import { API_PROTOCOL, applyProtocol, PartialMsg } from './messages/LabeledMsg';
import {
API_PROTOCOL,
applyClientProtocol,
PartialMsg
} from './messages/LabeledMsg';
import {
EnvData,
EnvDataHandler,
Expand Down Expand Up @@ -102,6 +106,10 @@ export class Client {
private _onWindowMessage = (event: MessageEvent) => {
let validated = null;

if (event.data && event.data.direction === 'ClientToHost') {
return;
}

try {
validated = validateIncoming(event.data);
} catch (e) {
Expand Down Expand Up @@ -230,8 +238,9 @@ export class Client {
}

private _sendToHost<T, V>(partialMsg: PartialMsg<T, V>): void {
const message = applyProtocol(partialMsg);
let validated = null;
const message = applyClientProtocol(partialMsg);

let validated: ClientToHost;

try {
validated = validateOutgoing(message);
Expand Down
48 changes: 37 additions & 11 deletions src/messages/LabeledMsg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
hardcoded,
map,
object,
optional,
string
} from 'decoders';

Expand All @@ -20,6 +21,12 @@ const version = require('../../package.json').version;
*/
export const API_PROTOCOL = 'iframe-coordinator';

/**
* Based on MessageDirection, hosts and clients can ignore messages that are not targeted at them.
* @external
*/
export type MessageDirection = 'ClientToHost' | 'HostToClient';

/**
* Labeled message is a general structure
* used by all coordinated messages between
Expand All @@ -35,6 +42,8 @@ export interface LabeledMsg<T, V> extends PartialMsg<T, V> {
protocol: 'iframe-coordinator';
/** library version */
version: string;
/** So that nested iframe-coordinators can ignore messages that don't apply */
direction?: MessageDirection;
}

/**
Expand All @@ -50,25 +59,36 @@ export interface PartialMsg<T, V> {

/**
* Takes an object with a `msgType` and `msg` and applies the appropriate
* `protocol` and `version` fields for the current version of the library.
* `direction`, `protocol` and `version` fields for the current version of the library.
* @param partialMsg
* @external
*/
export function applyProtocol<T, V>(
export function applyClientProtocol<T, V>(
partialMsg: PartialMsg<T, V>
): LabeledMsg<T, V> {
return { ...partialMsg, protocol: API_PROTOCOL, version };
return {
direction: 'ClientToHost',
...partialMsg,
protocol: API_PROTOCOL,
version
};
}

/**
* Converts a PartialMsg decoder into a LabeledMsg decoder
* @param msgDecoder
* Takes an object with a `msgType` and `msg` and applies the appropriate
* `direction`, `protocol` and `version` fields for the current version of the library.
* @param partialMsg
* @external
*/
export function labeledDecoder2<T, V>(
msgDecoder: Decoder<PartialMsg<T, V>>
): Decoder<LabeledMsg<T, V>> {
return map(msgDecoder, applyProtocol);
export function applyHostProtocol<T, V>(
partialMsg: PartialMsg<T, V>
): LabeledMsg<T, V> {
return {
direction: 'HostToClient',
...partialMsg,
protocol: API_PROTOCOL,
version
};
}

/**
Expand All @@ -81,13 +101,19 @@ export function labeledDecoder<T, V>(
msgDecoder: Decoder<V>
): Decoder<LabeledMsg<T, V>> {
return object({
// TODO: in 4.0.0 make protocol and verison fields mandatory
// TODO: in 4.0.0 make protocol and version fields mandatory
protocol: either(
constant<'iframe-coordinator'>(API_PROTOCOL),
hardcoded<'iframe-coordinator'>(API_PROTOCOL)
),
version: either(string, hardcoded('unknown')),
msgType: typeDecoder,
msg: msgDecoder
msg: msgDecoder,
direction: optional(directionDecoder)
});
}

const directionDecoder: Decoder<MessageDirection, unknown> = either(
constant('ClientToHost'),
constant('HostToClient')
);
4 changes: 2 additions & 2 deletions src/messages/Lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
optional,
string
} from 'decoders';
import { applyProtocol, labeledDecoder, LabeledMsg } from './LabeledMsg';
import { applyClientProtocol, labeledDecoder, LabeledMsg } from './LabeledMsg';

/**
* Client started indication. The client will
Expand Down Expand Up @@ -118,7 +118,7 @@ export class Lifecycle {
* A {@link LabeledStarted} message to send to the host application.
*/
public static get startedMessage(): LabeledStarted {
return applyProtocol({
return applyClientProtocol({
msgType: 'client_started',
msg: undefined
});
Expand Down
24 changes: 16 additions & 8 deletions src/messages/specs/ClientToHost.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ describe('ClientToHost', () => {
const expectedMessage = withClientId({
...testMessage,
protocol: 'iframe-coordinator',
version: 'unknown'
version: 'unknown',
direction: undefined
}) as LabeledPublication;

let testResult: ClientToHost;
Expand All @@ -60,7 +61,8 @@ describe('ClientToHost', () => {
const expectedMessage = withClientId({
...testMessage,
protocol: 'iframe-coordinator',
version: 'unknown'
version: 'unknown',
direction: undefined
});

let testResult: ClientToHost;
Expand Down Expand Up @@ -103,7 +105,8 @@ describe('ClientToHost', () => {
clientId: undefined
},
protocol: 'iframe-coordinator',
version: 'unknown'
version: 'unknown',
direction: undefined
} as ClientToHost;

let testResult: ClientToHost;
Expand Down Expand Up @@ -134,7 +137,8 @@ describe('ClientToHost', () => {
custom: undefined
},
protocol: 'iframe-coordinator',
version: 'unknown'
version: 'unknown',
direction: undefined
};

let testResult: ClientToHost;
Expand Down Expand Up @@ -164,7 +168,8 @@ describe('ClientToHost', () => {
custom: undefined
},
protocol: 'iframe-coordinator',
version: 'unknown'
version: 'unknown',
direction: undefined
};

let testResult: ClientToHost;
Expand All @@ -190,7 +195,8 @@ describe('ClientToHost', () => {
const expectedMessage: ClientToHost = {
...testMessage,
protocol: 'iframe-coordinator',
version: 'unknown'
version: 'unknown',
direction: undefined
} as LabeledNotification;

let testResult: ClientToHost;
Expand Down Expand Up @@ -233,7 +239,8 @@ describe('ClientToHost', () => {
...toastMessage,
msgType: 'notifyRequest',
protocol: 'iframe-coordinator',
version: 'unknown'
version: 'unknown',
direction: undefined
} as LabeledNotification;

expect(validate(toastMessage)).toEqual(expectedMessage);
Expand All @@ -252,7 +259,8 @@ describe('ClientToHost', () => {
const expectedMessage = {
...testMessage,
protocol: 'iframe-coordinator',
version: 'unknown'
version: 'unknown',
direction: undefined
} as LabeledNavRequest;

let testResult: ClientToHost;
Expand Down
21 changes: 11 additions & 10 deletions src/messages/specs/HostToClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HostToClient, validate } from '../HostToClient';
import { applyProtocol } from '../LabeledMsg';
import { applyClientProtocol } from '../LabeledMsg';

describe('HostToClient', () => {
describe('validating an invalid message type', () => {
Expand All @@ -17,7 +17,7 @@ describe('HostToClient', () => {

describe('validating publish type', () => {
describe('when payload is a string', () => {
const testMessage: HostToClient = applyProtocol({
const testMessage: HostToClient = applyClientProtocol({
msgType: 'publish',
msg: {
topic: 'test.topic',
Expand All @@ -30,7 +30,7 @@ describe('HostToClient', () => {
});
it('should return the validated message', () => {
expect(testResult).toEqual(
applyProtocol({
applyClientProtocol({
msgType: 'publish',
msg: { ...testMessage.msg, clientId: undefined }
})
Expand All @@ -39,7 +39,7 @@ describe('HostToClient', () => {
});

describe('when payload is an object', () => {
const testMessage: HostToClient = applyProtocol({
const testMessage: HostToClient = applyClientProtocol({
msgType: 'publish',
msg: {
topic: 'test.topic',
Expand All @@ -52,7 +52,7 @@ describe('HostToClient', () => {
});
it('should return the validated message', () => {
expect(testResult).toEqual(
applyProtocol({
applyClientProtocol({
msgType: 'publish',
msg: { ...testMessage.msg, clientId: undefined }
})
Expand Down Expand Up @@ -90,7 +90,8 @@ describe('HostToClient', () => {
clientId: undefined
},
protocol: 'iframe-coordinator',
version: 'unknown'
version: 'unknown',
direction: undefined
};

let testResult: HostToClient;
Expand All @@ -105,7 +106,7 @@ describe('HostToClient', () => {

describe('validating env_init type', () => {
describe('when given a proper environmental data payload', () => {
const testMessage: HostToClient = applyProtocol({
const testMessage: HostToClient = applyClientProtocol({
msgType: 'env_init',
msg: {
locale: 'nl-NL',
Expand All @@ -118,7 +119,7 @@ describe('HostToClient', () => {
it('should return the validated message', () => {
const testResult = validate(testMessage);
expect(testResult).toEqual(
applyProtocol({
applyClientProtocol({
msgType: 'env_init',
msg: {
...testMessage.msg,
Expand All @@ -130,7 +131,7 @@ describe('HostToClient', () => {
});

describe('when given a proper environmental data payload including custom data', () => {
const testMessage: HostToClient = applyProtocol({
const testMessage: HostToClient = applyClientProtocol({
msgType: 'env_init',
msg: {
locale: 'nl-NL',
Expand All @@ -147,7 +148,7 @@ describe('HostToClient', () => {
testResult = validate(testMessage);
});
it('should return the validated message', () => {
expect(testResult).toEqual(applyProtocol(testMessage));
expect(testResult).toEqual(applyClientProtocol(testMessage));
});
});

Expand Down
Loading

0 comments on commit 00d2cbf

Please sign in to comment.