homebridge-smartsystem 7.1.21 → 7.1.22
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/duotecno/types.js +4 -1
- package/package.json +1 -1
- package/server/platform.js +1 -1
- package/server/proxy.js +148 -65
- package/server/smartapp.js +72 -5
- package/server/views/assets/custom.css +63 -0
- package/server/views/assets/material-icons.woff2 +0 -0
- package/server/views/device-details.ejs +1 -1
- package/server/views/device-list.ejs +5 -5
- package/server/views/footer.ejs +1 -1
- package/server/views/head.ejs +5 -4
- package/server/views/homekit.ejs +2 -2
- package/server/views/link-details.ejs +1 -1
- package/server/views/link-list.ejs +5 -5
- package/server/views/login.ejs +3 -3
- package/server/views/master-detail.ejs +3 -3
- package/server/views/master-list.ejs +6 -6
- package/server/views/nav.ejs +22 -22
- package/server/views/node-details.ejs +4 -4
- package/server/views/power-binding.ejs +1 -1
- package/server/views/power-rule.ejs +1 -1
- package/server/views/power.ejs +7 -7
- package/server/views/proxy.ejs +55 -24
- package/server/views/scripts.ejs +4 -43
- package/server/views/service-list.ejs +1 -1
- package/server/views/settings.ejs +5 -1
- package/server/views/switch-details.ejs +1 -1
- package/server/views/switch-list.ejs +4 -4
- package/server/views/switch-script.ejs +1 -1
- package/server/views/units.ejs +3 -4
- package/server/webapp.js +98 -7
- package/server/views/style.ejs +0 -65
package/server/webapp.js
CHANGED
|
@@ -69,6 +69,18 @@ class Context {
|
|
|
69
69
|
this.parseCookies();
|
|
70
70
|
// can be used for sending different answers
|
|
71
71
|
this.jsonrequest = !!(((_a = req.headers['content-type']) === null || _a === void 0 ? void 0 : _a.toLowerCase().indexOf('application/json')) > -1);
|
|
72
|
+
// set by the cloud proxy (server/proxy.ts + masters.duotecno.eu) when this request came in
|
|
73
|
+
// through /connect/<uniqueId>/... -- every generated link/redirect must be prefixed with it
|
|
74
|
+
// so it keeps pointing back through the tunnel instead of at the cloud server's own root.
|
|
75
|
+
// The cloud server sends this header twice (once as "X-Forwarded-Prefix", once lowercase),
|
|
76
|
+
// which Node joins into a single comma-separated value -- take just the first occurrence.
|
|
77
|
+
let forwardedPrefix = req.headers['x-forwarded-prefix'];
|
|
78
|
+
if (typeof forwardedPrefix === "string" && forwardedPrefix.includes(",")) {
|
|
79
|
+
forwardedPrefix = forwardedPrefix.split(",")[0].trim();
|
|
80
|
+
}
|
|
81
|
+
this.basePath = (typeof forwardedPrefix === "string" && forwardedPrefix.startsWith("/connect/"))
|
|
82
|
+
? forwardedPrefix.replace(/\/+$/, "")
|
|
83
|
+
: "";
|
|
72
84
|
// helpers for ejs
|
|
73
85
|
this.today = new Date();
|
|
74
86
|
this.year = this.today.getFullYear();
|
|
@@ -232,6 +244,18 @@ class WebApp extends base_1.Base {
|
|
|
232
244
|
this.user = "";
|
|
233
245
|
this.password = "";
|
|
234
246
|
this.mdnsService = null; // mDNS service for hostname and service discovery
|
|
247
|
+
// Cache of validated "Authorization: Basic ..." header values -> expiry timestamp (ms).
|
|
248
|
+
// Used when a request comes in through the cloud proxy, where cookies don't survive the
|
|
249
|
+
// tunnel (see Context.basePath): avoids re-checking with pwOK() (which calls out to
|
|
250
|
+
// Homebridge UI) on every single request as long as the browser keeps resending the same
|
|
251
|
+
// Basic-Auth credentials. Entries expire after kBasicAuthCacheMs and are re-validated then.
|
|
252
|
+
this.authorizedBasicAuth = new Map();
|
|
253
|
+
// In-flight validations, keyed the same way, so that a burst of concurrent requests using
|
|
254
|
+
// the same not-yet-cached credentials (e.g. the handful of asset requests a single page
|
|
255
|
+
// load fires) share one pwOK()/Homebridge-login call instead of each firing their own --
|
|
256
|
+
// pwOK() also overwrites the shared this.token, so piling up parallel calls to it is not
|
|
257
|
+
// just wasteful but can stomp on other, unrelated sessions using that same token.
|
|
258
|
+
this.pendingBasicAuth = new Map();
|
|
235
259
|
this.port = 80;
|
|
236
260
|
(0, logger_1.log)("server", "Creating http server");
|
|
237
261
|
this.files = {};
|
|
@@ -440,6 +464,19 @@ class WebApp extends base_1.Base {
|
|
|
440
464
|
}
|
|
441
465
|
}
|
|
442
466
|
if (file) {
|
|
467
|
+
// layout assets (bundled CSS/JS, inline logo, favicon data URI) -- registered once at
|
|
468
|
+
// startup via addContent() under these fixed names, so any subclass that has them
|
|
469
|
+
// available gets them injected into every template automatically. Merged before the
|
|
470
|
+
// per-call objects so a call site can still override them explicitly if it ever needs to.
|
|
471
|
+
const bundleFile = this.getFile("bundle");
|
|
472
|
+
if (bundleFile)
|
|
473
|
+
context["bundleUrl"] = `${context.basePath}/files/bundle.${exports.kVersion}.js`;
|
|
474
|
+
const logoFile = this.getFile("logoWhiteInline");
|
|
475
|
+
if (logoFile)
|
|
476
|
+
context["logoWhite"] = logoFile.content;
|
|
477
|
+
const faviconFile = this.getFile("faviconDataUri");
|
|
478
|
+
if (faviconFile)
|
|
479
|
+
context["faviconDataUri"] = faviconFile.content;
|
|
443
480
|
// copy objects into context
|
|
444
481
|
for (const key in objects)
|
|
445
482
|
context[key] = objects[key];
|
|
@@ -468,8 +505,22 @@ class WebApp extends base_1.Base {
|
|
|
468
505
|
try {
|
|
469
506
|
if (this.needsLogin(context)) {
|
|
470
507
|
logger.debug("webapp", "checking ticket: " + context.cookies["DTicket"] + " = " + this.token + " for " + context.request + "/" + context.action + " => " + (context.cookies["DTicket"] == this.token));
|
|
471
|
-
// if a password is set, check if the tickets is valid
|
|
472
|
-
|
|
508
|
+
// if a password is set, check if the tickets is valid...
|
|
509
|
+
let authorized = !!this.token && (context.cookies["DTicket"] === this.token);
|
|
510
|
+
// the DTicket cookie never survives the cloud proxy (masters.duotecno.eu strips
|
|
511
|
+
// cookies on every forwarded request), so fall back to HTTP Basic-Auth there instead.
|
|
512
|
+
if (!authorized)
|
|
513
|
+
authorized = yield this.checkBasicAuth(context);
|
|
514
|
+
if (!authorized) {
|
|
515
|
+
if (context.basePath) {
|
|
516
|
+
// ask the browser natively for credentials instead of our own cookie-based
|
|
517
|
+
// /login page, since that page can't establish a session through the tunnel
|
|
518
|
+
return {
|
|
519
|
+
status: 401, type: "text/html",
|
|
520
|
+
header: { "WWW-Authenticate": 'Basic realm="Duotecno Gateway"' },
|
|
521
|
+
data: "<html><head><title>Login required</title></head><body>Login required</body></html>"
|
|
522
|
+
};
|
|
523
|
+
}
|
|
473
524
|
context.request = "login";
|
|
474
525
|
context.action = "";
|
|
475
526
|
}
|
|
@@ -513,10 +564,11 @@ class WebApp extends base_1.Base {
|
|
|
513
564
|
data: "<html><head><title>File " + filename + " not found</title></head><body>These are not the droids your are looking for</body></html>"
|
|
514
565
|
};
|
|
515
566
|
}
|
|
516
|
-
redirect(url, header = {}) {
|
|
567
|
+
redirect(context, url, header = {}) {
|
|
568
|
+
const target = url.startsWith("http") ? url : context.basePath + url;
|
|
517
569
|
return {
|
|
518
|
-
status: 303, type: "text/html", header: Object.assign({ "Location":
|
|
519
|
-
data: "<html><head><meta http-equiv=\"Refresh\" content=\"0; URL=" +
|
|
570
|
+
status: 303, type: "text/html", header: Object.assign({ "Location": target }, header),
|
|
571
|
+
data: "<html><head><meta http-equiv=\"Refresh\" content=\"0; URL=" + target + "\"></head></html>"
|
|
520
572
|
};
|
|
521
573
|
}
|
|
522
574
|
error(context, msg = "", json = false) {
|
|
@@ -534,6 +586,43 @@ class WebApp extends base_1.Base {
|
|
|
534
586
|
return true;
|
|
535
587
|
});
|
|
536
588
|
}
|
|
589
|
+
// Validates (and caches for kBasicAuthCacheMs) an "Authorization: Basic ..." header via
|
|
590
|
+
// pwOK(). Used as the login fallback for requests coming in through the cloud proxy.
|
|
591
|
+
checkBasicAuth(context) {
|
|
592
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
593
|
+
const authHeader = context.req.headers.authorization;
|
|
594
|
+
if (!authHeader || !authHeader.startsWith("Basic "))
|
|
595
|
+
return false;
|
|
596
|
+
const cachedUntil = this.authorizedBasicAuth.get(authHeader);
|
|
597
|
+
if (cachedUntil && (cachedUntil > Date.now()))
|
|
598
|
+
return true;
|
|
599
|
+
// collapse concurrent requests bearing the same (not-yet-cached) credentials onto one
|
|
600
|
+
// in-flight validation instead of each calling pwOK() independently
|
|
601
|
+
let pending = this.pendingBasicAuth.get(authHeader);
|
|
602
|
+
if (!pending) {
|
|
603
|
+
pending = this.validateBasicAuth(context, authHeader);
|
|
604
|
+
this.pendingBasicAuth.set(authHeader, pending);
|
|
605
|
+
pending.finally(() => this.pendingBasicAuth.delete(authHeader));
|
|
606
|
+
}
|
|
607
|
+
return pending;
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
validateBasicAuth(context, authHeader) {
|
|
611
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
612
|
+
const decoded = Buffer.from(authHeader.slice(6), "base64").toString("utf-8");
|
|
613
|
+
const sep = decoded.indexOf(":");
|
|
614
|
+
if (sep < 0)
|
|
615
|
+
return false;
|
|
616
|
+
const user = decoded.slice(0, sep);
|
|
617
|
+
const pw = decoded.slice(sep + 1);
|
|
618
|
+
if (yield this.pwOK(context, user, pw)) {
|
|
619
|
+
this.authorizedBasicAuth.set(authHeader, Date.now() + WebApp.kBasicAuthCacheMs);
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
this.authorizedBasicAuth.delete(authHeader);
|
|
623
|
+
return false;
|
|
624
|
+
});
|
|
625
|
+
}
|
|
537
626
|
doLogin(context) {
|
|
538
627
|
return __awaiter(this, void 0, void 0, function* () {
|
|
539
628
|
if (context.action === "logout") {
|
|
@@ -553,7 +642,7 @@ class WebApp extends base_1.Base {
|
|
|
553
642
|
const user = context.getParam({ name: "user", type: "string" });
|
|
554
643
|
if (yield this.pwOK(context, user, pw)) {
|
|
555
644
|
const path = context.getCookie("DPath") || "/";
|
|
556
|
-
return this.redirect(path, { "Set-Cookie": "DTicket=" + this.token });
|
|
645
|
+
return this.redirect(context, path, { "Set-Cookie": "DTicket=" + this.token });
|
|
557
646
|
}
|
|
558
647
|
else {
|
|
559
648
|
return this.ejs("login", context, { message: "authentication failed", user }, { "Set-Cookie": "DTicket=x" });
|
|
@@ -563,7 +652,8 @@ class WebApp extends base_1.Base {
|
|
|
563
652
|
doLogout(context) {
|
|
564
653
|
return __awaiter(this, void 0, void 0, function* () {
|
|
565
654
|
this.token = "";
|
|
566
|
-
|
|
655
|
+
this.authorizedBasicAuth.clear();
|
|
656
|
+
return this.redirect(context, "/", { "Set-Cookie": "DTicket=x" });
|
|
567
657
|
});
|
|
568
658
|
}
|
|
569
659
|
doRestart(json) {
|
|
@@ -622,4 +712,5 @@ class WebApp extends base_1.Base {
|
|
|
622
712
|
}
|
|
623
713
|
}
|
|
624
714
|
exports.WebApp = WebApp;
|
|
715
|
+
WebApp.kBasicAuthCacheMs = 2 * 60 * 1000;
|
|
625
716
|
//# sourceMappingURL=webapp.js.map
|
package/server/views/style.ejs
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
<style>
|
|
2
|
-
/* Page layout with sticky footer */
|
|
3
|
-
html, body { height: 100%; margin: 0; display: flex; flex-direction: column }
|
|
4
|
-
body { min-height: 100vh }
|
|
5
|
-
|
|
6
|
-
/* Main content area - grows to fill space */
|
|
7
|
-
body > *:not(nav):not(.page-footer) { flex: 1 0 auto }
|
|
8
|
-
|
|
9
|
-
form { padding-left: 7px; padding-right: 9px; padding-bottom: 20px }
|
|
10
|
-
form:last-of-type { padding-bottom: 80px }
|
|
11
|
-
|
|
12
|
-
nav, nav .nav-wrapper { height: 64px !important; line-height: 64px !important; flex-shrink: 0 }
|
|
13
|
-
nav .brand-logo { left: 10px !important; -webkit-transform: none !important; transform: none !important }
|
|
14
|
-
|
|
15
|
-
h1 { font-size: 30px }
|
|
16
|
-
h1.btn { margin-top: 18px }
|
|
17
|
-
h1 .btn { margin-left: 10px }
|
|
18
|
-
a span { margin-bottom: 7px; display: inline-block }
|
|
19
|
-
|
|
20
|
-
/* Sticky footer */
|
|
21
|
-
.page-footer { position: fixed; bottom: 0; left: 0; right: 0; width: 100%; flex-shrink: 0; z-index: 100; }
|
|
22
|
-
.page-footer p { text-align: right; padding-bottom: 4px; margin-right: 6px }
|
|
23
|
-
.page-footer a { float: left; margin-left: 10px; color: white; }
|
|
24
|
-
.page-footer { padding-top: 5px; padding-right: 6px }
|
|
25
|
-
.range-field { margin-bottom: -1em }
|
|
26
|
-
b { color: #ee6e73 }
|
|
27
|
-
.btn-small i { font-size: 1.1rem }
|
|
28
|
-
small { font-size: 70% }
|
|
29
|
-
.btn-floating.btn-small { width: 32px; height: 31px }
|
|
30
|
-
.rules p { display: inline-block; margin-bottom: 0px }
|
|
31
|
-
div.select-wrapper { max-width: 212px }
|
|
32
|
-
div.select-wrapper input.select-dropdown { font-size: 16px !important; height: 3rem !important; }
|
|
33
|
-
span.used { color: darkgrey }
|
|
34
|
-
.message { margin-left: 10px; font-size: 15px }
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/* Hamburger menu icon - positioned on right side */
|
|
38
|
-
nav .sidenav-trigger {
|
|
39
|
-
position: absolute; right: 10px; top: 0;
|
|
40
|
-
height: 64px; line-height: 64px; padding: 0 15px; margin: 0;
|
|
41
|
-
}
|
|
42
|
-
nav .sidenav-trigger i { line-height: 64px; font-size: 2rem }
|
|
43
|
-
|
|
44
|
-
/* Hide hamburger on desktop, show desktop menu */
|
|
45
|
-
@media only screen and (min-width: 993px) {
|
|
46
|
-
nav .sidenav-trigger { display: none !important; }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/* Show hamburger on mobile, hide desktop menu */
|
|
50
|
-
@media only screen and (max-width: 992px) {
|
|
51
|
-
nav .sidenav-trigger { display: block !important; }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/* Desktop navigation active state */
|
|
55
|
-
nav ul li.active { background-color: rgba(0, 0, 0, 0.1) }
|
|
56
|
-
|
|
57
|
-
/* Sidenav styling */
|
|
58
|
-
.sidenav { width: 280px; }
|
|
59
|
-
.sidenav .user-view { padding: 1px 32px 16px 16px }
|
|
60
|
-
.sidenav .user-view .name { font-size: 18px; font-weight: 500; margin-top: 16px; display: block; }
|
|
61
|
-
.sidenav li > a { height: auto; line-height: 1px; padding: 14px 32px; }
|
|
62
|
-
.sidenav .divider { margin: 8px 0; }
|
|
63
|
-
.sidenav li.active { background-color: rgba(0,0,0,0.05); }
|
|
64
|
-
|
|
65
|
-
</style>
|