Code Monkey home page Code Monkey logo

ibridge's Introduction

Node.js CI

ibridge logo

ibridge

A tiny, promise based, type safe library for easy, bidirectional and secure iframe communication.

how ibridge works

Quick start

For this library to work you need to use it both from the parent document and from the child document.

In the child page

// [optional] Define a context that will
// be available to all model functions
const context = {
  vehicles: {
    getCar(carId) {
      return Promise.resolve({ fake: true, carId });
    },
  },
};

// define a model which contains all the functions
// that the parent can remotely call and get their results
const model = {
  vehicles: {
    getCar(carId) {
      const context = this;
      return context.vehicles.getCar(carId);
    },
  },
  // fake model to show how ibridge handles model that throw
  getError() {
    return Promise.reject("fake error");
  },
};

// Instantiate the child
const ichild = new ibridge.Child(model, context);
// await for ibridge handshake with the parent
await ichild.handshake();

In the parent page

// instantiate the parent, this will create an iframe
// element and inject it into the dom (configurable).
const iparent = new ibridge.Parent({
  url: "http://www.child.com"
});

// await for ibridge handshake with the child
await iparent.handshake();

// Done! we are ready to call model functions
const value = await iparent.get("vehicles.getCar", 123);
// => {fake: true, cardId: 123}

try {
  // models can throw in the child and the error will be
  // propagated to the parent
  const value = await iparent.get("getError");
} catch (err) {
  console.log("getError throwed and error in the child")
  console.log(err)
  // => "fake error"
}

๐Ÿ› Debugging

ibridge is written with full debug integration, to enable verbose output in the browser do the following in both child and parent

localStorage.debug = "ibridge:*"

๐Ÿ”Œ Model

model is a great way of modeling a remote function call from parent to child i.e. the parent calls a function that lives inside the child and the child communicates back the return value of that function or throws an error.

model functions can return any serializable value or a promise to a serializable value and they can throw or Promise.reject and the parent will treat them as regular function calls.

Parent can also pass any serializable arguments to the model inside the child.

๐Ÿ—„๏ธ Context

context is a really nice way of structuring more complex models in the child, it allows you to share apis, state, socketio connections, configs, across all the model functions.

โœ‰๏ธ Free form communication

There might be times where you want more control over the parent child communication, don't worry, you don't need to devolve back to the lower level api of postMessage; ibridge has you covered:

// Send events to the child
iparent.emitToChild("ping", {value: "i am father"})

// listen to events from the child
iparent.on("pong", msg => console.log(msg))
// listen to events from the parent
ichild.on("ping", msg => {
  // send message to the parent
  ichild.emitToParent("pong", {value: "i am child"})
})

Besides the special emitToParent and emitToChild both parent and child are event emitters that extend the Emittery, which means you have much more versatility while building complex event flows between parent and children i.e. once, off, onAny, etc. are all supported, check Emittery docs for more information.

๐ŸŽ›๏ธ Api docs

The library is fully typed, check the code to see all configurations or even let your IDE guide the way.

Max handshake requests

By default ibridge will try 5 times to stablish connection with the child, you can alter this behavior by changing the static attribute:

ibridge.Parent.maxHandshakeRequests = x

Security

Although this library has a handshake mechanism to establish the communication "channel" there is nothing preventing an attacker from using the library or simply simulating the message formats.

The right way of doing this is by working with web standards to define what domains can be parent iframes of our child, this allows you to define an allowlist of secure domains that can use your site inside an iframe.

Please check the mdn documentation.

TLDR: you can define an allowlist of domains that can use your page inside an iframe:

Your page should set the following HTTP header:

Content-Security-Policy: frame-ancestors 'self' www.parent.com *.otherparent.com http://localhost:* ;

If you want to allow any domain then don't set this header.

If your page won't be hosted inside an iframe you want to set this up to 'none' for extra protection.

Logo license

The log is part of twitter emoji.

License: MIT

This is based on postmate, original implementation to the postmate contributors credit.

ibridge's People

Contributors

franleplant avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

ibridge's Issues

using global window makes it difficult to do test isolation

Child (and possibly elsewhere) expects a global window which makes it more difficult to test:

ibridge/src/Child.ts

Lines 30 to 32 in f1be11d

