|
| 1 | +# crx-bridge |
| 2 | + |
| 3 | +Messaging in Chrome extensions made super easy. Out of the box. |
| 4 | + |
| 5 | +## How much easy exactly? |
| 6 | + |
| 7 | +This much |
| 8 | + |
| 9 | +<a name="example"></a> |
| 10 | + |
| 11 | +```javascript |
| 12 | +// Inside devtools script |
| 13 | + |
| 14 | +import Bridge from 'crx-bridge'; |
| 15 | + |
| 16 | +async function main() { |
| 17 | + const response = await Bridge.sendMessage('are-we-ready', { really: true }, 'content-script'); |
| 18 | + console.log('ready status', response); // > ready status yes |
| 19 | +} |
| 20 | + |
| 21 | +main(); |
| 22 | +``` |
| 23 | + |
| 24 | +```javascript |
| 25 | +// Inside content script |
| 26 | + |
| 27 | +import Bridge from 'crx-bridge'; |
| 28 | + |
| 29 | +Bridge.onMessage('are-we-ready', async (message) => { |
| 30 | + const { data, sender, timestamp } = message; |
| 31 | + console.log(sender.path, timestamp); // > devtools@681 1509269137197 |
| 32 | + const someResult = await Bridge.sendMessage('do-stuff', { age: 2 }, 'background') |
| 33 | + return someResult.count > 4 ? 'yes' : 'no'); |
| 34 | +}) |
| 35 | + |
| 36 | +``` |
| 37 | + |
| 38 | +```javascript |
| 39 | +// Inside background script |
| 40 | + |
| 41 | +import Bridge from 'crx-bridge'; |
| 42 | + |
| 43 | +Bridge.onMessage('do-stuff', (message) => { |
| 44 | + const { data, sender, timestamp } = message; |
| 45 | + console.log(sender.path, timestamp); // > content-script@681 1509269137199 |
| 46 | + // return a Promise, sender will get resolved value |
| 47 | + return fetch(`https://api.example.com/stuff?age=${data.age}`) |
| 48 | +}) |
| 49 | + |
| 50 | +``` |
| 51 | + |
| 52 | +> Examples above require transpilation and/or bundling using `webpack`/`babel`/`rollup` |
| 53 | +
|
| 54 | +`crx-bridge` handles everything for you as efficiently as possible. No more `chrome.runtime.sendMessage` or `chrome.runtime.onConnect` or `chrome.runtime.connect` .... |
| 55 | + |
| 56 | +# Contents |
| 57 | + - [Setup](#setup) |
| 58 | + - [API](#api) |
| 59 | + - [Behaviour](#behaviour) |
| 60 | + - [Security Note](#security) |
| 61 | + - [Troubleshooting](#troubleshooting) |
| 62 | + |
| 63 | +<a name="setup"></a> |
| 64 | +# Setup |
| 65 | + |
| 66 | +### Install |
| 67 | + |
| 68 | +```bash |
| 69 | +$ npm i crx-bridge |
| 70 | +``` |
| 71 | + |
| 72 | +### Light it up |
| 73 | + |
| 74 | +Just `import Bridge from 'crx-bridge'` wherever you need it and use as shown in [example above](#example) |
| 75 | +> Even if your extension doesn't need a background page or wont be sending/receiving messages in background script. |
| 76 | +<br> `crx-bridge` uses background/event context as staging area for messages, therefore it **must** loaded in background/event page for it to work. |
| 77 | +<br> (Attempting to send message from any context will fail silently if `crx-bridge` isn't available in background page). |
| 78 | +<br> See [troubleshooting section](#troubleshooting) for more. |
| 79 | +
|
| 80 | + |
| 81 | +<a name="api"></a> |
| 82 | +# API |
| 83 | + |
| 84 | + |
| 85 | +## `Bridge.sendMessage(messageId: string, data: any, destination: string)` |
| 86 | +Sends a message to some other part of your extension, out of the box. |
| 87 | + |
| 88 | +Listener on the other may want to reply. Get the reply by `await`ing the returned `Promise` |
| 89 | + |
| 90 | +#### `messageId` |
| 91 | +> Required | `string` |
| 92 | +
|
| 93 | +Any `string` that both sides of your extension agree on. Could be `get-flag-count` or `getFlagCount`, as long as it's same on receiver's `onMessage` listener. |
| 94 | + |
| 95 | +#### `data` |
| 96 | +> Required | `any` |
| 97 | +
|
| 98 | +Any value you want to pass to other side, latter can access this value by refering to `data` property of first argument to `onMessage` callback function. |
| 99 | + |
| 100 | +#### `destination` |
| 101 | +> Required | `string` |
| 102 | +
|
| 103 | +The actual identifier of other endpoint. |
| 104 | +Example: `devtools` or `content-script` or `background` or `content-script@133` or `devtools@453` or `content-script@13:frame#2` |
| 105 | + |
| 106 | +Structure: |
| 107 | + - Must begin with known roots - `background` or `content-script` or `devtools` or `window` |
| 108 | + - `content-script`, `window` and `devtools` roots can be followed by `frame`(s), separated by `':'`, like `content-script:frame#3` (here #3 means third frame from top (zero-index based)) |
| 109 | + - Known root can only occur once or put another way, applicable roots can only be followed by `frame`(s), separated by `:` |
| 110 | + - `frame`(s) can be followed by `frame`(s), like `content-script@14:frame:frame#2` |
| 111 | + - `content-script`, `window` and `devtools` roots can be suffixed with `@tabId` to target specific tab. Example: `devtools@351`, points to devtools panel inspecting tab with id 351. |
| 112 | + |
| 113 | + Read `Behaviour` section to see how destinations (or endpoints) are treated. |
| 114 | + |
| 115 | + > Note: For security reasons, if you want to receive or send messages to or from `window` root context or any `frame` context, one of your extension's content script must call `Bridge.enableExternalMessaging(<namespace: string>)` to unlock message routing. Also call `Bridge.setNamespace(<namespace: string>)` in those `window` or `frame` contexts. Use same namespace string in those two calls, so `crx-bridge` knows which message belongs to which extension (in case multiple extensions are using `crx-bride` in one page) |
| 116 | +
|
| 117 | +___ |
| 118 | + |
| 119 | + ## `Bridge.onMessage(messageId: string, callback: fn)` |
| 120 | +Register one and only one listener, per messageId per context. That will be called upon `Bridge.sendMessage` from other side. |
| 121 | + |
| 122 | +Optionally, send a response to sender by returning any value or if async a `Promise`. |
| 123 | + |
| 124 | +#### `messageId` |
| 125 | +> Required | `string` |
| 126 | +
|
| 127 | +Any `string` that both sides of your extension agree on. Could be `get-flag-count` or `getFlagCount`, as long as it's same in sender's `sendMessage` call. |
| 128 | + |
| 129 | +#### `callback` |
| 130 | +> Required | `fn` |
| 131 | +
|
| 132 | +A callback function `Bridge` should call when a message is received with same `messageId`. The callback function will be called with one argument, a `BridgeMessage` which has `sender`, `data` and `timestamp` as its properties. |
| 133 | + |
| 134 | +Optionally, this callback can return a value or a `Promise`, resolved value will sent as reply to sender. |
| 135 | + |
| 136 | +Read [security note](#security) before using this. |
| 137 | + |
| 138 | +___ |
| 139 | + |
| 140 | + ## `Bridge.enableExternalMessaging(namespace: string)` |
| 141 | +> Caution: Dangerous action |
| 142 | +
|
| 143 | +Applicable to content scripts (noop if called from anywhere else) |
| 144 | + |
| 145 | +Unlocks the transmission of messages to and from `window` and `frame` contexts in the tab where it is called. |
| 146 | +`crx-bridge` by default won't transmit any payload to or from `window` and `frame` contexts for security reasons. |
| 147 | +This method can be called from a content script (in top frame of tab), which opens a gateway for messages. |
| 148 | + |
| 149 | +`window` = the top frame of any tab |
| 150 | +`frame`(s) = sub-frames as iframes of `window` or `frame`(s) |
| 151 | + |
| 152 | +#### `namespace` |
| 153 | +> Required | `string` |
| 154 | +
|
| 155 | +Can be a domain name reversed like `com.github.facebook.react_devtools` or any `uuid`. Call `Bridge.setNamespace` in `window` and `frame` contexts with same value, so that `crx-bridge` knows which payload belongs to which extension (in case there are other extensions using `crx-bridge` in a tab). Make sure namespace string is unique enough to |
| 156 | +ensure no collisions happen. |
| 157 | + |
| 158 | +___ |
| 159 | + |
| 160 | + ## `Bridge.setNamespace(namespace: string)` |
| 161 | + |
| 162 | +Applicable to scripts in regular web pages. |
| 163 | + |
| 164 | +Sets the namespace `Bridge` should use when relaying messages to and from `window` or `frame`(s). In a sense, it connects the callee context to the extension which called `Bridge.enableExternalMessaging(<namespace: string>)` in it's content script with same namespace. |
| 165 | + |
| 166 | +#### `namespace` |
| 167 | +> Required | `string` |
| 168 | +
|
| 169 | +Can be a domain name reversed like `com.github.facebook.react_devtools` or any `uuid`. Call `Bridge.setNamespace` in `window` and `frame` contexts with same value, so that `crx-bridge` knows which payload belongs to which extension (in case there are other extensions using `crx-bridge` in a tab). Make sure namespace string is unique enough to ensure no collisions happen. |
| 170 | + |
| 171 | +## Extras |
| 172 | + |
| 173 | +The following API is built on top of `Bridge.sendMessage` and `Bridge.onMessage`, in other words, it's just a wrapper, the routing and security rules still apply the same way. |
| 174 | + |
| 175 | +### `Bridge.openStream(channel: string, destination: string)` |
| 176 | + |
| 177 | +Opens a `Stream` between caller and destination. |
| 178 | + |
| 179 | +Returns a `Promise` which resolves with `Stream` when the destination is ready (loaded and `Bridge.onOpenStreamChannel` callback registered). |
| 180 | +Example below illustrates a use case for `Stream` |
| 181 | + |
| 182 | +#### `channel` |
| 183 | +> Required | `string` |
| 184 | +
|
| 185 | +`Stream`(s) are strictly scoped `Bridge.sendMessage`(s). Scopes could be different features of your extension that need to talk to the other side, and those scopes are named using a channel id. |
| 186 | + |
| 187 | +#### `destination` |
| 188 | +> Required | `string` |
| 189 | +
|
| 190 | +Same as `destination` in `Bridge.sendMessage(msgId, data, destination)` |
| 191 | + |
| 192 | +___ |
| 193 | + |
| 194 | +### `Bridge.onOpenStreamChannel(channel: string, callback: fn)` |
| 195 | + |
| 196 | +Registers a listener for when a `Stream` opens. |
| 197 | +Only one listener per channel per context |
| 198 | + |
| 199 | +#### `channel` |
| 200 | +> Required | `string` |
| 201 | +
|
| 202 | +`Stream`(s) are strictly scoped `Bridge.sendMessage`(s). Scopes could be different features of your extension that need to talk to the other side, and those scopes are named using a channel id. |
| 203 | + |
| 204 | +#### `callback` |
| 205 | +> Required | `fn` |
| 206 | +
|
| 207 | +Callback that should be called whenever `Stream` is opened from the other side. Callback will be called with one argument, the `Stream` object, documented below. |
| 208 | + |
| 209 | +`Stream`(s) can be opened by a malicious webpage(s) if a tab's content script has called `Bridge.enableExternalMessaging`, if working with sensitive information use `stream.info.endpoint.isInternal()` to check, if `false` call `stream.close()` immediately. |
| 210 | + |
| 211 | +### Stream Example |
| 212 | + |
| 213 | +```javascript |
| 214 | +// background.js |
| 215 | + |
| 216 | +// To-Do |
| 217 | + |
| 218 | +``` |
| 219 | + |
| 220 | +<a name="behaviour"></a> |
| 221 | +# Behaviour |
| 222 | + |
| 223 | +> Following rules apply to destination being specified in `Bridge.sendMessage(msgId, data, destination)` and `Bridge.openStream(channelId, initialData, destination)` |
| 224 | +
|
| 225 | + - Specifying `devtools` as destination from `content-script` will auto-route payload to inspecting `devtools` page if open and listening. |
| 226 | + |
| 227 | + - Specifying `content-script` as destination from `devtools` will auto-route the message to inspected window's top `content-script` page if listening. If page is loading, message will be queued up and deliverd when page is ready and listening. |
| 228 | + |
| 229 | + - If `frame` or `window` (which could be a script injected by content script) are source or destination of any payload, transmission must be first unlocked by calling `Bridge.enableExternalMessagin(<namespace: string>)` inside a content script, since `Bridge` will first deliver the payload to `content-script` using rules above, and latter will take over and forward accordingly. `content-script` <-> `window`, `window` <-> `frame`, `frame` <-> `frame` and `content-script` <-> `frame` messaging happens using `window.postMessage` API. Therefore to avoid conflicts, `Bridge` requires you to call `Bridge.setNamespace(uuidOrReverseDomain)` inside `window` and `frame` contexts (even if they are just relaying a payload across), before routing occurs. |
| 230 | + |
| 231 | + - The rule above does not apply to `frame`(s) inside `devtools`. Bridge assumes that everything you load up in devtools panel is all under your ownership and control. However in a tab, there might be other extensions using `crx-bridge`. Calling `Bridge.sendMessage(msgId, data, 'devtools:frame#1:frame')` from `content-script` will work out of the box. |
| 232 | + |
| 233 | + - Routing to and from `window` or `frame` contexts will only work if there is content script loaded in top frame of the page. The same page which hosts those `window` or `frame`(s). The content script must also have `crx-bridge` imported and a call to `Bridge.enableExternalMessagin(<namespace: string>)` |
| 234 | + |
| 235 | + - Specifying `devtools` or `content-script` or `window` from `background` will throw an error. When calling from `background`, destination must be suffixed with tab id. Like `devtools@745` for `devtools` inspecting tab id 745 or `content-script@351` for top `content-script` at tab id 351. |
| 236 | + |
| 237 | +<a name="security"></a> |
| 238 | + # Serious security note |
| 239 | + |
| 240 | + The following note only applies if and only if, you will be sending/receiving messages to/from `window` or `frame` contexts. There's no security concern if you will be only working with `content-script`, `background` or `devtools` scope. |
| 241 | + |
| 242 | + `window` and `frame` contexts get unlocked the moment you call `Bridge.enableExternalMessaging(namespace)` somewhere in your extenion's content script(s). |
| 243 | + |
| 244 | + Unlike `chrome.runtime.sendMessage` and `chrome.runtime.connect`, which requires extension's manifest to specify sites allowed to talk with the extension, `crx-bridge` has no such measure by design, which means any webpage whether you intended or not, can do `Bridge.sendMessage(msgId, data, 'background')` or something similar that produces same effect, as long as it uses same protocol used by `crx-bridge` and namespace set to same as yours. |
| 245 | + |
| 246 | + So to be safe, if you will be interacting with `window` or `frame` contexts, treat `crx-bridge` as you would treat `window.postMessage` API. |
| 247 | + |
| 248 | + As an example if you plan on having something critical, **always** verify the `sender` before responding: |
| 249 | + |
| 250 | +```javascript |
| 251 | +// background.js |
| 252 | + |
| 253 | +Bridge.onMessage('getUserBrowsingHistory', (message) => { |
| 254 | + const { data, sender } = message; |
| 255 | + // Respond only if request is from 'devtools', 'content-script' or 'background' endpoint |
| 256 | + if(sender.isInternal()) { |
| 257 | + const { range } = data; |
| 258 | + return getHistory(range); |
| 259 | + } |
| 260 | +}) |
| 261 | +``` |
| 262 | + |
| 263 | +> Since `crx-bridge` assumes you have full control over what loads in your devtools panel, `sender.isInternal()` will return `true` for `devtools:frame#2:frame#1` |
| 264 | +<br> Alternatively use `sender.isReallyInternal()` to return `false` in such cases. |
| 265 | +
|
| 266 | +<a name="troubleshooting"></a> |
| 267 | +# Troubleshooting |
| 268 | + |
| 269 | + - Doesn't work? |
| 270 | + <br>If `window` and `frame` contexts are not part of the puzzle, `crx-bridge` works out of the box for messaging between `devtools` <-> `background` <-> `content-script`(s). If even that is not working, it's likely that `crx-bridge` hasn't been loaded in background page of your extension, which is used by `crx-bridge` as a staging area. If you don't need a background page for yourself, here's bare minimum to get `crx-bridge` going. |
| 271 | + ```javascript |
| 272 | + // background.js (requires transpilation/bundling using webpack(recommended)) |
| 273 | + import 'crx-bridge'; |
| 274 | + ``` |
| 275 | + |
| 276 | + ```javascript |
| 277 | + // manifest.json |
| 278 | + { |
| 279 | + "background": { |
| 280 | + "scripts": ["path/to/transpiled/background.js"] |
| 281 | + } |
| 282 | + } |
| 283 | + ``` |
| 284 | + |
| 285 | + - Can't send messages to `window`? |
| 286 | + <br>Sending or receiving messages from or to `window` (and `frame`(s)) requires you to open the messaging gateway in content script(s) for that particular tab. Call `Bridge.enableExternalMessaging(<namespaceA: string>)` in any of your content script(s) and call `Bridge.setNamespace(<namespaceB: string>)` in webpage or `window` context. Make sure that `namespaceA === namespaceB`. If you're doing this, read the [security note above](#security) |
0 commit comments