Skip to content

Commit 47df5df

Browse files
authored
Add basic extension add-on framework. (#2155)
There are a lot of things we can do to create more extension points, but this at least provides a starting point which already opens up a lot of possiblities. Fixes #2143 Fixes #2144
1 parent 7191a5d commit 47df5df

File tree

14 files changed

+273
-10
lines changed

14 files changed

+273
-10
lines changed

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"express-ws": "^4.0.0",
5252
"find": "^0.3.0",
5353
"gateway-addon": "https://github.com/mozilla-iot/gateway-addon-node",
54+
"glob-to-regexp": "^0.4.1",
5455
"http-proxy": "^1.17.0",
5556
"ip-regex": "^4.1.0",
5657
"jsonwebtoken": "^8.5.1",

src/addon-manager.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class AddonManager extends EventEmitter {
4848
this.notifiers = new Map();
4949
this.devices = {};
5050
this.outlets = {};
51+
this.extensions = {};
5152
this.deferredAdd = null;
5253
this.deferredRemove = null;
5354
this.addonsLoaded = false;
@@ -226,6 +227,27 @@ class AddonManager extends EventEmitter {
226227
(n) => n.getPackageName() === packageId);
227228
}
228229

230+
/**
231+
* @method getExtensions
232+
* @returns Returns a Map of the loaded extensions. The dictionary
233+
* key corresponds to the extension ID.
234+
*/
235+
getExtensions() {
236+
return this.extensions;
237+
}
238+
239+
/**
240+
* @method getExtensionsByPackageId
241+
* @returns Returns a Map of loaded extensions with the given package ID.
242+
*/
243+
getExtensionsByPackageId(packageId) {
244+
if (this.extensions.hasOwnProperty(packageId)) {
245+
return this.extensions[packageId];
246+
}
247+
248+
return {};
249+
}
250+
229251
/**
230252
* @method getDevice
231253
* @returns Returns the device with the indicated id.
@@ -602,6 +624,17 @@ class AddonManager extends EventEmitter {
602624
throw new Error(`Add-on not enabled: ${manifest.id}`);
603625
}
604626

627+
if (manifest.content_scripts && manifest.web_accessible_resources) {
628+
this.extensions[manifest.id] = {
629+
extensions: manifest.content_scripts,
630+
resources: manifest.web_accessible_resources,
631+
};
632+
}
633+
634+
if (!manifest.exec && config.get('ipc.protocol') !== 'inproc') {
635+
return;
636+
}
637+
605638
const errorCallback = (packageId, err) => {
606639
console.error(`Failed to load add-on ${packageId}:`, err);
607640
};
@@ -843,6 +876,10 @@ class AddonManager extends EventEmitter {
843876
return Promise.resolve();
844877
}
845878

879+
if (this.extensions.hasOwnProperty(packageId)) {
880+
delete this.extensions[packageId];
881+
}
882+
846883
const plugin = this.getPlugin(packageId);
847884
let pluginProcess = {};
848885
if (plugin) {

src/addon-utils.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,11 @@ function validateManifestJson(manifest) {
129129
version: '',
130130
};
131131

132-
if (config.get('ipc.protocol') !== 'inproc') {
133-
// If we're not using in-process plugins, then we also need the exec
134-
// keyword to exist.
132+
if (config.get('ipc.protocol') !== 'inproc' &&
133+
// eslint-disable-next-line max-len
134+
manifest.gateway_specific_settings.webthings.primary_type !== 'extension') {
135+
// If we're not using in-process plugins, and this is not an extension,
136+
// then we also need the exec keyword to exist.
135137
manifestTemplate.gateway_specific_settings.webthings.exec = '';
136138
}
137139

@@ -429,6 +431,8 @@ function loadManifestJson(packageId) {
429431
version: manifest.version,
430432
primary_type: manifest.gateway_specific_settings.webthings.primary_type,
431433
exec: manifest.gateway_specific_settings.webthings.exec,
434+
content_scripts: manifest.content_scripts,
435+
web_accessible_resources: manifest.web_accessible_resources,
432436
enabled: false,
433437
};
434438

src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ exports.LOGS_PATH = '/logs';
3636
exports.PUSH_PATH = '/push';
3737
exports.PING_PATH = '/ping';
3838
exports.PROXY_PATH = '/proxy';
39+
exports.EXTENSIONS_PATH = '/extensions';
3940
// Remember we end up in the build/* directory so these paths looks slightly
4041
// different than you might expect.
4142
exports.STATIC_PATH = path.join(__dirname, '../static');
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use strict';
2+
3+
const AddonManager = require('../addon-manager');
4+
const UserProfile = require('../user-profile');
5+
const express = require('express');
6+
const fs = require('fs');
7+
const globToRegExp = require('glob-to-regexp');
8+
const jwtMiddleware = require('../jwt-middleware');
9+
const path = require('path');
10+
11+
const auth = jwtMiddleware.middleware();
12+
const ExtensionsController = express.Router();
13+
14+
ExtensionsController.get('/', auth, (request, response) => {
15+
const map = {};
16+
for (const [key, value] of Object.entries(AddonManager.getExtensions())) {
17+
map[key] = value.extensions;
18+
}
19+
response.status(200).json(map);
20+
});
21+
22+
ExtensionsController.get('/:extensionId/*', (request, response) => {
23+
const extensionId = request.params.extensionId;
24+
const relPath = request.path.split('/').slice(2).join('/');
25+
26+
// make sure the extension is installed and enabled
27+
const extensions = AddonManager.getExtensions();
28+
if (!extensions.hasOwnProperty(extensionId)) {
29+
response.status(404).send();
30+
return;
31+
}
32+
33+
// make sure the requested resource is listed in the extension's
34+
// web_accessible_resources array
35+
let matched = false;
36+
const resources = extensions[extensionId].resources;
37+
for (let resource of resources) {
38+
resource = globToRegExp(resource);
39+
if (resource.test(relPath)) {
40+
matched = true;
41+
break;
42+
}
43+
}
44+
45+
if (!matched) {
46+
response.status(404).send();
47+
return;
48+
}
49+
50+
// make sure the file actually exists
51+
const fullPath = path.join(UserProfile.addonsDir, extensionId, relPath);
52+
if (!fs.existsSync(fullPath)) {
53+
response.status(404).send();
54+
return;
55+
}
56+
57+
// finally, send the file
58+
response.sendFile(fullPath);
59+
});
60+
61+
module.exports = ExtensionsController;

src/router.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ const Router = {
5656
// First look for a static file
5757
const staticHandler = express.static(Constants.BUILD_STATIC_PATH);
5858
app.use(Constants.UPLOADS_PATH, express.static(UserProfile.uploadsDir));
59+
app.use(Constants.EXTENSIONS_PATH, nocache,
60+
require('./controllers/extensions_controller'));
5961
app.use((request, response, next) => {
6062
if (request.path === '/' && request.accepts('html')) {
6163
// We need this to hit RootController.

static/css/app.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ h1, h2, h3, h4, h5, h6 {
249249
z-index: 0;
250250
}
251251

252+
#menu-button.menu-shown {
253+
z-index: 1000;
254+
}
255+
252256
#menu-button {
253257
background: no-repeat center/100% url('../optimized-images/menu.svg');
254258
}

static/css/menu.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
background-color: #5288af;
1212
margin: 0;
1313
transition: transform 0.25s ease;
14+
z-index: 1000;
1415
}
1516

1617
#main-menu.hidden {
@@ -105,6 +106,7 @@
105106
height: 100%;
106107
width: 100%;
107108
animation: show-scrim 0.25s ease 0s;
109+
z-index: 999;
108110
}
109111

110112
#menu-scrim.hidden {

static/js/api.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,8 +408,17 @@ const API = {
408408
return res.json();
409409
});
410410
},
411+
412+
getExtensions: function() {
413+
return fetch('/extensions', {
414+
headers: this.headers(),
415+
}).then((res) => {
416+
return res.json();
417+
});
418+
},
411419
};
412420

421+
// Elevate this to the window level.
413422
window.API = API;
414423

415424
module.exports = API;

0 commit comments

Comments
 (0)