constructor(model: TModel, context?: TContext) {
super();
this.child = window;

maybe do DI instead? constructor(model: TModel, context?: TContext, wnd?: Window) { this.window = wnd ?? window

i didn't foresee that and extended Child which means i can't use mocha before/after and must have a global persistent jsdom -- which also means i can't do parallel tests.

[Idea] The way of to create an instance by an existing iframe.

Hi.

I have a proposal for improvement. How about having an option to specify an already existing iframe element when creating the parent instance?

It would be nice to have two ways to do this, one by passing the iframe element and the other by passing the selector string.

For example, something like the following.

const existingIframe = document.getElementById('existingElementId')
const iparent = new ibridge.Parent({
  target: existingIframe
})
const iparent = new ibridge.Parent({
  target: 'existingElementId'
})

Browser level integration testing

At the moment we are only doing unit tests with Jsdom, but we are not being able to run a real integration testing. Jsdom has severe limitations and so the validations we are performing with jsdom are also limited.

I have used selenium in the past and it looks like it might be a really good tool to use testing. We already have a model test in integration.test.ts, but the thing that will take most of the time is actually setting up with CI infrastructure so that those tests can effectively run in CI and that, in the case of selenium and other browser level testing tools is not trivial since they rely on actual browsers.

build/bundling issues - webpack "development" and emittery import

the published package seems to be using webpack "development" which is probably the reason for the "/*!" prefixed comments in the result. could this be changed to production?

this is probably tsconfig collateral damage, but i don't use esmoduleinterop because i've found it to create as many problems as it solves. this causes issues with my build when ibridge is included because it complains about the emittery include: import emittery from 'emittery'. changing this to import emittery = require('emittery') fixes it if i edit the type directly. as for why my tsconfig is impacting a dependency... ๐Ÿคท

my workaround is to use a merged declaration:

import { Child } from 'ibridge';
import Emittery = require('emittery');

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Something extends Emittery {}

export interface Something extends Child {
   stuff() {
      // this now works
      this.on('stuff', ...)
   }
}

talk with `postmate` team

  • let the postmate team know this exist
  • talk about merging these efforts together
  • explain the reasoning behind these changes and what the changes are
  • talk about the future!

emitToParent (maybe others) requires two args when second should be optional

thanks for putting this together. it does a good job of addressing some of my nagging problems with postmate.

this currently has a ts error: emitToParent('some_event') and expects a second data: unknown argument. i believe it should be data?: unknown. my workaround is to do emitToParent('some_event', undefined)

v1 and post-me merge discussion

Refs

Objectives

  • see if we can merge post-me and ibridge to consolidate efforts
  • if the merge is not possible I would like to take the better ideas from post-me and use them on ibridge
  • solve some of the issues users have opened on ibridge holistically

Step 0: browser level automatic integration testing

Before anything else I want to setup a proper browser level integration testing for ibridge (and potentially the resulting merge). Right now I am using jsdoc with a bunch of mocks which is a similar approach to what post-me is doing although they are doing it arguably better and so the library does have automatic testing coverage but I find that the real test is always open the examples manually and checking that the thing really works because that way I can observe the inner workings of browsers and how they behave regarding origins and weird stuff that may happen at the postMessage border.

This will give us the confidence to iterate more quickly and to add more test cases when adding more features to this lib, unfortunately jsdom has some limitations in its implementation and for all intents and purposes is not a real browser.

ref

What do we need?

  • headless browser testing, the tool really does not matter, can be selenium, browser stack, etc
  • it must be easily compatible with github actions
  • it should be pretty straight forward to install (i.e. npm install is all that's needed)

I have experience with selenium and it would be more than enough but I haven't had the chance to set it up properly, that's probably going to be my first line of investigation.

Step 1: the merge

I would like to focus on the things I like the most from both libraries

ibridge

  • I love the name
  • we have a logo
  • visual docs and a good amount of docs
  • I like the idea of having a single event listener the dispatcher and then delegating the event handling to Emittery, this allows us to express complex event driven logic in a very natural way i.e. the handshake and also allow users to build more complex event driven flows using higher level event emitter apis that I believe are far superior that the native ones.
  • I like the idea of making the handshake a simple method instead of a separate thing like postmate and post-me do because it allows us to reuse the main dispatcher and all the high level goodies and infrastructure but also it enables us and users to recall the handshake if needed.
  • I like of course emitToParent and emitToChild methods (see the post-me comments below).
  • I like how get is structured (which is pretty similar to what post-me does)
  • I like how get accepts an object path to look for the model function, this allows users to build more complex models and structure them more naturally
  • I like the idea of model context, perhaps the implementation is not ideal but I do believe that solving that issue is important
  • i love the idea of having a single wrapper postMessage event that internally uses a higher level structure that can be easily mapped into Emittery (or any other even emitter really) events.

Post-me

  • webworker support
  • bidirectional get support (although post-me calls it call which is probably a better name)
  • the concept of localHandle and remoteHandle, these are amazing plus I love local and remote as words to define the real concepts, it is much better than parent and simply emit.
  • it doesn't try to create the iframe like ibridge and postmate do, instead it accepts a window and lets consumer handle that (is not that hard after all)
  • it accepts the origin as an argument instead of trying to be smart about it and detect it. I don't believe that restricting origins really adds and strong security layer to the library or to the postMessage (im open to being proven wrong) and I believe that the real security happens at the HTTP headers such as content-policy: frame-ancestors ..., so I really like not having to worry to much about this inside the library. If the consumers want to add the extra layer of security with a custom origin then let them handle that by themselves. There are some considerations to have here https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Syntax , maybe we can enable the autodetect mode in the child and in the parent, something like what we have today in ibridge

Comments out of the above

  • i looks like the v1 solution can be structured as a
    • low level w.postMessage and w.addEventListener('message') abstraction that is generic on w (i.e. window, worker, etc) that does not care how that window is created, be it the main window of the parent document or the window of an iframe or the global object of a web worker.
    • a handshake mechanism between parent and child, local and remote, messenger and messenger, server and client, etc
    • a higher level api on top that abstracts common stuff such as hidden iframe creation, web worker spawning etc.

So for example, today in ibridge.Parent we have

ibridge/src/Parent.ts

Lines 38 to 68 in f1be11d

constructor({
container = document.body,
url,
name = "",
classList = [],
showIframe = false,
}: IConstructorArgs) {
super();
this.url = url;
this.container = container;
this.parent = window;
this.frame = document.createElement("iframe");
this.frame.name = name;
this.frame.classList.add(...classList);
if (!showIframe) {
// Make it invisible
this.frame.style.width = "0";
this.frame.style.height = "0";
this.frame.style.border = "0";
}
debug("Loading frame %s", url);
this.container.appendChild(this.frame);
this.child =
this.frame.contentWindow ||
(this.frame.contentDocument as any)?.parentWindow;
this.childOrigin = resolveOrigin(url);
debug("setting up main listeners");
this.parent.addEventListener("message", this.dispatcher.bind(this), false);
}

and in the handshake we have

ibridge/src/Parent.ts

Lines 132 to 133 in 8b13eb1

// kick the iframe loading process off
this.frame.src = this.url;

This could probably be abstracted away with a promise passed as a parameter

function getRemoteWindow(container: HtmlElement, url: string): Promise<Window> {
  return new Promise((resolve) => {
    const iframe = document.createElement("iframe");
    container.appendChild(iframe);
    iframe.src = url;

    iframe.onLoad(() => resolve(iframe.contentWindow))
  })
}


// usage

const iparent  = new ibridge.Parent({getRemoteWindow: () => getRemoteWindow(someContainer, someUrl)})


// or even

const remoteWindow = await getRemoteWindow(someContainer, someUrl)
const remoteOrigin = remoteWindow.origin
const iparent = new ibridge.Parent({remoteWindow, remoteOrigin})

This enables us to remove the logic of setting up the iframe, and hooking the onLoad event
at the right moment (I had problems with this in the past) and simply changing it for
a promise that resolves when the window is abailable and loaded, no need to worry about anything
else really. We could provide the getRemoteWindow as a higher level api like we do today but this enables
us to easily let users do whatever they want with the window creating plus it can be useful when also supporting webworkers etc.

Style

  • post-me has a style that I'm not super fond of: top level functions use arrow functions and in some cases it gets overly complicated, my motto is clarity then simplicity... and I'd like to maximize that

Closing

There's so much work to do Im getting excited, really good ideas on the table!

cc @alesgenova

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.