From f99341bdfc57fea4c38801ca9bfd20cd73571d68 Mon Sep 17 00:00:00 2001 From: Uri Kutner Date: Tue, 12 Oct 2021 16:26:02 +0300 Subject: [PATCH 1/7] false childOrigin to skip origin check --- src/parent/connectToChild.ts | 8 +++++--- src/parent/handleAckMessageFactory.ts | 8 ++++++-- src/parent/handleSynMessageFactory.ts | 8 ++++++-- test/connectionManagement.spec.js | 13 +++++++++++++ 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/parent/connectToChild.ts b/src/parent/connectToChild.ts index eec0f78..33dc266 100644 --- a/src/parent/connectToChild.ts +++ b/src/parent/connectToChild.ts @@ -30,8 +30,9 @@ type Options = { * The child origin to use to secure communication. If * not provided, the child origin will be derived from the * iframe's src or srcdoc value. + * Use `false` to skip original url check. */ - childOrigin?: string; + childOrigin?: string | false; /** * The amount of time, in milliseconds, Penpal should wait * for the iframe to respond before rejecting the connection promise. @@ -55,7 +56,7 @@ export default ( const destructor = createDestructor('Parent', log); const { onDestroy, destroy } = destructor; - if (!childOrigin) { + if (childOrigin === undefined) { validateIframeHasSrcOrSrcDoc(iframe); childOrigin = getOriginFromSrc(iframe.src); } @@ -63,7 +64,8 @@ export default ( // If event.origin is "null", the remote protocol is file: or data: and we // must post messages with "*" as targetOrigin when sending messages. // https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Using_window.postMessage_in_extensions - const originForSending = childOrigin === 'null' ? '*' : childOrigin; + const originForSending = + childOrigin === 'null' || childOrigin === false ? '*' : childOrigin; const serializedMethods = serializeMethods(methods); const handleSynMessage = handleSynMessageFactory( log, diff --git a/src/parent/handleAckMessageFactory.ts b/src/parent/handleAckMessageFactory.ts index bb4e4b5..2c3bd31 100644 --- a/src/parent/handleAckMessageFactory.ts +++ b/src/parent/handleAckMessageFactory.ts @@ -8,7 +8,7 @@ import connectCallSender from '../connectCallSender'; */ export default ( serializedMethods: SerializedMethods, - childOrigin: string, + childOrigin: string | false, originForSending: string, destructor: Destructor, log: Function @@ -23,7 +23,11 @@ export default ( const callSender: CallSender = {}; return (event: MessageEvent): CallSender | undefined => { - if (event.origin !== childOrigin) { + if (childOrigin === false) { + log( + `Parent: Handshake - Received ACK message, skipping event.origin check, since childOrigin is "null"` + ); + } else if (event.origin !== childOrigin) { log( `Parent: Handshake - Received ACK message from origin ${event.origin} which did not match expected origin ${childOrigin}` ); diff --git a/src/parent/handleSynMessageFactory.ts b/src/parent/handleSynMessageFactory.ts index 535cfb1..0d2d9cb 100644 --- a/src/parent/handleSynMessageFactory.ts +++ b/src/parent/handleSynMessageFactory.ts @@ -7,11 +7,15 @@ import { MessageType } from '../enums'; export default ( log: Function, serializedMethods: SerializedMethods, - childOrigin: string, + childOrigin: string | false, originForSending: string ) => { return (event: MessageEvent) => { - if (event.origin !== childOrigin) { + if (childOrigin === false) { + log( + `Parent: Handshake - Received SYN message, skipping event.origin check, since childOrigin is "null"` + ); + } else if (event.origin !== childOrigin) { log( `Parent: Handshake - Received SYN message from origin ${event.origin} which did not match expected origin ${childOrigin}` ); diff --git a/test/connectionManagement.spec.js b/test/connectionManagement.spec.js index 58633f0..58bcd3f 100644 --- a/test/connectionManagement.spec.js +++ b/test/connectionManagement.spec.js @@ -31,6 +31,19 @@ describe('connection management', () => { await connection.promise; }); + it('connects to iframe connecting to parent with mis-matching origin, when childOrigin is false', async () => { + const iframe = createAndAddIframe(); + iframe.src = `${CHILD_SERVER}/matchingParentOrigin.html`; + + const connection = Penpal.connectToChild({ + debug: true, + iframe, + childOrigin: false, + }); + + await connection.promise; + }); + it('connects to iframe connecting to parent with matching origin regex', async () => { const iframe = createAndAddIframe(); iframe.src = `${CHILD_SERVER}/matchingParentOriginRegex.html`; From 5d3c89331dfa0d83e873895c36dd0401655fa400 Mon Sep 17 00:00:00 2001 From: Uri Kutner Date: Tue, 12 Oct 2021 16:35:38 +0300 Subject: [PATCH 2/7] fix test --- src/parent/connectToChild.ts | 2 +- test/connectionManagement.spec.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/parent/connectToChild.ts b/src/parent/connectToChild.ts index 33dc266..13689fc 100644 --- a/src/parent/connectToChild.ts +++ b/src/parent/connectToChild.ts @@ -56,7 +56,7 @@ export default ( const destructor = createDestructor('Parent', log); const { onDestroy, destroy } = destructor; - if (childOrigin === undefined) { + if (!childOrigin && childOrigin !== false) { validateIframeHasSrcOrSrcDoc(iframe); childOrigin = getOriginFromSrc(iframe.src); } diff --git a/test/connectionManagement.spec.js b/test/connectionManagement.spec.js index 58bcd3f..7d5a2fd 100644 --- a/test/connectionManagement.spec.js +++ b/test/connectionManagement.spec.js @@ -31,9 +31,8 @@ describe('connection management', () => { await connection.promise; }); - it('connects to iframe connecting to parent with mis-matching origin, when childOrigin is false', async () => { + it('connects to iframe with mis-matching origin, when childOrigin is false', async () => { const iframe = createAndAddIframe(); - iframe.src = `${CHILD_SERVER}/matchingParentOrigin.html`; const connection = Penpal.connectToChild({ debug: true, @@ -41,6 +40,10 @@ describe('connection management', () => { childOrigin: false, }); + // set iframe src after setup, so connectToChild will not use it as `childOrigin`, + // and unless `childOrigin` is `false` + iframe.src = `${CHILD_SERVER}/matchingParentOrigin.html`; + await connection.promise; }); From cfd77f1051cd078a68a413164623e72e36f322ec Mon Sep 17 00:00:00 2001 From: Uri Kutner Date: Tue, 12 Oct 2021 19:48:47 +0300 Subject: [PATCH 3/7] apply the same logic for originForReceiving --- src/connectCallReceiver.ts | 2 +- src/connectCallSender.ts | 5 ++++- src/types.ts | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/connectCallReceiver.ts b/src/connectCallReceiver.ts index 0e09f9f..11ccb11 100644 --- a/src/connectCallReceiver.ts +++ b/src/connectCallReceiver.ts @@ -35,7 +35,7 @@ export default ( return; } - if (event.origin !== originForReceiving) { + if (originForReceiving !== false && event.origin !== originForReceiving) { log( `${localName} received message from origin ${event.origin} which did not match expected origin ${originForReceiving}` ); diff --git a/src/connectCallSender.ts b/src/connectCallSender.ts index 866b662..9cb31fd 100644 --- a/src/connectCallSender.ts +++ b/src/connectCallSender.ts @@ -85,7 +85,10 @@ export default ( return; } - if (event.origin !== originForReceiving) { + if ( + originForReceiving !== false && + event.origin !== originForReceiving + ) { log( `${localName} received message from origin ${event.origin} which did not match expected origin ${originForReceiving}` ); diff --git a/src/types.ts b/src/types.ts index 51bd250..3121a53 100644 --- a/src/types.ts +++ b/src/types.ts @@ -130,5 +130,5 @@ export type WindowsInfo = { /** * Origin that should be used for receiving messages from the remote window. */ - originForReceiving: string; + originForReceiving: string | false; }; From 177f1e3e6a605d55b7760adbc17e2b010346799c Mon Sep 17 00:00:00 2001 From: Aaron Hardy Date: Tue, 12 Oct 2021 22:10:43 -0600 Subject: [PATCH 4/7] Changing childOrigin to support wildcard instead of false. --- src/connectCallReceiver.ts | 2 +- src/parent/connectToChild.ts | 7 +++---- src/parent/handleAckMessageFactory.ts | 8 ++------ src/parent/handleSynMessageFactory.ts | 8 ++------ src/types.ts | 2 +- test/connectionManagement.spec.js | 8 ++++---- 6 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/connectCallReceiver.ts b/src/connectCallReceiver.ts index 11ccb11..61d7054 100644 --- a/src/connectCallReceiver.ts +++ b/src/connectCallReceiver.ts @@ -35,7 +35,7 @@ export default ( return; } - if (originForReceiving !== false && event.origin !== originForReceiving) { + if (originForReceiving !== '*' && event.origin !== originForReceiving) { log( `${localName} received message from origin ${event.origin} which did not match expected origin ${originForReceiving}` ); diff --git a/src/parent/connectToChild.ts b/src/parent/connectToChild.ts index 13689fc..80c4fe0 100644 --- a/src/parent/connectToChild.ts +++ b/src/parent/connectToChild.ts @@ -32,7 +32,7 @@ type Options = { * iframe's src or srcdoc value. * Use `false` to skip original url check. */ - childOrigin?: string | false; + childOrigin?: string; /** * The amount of time, in milliseconds, Penpal should wait * for the iframe to respond before rejecting the connection promise. @@ -56,7 +56,7 @@ export default ( const destructor = createDestructor('Parent', log); const { onDestroy, destroy } = destructor; - if (!childOrigin && childOrigin !== false) { + if (!childOrigin) { validateIframeHasSrcOrSrcDoc(iframe); childOrigin = getOriginFromSrc(iframe.src); } @@ -64,8 +64,7 @@ export default ( // If event.origin is "null", the remote protocol is file: or data: and we // must post messages with "*" as targetOrigin when sending messages. // https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Using_window.postMessage_in_extensions - const originForSending = - childOrigin === 'null' || childOrigin === false ? '*' : childOrigin; + const originForSending = childOrigin === 'null' ? '*' : childOrigin; const serializedMethods = serializeMethods(methods); const handleSynMessage = handleSynMessageFactory( log, diff --git a/src/parent/handleAckMessageFactory.ts b/src/parent/handleAckMessageFactory.ts index 2c3bd31..7fd7f16 100644 --- a/src/parent/handleAckMessageFactory.ts +++ b/src/parent/handleAckMessageFactory.ts @@ -8,7 +8,7 @@ import connectCallSender from '../connectCallSender'; */ export default ( serializedMethods: SerializedMethods, - childOrigin: string | false, + childOrigin: string, originForSending: string, destructor: Destructor, log: Function @@ -23,11 +23,7 @@ export default ( const callSender: CallSender = {}; return (event: MessageEvent): CallSender | undefined => { - if (childOrigin === false) { - log( - `Parent: Handshake - Received ACK message, skipping event.origin check, since childOrigin is "null"` - ); - } else if (event.origin !== childOrigin) { + if (childOrigin !== '*' && event.origin !== childOrigin) { log( `Parent: Handshake - Received ACK message from origin ${event.origin} which did not match expected origin ${childOrigin}` ); diff --git a/src/parent/handleSynMessageFactory.ts b/src/parent/handleSynMessageFactory.ts index 0d2d9cb..1320e65 100644 --- a/src/parent/handleSynMessageFactory.ts +++ b/src/parent/handleSynMessageFactory.ts @@ -7,15 +7,11 @@ import { MessageType } from '../enums'; export default ( log: Function, serializedMethods: SerializedMethods, - childOrigin: string | false, + childOrigin: string, originForSending: string ) => { return (event: MessageEvent) => { - if (childOrigin === false) { - log( - `Parent: Handshake - Received SYN message, skipping event.origin check, since childOrigin is "null"` - ); - } else if (event.origin !== childOrigin) { + if (childOrigin !== '*' && event.origin !== childOrigin) { log( `Parent: Handshake - Received SYN message from origin ${event.origin} which did not match expected origin ${childOrigin}` ); diff --git a/src/types.ts b/src/types.ts index 3121a53..51bd250 100644 --- a/src/types.ts +++ b/src/types.ts @@ -130,5 +130,5 @@ export type WindowsInfo = { /** * Origin that should be used for receiving messages from the remote window. */ - originForReceiving: string | false; + originForReceiving: string; }; diff --git a/test/connectionManagement.spec.js b/test/connectionManagement.spec.js index 7d5a2fd..2a40346 100644 --- a/test/connectionManagement.spec.js +++ b/test/connectionManagement.spec.js @@ -31,17 +31,17 @@ describe('connection management', () => { await connection.promise; }); - it('connects to iframe with mis-matching origin, when childOrigin is false', async () => { + it('connects to iframe when childOrigin is false', async () => { const iframe = createAndAddIframe(); const connection = Penpal.connectToChild({ debug: true, iframe, - childOrigin: false, + childOrigin: '*', }); - // set iframe src after setup, so connectToChild will not use it as `childOrigin`, - // and unless `childOrigin` is `false` + // Set iframe src after calling connectToChild, so connectToChild would + // typically fail had we not set childOrigin to *. iframe.src = `${CHILD_SERVER}/matchingParentOrigin.html`; await connection.promise; From ab4c3b11b8a0e532581470d205f0cd448e488fa1 Mon Sep 17 00:00:00 2001 From: Aaron Hardy Date: Tue, 12 Oct 2021 22:12:40 -0600 Subject: [PATCH 5/7] Fixed a comment and test description. --- src/parent/connectToChild.ts | 1 - test/connectionManagement.spec.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/parent/connectToChild.ts b/src/parent/connectToChild.ts index 80c4fe0..eec0f78 100644 --- a/src/parent/connectToChild.ts +++ b/src/parent/connectToChild.ts @@ -30,7 +30,6 @@ type Options = { * The child origin to use to secure communication. If * not provided, the child origin will be derived from the * iframe's src or srcdoc value. - * Use `false` to skip original url check. */ childOrigin?: string; /** diff --git a/test/connectionManagement.spec.js b/test/connectionManagement.spec.js index 2a40346..d14a4c3 100644 --- a/test/connectionManagement.spec.js +++ b/test/connectionManagement.spec.js @@ -31,7 +31,7 @@ describe('connection management', () => { await connection.promise; }); - it('connects to iframe when childOrigin is false', async () => { + it('connects to iframe when childOrigin is *', async () => { const iframe = createAndAddIframe(); const connection = Penpal.connectToChild({ From 54b2bec465d323930fa0ea2bbc0e534524614867 Mon Sep 17 00:00:00 2001 From: Aaron Hardy Date: Tue, 12 Oct 2021 22:45:38 -0600 Subject: [PATCH 6/7] Switched false to wildcard. --- src/connectCallSender.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connectCallSender.ts b/src/connectCallSender.ts index 9cb31fd..28f44a1 100644 --- a/src/connectCallSender.ts +++ b/src/connectCallSender.ts @@ -86,7 +86,7 @@ export default ( } if ( - originForReceiving !== false && + originForReceiving !== '*' && event.origin !== originForReceiving ) { log( From 8ee0a89cdf090dce7c1b6013862987a4295f86fe Mon Sep 17 00:00:00 2001 From: Aaron Hardy Date: Wed, 13 Oct 2021 21:42:30 -0600 Subject: [PATCH 7/7] Added support for wildcard childOrigin. Added appropriate documentation. --- README.md | 92 ++++++++++++++++++-------- scripts/test.js | 3 + test/childFixtures/redirect.html | 15 +++++ test/connectionManagement.spec.js | 103 ++++++++++++++++-------------- test/constants.js | 1 + 5 files changed, 141 insertions(+), 73 deletions(-) create mode 100644 test/childFixtures/redirect.html diff --git a/README.md b/README.md index 0e83031..36ca973 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,9 @@ import { connectToChild } from 'penpal'; const iframe = document.createElement('iframe'); iframe.src = 'http://example.com/iframe.html'; + +// This conditional is not Penpal-specific. It's merely +// an example of how you can add an iframe to the document. if ( document.readyState === 'complete' || document.readyState === 'interactive' @@ -58,10 +61,11 @@ if ( }); } +// This is where the magic begins. const connection = connectToChild({ - // The iframe to which a connection should be made + // The iframe to which a connection should be made. iframe, - // Methods the parent is exposing to the child + // Methods the parent is exposing to the child. methods: { add(num1, num2) { return num1 + num2; @@ -81,13 +85,14 @@ connection.promise.then((child) => { import { connectToParent } from 'penpal'; const connection = connectToParent({ - // Methods child is exposing to parent + // Methods child is exposing to parent. methods: { multiply(num1, num2) { return num1 * num2; }, divide(num1, num2) { - // Return a promise if the value being returned requires asynchronous processing. + // Return a promise if the value being + // returned requires asynchronous processing. return new Promise((resolve) => { setTimeout(() => { resolve(num1 / num2); @@ -106,47 +111,79 @@ connection.promise.then((parent) => { ### `connectToChild(options: Object) => Object` -**For Penpal to operate correctly, you must ensure that `connectToChild` is called before the iframe has called `connectToParent`.** As shown in the example above, it is safe to set the `src` or `srcdoc` property of the iframe and append the iframe to the document before calling `connectToChild` as long as they are both done in the same [JavaScript event loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop). Alternatively, you can always append the iframe to the document _after_ calling `connectToChild` instead of _before_. +**For Penpal to operate correctly, you must ensure that `connectToChild` is called before the iframe calls `connectToParent`.** As shown in the example above, it is safe to set the `src` or `srcdoc` property of the iframe and append the iframe to the document before calling `connectToChild` as long as they are both done in the same [JavaScript event loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop). Alternatively, you can always append the iframe to the document _after_ calling `connectToChild` instead of _before_. #### Options -`options.iframe: HTMLIFrameElement` (required) The iframe element to which Penpal should connect. Unless you provide the `childOrigin` option, you will need to have set either the `src` or `srcdoc` property on the iframe prior to calling `connectToChild` so that Penpal can automatically derive the child origin. In addition to regular URLs, [data URIs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) and [file URIs](https://en.wikipedia.org/wiki/File_URI_scheme) are also supported. +`options.iframe: HTMLIFrameElement` (required) + +The iframe element to which Penpal should connect. Unless you provide the `childOrigin` option, you will need to have set either the `src` or `srcdoc` property on the iframe prior to calling `connectToChild` so that Penpal can automatically derive the child origin. In addition to regular URLs, [data URIs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) and [file URIs](https://en.wikipedia.org/wiki/File_URI_scheme) are also supported. + +`options.methods: Object` (optional) + +An object containing methods which should be exposed for the child iframe to call. The keys of the object are the method names and the values are the functions. Nested objects with function values are recursively included. If a function requires asynchronous processing to determine its return value, make the function immediately return a promise and resolve the promise once the value has been determined. + +`options.childOrigin: string` (optional) + +In the vast majority of cases, Penpal can automatically determine the child origin based on the `src` or `srcdoc` property that you have set on the iframe. This will automatically restrict communication to that origin. -`options.methods: Object` (optional) An object containing methods which should be exposed for the child iframe to call. The keys of the object are the method names and the values are the functions. Nested objects with function values are recursively included. If a function requires asynchronous processing to determine its return value, make the function immediately return a promise and resolve the promise once the value has been determined. +In some rare cases, particularly when using the `file://` protocol on various devices, browsers are inconsistent in how they report and handle origins. If you receive an error saying that the parent received a handshake from an unexpected origin, you may need to manually pass the child origin using this option. -`options.childOrigin: string` (optional) In the vast majority of cases, Penpal can automatically determine the child origin based on the `src` or `srcdoc` property that you have set on the iframe. Unfortunately, browsers are inconsistent in certain cases, particularly when using the `file://` protocol on various devices. If you receive an error saying that the parent received a handshake from an unexpected origin, you may need to manually pass the child origin using this option. +In other [niche scenarios](https://github.com/Aaronius/penpal/issues/73), you may want the parent to be able to communicate with any child origin. In this case, you can set `childOrigin` to `*`. **This is discouraged.** To illustrate the risk, if a nefarious attacker manages to create a link within the child page that another user can click (for example, if you fail to inadequately escape HTML in a message board comment), and that link navigates the unsuspecting user's iframe to a nefarious URL, then the page at the nefarious URL could start communicating with your parent window. -`options.timeout: number` (optional) The amount of time, in milliseconds, Penpal should wait for the child to respond before rejecting the connection promise. There is no timeout by default. +Regardless of how you configure `childOrigin`, communication will always be restricted to only the iframe to which you are connecting. -`options.debug: boolean` (optional) Enables or disables debug logging. Debug logging is disabled by default. +`options.timeout: number` (optional) + +The amount of time, in milliseconds, Penpal should wait for the child to respond before rejecting the connection promise. There is no timeout by default. + +`options.debug: boolean` (optional) + +Enables or disables debug logging. Debug logging is disabled by default. #### Return value The return value of `connectToChild` is a `connection` object with the following properties: -`connection.promise: Promise` A promise which will be resolved once communication has been established. The promise will be resolved with an object containing the methods which the child has exposed. Note that these aren't actual memory references to the methods the child exposed, but instead proxy methods Penpal has created with the same names and signatures. When one of these methods is called, Penpal will immediately return a promise and then go to work sending a message to the child, calling the actual method within the child with the arguments you have passed, and then sending the return value back to the parent. The promise you received will then be resolved with the return value. +`connection.promise: Promise` + +A promise which will be resolved once communication has been established. The promise will be resolved with an object containing the methods which the child has exposed. Note that these aren't actual memory references to the methods the child exposed, but instead proxy methods Penpal has created with the same names and signatures. When one of these methods is called, Penpal will immediately return a promise and then go to work sending a message to the child, calling the actual method within the child with the arguments you have passed, and then sending the return value back to the parent. The promise you received will then be resolved with the return value. + +`connection.destroy: Function` -`connection.destroy: Function` A method that, when called, will disconnect any messaging channels. You may call this even before a connection has been established. +A method that, when called, will disconnect any messaging channels. You may call this even before a connection has been established. ### `connectToParent([options: Object]) => Object` #### Options -`options.parentOrigin: string | RegExp` (optional) The origin of the parent window which your iframe will be communicating with. If this is not provided, communication will not be restricted to any particular parent origin resulting in any webpage being able to load your webpage into an iframe and communicate with it. +`options.parentOrigin: string | RegExp` (optional **but highly recommended!**) -`options.methods: Object` (optional) An object containing methods which should be exposed for the parent window to call. The keys of the object are the method names and the values are the functions. Nested objects with function values are recursively included. If a function requires asynchronous processing to determine its return value, make the function immediately return a promise and resolve the promise once the value has been determined. +The origin of the parent window which your iframe will be communicating with. If this is not provided, communication will not be restricted to any particular parent origin resulting in any webpage being able to load your webpage into an iframe and communicate with it. -`options.timeout: number` (optional) The amount of time, in milliseconds, Penpal should wait for the parent to respond before rejecting the connection promise. There is no timeout by default. +`options.methods: Object` (optional) -`options.debug: boolean` (optional) Enables or disables debug logging. Debug logging is disabled by default. +An object containing methods which should be exposed for the parent window to call. The keys of the object are the method names and the values are the functions. Nested objects with function values are recursively included. If a function requires asynchronous processing to determine its return value, make the function immediately return a promise and resolve the promise once the value has been determined. + +`options.timeout: number` (optional) + +The amount of time, in milliseconds, Penpal should wait for the parent to respond before rejecting the connection promise. There is no timeout by default. + +`options.debug: boolean` (optional) + +Enables or disables debug logging. Debug logging is disabled by default. #### Return value -The return value of `connectToParent` is a `connection` object with the following property: +The return value of `connectToParent` is a `connection` object with the following properties: + +`connection.promise: Promise` + +A promise which will be resolved once communication has been established. The promise will be resolved with an object containing the methods which the parent has exposed. Note that these aren't actual memory references to the methods the parent exposed, but instead proxy methods Penpal has created with the same names and signatures. When one of these methods is called, Penpal will immediately return a promise and then go to work sending a message to the parent, calling the actual method within the parent with the arguments you have passed, and then sending the return value back to the child. The promise you received will then be resolved with the return value. -`connection.promise: Promise` A promise which will be resolved once communication has been established. The promise will be resolved with an object containing the methods which the parent has exposed. Note that these aren't actual memory references to the methods the parent exposed, but instead proxy methods Penpal has created with the same names and signatures. When one of these methods is called, Penpal will immediately return a promise and then go to work sending a message to the parent, calling the actual method within the parent with the arguments you have passed, and then sending the return value back to the child. The promise you received will then be resolved with the return value. +`connection.destroy: Function` -`connection.destroy: Function` A method that, when called, will disconnect any messaging channels. You may call this even before a connection has been established. +A method that, when called, will disconnect any messaging channels. You may call this even before a connection has been established. ## Reconnection @@ -158,12 +195,17 @@ NOTE: Currently there is no API to notify consumers of a reconnection. If this i Penpal will throw (or reject promises with) errors in certain situations. Each error will have a `code` property which may be used for programmatic decisioning (e.g., do something if the error was due to a connection timing out) along with a `message` describing the problem. Errors may be thrown with the following codes: -- `ConnectionDestroyed` - - This error will be thrown when attempting to call a method on `child` or `parent` objects and the connection was previously destroyed. -- `ConnectionTimeout` - - `connection.promise` will be rejected with this error after the `timeout` duration has elapsed and a connection has not been established. -- `NoIframeSrc` - - This error will be thrown when the iframe passed into `connectToChild` does not have `src` or `srcdoc` set. +`ConnectionDestroyed` + +This error will be thrown when attempting to call a method on `child` or `parent` objects and the connection was previously destroyed. + +`ConnectionTimeout` + +The promise found at `connection.promise` will be rejected with this error after the `timeout` duration has elapsed and a connection has not been established. + +`NoIframeSrc` + +This error will be thrown when the iframe passed into `connectToChild` does not have `src` or `srcdoc` set. For your convenience, these error codes can be imported as follows: diff --git a/scripts/test.js b/scripts/test.js index e85cfd6..501a60d 100755 --- a/scripts/test.js +++ b/scripts/test.js @@ -18,6 +18,9 @@ const serveChildViews = () => { .use(serveStatic('test/childFixtures')); http.createServer(childViewsApp).listen(9000); + // Host the child views on two ports so tests can do interesting + // things like redirect the iframe between two origins. + http.createServer(childViewsApp).listen(9001); }; const runTests = () => { diff --git a/test/childFixtures/redirect.html b/test/childFixtures/redirect.html new file mode 100644 index 0000000..5e93b0b --- /dev/null +++ b/test/childFixtures/redirect.html @@ -0,0 +1,15 @@ + + + + Test Iframe + + + + Test Iframe + + diff --git a/test/connectionManagement.spec.js b/test/connectionManagement.spec.js index d14a4c3..e6ee65c 100644 --- a/test/connectionManagement.spec.js +++ b/test/connectionManagement.spec.js @@ -1,6 +1,26 @@ -import { CHILD_SERVER } from './constants'; +import { CHILD_SERVER, CHILD_SERVER_ALTERNATE } from './constants'; import { createAndAddIframe } from './utils'; +/** + * Asserts that no connection is successfully made between the parent and the + * child. + */ +const expectNoSuccessfulConnection = (connectionPromise, iframe) => { + const spy = jasmine.createSpy(); + + connectionPromise.then(spy); + + return new Promise((resolve) => { + iframe.addEventListener('load', function () { + // Give Penpal time to try to make a handshake. + setTimeout(() => { + expect(spy).not.toHaveBeenCalled(); + resolve(); + }, 100); + }); + }); +}; + describe('connection management', () => { it('connects to iframe when correct child origin provided', async () => { const iframe = createAndAddIframe(); @@ -31,22 +51,6 @@ describe('connection management', () => { await connection.promise; }); - it('connects to iframe when childOrigin is *', async () => { - const iframe = createAndAddIframe(); - - const connection = Penpal.connectToChild({ - debug: true, - iframe, - childOrigin: '*', - }); - - // Set iframe src after calling connectToChild, so connectToChild would - // typically fail had we not set childOrigin to *. - iframe.src = `${CHILD_SERVER}/matchingParentOrigin.html`; - - await connection.promise; - }); - it('connects to iframe connecting to parent with matching origin regex', async () => { const iframe = createAndAddIframe(); iframe.src = `${CHILD_SERVER}/matchingParentOriginRegex.html`; @@ -59,7 +63,7 @@ describe('connection management', () => { await connection.promise; }); - it("doesn't connect to iframe when incorrect child origin provided", (done) => { + it("doesn't connect to iframe when incorrect child origin provided", async () => { const iframe = createAndAddIframe(); const connection = Penpal.connectToChild({ @@ -73,20 +77,10 @@ describe('connection management', () => { // needed when childOrigin is not passed. iframe.src = `${CHILD_SERVER}/default.html`; - const spy = jasmine.createSpy(); - - connection.promise.then(spy); - - iframe.addEventListener('load', function () { - // Give Penpal time to try to make a handshake. - setTimeout(() => { - expect(spy).not.toHaveBeenCalled(); - done(); - }, 100); - }); + await expectNoSuccessfulConnection(connection.promise, iframe); }); - it("doesn't connect to iframe connecting to mismatched parent origin", (done) => { + it("doesn't connect to iframe connecting to mismatched parent origin", async () => { const iframe = createAndAddIframe( `${CHILD_SERVER}/mismatchedParentOrigin.html` ); @@ -95,39 +89,52 @@ describe('connection management', () => { iframe, }); - const spy = jasmine.createSpy(); + await expectNoSuccessfulConnection(connection.promise, iframe); + }); - connection.promise.then(spy); + it("doesn't connect to iframe connecting to mismatched parent origin regex", async () => { + const iframe = createAndAddIframe( + `${CHILD_SERVER}/mismatchedParentOriginRegex.html` + ); - iframe.addEventListener('load', function () { - // Give Penpal time to try to make a handshake. - setTimeout(() => { - expect(spy).not.toHaveBeenCalled(); - done(); - }, 100); + const connection = Penpal.connectToChild({ + iframe, }); + + await expectNoSuccessfulConnection(connection.promise, iframe); }); - it("doesn't connect to iframe connecting to mismatched parent origin regex", (done) => { + it('connects to iframe when child redirects to different origin and child origin is set to *', async () => { + const redirectToUrl = encodeURIComponent( + `${CHILD_SERVER_ALTERNATE}/default.html` + ); const iframe = createAndAddIframe( - `${CHILD_SERVER}/mismatchedParentOriginRegex.html` + `${CHILD_SERVER}/redirect.html?to=${redirectToUrl}` ); const connection = Penpal.connectToChild({ + debug: true, iframe, + childOrigin: '*', }); - const spy = jasmine.createSpy(); + await connection.promise; + }); - connection.promise.then(spy); + it("doesn't connect to iframe when child redirects to different origin and child origin is not set", async () => { + const redirectToUrl = encodeURIComponent( + `${CHILD_SERVER_ALTERNATE}/default.html` + ); + const iframe = createAndAddIframe( + `${CHILD_SERVER}/redirect.html?to=${redirectToUrl}` + ); - iframe.addEventListener('load', function () { - // Give Penpal time to try to make a handshake. - setTimeout(() => { - expect(spy).not.toHaveBeenCalled(); - done(); - }, 100); + const connection = Penpal.connectToChild({ + debug: true, + iframe, }); + + await expectNoSuccessfulConnection(connection.promise, iframe); }); it('reconnects after child reloads', (done) => { diff --git a/test/constants.js b/test/constants.js index 2d54f05..aab731d 100644 --- a/test/constants.js +++ b/test/constants.js @@ -1 +1,2 @@ export const CHILD_SERVER = `http://${window.location.hostname}:9000`; +export const CHILD_SERVER_ALTERNATE = `http://${window.location.hostname}:9001`;