tableflip / postmsg-rpc

Tiny RPC over window.postMessage library
http://npm.im/postmsg-rpc
MIT License
27 stars 3 forks source link

Replying to source iframe #1

Open kunaakos opened 6 years ago

kunaakos commented 6 years ago

Hi!

I ran into an issue with multiple iframes from different domains communicating with the main window. I cannot create listeners on the main window from the iframes' contexts because of cross-domain restrictions. The way this was handled before using postmsg-rpc, was by dispatching an event on the window we got the original message from:

window.addEventListener('message', (event) => {
  // ...
  event.source.postMessage(msg, '*')
});

Would it be possible to add this as an option to postmsg-rpc? Something like opts.replyToSender Happy to submit a PR!

alanshaw commented 6 years ago

I'm not sure I understand the issue correctly. Can you not pass (...args) => window.parent.postMessage(...args) as the postMessage option to caller?

https://github.com/tableflip/postmsg-rpc#callerfuncname-options

kunaakos commented 6 years ago

Yeah, but to get the return value, I need to pass window.parent.addEventListener as the addListener option to caller - and creating event listeners on the parent window is not allowed from cross-domain iframes.

This is what I'm currently doing, exposing functions with hack() instead of expose():

const exposeOptions = {
    postMessage: (returnValue, targetOrigin) => {
        returnValue.res.source.postMessage({
            ...returnValue,
            res: returnValue.res.data
        }, targetOrigin);
    },
    getMessageData: (event) => {
        return {
            ...event.data,
            args: event.data.args
                ? [event.source, ...event.data.args]
                : [event.source]
        };
    }
};

function hack(fnName, fn) {
    const wrappedFn = async (...args) => {
        return {
            source: args[0],
            data: await fn(...args.slice(1))
        };
    };
    expose(fnName, wrappedFn, exposeOptions);
}

If I'm missing something, and there's a better way, lemme know.

alanshaw commented 6 years ago

Yeah, but to get the return value, I need to pass window.parent.addEventListener as the addListener option to caller - and creating event listeners on the parent window is allowed from cross-domain iframes.

You should use the default - window.addEventListener?

kunaakos commented 6 years ago

correction: *not allowed from cross-domain iframes - that's a confusing typo to make, sry

If I use the default, I need the above hack. That works. But if window.postMessage() is called in the parent, and an event listener is created using window.addEventlistener() in the child (iframe) context, it doesn't, because events are dispatched on the parent window object, not the child.

alanshaw commented 6 years ago

In the parent can you pass postMessage option to expose as (...args) => document.querySelector('iframe').contentWindow.postMessage(...args)

kunaakos commented 6 years ago

I have several, dynamically inserted iframes on the page, and several of those will call the same exposed method, so that approach doesn't work :(

alanshaw commented 6 years ago

Gotcha, I think your hack is ok for now. I wanted to add this a while ago but my primary use for this library is in a webextension context where the postMessage API is similar but not the same. I'd like to add it in a way that allows it to be used in a web page but also in a web extension.

FYI the web extension API https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/onMessage

kunaakos commented 6 years ago

I was thinking about sth like:

export default function expose (funcName, func, opts) {
  opts = opts || {}
  // ...
  const replyToEmitter = opts.replyToEmitter || false
  // ...
  const handler = function (event) {
    // ...
    const dispatch = replyToEmitter
      ? (msg) => event.source.postMessage(msg, targetOrigin)
      : (msg) => postMessage(msg, targetOrigin)
    // ...
    const onSuccess = (res) => {
      msg.res = res
      dispatch(msg)
    }
    const onError = (err) => {
      // ...
      dispatch(msg)
    }
    // ...
  }

This doesn't break compatibility AFAIK, but makes the config a bit more confusing, since setting opts.replyToEmitter to true would make opts.postMessage unnecessary. Or maybe adding something like 'emitter' as a possible value to opts.postMessage would be cleaner?

Either way, happy to submit a PR, but wasn't sure about how to approach this.