signalk-siparu 0.2.1 → 0.2.2
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/README.md
CHANGED
|
@@ -111,6 +111,13 @@ commit, names each write route, and an eighth one fails the build.
|
|
|
111
111
|
| `POST /voyages/:id/merge-previous` | Join this passage to the one before it, when one trip was recorded as two |
|
|
112
112
|
| `POST /voyages/:id/undo-merge` | Separate a passage back into what it was made of |
|
|
113
113
|
|
|
114
|
+
Those seven, and `GET /pair/status` with them, answer only requests that came from the
|
|
115
|
+
boat's own screen. A browser marks every request with its origin, and one from a page on
|
|
116
|
+
another site gets 403: on a server with security off, which is the default, an advert
|
|
117
|
+
frame in any tab on the boat's network could otherwise unpair her or lift the pairing
|
|
118
|
+
code off the helm. A caller with no browser headers, curl or a script on her own network,
|
|
119
|
+
is not affected. Neither is the reading surface above.
|
|
120
|
+
|
|
114
121
|
The two voyage edits are reachable from the boat's own network only. What the
|
|
115
122
|
shore can ask her is a closed set of five questions, all of them reads
|
|
116
123
|
(`history`, `snapshots`, `voyages`, `track`, `phases`); nothing on that socket
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-siparu",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Kept aboard, proven ashore. An impartial, timestamped record of every voyage: position, wind, depth and logbook, written on the boat and readable from anywhere. A read-only Signal K plugin with a built-in dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"signalk-node-server-plugin",
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.allowConfigure = allowConfigure;
|
|
4
4
|
exports.registerConfigRoutes = registerConfigRoutes;
|
|
5
5
|
const pairing_1 = require("./pairing");
|
|
6
|
+
const origin_guard_1 = require("./origin-guard");
|
|
6
7
|
function allowConfigure(app, req) {
|
|
7
8
|
try {
|
|
8
9
|
const ss = app.securityStrategy;
|
|
@@ -43,7 +44,7 @@ function registerConfigRoutes(router, deps) {
|
|
|
43
44
|
router.get('/config/fuel-paths', (_req, res) => {
|
|
44
45
|
res.json(fuelPathsView());
|
|
45
46
|
});
|
|
46
|
-
router.post('/config/fuel-paths', (req, res) => {
|
|
47
|
+
router.post('/config/fuel-paths', (0, origin_guard_1.sameOrigin)((req, res) => {
|
|
47
48
|
if (!(0, pairing_1.securityOff)(app, req) && !allowConfigure(app, req)) {
|
|
48
49
|
res.status(403).json({ error: 'admin_required' });
|
|
49
50
|
return;
|
|
@@ -66,5 +67,5 @@ function registerConfigRoutes(router, deps) {
|
|
|
66
67
|
if (!res.headersSent)
|
|
67
68
|
res.status(500).json({ error: 'internal' });
|
|
68
69
|
});
|
|
69
|
-
});
|
|
70
|
+
}));
|
|
70
71
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.crossSite = crossSite;
|
|
4
|
+
exports.sameOrigin = sameOrigin;
|
|
5
|
+
function crossSite(headers) {
|
|
6
|
+
const site = header(headers, 'sec-fetch-site');
|
|
7
|
+
if (site) {
|
|
8
|
+
return site !== 'same-origin' && site !== 'none';
|
|
9
|
+
}
|
|
10
|
+
const origin = header(headers, 'origin');
|
|
11
|
+
if (!origin)
|
|
12
|
+
return false;
|
|
13
|
+
let named;
|
|
14
|
+
try {
|
|
15
|
+
named = new URL(origin).host;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return named !== header(headers, 'host');
|
|
21
|
+
}
|
|
22
|
+
function sameOrigin(handler) {
|
|
23
|
+
return (req, res) => {
|
|
24
|
+
if (crossSite(req.headers)) {
|
|
25
|
+
res.status(403).json({ error: 'cross_site' });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
handler(req, res);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function header(headers, name) {
|
|
32
|
+
const value = headers?.[name];
|
|
33
|
+
return typeof value === 'string' ? value.toLowerCase() : undefined;
|
|
34
|
+
}
|
package/plugin/dist/pairing.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.maskEmail = maskEmail;
|
|
|
6
6
|
exports.registerPairRoutes = registerPairRoutes;
|
|
7
7
|
exports.retryPendingUnlinks = retryPendingUnlinks;
|
|
8
8
|
exports.__resetPairingState = __resetPairingState;
|
|
9
|
+
const origin_guard_1 = require("./origin-guard");
|
|
9
10
|
function securityOff(app, req) {
|
|
10
11
|
try {
|
|
11
12
|
const ss = app.securityStrategy;
|
|
@@ -68,6 +69,7 @@ async function relay(url, path, body, boatToken) {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
const NOT_YOUR_BOAT = 'That account does not own this boat. She is still linked as before.';
|
|
72
|
+
const BOAT_LIMIT = 'That account already holds as many boats as its plan allows. Make room for her there, then pair again.';
|
|
71
73
|
function maskEmail(email) {
|
|
72
74
|
if (!email)
|
|
73
75
|
return null;
|
|
@@ -85,7 +87,7 @@ function registerPairRoutes(router, deps) {
|
|
|
85
87
|
pairedAt: remote.pairedAt,
|
|
86
88
|
uplink: uplinkStatus() ?? undefined
|
|
87
89
|
});
|
|
88
|
-
router.get('/pair/status', (req, res) => {
|
|
90
|
+
router.get('/pair/status', (0, origin_guard_1.sameOrigin)((req, res) => {
|
|
89
91
|
const json = (screen) => {
|
|
90
92
|
const status = { ...screen };
|
|
91
93
|
if (securityOff(app, req))
|
|
@@ -139,8 +141,8 @@ function registerPairRoutes(router, deps) {
|
|
|
139
141
|
app.debug(`pair status: ${String(e)}`);
|
|
140
142
|
}
|
|
141
143
|
})();
|
|
142
|
-
});
|
|
143
|
-
router.post('/pair/start', (_req, res) => {
|
|
144
|
+
}));
|
|
145
|
+
router.post('/pair/start', (0, origin_guard_1.sameOrigin)((_req, res) => {
|
|
144
146
|
void (async () => {
|
|
145
147
|
lastError = null;
|
|
146
148
|
try {
|
|
@@ -163,8 +165,8 @@ function registerPairRoutes(router, deps) {
|
|
|
163
165
|
res.status(502).json({ state: 'error', message: lastError });
|
|
164
166
|
}
|
|
165
167
|
})();
|
|
166
|
-
});
|
|
167
|
-
router.post('/pair/approve', (_req, res) => {
|
|
168
|
+
}));
|
|
169
|
+
router.post('/pair/approve', (0, origin_guard_1.sameOrigin)((_req, res) => {
|
|
168
170
|
void (async () => {
|
|
169
171
|
if (!deviceCode) {
|
|
170
172
|
res.status(409).json({ error: 'no_pairing_in_progress' });
|
|
@@ -194,17 +196,18 @@ function registerPairRoutes(router, deps) {
|
|
|
194
196
|
}
|
|
195
197
|
catch (e) {
|
|
196
198
|
app.error(`pair approve failed: ${String(e)}`);
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
+
const answered = e instanceof RelayRefused && (e.code === 'not_your_boat' || e.code === 'boat_limit');
|
|
200
|
+
if (answered)
|
|
199
201
|
deviceCode = userCode = expiresAt = null;
|
|
200
|
-
|
|
202
|
+
const message = e instanceof RelayRefused && e.code === 'boat_limit' ? BOAT_LIMIT : NOT_YOUR_BOAT;
|
|
203
|
+
res.status(answered ? 409 : 502).json({
|
|
201
204
|
state: 'error',
|
|
202
|
-
message:
|
|
205
|
+
message: answered ? message : 'Could not finish pairing. Try again.'
|
|
203
206
|
});
|
|
204
207
|
}
|
|
205
208
|
})();
|
|
206
|
-
});
|
|
207
|
-
router.post('/pair/deny', (_req, res) => {
|
|
209
|
+
}));
|
|
210
|
+
router.post('/pair/deny', (0, origin_guard_1.sameOrigin)((_req, res) => {
|
|
208
211
|
void (async () => {
|
|
209
212
|
if (deviceCode) {
|
|
210
213
|
await relay(relayUrl, '/pair/deny', { device_code: deviceCode }).catch(() => { });
|
|
@@ -213,8 +216,8 @@ function registerPairRoutes(router, deps) {
|
|
|
213
216
|
const remote = getRemote();
|
|
214
217
|
res.json(remote ? paired(remote) : { state: 'idle' });
|
|
215
218
|
})();
|
|
216
|
-
});
|
|
217
|
-
router.post('/pair/reset', (_req, res) => {
|
|
219
|
+
}));
|
|
220
|
+
router.post('/pair/reset', (0, origin_guard_1.sameOrigin)((_req, res) => {
|
|
218
221
|
void (async () => {
|
|
219
222
|
const remote = getRemote();
|
|
220
223
|
if (remote?.boatToken) {
|
|
@@ -240,7 +243,7 @@ function registerPairRoutes(router, deps) {
|
|
|
240
243
|
if (!res.headersSent)
|
|
241
244
|
res.status(500).json({ state: 'error', message: 'Could not turn off remote viewing.' });
|
|
242
245
|
});
|
|
243
|
-
});
|
|
246
|
+
}));
|
|
244
247
|
}
|
|
245
248
|
async function retryPendingUnlinks(relayUrl, getPending, clear, log) {
|
|
246
249
|
for (const pending of getPending()) {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerVoyageEditRoutes = registerVoyageEditRoutes;
|
|
4
4
|
const pairing_1 = require("./pairing");
|
|
5
5
|
const config_routes_1 = require("./config-routes");
|
|
6
|
+
const origin_guard_1 = require("./origin-guard");
|
|
6
7
|
const STATUS = {
|
|
7
8
|
not_found: 404,
|
|
8
9
|
no_previous: 409,
|
|
@@ -40,6 +41,6 @@ function registerVoyageEditRoutes(router, deps) {
|
|
|
40
41
|
});
|
|
41
42
|
};
|
|
42
43
|
};
|
|
43
|
-
router.post('/voyages/:id/merge-previous', edit('merge-previous', mergeWithPrevious));
|
|
44
|
-
router.post('/voyages/:id/undo-merge', edit('undo-merge', undoMerge));
|
|
44
|
+
router.post('/voyages/:id/merge-previous', (0, origin_guard_1.sameOrigin)(edit('merge-previous', mergeWithPrevious)));
|
|
45
|
+
router.post('/voyages/:id/undo-merge', (0, origin_guard_1.sameOrigin)(edit('undo-merge', undoMerge)));
|
|
45
46
|
}
|