Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 54 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# scireum BUZZ

Die BUZZ Schnittstelle ist eine Nachrichten-orientierte API zum Verbinden von unterschiedlichen Webseiten sowie nativen
Anwendungen wie beispielsweise Apps in Electron, Android oder iOS. Über die Schnittstelle können beispielsweise
Anwendungen wie beispielsweise Apps in Electron, Android oder iOS. Über die Schnittstelle können beispielsweise
Konfiguratoren oder Produkt-Auswahlhilfen (z.B. OXOMI) in Webshops integriert werden. Weiterhin kann ein Shop direkt
mit einem Handwerker-ERP Daten austauschen.

Expand All @@ -24,10 +24,10 @@ const catalog = new buzz.Connector({
Anschließend kann geprüft werden, ob die Gegenstelle vorhanden ist und bestimmte Funktionen unterstützt:

```javascript
catalog.queryCapability('item', function() {
catalog.queryCapability('item', function () {
// Die Gegenstelle kann Informationen zur Artikeln bieten -> anfragen und anzeigen...
document.querySelectorAll('.item').forEach(function(node) {
catalog.call('item', {}, {item: node.dataset['item']}, function(response) {
document.querySelectorAll('.item').forEach(function (node) {
catalog.call('item', {}, {item: node.dataset['item']}, function (response) {
node.querySelector('.price').textContent = response.payload().price;
node.querySelector('.availability').textContent = response.payload().availability;
});
Expand All @@ -36,6 +36,7 @@ catalog.queryCapability('item', function() {
```

Wird aktiv selbst eine fremde Komponente eingebunden, so kann ein eigener Link eingerichtet werden:

```javascript
const client = new buzz.Connector({
name: 'shop',
Expand All @@ -44,18 +45,52 @@ const client = new buzz.Connector({
```

Dieser kann dann der Komponente mitgeteilt werden, oder an einen iFrame angedockt werden:

```javascript
new buzz.installDownlink(window.catalogFrame, {link: 'inner'});
```

Alternativ kann ein iFrame auch direkt an den "Haupt-link" konfiguriert werden, wenn keine Schnittstelle "nach außen"
benötigt wird:

```javascript
new buzz.installDownlink(window.catalogFrame, {});
```

Beispiele finden sich in [shop.html](shop.html) sowie [catalog.html](catalog.html).

## Härtung der Origin-Kommunikation (optional)

BUZZ wird bewusst auf beliebigen Integrator-Domains eingebunden. Eine statische, global erzwungene Origin-Whitelist
würde deshalb potenziell Integrationen brechen. Standardmäßig akzeptiert BUZZ daher Nachrichten von beliebigen Origins
und sendet Cross-Frame-Nachrichten mit `targetOrigin = '*'` (unverändertes Verhalten).

Integrationen, die ihre Origins kennen, können die Kommunikation **optional** härten. Same-Window-Bus-Nachrichten
(`event.source === window`) werden dabei immer akzeptiert, da sie keine Origin-Grenze überschreiten; gefiltert
werden ausschließlich echte Cross-Frame-Nachrichten.

Global (betrifft Connector-Listener und den Uplink zum Parent-Fenster):

```javascript
buzz.configure({
allowedOrigins: ['https://shop.example.com'], // nur diese Origins dürfen Cross-Frame-Nachrichten senden
targetOrigin: 'https://shop.example.com' // Ziel-Origin beim Weiterleiten an das Parent-Fenster
});
```

Pro Downlink (betrifft die Kommunikation mit einem konkreten Child-iFrame):

```javascript
buzz.installDownlink(window.catalogFrame, {
link: 'inner',
targetOrigin: 'https://configurator.example.com', // Ziel-Origin beim Posten an das Child-Frame
allowedOrigins: ['https://configurator.example.com'] // akzeptierte Origins für Nachrichten aus dem Child-Frame
});
```

Werden die Optionen weggelassen, bleibt das bisherige Verhalten vollständig erhalten (kein Breaking Change).
Die Fenster-Identität (`event.source`) wird bei Downlinks unabhängig von `allowedOrigins` weiterhin geprüft.

## Standard-Nachrichten

Um einen sinnvollen Austausch unterschiedlicher Systeme zu ermöglichen, werden im folgenden Nachrichten für gängige
Expand All @@ -66,15 +101,19 @@ nach Bedarf mi-übermittelt werden.

Elemente wie Konfiguratoren oder erweiterte Artikeldatenbanken benötigen Informationen bezüglich Preis und Verfügbarkeit
eines Artikels. Hierfür wird die "capability" bzw. der Nachrichten-Type **priceAvailability** verwendet. Um die
Anzeige weiter anzupassen, können Artikelnummer, Type und Kurztext überschrieben werden (um so z.B. eine Herstellernummer
Anzeige weiter anzupassen, können Artikelnummer, Type und Kurztext überschrieben werden (um so z.B. eine
Herstellernummer
durch eine eigene Artikelnummer zu ersetzen).

**Request**

* version: Versionsnummer der Nachricht. Derzeit immer "1".
* supplierNumber: Gibt die Herstellernummer/Lieferantennummer an, falls bekannt. Falls ein eigener Artikel angefragt wird, kann hier "-" verwendet werden. Falls die Herstellernummer unbekannt ist, kann der Parameter weggelassen werden.
* supplierNumber: Gibt die Herstellernummer/Lieferantennummer an, falls bekannt. Falls ein eigener Artikel angefragt
wird, kann hier "-" verwendet werden. Falls die Herstellernummer unbekannt ist, kann der Parameter weggelassen werden.
* itemNumber: Gibt die Artikelnummer an, für die Informationen geliefert werden.

**Response**

* itemNumber: Die effektive Artikelnummer die angezeigt werden soll (optional)
* model: Die effektive Type die angezeigt werden soll (optional)
* shortText: Der effektive Kurztext der angezeigt werden soll (optional)
Expand All @@ -89,6 +128,7 @@ Neben der reinen Anzeige von Preis- und Verfügbarkeit, können mit **itemData**
werden. Die Anfrage ist hierbei gleich wie bei **priceAvailability**.

**Response**

* *Felder aus **priceAvailability** Antwort*
* previewImageUrl: Url zu einem Vorschaubild (optional)
* datasheetUrl: Url zu einem Datenblatt / Produkt-Detailseite (optional)
Expand All @@ -98,16 +138,16 @@ werden. Die Anfrage ist hierbei gleich wie bei **priceAvailability**.
Um Artikel in den Warenkorb zu legen, werden unterschiedliche Capabilities / Nachrichten verwendet:

* **addItemToBasket**
* supplierNumber: Lieferantennummer
* itemNumber: Artikelnummer
* quantity: Menge
* unit: Mengeneinheit (optional)
* shortText: Kurzbeschreibung der Position
* supplierName: Name des Herstellers / der Marke
* previewImageUrl: Vorschaubild für den Artikel

* supplierNumber: Lieferantennummer
* itemNumber: Artikelnummer
* quantity: Menge
* unit: Mengeneinheit (optional)
* shortText: Kurzbeschreibung der Position
* supplierName: Name des Herstellers / der Marke
* previewImageUrl: Vorschaubild für den Artikel

## Lizenz

```
MIT License

Expand Down
79 changes: 73 additions & 6 deletions buzz.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,33 @@
*/
let idCounter = 0;

/**
* Holds the optional, opt-in origin-hardening configuration.
*
* When left at its defaults, the library behaves exactly as before: messages from any origin are accepted, and
* cross-frame messages are posted with a '*' target origin. A hard global whitelist is intentionally NOT enforced
* by default because buzz is often embedded in arbitrary integrator domains - see buzz.configure().
*
* @type {{allowedOrigins: (string[]|null), targetOrigin: string}}
*/
const securityConfig = {
allowedOrigins: null,
targetOrigin: '*'
};

/**
* Determines whether a cross-frame message from the given origin may be processed.
*
* Returns true unless an explicit allowlist has been configured via buzz.configure(). This keeps the default
* behavior unchanged so existing integrations on arbitrary integrator domains keep working.
*
* @param {string} origin the origin of the received message
* @returns {boolean} true if the origin is allowed, false otherwise
*/
function isOriginAllowed(origin) {
return securityConfig.allowedOrigins === null || securityConfig.allowedOrigins.indexOf(origin) !== -1;
}

/**
* Generates a unique message ID.
*
Expand Down Expand Up @@ -178,15 +205,18 @@
window.addEventListener('message', function (event) {
try {
const data = JSON.parse(event.data);
if (isBuzzMessage(data, _me.link) && data.sender !== _me.uid && (!data.receiver || data.receiver === _me.uid)) {
if (isBuzzMessage(data, _me.link)
&& data.sender !== _me.uid
&& (!data.receiver || data.receiver === _me.uid)
&& (event.source === window || isOriginAllowed(event.origin))) {
if (_me.options.hasOwnProperty('customMessageCallback')) {
_me.options.customMessageCallback(new buzz.Message(_me, data));
} else {
const callback = _me.capabilities[data.type];
if (callback != null) {
try {
callback(new buzz.Message(_me, data));
} catch(error) {
} catch (error) {
console.log("BUZZ handler failed to execute!", error);
}
}
Expand Down Expand Up @@ -258,6 +288,33 @@
}


/**
* Optionally hardens the cross-frame communication against foreign frames.
*
* This is fully opt-in and backwards compatible: without calling this (or with empty options), buzz keeps its
* previous behavior of accepting messages from any origin and posting cross-frame messages with a '*' target origin.
* A hard global whitelist is intentionally NOT enforced, because buzz/oxomi.js run on arbitrary integrator domains
* and a static whitelist would break legitimate integrations.
*
* Same-window bus messages (event.source === window) are always accepted regardless of the allowlist, as they never
* cross an origin boundary; only genuine cross-frame messages are filtered.
*
* @param {Object} [options] the hardening options
* @param {string[]} [options.allowedOrigins] origins from which cross-frame messages are accepted (uplink
* forwarding and direct cross-frame posts). When omitted, messages from any origin are accepted (default).
* @param {string} [options.targetOrigin] the target origin used when forwarding messages to the parent
* window via the uplink. Defaults to '*'.
*/
buzz.configure = function (options) {
options = options || {};
if (Array.isArray(options.allowedOrigins)) {
securityConfig.allowedOrigins = options.allowedOrigins;
}
if (typeof options.targetOrigin === 'string') {
securityConfig.targetOrigin = options.targetOrigin;
}
}
Comment thread
sabieber marked this conversation as resolved.

/**
* Enables the built-in debugger, which logs all messages to the console.
*/
Expand Down Expand Up @@ -287,11 +344,21 @@
* @param {HTMLIFrameElement} childFrame the iFrame to connect
* @param {Object} options the options to pass in
* @param {string} [options.link] the link to connect to. This can be left empty, to use the default link.
* @param {string} [options.targetOrigin] the target origin used when posting messages to the child frame.
* Defaults to '*' (unchanged behavior) when omitted.
* @param {string[]} [options.allowedOrigins] origins from which messages sent by the child frame are accepted.
* When omitted, messages from the child window are accepted regardless of origin (unchanged behavior). The
* child window identity (event.source) is always verified independently of this option.
* @param {Object} extensions a JSON object which will be appended to the payload of each message received from the
* childFrame.
*/
buzz.installDownlink = function (childFrame, options, extensions) {
const link = options.link || LINK_NAME_BUZZ_ROOT;
const childTargetOrigin = options.targetOrigin || '*';
const childAllowedOrigins = Array.isArray(options.allowedOrigins) ? options.allowedOrigins : null;
const isChildOriginAllowed = function (origin) {
return childAllowedOrigins === null || childAllowedOrigins.indexOf(origin) !== -1;
};
console.log('scireum BUZZ - Installing a downlink for bus ' + link + '... ', window, childFrame.contentWindow);
window.addEventListener('message', function (event) {
if (event.source === window) {
Expand All @@ -300,14 +367,14 @@
const data = JSON.parse(event.data);
if (isBuzzMessage(data, link) && !data.uplink) {
data.buzzLink = LINK_NAME_BUZZ_ROOT;
childFrame.contentWindow.postMessage(JSON.stringify(data), '*');
childFrame.contentWindow.postMessage(JSON.stringify(data), childTargetOrigin);
}
} catch (ignored) {
// Only triggered, if an external message (which is either not a string or
// isn't well-formed JSON) is received. In any case, we can discard this error
// as it wasn't a BUZZ message anyway and we'd only jam the browser console...
}
} else if (childFrame && childFrame.contentWindow && event.source === childFrame.contentWindow) {
} else if (childFrame && childFrame.contentWindow && event.source === childFrame.contentWindow && isChildOriginAllowed(event.origin)) {
// Receive messages from child window...
try {
const data = JSON.parse(event.data);
Expand Down Expand Up @@ -345,10 +412,10 @@
window.addEventListener('message', function (event) {
try {
const data = JSON.parse(event.data);
if (event.source !== window.parent) {
if (event.source !== window.parent && (event.source === window || isOriginAllowed(event.origin))) {
if (isBuzzMessage(data, LINK_NAME_BUZZ_ROOT)) {
data.buzzLink = LINK_NAME_UPLINK;
window.parent.postMessage(JSON.stringify(data), "*");
window.parent.postMessage(JSON.stringify(data), securityConfig.targetOrigin);
}
}
} catch (ignored) {
Expand Down