boomack 0.13.1 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/public/css/main.css +4 -2
- package/client/views/parts/slot.ejs +14 -2
- package/default-config.yaml +5 -0
- package/package.json +1 -1
- package/server-build/index.js +7 -0
- package/server-build/model/display-request.js +18 -2
- package/server-build/model/display-request.test.js +61 -3
- package/server-build/model/layout.js +4 -0
- package/server-build/model/layout.test.js +9 -1
- package/server-build/pipeline.js +1 -1
- package/server-build/routes/display-requests.js +12 -16
- package/server-build/service/panels.js +1 -1
- package/server-build/typedefs.js +2 -0
- package/server-build/utils.js +18 -0
|
@@ -199,11 +199,13 @@ header.home-header {
|
|
|
199
199
|
.slot, .slot.ui.segment {
|
|
200
200
|
margin: 0;
|
|
201
201
|
padding: 0;
|
|
202
|
-
display: flex;
|
|
203
|
-
flex-direction: column;
|
|
204
202
|
overflow: hidden;
|
|
205
203
|
position: relative;
|
|
206
204
|
}
|
|
205
|
+
.layout-grid .slot {
|
|
206
|
+
display: flex;
|
|
207
|
+
flex-direction: column;
|
|
208
|
+
}
|
|
207
209
|
.slot.ui.segment.without-border {
|
|
208
210
|
border: none;
|
|
209
211
|
background: transparent;
|
|
@@ -49,11 +49,11 @@ function replaceThemePropsInStr(s) {
|
|
|
49
49
|
%><%= slot.zoom !== 1.0 && !contentScale ? ' zoom' : '' %><%
|
|
50
50
|
%>"
|
|
51
51
|
style="<%
|
|
52
|
-
if (!singled && panel.layout.grid) {
|
|
52
|
+
if (!singled && panel.layout.type === 'grid') {
|
|
53
53
|
%>grid-column: <%= slot.column + 1 %> / span <%= slot.columnSpan %>;<%
|
|
54
54
|
%>grid-row: <%= slot.row + 1 %> / span <%= slot.rowSpan %><%
|
|
55
55
|
}
|
|
56
|
-
if (panel.layout.document && commandSrc) {
|
|
56
|
+
if (panel.layout.type === 'document' && commandSrc) {
|
|
57
57
|
%>height: calc(100vh - 2em);<%
|
|
58
58
|
}
|
|
59
59
|
%>"
|
|
@@ -185,6 +185,9 @@ function replaceThemePropsInStr(s) {
|
|
|
185
185
|
if (slot.minHeight) {
|
|
186
186
|
%>min-height: <%= slot.minHeight %>;<%
|
|
187
187
|
}
|
|
188
|
+
if (slot.height) {
|
|
189
|
+
%>height: <%= slot.height %>;<%
|
|
190
|
+
}
|
|
188
191
|
%>">
|
|
189
192
|
<% if (slot.showId) { %><div class="slot-id"><%= slot.id %></div><% } %>
|
|
190
193
|
</div>
|
|
@@ -192,6 +195,9 @@ function replaceThemePropsInStr(s) {
|
|
|
192
195
|
if (slot.minHeight) {
|
|
193
196
|
%>min-height: <%= slot.minHeight %>;<%
|
|
194
197
|
}
|
|
198
|
+
if (slot.height) {
|
|
199
|
+
%>height: <%= slot.height %>;<%
|
|
200
|
+
}
|
|
195
201
|
if (commandBackground) {
|
|
196
202
|
%>background: <%= replaceThemePropsInStr(commandBackground) %>;<%
|
|
197
203
|
}
|
|
@@ -200,6 +206,12 @@ function replaceThemePropsInStr(s) {
|
|
|
200
206
|
if (!commandContent) {
|
|
201
207
|
%>display:none;<%
|
|
202
208
|
}
|
|
209
|
+
if (slot.height) {
|
|
210
|
+
%>height: <%= slot.height %>;<%
|
|
211
|
+
}
|
|
212
|
+
if (slot.maxHeight) {
|
|
213
|
+
%>max-height:<%= slot.maxHeight %>;overflow-y:scroll;<%
|
|
214
|
+
}
|
|
203
215
|
if (slot.colorFilter) {
|
|
204
216
|
if (_.isString(slot.colorFilter) && slot.colorFilter !== 'theme') {
|
|
205
217
|
%>filter: <%= slot.colorFilter %>;<%
|
package/default-config.yaml
CHANGED
|
@@ -177,6 +177,11 @@ api:
|
|
|
177
177
|
yaml: true
|
|
178
178
|
# The size limit for API requests in JSON or YAML
|
|
179
179
|
maxBodySize: 16 MB
|
|
180
|
+
# A list with allowed root paths for file URLs in display requests.
|
|
181
|
+
# Allows displaying files directly from the filesystem of the server.
|
|
182
|
+
# Empty by default for security reasons.
|
|
183
|
+
# Examples: /home/me/media, or C:\\Users\\Me\\Pictures
|
|
184
|
+
fileSrcRoots: []
|
|
180
185
|
|
|
181
186
|
cache:
|
|
182
187
|
# Control how cached resources are handled
|
package/package.json
CHANGED
package/server-build/index.js
CHANGED
|
@@ -140,6 +140,11 @@ function showConfiguration(cb) {
|
|
|
140
140
|
_.forEach(cfg.get('init.actions'), (a, aId) => {
|
|
141
141
|
txt += `\n ${aId} (${a.type})`;
|
|
142
142
|
});
|
|
143
|
+
const fileSrcRoots = cfg.get('api.request.fileSrcRoots');
|
|
144
|
+
txt += `\nFile Source Roots: ${_.size(fileSrcRoots)}`;
|
|
145
|
+
_.forEach(fileSrcRoots, r => {
|
|
146
|
+
txt += `\n ${utils.normalizePath(r)}`;
|
|
147
|
+
});
|
|
143
148
|
log.verbose('Configuration: \n' + txt);
|
|
144
149
|
cb(null);
|
|
145
150
|
}
|
|
@@ -189,6 +194,7 @@ function loadPlugIns(cb) {
|
|
|
189
194
|
const explicitPkgNames = new Set(cfg.get('plugins.packages', []));
|
|
190
195
|
let success = true;
|
|
191
196
|
for (const name of explicitPkgNames) {
|
|
197
|
+
log.verbose(`Loading explicit plugin '${name}'...`);
|
|
192
198
|
success && (success = loadPlugIn(pluginRoot, name));
|
|
193
199
|
}
|
|
194
200
|
if (discover) {
|
|
@@ -201,6 +207,7 @@ function loadPlugIns(cb) {
|
|
|
201
207
|
log.error(`Discovering plugins failed with: ${err}`);
|
|
202
208
|
}
|
|
203
209
|
for (const name of discoveredPkgNames) {
|
|
210
|
+
log.verbose(`Loading discovered plugin '${name}'...`);
|
|
204
211
|
success && (success = loadPlugIn(pluginRoot, name));
|
|
205
212
|
}
|
|
206
213
|
}
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @module
|
|
4
4
|
*/
|
|
5
|
+
const { fileURLToPath } = require('url');
|
|
6
|
+
const path = require('path');
|
|
5
7
|
const _ = require('lodash');
|
|
6
8
|
const mimeType = require('../service/mime-type');
|
|
7
9
|
const utils = require('../utils');
|
|
10
|
+
const cfg = require('../service/config');
|
|
8
11
|
const presets = require('../service/presets');
|
|
9
12
|
const typeMappings = require('../service/media-types');
|
|
10
13
|
const displayRequestDefaults = {
|
|
@@ -53,13 +56,26 @@ function sanitizeOptions(options) {
|
|
|
53
56
|
options.requires = utils.enforceArrayOf(options.requires, utils.enforceString);
|
|
54
57
|
return options;
|
|
55
58
|
}
|
|
59
|
+
function isValidFileSource(filename) {
|
|
60
|
+
if (!path.isAbsolute(filename))
|
|
61
|
+
return false;
|
|
62
|
+
filename = utils.normalizePath(filename);
|
|
63
|
+
return _.some(cfg.get('api.request.fileSrcRoots'), root => filename.startsWith(utils.normalizePath(root, true)));
|
|
64
|
+
}
|
|
56
65
|
function isValidSourceUrl(src) {
|
|
57
66
|
if (!_.isString(src))
|
|
58
67
|
return false;
|
|
59
|
-
|
|
68
|
+
const isHttpUrl = src.startsWith('http://') || src.startsWith('https://');
|
|
69
|
+
const isFileUrl = src.startsWith('file://');
|
|
70
|
+
if (!isHttpUrl && !isFileUrl)
|
|
71
|
+
return false;
|
|
72
|
+
if (isFileUrl && _.isEmpty(cfg.get('api.request.fileSrcRoots')))
|
|
60
73
|
return false;
|
|
61
74
|
try {
|
|
62
|
-
new URL(src);
|
|
75
|
+
const uri = new URL(src);
|
|
76
|
+
if (isFileUrl && !isValidFileSource(fileURLToPath(uri))) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
63
79
|
}
|
|
64
80
|
catch (err) {
|
|
65
81
|
return false;
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
const expect = require('chai').expect;
|
|
5
5
|
const mimeType = require('../service/mime-type');
|
|
6
6
|
const displayRequest = require('./display-request');
|
|
7
|
+
const cfg = require('../service/config');
|
|
8
|
+
beforeEach(function () {
|
|
9
|
+
cfg.reset();
|
|
10
|
+
});
|
|
7
11
|
describe('model/display-request', function () {
|
|
8
12
|
describe('sanitize', function () {
|
|
9
13
|
it('should return an error, if no content is given', function (done) {
|
|
@@ -65,9 +69,41 @@ describe('model/display-request', function () {
|
|
|
65
69
|
done();
|
|
66
70
|
});
|
|
67
71
|
});
|
|
68
|
-
it('should return a sanitized request, if only src is given', function (done) {
|
|
72
|
+
it('should return a sanitized request, if only http src is given', function (done) {
|
|
73
|
+
displayRequest.sanitize({
|
|
74
|
+
src: 'http://localhost/demo.html',
|
|
75
|
+
}, (err, x) => {
|
|
76
|
+
expect(err).to.be.null;
|
|
77
|
+
expect(x).to.be.an('object');
|
|
78
|
+
expect(x).to.deep.equal({
|
|
79
|
+
panel: 'default',
|
|
80
|
+
slot: null,
|
|
81
|
+
title: null,
|
|
82
|
+
type: null,
|
|
83
|
+
text: null,
|
|
84
|
+
data: null,
|
|
85
|
+
stream: null,
|
|
86
|
+
src: 'http://localhost/demo.html',
|
|
87
|
+
options: {
|
|
88
|
+
debug: false,
|
|
89
|
+
transformation: null,
|
|
90
|
+
syntax: null,
|
|
91
|
+
renderer: null,
|
|
92
|
+
iframe: null,
|
|
93
|
+
cache: 'auto',
|
|
94
|
+
extend: 'no',
|
|
95
|
+
scale: 'auto',
|
|
96
|
+
requires: [],
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
done();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it('should return a sanitized request, if only file src is given', function (done) {
|
|
103
|
+
const prefix = process.platform === 'win32' ? 'C:\\Users\\Me' : '/home/me';
|
|
104
|
+
cfg.set('api.request.fileSrcRoots', [prefix]);
|
|
69
105
|
displayRequest.sanitize({
|
|
70
|
-
src:
|
|
106
|
+
src: `file://${prefix}/demo.html`,
|
|
71
107
|
}, (err, x) => {
|
|
72
108
|
expect(err).to.be.null;
|
|
73
109
|
expect(x).to.be.an('object');
|
|
@@ -79,7 +115,7 @@ describe('model/display-request', function () {
|
|
|
79
115
|
text: null,
|
|
80
116
|
data: null,
|
|
81
117
|
stream: null,
|
|
82
|
-
src:
|
|
118
|
+
src: `file://${prefix}/demo.html`,
|
|
83
119
|
options: {
|
|
84
120
|
debug: false,
|
|
85
121
|
transformation: null,
|
|
@@ -95,6 +131,28 @@ describe('model/display-request', function () {
|
|
|
95
131
|
done();
|
|
96
132
|
});
|
|
97
133
|
});
|
|
134
|
+
it('should return an error, if disallowed file src is given', function (done) {
|
|
135
|
+
const prefix1 = process.platform === 'win32' ? 'C:\\Users\\Me' : '/home/me';
|
|
136
|
+
const prefix2 = process.platform === 'win32' ? 'C:\\Windows' : '/etc';
|
|
137
|
+
cfg.set('api.request.fileSrcRoots', [prefix1]);
|
|
138
|
+
displayRequest.sanitize({
|
|
139
|
+
src: `file://${prefix2}/secret.txt`,
|
|
140
|
+
}, (err, x) => {
|
|
141
|
+
expect(err).to.exist;
|
|
142
|
+
expect(x).to.be.undefined;
|
|
143
|
+
done();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
it('should return an error, if relative file src is given', function (done) {
|
|
147
|
+
cfg.set('api.request.fileSrcRoots', ['/relative']);
|
|
148
|
+
displayRequest.sanitize({
|
|
149
|
+
src: 'file://relative/demo.html',
|
|
150
|
+
}, (err, x) => {
|
|
151
|
+
expect(err).to.exist;
|
|
152
|
+
expect(x).to.be.undefined;
|
|
153
|
+
done();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
98
156
|
it('should sanitize request', function (done) {
|
|
99
157
|
displayRequest.sanitize({
|
|
100
158
|
unknown: 'unknown',
|
|
@@ -29,6 +29,8 @@ const gridSlotDefaults = _.defaults({
|
|
|
29
29
|
}, slotDefaults);
|
|
30
30
|
const documentSlotDefaults = _.defaults({
|
|
31
31
|
minHeight: '0',
|
|
32
|
+
height: null,
|
|
33
|
+
maxHeight: null,
|
|
32
34
|
}, slotDefaults);
|
|
33
35
|
const slotTemplateDefaults = {
|
|
34
36
|
history: 0,
|
|
@@ -43,6 +45,8 @@ const slotTemplateDefaults = {
|
|
|
43
45
|
border: true,
|
|
44
46
|
zoom: 1.0,
|
|
45
47
|
minHeight: '0',
|
|
48
|
+
height: null,
|
|
49
|
+
maxHeight: null,
|
|
46
50
|
};
|
|
47
51
|
const gridLayoutDefaults = {
|
|
48
52
|
columns: 1,
|
|
@@ -105,6 +105,8 @@ describe('model/layout', function () {
|
|
|
105
105
|
border: true,
|
|
106
106
|
zoom: 1.0,
|
|
107
107
|
minHeight: '0',
|
|
108
|
+
height: null,
|
|
109
|
+
maxHeight: null,
|
|
108
110
|
},
|
|
109
111
|
autoSlots: 0,
|
|
110
112
|
nextIndex: 1,
|
|
@@ -228,6 +230,8 @@ describe('model/layout', function () {
|
|
|
228
230
|
noMaximize: true,
|
|
229
231
|
zoom: 1.25,
|
|
230
232
|
minHeight: '20px',
|
|
233
|
+
height: '200px',
|
|
234
|
+
maxHeight: '50vh',
|
|
231
235
|
},
|
|
232
236
|
autoSlots: 0,
|
|
233
237
|
nextIndex: 1,
|
|
@@ -250,6 +254,8 @@ describe('model/layout', function () {
|
|
|
250
254
|
zoom: 1.5,
|
|
251
255
|
colorFilter: 'saturate(130%)',
|
|
252
256
|
minHeight: '25vh',
|
|
257
|
+
height: '20rem',
|
|
258
|
+
maxHeight: '100vh',
|
|
253
259
|
},
|
|
254
260
|
stash: {
|
|
255
261
|
id: 'stash',
|
|
@@ -270,7 +276,9 @@ describe('model/layout', function () {
|
|
|
270
276
|
0.0, -1.0, 0.0, 1.0,
|
|
271
277
|
0.0, 0.0, 1.0, 0.0,
|
|
272
278
|
],
|
|
273
|
-
minHeight: '
|
|
279
|
+
minHeight: '2rem',
|
|
280
|
+
height: '5em',
|
|
281
|
+
maxHeight: '100px',
|
|
274
282
|
},
|
|
275
283
|
},
|
|
276
284
|
title: 'Panel Title',
|
package/server-build/pipeline.js
CHANGED
|
@@ -170,19 +170,17 @@ exports.setup = function (app, io) {
|
|
|
170
170
|
return null;
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
-
let data;
|
|
174
173
|
try {
|
|
175
174
|
if (cfg.getBoolean('api.request.yaml')) {
|
|
176
|
-
|
|
175
|
+
return YAML.parse(s, { schema: 'yaml-1.1' });
|
|
177
176
|
}
|
|
178
177
|
else {
|
|
179
|
-
|
|
178
|
+
return JSON.parse(s);
|
|
180
179
|
}
|
|
181
180
|
}
|
|
182
181
|
catch (e) {
|
|
183
182
|
return null;
|
|
184
183
|
}
|
|
185
|
-
return _.isArray(data) ? data : [data];
|
|
186
184
|
}
|
|
187
185
|
function titleFromHttpHeader(req) {
|
|
188
186
|
const titleHeader = req.get('X-Boomack-Title');
|
|
@@ -191,16 +189,12 @@ exports.setup = function (app, io) {
|
|
|
191
189
|
null;
|
|
192
190
|
}
|
|
193
191
|
function optionsFromHttpHeader(req) {
|
|
194
|
-
let result = [];
|
|
195
|
-
const presetHeader = req.get('X-Boomack-Presets');
|
|
196
|
-
const presets = decodePresets(presetHeader);
|
|
197
|
-
if (presets)
|
|
198
|
-
result = presets;
|
|
199
192
|
const optionsHeader = req.get('X-Boomack-Options');
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
193
|
+
return decodeOptions(optionsHeader);
|
|
194
|
+
}
|
|
195
|
+
function presetsFromHttpHeader(req) {
|
|
196
|
+
const presetHeader = req.get('X-Boomack-Presets');
|
|
197
|
+
return decodePresets(presetHeader);
|
|
204
198
|
}
|
|
205
199
|
function handleStreamingDisplayRequest(req, res, panelId, slotId) {
|
|
206
200
|
const type = req.get('Content-Type');
|
|
@@ -213,6 +207,7 @@ exports.setup = function (app, io) {
|
|
|
213
207
|
return;
|
|
214
208
|
}
|
|
215
209
|
const title = titleFromHttpHeader(req);
|
|
210
|
+
const presets = presetsFromHttpHeader(req);
|
|
216
211
|
const options = optionsFromHttpHeader(req);
|
|
217
212
|
const displayRequest = {
|
|
218
213
|
panel: panelId,
|
|
@@ -222,6 +217,8 @@ exports.setup = function (app, io) {
|
|
|
222
217
|
};
|
|
223
218
|
if (title)
|
|
224
219
|
displayRequest.title = title;
|
|
220
|
+
if (presets)
|
|
221
|
+
displayRequest.presets = presets;
|
|
225
222
|
if (options)
|
|
226
223
|
displayRequest.options = options;
|
|
227
224
|
handleDisplayRequest([displayRequest], req.authorization, res);
|
|
@@ -236,8 +233,7 @@ exports.setup = function (app, io) {
|
|
|
236
233
|
message: `The target panel "${panelId}" does not exist.`,
|
|
237
234
|
});
|
|
238
235
|
}
|
|
239
|
-
|
|
240
|
-
handleStreamingDisplayRequest(req, res, panelId, slotId);
|
|
236
|
+
handleStreamingDisplayRequest(req, res, panelId, null);
|
|
241
237
|
});
|
|
242
238
|
app.post('/v1/panels/:panelId/slots/:slotId/display', apiAuth('content.display'), (req, res) => {
|
|
243
239
|
const panelId = req.params.panelId;
|
|
@@ -252,7 +248,7 @@ exports.setup = function (app, io) {
|
|
|
252
248
|
}
|
|
253
249
|
const slotId = req.params.slotId;
|
|
254
250
|
const slot = panel.layout.slots[slotId];
|
|
255
|
-
if (!slot) {
|
|
251
|
+
if (panel.layout.type != 'document' && !slot) {
|
|
256
252
|
error(res, {
|
|
257
253
|
status: 404,
|
|
258
254
|
title: 'Target Not Found',
|
|
@@ -203,7 +203,7 @@ exports.pushDisplayCommand = function (displayCommand, cb) {
|
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
205
205
|
const slotId = displayCommand.slot;
|
|
206
|
-
if (!panel.content.has(slotId)) {
|
|
206
|
+
if (panel.layout.type !== 'document' && !panel.content.has(slotId)) {
|
|
207
207
|
cb(new BadRequestError(`Panel '${panelId}' has no slot '${slotId}'`));
|
|
208
208
|
return;
|
|
209
209
|
}
|
package/server-build/typedefs.js
CHANGED
|
@@ -98,6 +98,8 @@
|
|
|
98
98
|
*
|
|
99
99
|
* @typedef {Slot} DocumentSlot
|
|
100
100
|
* @property {string} minHeight - The CSS length for the minimal height of the slot.
|
|
101
|
+
* @property {string} height - The CSS length for the height of the slot.
|
|
102
|
+
* @property {string} maxHeight - The CSS length for the maximal height of the slot.
|
|
101
103
|
*/
|
|
102
104
|
/**
|
|
103
105
|
* The structure to describe a general panel layout.
|
package/server-build/utils.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @module
|
|
4
4
|
*/
|
|
5
|
+
const path = require('path');
|
|
5
6
|
const zlib = require('zlib');
|
|
6
7
|
const _ = require('lodash');
|
|
7
8
|
exports.escapeRegEx = s => {
|
|
@@ -245,6 +246,23 @@ exports.isHttpUrl = function (urlStr) {
|
|
|
245
246
|
}
|
|
246
247
|
return srcUrl.protocol == 'http:' || srcUrl.protocol == 'https:';
|
|
247
248
|
};
|
|
249
|
+
/**
|
|
250
|
+
* Normalize a filesystem path.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} filename An absolute path of a file or directory
|
|
253
|
+
* @param {boolean|null} forceTrailingSlash Assures a trailing slash on the end of the path
|
|
254
|
+
* @returns {string} The normalized path
|
|
255
|
+
*/
|
|
256
|
+
exports.normalizePath = function (filename, forceTrailingSlash) {
|
|
257
|
+
filename = path.normalize(filename);
|
|
258
|
+
if (process.platform === 'win32') {
|
|
259
|
+
filename = filename.toLowerCase();
|
|
260
|
+
}
|
|
261
|
+
if (forceTrailingSlash && !filename.endsWith(path.sep)) {
|
|
262
|
+
filename = filename + path.sep;
|
|
263
|
+
}
|
|
264
|
+
return filename;
|
|
265
|
+
};
|
|
248
266
|
const dataAmountPattern = /^([+-]?(?:0|[1-9][\d,]*))\s*(?:([KMG])[B]?)?$/i;
|
|
249
267
|
/**
|
|
250
268
|
* Parse data amount values from string to number.
|