locadot 2.1.0 → 2.3.0
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/CHANGELOG.md +32 -0
- package/README.md +56 -4
- package/dist/cli/commands/hosts.js +29 -1
- package/dist/cli/commands/remote.js +21 -1
- package/dist/cli/commands/tunnel.js +9 -3
- package/dist/dashboard/api.js +35 -5
- package/dist/dashboard/index.js +3 -1
- package/dist/dashboard/login.js +17 -6
- package/dist/dashboard/page.js +778 -86
- package/dist/index.js +16 -2
- package/dist/lib/allow.js +60 -0
- package/dist/lib/hosts.js +52 -4
- package/dist/lib/ui-auth.js +5 -0
- package/dist/proxy/guard.js +30 -1
- package/dist/proxy/hub.js +11 -3
- package/dist/proxy/options.js +10 -2
- package/dist/proxy/request.js +3 -1
- package/dist/proxy/router.js +38 -12
- package/dist/proxy/tunnel.js +62 -22
- package/dist/server/index.js +2 -0
- package/dist/utils/startup.js +124 -34
- package/dist/utils/trust.js +52 -2
- package/package.json +1 -1
package/dist/dashboard/page.js
CHANGED
|
@@ -348,11 +348,16 @@ td.badges .badge + .badge { margin-left: 4px; }
|
|
|
348
348
|
.btn:hover:not(:disabled) { background: var(--surface-2); border-color: var(--muted-dim); }
|
|
349
349
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
350
350
|
.btn-sm { height: 28px; padding: 0 10px; font-size: 12px; }
|
|
351
|
+
.btn-sm.btn-icon { width: 28px; padding: 0; color: var(--muted); }
|
|
352
|
+
.btn-icon svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
353
|
+
.btn-icon.is-set { color: var(--accent); border-color: transparent; background: var(--accent-bg); }
|
|
351
354
|
.btn-block { width: 100%; }
|
|
352
355
|
.btn-primary { border-color: var(--accent); background: var(--accent); color: var(--accent-fg); }
|
|
353
356
|
.btn-primary:hover:not(:disabled) { background: var(--accent-strong); border-color: var(--accent-strong); }
|
|
354
357
|
.btn-danger { border-color: var(--down); color: var(--down); background: transparent; }
|
|
355
358
|
.btn-danger:hover:not(:disabled) { background: var(--down-bg); border-color: var(--down); }
|
|
359
|
+
.modal-foot .btn-danger { background: var(--down); border-color: var(--down); color: #fff; }
|
|
360
|
+
.modal-foot .btn-danger:hover:not(:disabled) { background: var(--down); filter: brightness(0.92); }
|
|
356
361
|
.btn-ghost { background: transparent; border-color: transparent; box-shadow: none; color: var(--muted); }
|
|
357
362
|
.btn-ghost:hover:not(:disabled) { background: var(--surface-2); border-color: transparent; color: var(--fg); }
|
|
358
363
|
.btn-ghost-danger { background: transparent; border-color: transparent; box-shadow: none; color: var(--muted); }
|
|
@@ -575,6 +580,8 @@ td .inline-check + .inline-check { margin-top: 4px; }
|
|
|
575
580
|
.inline-check { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
|
|
576
581
|
.tunnel-cell { white-space: normal; }
|
|
577
582
|
.tunnel-status { display: flex; align-items: center; gap: 6px; max-width: 240px; }
|
|
583
|
+
.tunnel-login { display: flex; flex-direction: column; gap: 2px; font-size: 12px; white-space: nowrap; }
|
|
584
|
+
.tunnel-login a { font-weight: 500; }
|
|
578
585
|
.tunnel-status a { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; display: inline-block; vertical-align: middle; font-size: 12px; }
|
|
579
586
|
.tunnel-error { color: var(--down); cursor: help; }
|
|
580
587
|
.tunnel-error-text {
|
|
@@ -729,6 +736,139 @@ details.card[open] summary { border-bottom: 1px solid var(--border); border-bott
|
|
|
729
736
|
margin: 0;
|
|
730
737
|
}
|
|
731
738
|
|
|
739
|
+
/* ---------- allow dialog ---------- */
|
|
740
|
+
#allow-dialog {
|
|
741
|
+
width: min(520px, calc(100vw - 32px));
|
|
742
|
+
padding: 0;
|
|
743
|
+
border: 1px solid var(--border);
|
|
744
|
+
border-radius: var(--radius);
|
|
745
|
+
background: var(--surface);
|
|
746
|
+
color: var(--fg);
|
|
747
|
+
box-shadow: var(--shadow-md);
|
|
748
|
+
}
|
|
749
|
+
#allow-dialog::backdrop { background: rgba(0, 0, 0, 0.45); }
|
|
750
|
+
.modal-head {
|
|
751
|
+
display: flex;
|
|
752
|
+
align-items: flex-start;
|
|
753
|
+
justify-content: space-between;
|
|
754
|
+
gap: 12px;
|
|
755
|
+
padding: 16px 20px;
|
|
756
|
+
border-bottom: 1px solid var(--border);
|
|
757
|
+
}
|
|
758
|
+
.modal-title { font-size: 15px; font-weight: 600; margin: 0; letter-spacing: -0.01em; }
|
|
759
|
+
.modal-sub { margin: 2px 0 0; font-size: 12.5px; color: var(--muted); }
|
|
760
|
+
.modal-close { width: 28px; height: 28px; padding: 0; border-radius: 999px; flex-shrink: 0; }
|
|
761
|
+
.modal-close svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
|
|
762
|
+
.modal-body { padding: 20px; display: flex; flex-direction: column; gap: 14px; }
|
|
763
|
+
.modal-desc { margin: 0; font-size: 13px; color: var(--muted); line-height: 1.5; }
|
|
764
|
+
.modal-warn {
|
|
765
|
+
font-size: 12.5px;
|
|
766
|
+
color: var(--warn);
|
|
767
|
+
background: var(--warn-bg);
|
|
768
|
+
border: 1px solid var(--warn);
|
|
769
|
+
border-radius: var(--radius-sm);
|
|
770
|
+
padding: 8px 12px;
|
|
771
|
+
line-height: 1.5;
|
|
772
|
+
}
|
|
773
|
+
.allow-list { display: flex; flex-direction: column; gap: 6px; max-height: 220px; overflow-y: auto; }
|
|
774
|
+
.allow-row {
|
|
775
|
+
display: flex;
|
|
776
|
+
align-items: center;
|
|
777
|
+
justify-content: space-between;
|
|
778
|
+
gap: 10px;
|
|
779
|
+
padding: 6px 10px;
|
|
780
|
+
border: 1px solid var(--border);
|
|
781
|
+
border-radius: var(--radius-sm);
|
|
782
|
+
background: var(--surface-2);
|
|
783
|
+
}
|
|
784
|
+
.allow-row span { font-size: 13px; word-break: break-all; }
|
|
785
|
+
.allow-remove {
|
|
786
|
+
display: inline-flex;
|
|
787
|
+
align-items: center;
|
|
788
|
+
justify-content: center;
|
|
789
|
+
width: 22px;
|
|
790
|
+
height: 22px;
|
|
791
|
+
padding: 0;
|
|
792
|
+
font-size: 15px;
|
|
793
|
+
line-height: 1;
|
|
794
|
+
border-radius: var(--radius-xs);
|
|
795
|
+
border: 1px solid transparent;
|
|
796
|
+
background: transparent;
|
|
797
|
+
color: var(--muted);
|
|
798
|
+
cursor: pointer;
|
|
799
|
+
flex-shrink: 0;
|
|
800
|
+
}
|
|
801
|
+
.allow-remove:hover { color: var(--down); background: var(--down-bg); border-color: transparent; }
|
|
802
|
+
.allow-empty { font-size: 13px; padding: 4px 0; }
|
|
803
|
+
.allow-add-row { display: flex; gap: 8px; }
|
|
804
|
+
.allow-add-row .input { flex: 1; }
|
|
805
|
+
.allow-add-row .btn { height: auto; align-self: stretch; padding: 0 14px; }
|
|
806
|
+
.modal-hint { margin: 0; font-size: 12px; color: var(--muted-dim); }
|
|
807
|
+
.modal-foot {
|
|
808
|
+
display: flex;
|
|
809
|
+
justify-content: flex-end;
|
|
810
|
+
gap: 8px;
|
|
811
|
+
padding: 16px 20px;
|
|
812
|
+
border-top: 1px solid var(--border);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/* ---------- confirm / prompt / share dialogs ---------- */
|
|
816
|
+
#confirm-dialog, #prompt-dialog, #share-dialog {
|
|
817
|
+
padding: 0;
|
|
818
|
+
border: 1px solid var(--border);
|
|
819
|
+
border-radius: var(--radius);
|
|
820
|
+
background: var(--surface);
|
|
821
|
+
color: var(--fg);
|
|
822
|
+
box-shadow: var(--shadow-md);
|
|
823
|
+
}
|
|
824
|
+
#confirm-dialog::backdrop, #prompt-dialog::backdrop, #share-dialog::backdrop { background: rgba(0, 0, 0, 0.45); }
|
|
825
|
+
#confirm-dialog { width: min(420px, calc(100vw - 32px)); }
|
|
826
|
+
#prompt-dialog { width: min(420px, calc(100vw - 32px)); }
|
|
827
|
+
#share-dialog { width: min(480px, calc(100vw - 32px)); }
|
|
828
|
+
.share-options { display: flex; flex-direction: column; gap: 10px; }
|
|
829
|
+
.option-card {
|
|
830
|
+
display: flex;
|
|
831
|
+
align-items: flex-start;
|
|
832
|
+
gap: 12px;
|
|
833
|
+
width: 100%;
|
|
834
|
+
text-align: left;
|
|
835
|
+
padding: 14px;
|
|
836
|
+
border: 1px solid var(--border);
|
|
837
|
+
border-radius: var(--radius);
|
|
838
|
+
background: var(--surface-2);
|
|
839
|
+
color: inherit;
|
|
840
|
+
font: inherit;
|
|
841
|
+
cursor: pointer;
|
|
842
|
+
}
|
|
843
|
+
.option-card:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); }
|
|
844
|
+
.option-card[aria-checked="true"] { border-color: var(--accent); background: var(--accent-bg); }
|
|
845
|
+
.option-dot {
|
|
846
|
+
width: 16px;
|
|
847
|
+
height: 16px;
|
|
848
|
+
margin-top: 2px;
|
|
849
|
+
flex-shrink: 0;
|
|
850
|
+
border-radius: 999px;
|
|
851
|
+
border: 1.5px solid var(--border-strong);
|
|
852
|
+
background: var(--surface);
|
|
853
|
+
position: relative;
|
|
854
|
+
}
|
|
855
|
+
.option-card[aria-checked="true"] .option-dot { border-color: var(--accent); }
|
|
856
|
+
.option-card[aria-checked="true"] .option-dot::after {
|
|
857
|
+
content: "";
|
|
858
|
+
position: absolute;
|
|
859
|
+
top: 3px;
|
|
860
|
+
left: 3px;
|
|
861
|
+
width: 8px;
|
|
862
|
+
height: 8px;
|
|
863
|
+
border-radius: 999px;
|
|
864
|
+
background: var(--accent);
|
|
865
|
+
}
|
|
866
|
+
.option-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
|
867
|
+
.option-title { font-size: 13.5px; font-weight: 600; }
|
|
868
|
+
.option-desc { font-size: 12.5px; color: var(--muted); line-height: 1.45; }
|
|
869
|
+
.option-extra { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; }
|
|
870
|
+
.option-extra .field-error { margin-top: 4px; }
|
|
871
|
+
|
|
732
872
|
/* ---------- toasts ---------- */
|
|
733
873
|
.toast-container {
|
|
734
874
|
position: fixed;
|
|
@@ -1061,6 +1201,10 @@ const clientJs = `
|
|
|
1061
1201
|
var connectDomainInput = document.getElementById("connect-domain");
|
|
1062
1202
|
var hubLocalhostRow = document.getElementById("hub-localhost-row");
|
|
1063
1203
|
var hubLocalhostSwitch = document.getElementById("hub-localhost");
|
|
1204
|
+
var hubPanelRow = document.getElementById("hub-panel-row");
|
|
1205
|
+
var hubPanelSwitch = document.getElementById("hub-panel");
|
|
1206
|
+
var hubPanelSub = document.getElementById("hub-panel-sub");
|
|
1207
|
+
var hubPanelUrl = "";
|
|
1064
1208
|
var connectSubmitBtn = document.getElementById("connect-submit");
|
|
1065
1209
|
var connectErrorEl = document.getElementById("connect-error");
|
|
1066
1210
|
var remotesEmptyEl = document.getElementById("remotes-empty");
|
|
@@ -1076,6 +1220,7 @@ const clientJs = `
|
|
|
1076
1220
|
var startupBusy = false;
|
|
1077
1221
|
var cfInstallBusy = false;
|
|
1078
1222
|
var cloudflaredInstalled = false;
|
|
1223
|
+
var cloudflareLoggedIn = false;
|
|
1079
1224
|
var proxyStopped = false;
|
|
1080
1225
|
var pollTimer = null;
|
|
1081
1226
|
var remotesPollTimer = null;
|
|
@@ -1293,6 +1438,7 @@ const clientJs = `
|
|
|
1293
1438
|
|
|
1294
1439
|
var cf = sys.cloudflared || { installed: false };
|
|
1295
1440
|
cloudflaredInstalled = !!cf.installed;
|
|
1441
|
+
cloudflareLoggedIn = !!cf.loggedIn;
|
|
1296
1442
|
if (cf.installed) {
|
|
1297
1443
|
cfValueEl.textContent = "Installed";
|
|
1298
1444
|
cfValueEl.className = "tile-value ok";
|
|
@@ -1374,11 +1520,13 @@ const clientJs = `
|
|
|
1374
1520
|
revokeBtn.className = "btn btn-sm btn-danger";
|
|
1375
1521
|
revokeBtn.textContent = "Revoke";
|
|
1376
1522
|
revokeBtn.addEventListener("click", function () {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1523
|
+
confirmDialog({ title: "Revoke " + peer.name + "?", message: "This peer will lose access immediately.", confirmLabel: "Revoke", danger: true }).then(function (ok) {
|
|
1524
|
+
if (!ok) return;
|
|
1525
|
+
revokeBtn.disabled = true;
|
|
1526
|
+
apiFetch("/api/peers/" + encodeURIComponent(peer.id), { method: "DELETE" })
|
|
1527
|
+
.then(function () { showToast("Revoked " + peer.name, "success"); return loadHub(); })
|
|
1528
|
+
.catch(function (err) { apiError(err, "Couldn't revoke peer"); revokeBtn.disabled = false; });
|
|
1529
|
+
});
|
|
1382
1530
|
});
|
|
1383
1531
|
controls.appendChild(revokeBtn);
|
|
1384
1532
|
row.appendChild(controls);
|
|
@@ -1509,6 +1657,9 @@ const clientJs = `
|
|
|
1509
1657
|
|
|
1510
1658
|
hubLocalhostRow.hidden = !data.config;
|
|
1511
1659
|
setSwitch(hubLocalhostSwitch, data.localhost !== false);
|
|
1660
|
+
hubPanelUrl = status === "up" && hub.url ? hub.url : "";
|
|
1661
|
+
hubPanelRow.hidden = !data.config;
|
|
1662
|
+
renderHubPanel(data.panel === true);
|
|
1512
1663
|
|
|
1513
1664
|
hubErrorRow.hidden = !(status === "error" && hub.error);
|
|
1514
1665
|
if (status === "error" && hub.error) hubErrorRow.textContent = hub.error;
|
|
@@ -1558,13 +1709,15 @@ const clientJs = `
|
|
|
1558
1709
|
});
|
|
1559
1710
|
|
|
1560
1711
|
hubStopBtn.addEventListener("click", function () {
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1712
|
+
confirmDialog({ title: "Stop remote access?", message: "Connected peers will be disconnected.", confirmLabel: "Stop remote access", danger: true }).then(function (ok) {
|
|
1713
|
+
if (!ok) return;
|
|
1714
|
+
hubStopBtn.disabled = true;
|
|
1715
|
+
hubStopBtn.classList.add("busy");
|
|
1716
|
+
apiFetch("/api/hub", { method: "POST", body: JSON.stringify({ mode: "off" }) })
|
|
1717
|
+
.then(function () { showToast("Remote access stopped", "success"); return loadHub(); })
|
|
1718
|
+
.catch(function (err) { apiError(err, "Couldn't stop remote access"); })
|
|
1719
|
+
.then(function () { hubStopBtn.disabled = false; hubStopBtn.classList.remove("busy"); });
|
|
1720
|
+
});
|
|
1568
1721
|
});
|
|
1569
1722
|
|
|
1570
1723
|
// ---------- remote access: connected machines (receiver) ----------
|
|
@@ -1626,11 +1779,12 @@ const clientJs = `
|
|
|
1626
1779
|
urlEditBtn.className = "btn btn-sm btn-ghost";
|
|
1627
1780
|
urlEditBtn.textContent = "Update URL";
|
|
1628
1781
|
urlEditBtn.addEventListener("click", function () {
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1782
|
+
promptDialog({ title: "Update URL", message: "New URL for " + remote.name, label: "URL", value: remote.url, confirmLabel: "Save" }).then(function (next) {
|
|
1783
|
+
if (!next || !next.trim() || next.trim() === remote.url) return;
|
|
1784
|
+
apiFetch("/api/remotes/" + encodeURIComponent(remote.name), { method: "PUT", body: JSON.stringify({ url: next.trim() }) })
|
|
1785
|
+
.then(function () { showToast("Updated URL for " + remote.name, "success"); return loadRemotes(); })
|
|
1786
|
+
.catch(function (err) { apiError(err, "Couldn't update URL"); });
|
|
1787
|
+
});
|
|
1634
1788
|
});
|
|
1635
1789
|
urlRow.appendChild(urlEditBtn);
|
|
1636
1790
|
item.appendChild(urlRow);
|
|
@@ -1656,11 +1810,13 @@ const clientJs = `
|
|
|
1656
1810
|
disconnectBtn.className = "btn btn-sm btn-danger";
|
|
1657
1811
|
disconnectBtn.textContent = "Disconnect";
|
|
1658
1812
|
disconnectBtn.addEventListener("click", function () {
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1813
|
+
confirmDialog({ title: "Disconnect from " + remote.name + "?", message: "This removes its hosts too.", confirmLabel: "Disconnect", danger: true }).then(function (ok) {
|
|
1814
|
+
if (!ok) return;
|
|
1815
|
+
disconnectBtn.disabled = true;
|
|
1816
|
+
apiFetch("/api/remotes/" + encodeURIComponent(remote.name), { method: "DELETE" })
|
|
1817
|
+
.then(function () { showToast("Disconnected " + remote.name, "success"); return Promise.all([loadRemotes(), loadHosts()]); })
|
|
1818
|
+
.catch(function (err) { apiError(err, "Couldn't disconnect"); disconnectBtn.disabled = false; });
|
|
1819
|
+
});
|
|
1664
1820
|
});
|
|
1665
1821
|
actions.appendChild(disconnectBtn);
|
|
1666
1822
|
item.appendChild(actions);
|
|
@@ -1711,11 +1867,13 @@ const clientJs = `
|
|
|
1711
1867
|
delBtn.className = "btn btn-sm btn-danger";
|
|
1712
1868
|
delBtn.textContent = "Delete";
|
|
1713
1869
|
delBtn.addEventListener("click", function () {
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1870
|
+
confirmDialog({ title: "Delete " + h.host + " on " + remote.name + "?", message: "This can't be undone.", confirmLabel: "Delete", danger: true }).then(function (ok) {
|
|
1871
|
+
if (!ok) return;
|
|
1872
|
+
delBtn.disabled = true;
|
|
1873
|
+
apiFetch("/api/remotes/" + encodeURIComponent(remote.name) + "/hosts/" + encodeURIComponent(h.host), { method: "DELETE" })
|
|
1874
|
+
.then(function () { showToast("Deleted " + h.host, "success"); return Promise.all([loadRemotes(), loadHosts()]); })
|
|
1875
|
+
.catch(function (err) { apiError(err, "Couldn't delete host"); delBtn.disabled = false; });
|
|
1876
|
+
});
|
|
1719
1877
|
});
|
|
1720
1878
|
hostRow.appendChild(delBtn);
|
|
1721
1879
|
}
|
|
@@ -1874,6 +2032,29 @@ const clientJs = `
|
|
|
1874
2032
|
return row;
|
|
1875
2033
|
}
|
|
1876
2034
|
|
|
2035
|
+
function renderHubPanel(on) {
|
|
2036
|
+
setSwitch(hubPanelSwitch, on);
|
|
2037
|
+
// Turning it on needs a password; turning it off never does.
|
|
2038
|
+
hubPanelSwitch.disabled = !on && !UI_AUTH;
|
|
2039
|
+
if (!UI_AUTH && !on) hubPanelSub.textContent = "Set a dashboard password in Settings first. Visitors sign in with it.";
|
|
2040
|
+
else if (on) hubPanelSub.textContent = "Open " + (hubPanelUrl || "the public URL") + " in any browser and sign in with the dashboard password.";
|
|
2041
|
+
else hubPanelSub.textContent = "Open this dashboard from any browser at the public URL, after signing in with the dashboard password.";
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
hubPanelSwitch.addEventListener("click", function () {
|
|
2045
|
+
var next = hubPanelSwitch.getAttribute("aria-checked") !== "true";
|
|
2046
|
+
hubPanelSwitch.disabled = true;
|
|
2047
|
+
apiFetch("/api/hub/panel", { method: "PUT", body: JSON.stringify({ enabled: next }) })
|
|
2048
|
+
.then(function () {
|
|
2049
|
+
renderHubPanel(next);
|
|
2050
|
+
showToast(next ? "The dashboard is shared at the public URL" : "The dashboard is no longer shared", "success");
|
|
2051
|
+
})
|
|
2052
|
+
.catch(function (err) {
|
|
2053
|
+
apiError(err, "Couldn't change dashboard sharing");
|
|
2054
|
+
renderHubPanel(!next);
|
|
2055
|
+
});
|
|
2056
|
+
});
|
|
2057
|
+
|
|
1877
2058
|
hubLocalhostSwitch.addEventListener("click", function () {
|
|
1878
2059
|
var next = hubLocalhostSwitch.getAttribute("aria-checked") !== "true";
|
|
1879
2060
|
hubLocalhostSwitch.disabled = true;
|
|
@@ -1931,29 +2112,31 @@ const clientJs = `
|
|
|
1931
2112
|
|
|
1932
2113
|
stopProxyBtn.addEventListener("click", function () {
|
|
1933
2114
|
if (proxyStopped) return;
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2115
|
+
confirmDialog({ title: "Stop the locadot proxy?", message: "Run \\"locadot start\\" to bring it back up.", confirmLabel: "Stop proxy", danger: true }).then(function (ok) {
|
|
2116
|
+
if (!ok) return;
|
|
2117
|
+
stopProxyBtn.disabled = true;
|
|
2118
|
+
stopProxyBtn.classList.add("busy");
|
|
2119
|
+
apiFetch("/api/proxy/stop", { method: "POST" })
|
|
2120
|
+
.then(function () {
|
|
2121
|
+
proxyStopped = true;
|
|
2122
|
+
clear(stoppedBanner);
|
|
2123
|
+
stoppedBanner.appendChild(document.createTextNode("Proxy stopped. Run "));
|
|
2124
|
+
var code = document.createElement("code");
|
|
2125
|
+
code.className = "mono";
|
|
2126
|
+
code.textContent = "locadot start";
|
|
2127
|
+
stoppedBanner.appendChild(code);
|
|
2128
|
+
stoppedBanner.appendChild(document.createTextNode(" to bring it back."));
|
|
2129
|
+
stoppedBanner.hidden = false;
|
|
2130
|
+
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
2131
|
+
if (remotesPollTimer) { clearInterval(remotesPollTimer); remotesPollTimer = null; }
|
|
2132
|
+
showToast("Proxy stopped", "warn");
|
|
2133
|
+
})
|
|
2134
|
+
.catch(function (err) {
|
|
2135
|
+
apiError(err, "Couldn't stop the proxy");
|
|
2136
|
+
stopProxyBtn.disabled = false;
|
|
2137
|
+
stopProxyBtn.classList.remove("busy");
|
|
2138
|
+
});
|
|
2139
|
+
});
|
|
1957
2140
|
});
|
|
1958
2141
|
|
|
1959
2142
|
cfInstallBtn.addEventListener("click", function () {
|
|
@@ -2102,12 +2285,36 @@ const clientJs = `
|
|
|
2102
2285
|
link.textContent = t.url.replace("https://", "");
|
|
2103
2286
|
link.title = t.url;
|
|
2104
2287
|
statusWrap.appendChild(link);
|
|
2288
|
+
if (t.mode === "custom") {
|
|
2289
|
+
var modeBadge = document.createElement("span");
|
|
2290
|
+
modeBadge.className = "badge badge-accent";
|
|
2291
|
+
modeBadge.textContent = "custom";
|
|
2292
|
+
statusWrap.appendChild(modeBadge);
|
|
2293
|
+
}
|
|
2105
2294
|
statusWrap.appendChild(makeCopyButton(function () { return t.url; }));
|
|
2106
2295
|
} else if (t.status === "starting") {
|
|
2107
2296
|
var spin = document.createElement("span");
|
|
2108
2297
|
spin.className = "spinner";
|
|
2109
2298
|
statusWrap.appendChild(spin);
|
|
2110
2299
|
statusWrap.appendChild(document.createTextNode("Starting\\u2026"));
|
|
2300
|
+
} else if (t.status === "login") {
|
|
2301
|
+
var loginDot = document.createElement("span");
|
|
2302
|
+
loginDot.className = "dot warn";
|
|
2303
|
+
statusWrap.appendChild(loginDot);
|
|
2304
|
+
var loginText = document.createElement("span");
|
|
2305
|
+
loginText.className = "tunnel-login";
|
|
2306
|
+
var loginLabel = document.createElement("span");
|
|
2307
|
+
loginLabel.textContent = "Waiting for Cloudflare login";
|
|
2308
|
+
loginText.appendChild(loginLabel);
|
|
2309
|
+
if (t.loginUrl) {
|
|
2310
|
+
var loginLink = document.createElement("a");
|
|
2311
|
+
loginLink.href = t.loginUrl;
|
|
2312
|
+
loginLink.target = "_blank";
|
|
2313
|
+
loginLink.rel = "noopener noreferrer";
|
|
2314
|
+
loginLink.textContent = "Log in to Cloudflare \u2197";
|
|
2315
|
+
loginText.appendChild(loginLink);
|
|
2316
|
+
}
|
|
2317
|
+
statusWrap.appendChild(loginText);
|
|
2111
2318
|
} else if (t.status === "error") {
|
|
2112
2319
|
var errDot = document.createElement("span");
|
|
2113
2320
|
errDot.className = "dot down";
|
|
@@ -2129,35 +2336,392 @@ const clientJs = `
|
|
|
2129
2336
|
|
|
2130
2337
|
function shareButton(row) {
|
|
2131
2338
|
var t = tunnelInfo(row);
|
|
2132
|
-
var isOn = t.enabled || t.status === "up" || t.status === "starting";
|
|
2339
|
+
var isOn = t.enabled || t.status === "up" || t.status === "starting" || t.status === "login";
|
|
2133
2340
|
var isRetry = !isOn && t.status === "error";
|
|
2134
2341
|
var shareBtn = document.createElement("button");
|
|
2135
2342
|
shareBtn.type = "button";
|
|
2136
2343
|
shareBtn.className = "btn btn-sm" + (isOn ? " btn-danger" : "");
|
|
2137
2344
|
shareBtn.textContent = isOn ? "Unshare" : (isRetry ? "Retry" : "Share");
|
|
2138
|
-
shareBtn.title = isOn ? "Stop the public tunnel" :
|
|
2345
|
+
shareBtn.title = isOn ? "Stop the public tunnel" : "Share on a public URL";
|
|
2139
2346
|
if (!cloudflaredInstalled && !isOn) {
|
|
2140
2347
|
shareBtn.disabled = true;
|
|
2141
2348
|
shareBtn.title = "Install cloudflared first";
|
|
2142
2349
|
}
|
|
2143
2350
|
shareBtn.addEventListener("click", function () {
|
|
2144
|
-
if (
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2351
|
+
if (isOn) {
|
|
2352
|
+
confirmDialog({ title: "Stop sharing " + row.host + "?", message: "The public URL for " + row.host + " stops working.", confirmLabel: "Stop sharing", danger: true }).then(function (ok) {
|
|
2353
|
+
if (!ok) return;
|
|
2354
|
+
shareBtn.disabled = true;
|
|
2355
|
+
shareBtn.classList.add("busy");
|
|
2356
|
+
apiFetch("/api/hosts/" + encodeURIComponent(row.host), { method: "PUT", body: JSON.stringify({ tunnel: false }) })
|
|
2357
|
+
.then(function () {
|
|
2358
|
+
showToast("Stopping tunnel for " + row.host, "success");
|
|
2359
|
+
return loadHosts();
|
|
2360
|
+
})
|
|
2361
|
+
.catch(function (err) {
|
|
2362
|
+
apiError(err, "Couldn't change sharing for " + row.host);
|
|
2363
|
+
shareBtn.disabled = false;
|
|
2364
|
+
shareBtn.classList.remove("busy");
|
|
2365
|
+
});
|
|
2156
2366
|
});
|
|
2367
|
+
} else {
|
|
2368
|
+
openShareDialog(row);
|
|
2369
|
+
}
|
|
2157
2370
|
});
|
|
2158
2371
|
return shareBtn;
|
|
2159
2372
|
}
|
|
2160
2373
|
|
|
2374
|
+
// ---------- share dialog ----------
|
|
2375
|
+
|
|
2376
|
+
var shareDialogEl = document.getElementById("share-dialog");
|
|
2377
|
+
var shareHostEl = document.getElementById("share-host");
|
|
2378
|
+
var shareCloseBtn = document.getElementById("share-close");
|
|
2379
|
+
var shareCancelBtn = document.getElementById("share-cancel");
|
|
2380
|
+
var shareSubmitBtn = document.getElementById("share-submit");
|
|
2381
|
+
var shareOptionRandom = document.getElementById("share-option-random");
|
|
2382
|
+
var shareOptionCustom = document.getElementById("share-option-custom");
|
|
2383
|
+
var shareCustomExtra = document.getElementById("share-custom-extra");
|
|
2384
|
+
var shareDomainInput = document.getElementById("share-domain-input");
|
|
2385
|
+
var shareDomainError = document.getElementById("share-domain-error");
|
|
2386
|
+
var shareDomainHint = document.getElementById("share-domain-hint");
|
|
2387
|
+
var shareErrorEl = document.getElementById("share-error");
|
|
2388
|
+
var shareRow = null;
|
|
2389
|
+
var shareMode = "random";
|
|
2390
|
+
var HOSTNAME_RE = /^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,}$/;
|
|
2391
|
+
|
|
2392
|
+
function normalizeHostname(value) {
|
|
2393
|
+
return String(value || "").trim().toLowerCase().replace(/^https?:\\/\\//, "").replace(/\\/$/, "");
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
function validateHostname(raw) {
|
|
2397
|
+
var value = normalizeHostname(raw);
|
|
2398
|
+
if (!value) return "Enter a hostname.";
|
|
2399
|
+
if (value.slice(-10) === ".localhost") return "Hostname can't be a .localhost address.";
|
|
2400
|
+
if (!HOSTNAME_RE.test(value)) return "Enter a valid hostname, e.g. app.example.com.";
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function setShareMode(mode) {
|
|
2405
|
+
shareMode = mode;
|
|
2406
|
+
shareOptionRandom.setAttribute("aria-checked", String(mode === "random"));
|
|
2407
|
+
shareOptionCustom.setAttribute("aria-checked", String(mode === "custom"));
|
|
2408
|
+
shareCustomExtra.hidden = mode !== "custom";
|
|
2409
|
+
shareDomainHint.textContent = cloudflareLoggedIn
|
|
2410
|
+
? "The DNS record is created or updated for you."
|
|
2411
|
+
: "You'll be asked to log in to Cloudflare once; the login link appears on the row.";
|
|
2412
|
+
shareDomainError.hidden = true;
|
|
2413
|
+
shareDomainError.textContent = "";
|
|
2414
|
+
shareErrorEl.hidden = true;
|
|
2415
|
+
shareErrorEl.textContent = "";
|
|
2416
|
+
if (mode === "custom") setTimeout(function () { shareDomainInput.focus(); }, 0);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function closeShareDialog() {
|
|
2420
|
+
shareRow = null;
|
|
2421
|
+
shareDialogEl.close();
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
function openShareDialog(row) {
|
|
2425
|
+
shareRow = row;
|
|
2426
|
+
shareHostEl.textContent = row.host;
|
|
2427
|
+
shareErrorEl.hidden = true;
|
|
2428
|
+
shareErrorEl.textContent = "";
|
|
2429
|
+
shareDomainError.hidden = true;
|
|
2430
|
+
shareDomainError.textContent = "";
|
|
2431
|
+
var hasDomain = typeof row.tunnelDomain === "string" && !!row.tunnelDomain;
|
|
2432
|
+
shareDomainInput.value = hasDomain ? row.tunnelDomain : "";
|
|
2433
|
+
setShareMode(hasDomain ? "custom" : "random");
|
|
2434
|
+
shareSubmitBtn.disabled = false;
|
|
2435
|
+
shareSubmitBtn.classList.remove("busy");
|
|
2436
|
+
shareCancelBtn.disabled = false;
|
|
2437
|
+
shareDialogEl.showModal();
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
shareOptionRandom.addEventListener("click", function () { setShareMode("random"); });
|
|
2441
|
+
shareOptionCustom.addEventListener("click", function () { setShareMode("custom"); });
|
|
2442
|
+
shareOptionRandom.addEventListener("keydown", function (e) {
|
|
2443
|
+
if (e.key === "ArrowDown" || e.key === "ArrowRight") { e.preventDefault(); setShareMode("custom"); shareOptionCustom.focus(); }
|
|
2444
|
+
});
|
|
2445
|
+
shareOptionCustom.addEventListener("keydown", function (e) {
|
|
2446
|
+
if (e.key === "ArrowUp" || e.key === "ArrowLeft") { e.preventDefault(); setShareMode("random"); shareOptionRandom.focus(); }
|
|
2447
|
+
});
|
|
2448
|
+
shareCancelBtn.addEventListener("click", closeShareDialog);
|
|
2449
|
+
shareCloseBtn.addEventListener("click", closeShareDialog);
|
|
2450
|
+
shareDialogEl.addEventListener("click", function (e) {
|
|
2451
|
+
if (e.target === shareDialogEl) closeShareDialog();
|
|
2452
|
+
});
|
|
2453
|
+
shareDomainInput.addEventListener("keydown", function (e) {
|
|
2454
|
+
if (e.key === "Enter") { e.preventDefault(); shareSubmitBtn.click(); }
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
shareSubmitBtn.addEventListener("click", function () {
|
|
2458
|
+
if (!shareRow) return;
|
|
2459
|
+
var row = shareRow;
|
|
2460
|
+
var body = { tunnel: true };
|
|
2461
|
+
if (shareMode === "custom") {
|
|
2462
|
+
var err = validateHostname(shareDomainInput.value);
|
|
2463
|
+
if (err) {
|
|
2464
|
+
shareDomainError.textContent = err;
|
|
2465
|
+
shareDomainError.hidden = false;
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
body.domain = normalizeHostname(shareDomainInput.value);
|
|
2469
|
+
}
|
|
2470
|
+
shareSubmitBtn.disabled = true;
|
|
2471
|
+
shareSubmitBtn.classList.add("busy");
|
|
2472
|
+
shareCancelBtn.disabled = true;
|
|
2473
|
+
apiFetch("/api/hosts/" + encodeURIComponent(row.host), { method: "PUT", body: JSON.stringify(body) })
|
|
2474
|
+
.then(function () {
|
|
2475
|
+
showToast("Starting tunnel for " + row.host, "success");
|
|
2476
|
+
closeShareDialog();
|
|
2477
|
+
return loadHosts();
|
|
2478
|
+
})
|
|
2479
|
+
.catch(function (err2) {
|
|
2480
|
+
shareErrorEl.textContent = (err2 && err2.message) ? err2.message : String(err2);
|
|
2481
|
+
shareErrorEl.hidden = false;
|
|
2482
|
+
})
|
|
2483
|
+
.then(function () {
|
|
2484
|
+
shareSubmitBtn.disabled = false;
|
|
2485
|
+
shareSubmitBtn.classList.remove("busy");
|
|
2486
|
+
shareCancelBtn.disabled = false;
|
|
2487
|
+
});
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
var allowDialog = document.getElementById("allow-dialog");
|
|
2491
|
+
var allowHostEl = document.getElementById("allow-host");
|
|
2492
|
+
var allowWarnEl = document.getElementById("allow-warn");
|
|
2493
|
+
var allowListEl = document.getElementById("allow-list");
|
|
2494
|
+
var allowInput = document.getElementById("allow-input");
|
|
2495
|
+
var allowAddBtn = document.getElementById("allow-add");
|
|
2496
|
+
var allowErrorEl = document.getElementById("allow-error");
|
|
2497
|
+
var allowCancelBtn = document.getElementById("allow-cancel");
|
|
2498
|
+
var allowSaveBtn = document.getElementById("allow-save");
|
|
2499
|
+
var allowCloseBtn = document.getElementById("allow-close");
|
|
2500
|
+
var allowOriginal = [];
|
|
2501
|
+
var allowCurrent = [];
|
|
2502
|
+
var allowRow = null;
|
|
2503
|
+
|
|
2504
|
+
function allowListsEqual(a, b) {
|
|
2505
|
+
if (a.length !== b.length) return false;
|
|
2506
|
+
for (var i = 0; i < a.length; i++) {
|
|
2507
|
+
if (a[i] !== b[i]) return false;
|
|
2508
|
+
}
|
|
2509
|
+
return true;
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
function updateAllowSave() {
|
|
2513
|
+
allowSaveBtn.disabled = allowListsEqual(allowCurrent, allowOriginal);
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
function renderAllowList() {
|
|
2517
|
+
clear(allowListEl);
|
|
2518
|
+
if (!allowCurrent.length) {
|
|
2519
|
+
var empty = document.createElement("div");
|
|
2520
|
+
empty.className = "dim allow-empty";
|
|
2521
|
+
empty.textContent = "No internal addresses. Shared visitors reach public hosts only.";
|
|
2522
|
+
allowListEl.appendChild(empty);
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
allowCurrent.forEach(function (entry) {
|
|
2526
|
+
var entryRow = document.createElement("div");
|
|
2527
|
+
entryRow.className = "allow-row";
|
|
2528
|
+
var text = document.createElement("span");
|
|
2529
|
+
text.className = "mono";
|
|
2530
|
+
text.textContent = entry;
|
|
2531
|
+
entryRow.appendChild(text);
|
|
2532
|
+
var removeBtn = document.createElement("button");
|
|
2533
|
+
removeBtn.type = "button";
|
|
2534
|
+
removeBtn.className = "allow-remove";
|
|
2535
|
+
removeBtn.textContent = "\\u00d7";
|
|
2536
|
+
removeBtn.setAttribute("aria-label", "Remove " + entry);
|
|
2537
|
+
removeBtn.addEventListener("click", function () {
|
|
2538
|
+
allowCurrent = allowCurrent.filter(function (item) { return item !== entry; });
|
|
2539
|
+
renderAllowList();
|
|
2540
|
+
updateAllowSave();
|
|
2541
|
+
});
|
|
2542
|
+
entryRow.appendChild(removeBtn);
|
|
2543
|
+
allowListEl.appendChild(entryRow);
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function allowAddEntry() {
|
|
2548
|
+
var value = allowInput.value.trim().toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\\/\\//, "").replace(/\\/$/, "");
|
|
2549
|
+
allowErrorEl.hidden = true;
|
|
2550
|
+
allowErrorEl.textContent = "";
|
|
2551
|
+
if (!value || /\\s/.test(value)) {
|
|
2552
|
+
allowErrorEl.textContent = "Enter a host or host:port, e.g. localhost:3000.";
|
|
2553
|
+
allowErrorEl.hidden = false;
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
if (allowCurrent.indexOf(value) !== -1) {
|
|
2557
|
+
allowErrorEl.textContent = value + " is already in the list.";
|
|
2558
|
+
allowErrorEl.hidden = false;
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
allowCurrent.push(value);
|
|
2562
|
+
allowInput.value = "";
|
|
2563
|
+
renderAllowList();
|
|
2564
|
+
updateAllowSave();
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
function closeAllowDialog() {
|
|
2568
|
+
allowDialog.close();
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
function openAllowDialog(row) {
|
|
2572
|
+
allowRow = row;
|
|
2573
|
+
allowOriginal = (row.allow || []).slice();
|
|
2574
|
+
allowCurrent = allowOriginal.slice();
|
|
2575
|
+
allowHostEl.textContent = row.host;
|
|
2576
|
+
allowWarnEl.hidden = !!row.cors;
|
|
2577
|
+
allowErrorEl.hidden = true;
|
|
2578
|
+
allowErrorEl.textContent = "";
|
|
2579
|
+
allowInput.value = "";
|
|
2580
|
+
renderAllowList();
|
|
2581
|
+
allowSaveBtn.disabled = true;
|
|
2582
|
+
allowSaveBtn.classList.remove("busy");
|
|
2583
|
+
allowCancelBtn.disabled = false;
|
|
2584
|
+
allowDialog.showModal();
|
|
2585
|
+
setTimeout(function () { allowInput.focus(); }, 0);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
allowAddBtn.addEventListener("click", allowAddEntry);
|
|
2589
|
+
allowInput.addEventListener("keydown", function (e) {
|
|
2590
|
+
if (e.key === "Enter") { e.preventDefault(); allowAddEntry(); }
|
|
2591
|
+
});
|
|
2592
|
+
allowCancelBtn.addEventListener("click", closeAllowDialog);
|
|
2593
|
+
allowCloseBtn.addEventListener("click", closeAllowDialog);
|
|
2594
|
+
allowDialog.addEventListener("click", function (e) {
|
|
2595
|
+
if (e.target === allowDialog) closeAllowDialog();
|
|
2596
|
+
});
|
|
2597
|
+
|
|
2598
|
+
allowSaveBtn.addEventListener("click", function () {
|
|
2599
|
+
if (!allowRow) return;
|
|
2600
|
+
var host = allowRow.host;
|
|
2601
|
+
allowSaveBtn.disabled = true;
|
|
2602
|
+
allowSaveBtn.classList.add("busy");
|
|
2603
|
+
allowCancelBtn.disabled = true;
|
|
2604
|
+
apiFetch("/api/hosts/" + encodeURIComponent(host), { method: "PUT", body: JSON.stringify({ allow: allowCurrent }) })
|
|
2605
|
+
.then(function () {
|
|
2606
|
+
showToast("Saved internal access for " + host, "success");
|
|
2607
|
+
closeAllowDialog();
|
|
2608
|
+
return loadHosts();
|
|
2609
|
+
})
|
|
2610
|
+
.catch(function (err) {
|
|
2611
|
+
allowErrorEl.textContent = err && err.message ? err.message : String(err);
|
|
2612
|
+
allowErrorEl.hidden = false;
|
|
2613
|
+
allowSaveBtn.disabled = false;
|
|
2614
|
+
allowSaveBtn.classList.remove("busy");
|
|
2615
|
+
allowCancelBtn.disabled = false;
|
|
2616
|
+
});
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2619
|
+
// ---------- confirm / prompt dialogs ----------
|
|
2620
|
+
|
|
2621
|
+
var confirmDialogEl = document.getElementById("confirm-dialog");
|
|
2622
|
+
var confirmTitleEl = document.getElementById("confirm-title");
|
|
2623
|
+
var confirmMessageEl = document.getElementById("confirm-message");
|
|
2624
|
+
var confirmCancelBtn = document.getElementById("confirm-cancel");
|
|
2625
|
+
var confirmOkBtn = document.getElementById("confirm-ok");
|
|
2626
|
+
var confirmCloseBtn = document.getElementById("confirm-close");
|
|
2627
|
+
var confirmResolve = null;
|
|
2628
|
+
|
|
2629
|
+
function closeConfirmDialog(result) {
|
|
2630
|
+
var resolve = confirmResolve;
|
|
2631
|
+
confirmResolve = null;
|
|
2632
|
+
confirmDialogEl.close();
|
|
2633
|
+
if (resolve) resolve(result);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function confirmDialog(opts) {
|
|
2637
|
+
opts = opts || {};
|
|
2638
|
+
return new Promise(function (resolve) {
|
|
2639
|
+
confirmResolve = resolve;
|
|
2640
|
+
confirmTitleEl.textContent = opts.title || "Are you sure?";
|
|
2641
|
+
confirmMessageEl.textContent = opts.message || "";
|
|
2642
|
+
confirmOkBtn.textContent = opts.confirmLabel || "OK";
|
|
2643
|
+
confirmOkBtn.className = "btn " + (opts.danger ? "btn-danger" : "btn-primary");
|
|
2644
|
+
confirmDialogEl.showModal();
|
|
2645
|
+
setTimeout(function () { confirmOkBtn.focus(); }, 0);
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
confirmCancelBtn.addEventListener("click", function () { closeConfirmDialog(false); });
|
|
2650
|
+
confirmCloseBtn.addEventListener("click", function () { closeConfirmDialog(false); });
|
|
2651
|
+
confirmOkBtn.addEventListener("click", function () { closeConfirmDialog(true); });
|
|
2652
|
+
confirmDialogEl.addEventListener("click", function (e) {
|
|
2653
|
+
if (e.target === confirmDialogEl) closeConfirmDialog(false);
|
|
2654
|
+
});
|
|
2655
|
+
confirmDialogEl.addEventListener("close", function () {
|
|
2656
|
+
if (confirmResolve) { var resolve = confirmResolve; confirmResolve = null; resolve(false); }
|
|
2657
|
+
});
|
|
2658
|
+
|
|
2659
|
+
var promptDialogEl = document.getElementById("prompt-dialog");
|
|
2660
|
+
var promptTitleEl = document.getElementById("prompt-title");
|
|
2661
|
+
var promptMessageEl = document.getElementById("prompt-message");
|
|
2662
|
+
var promptLabelEl = document.getElementById("prompt-label");
|
|
2663
|
+
var promptInput = document.getElementById("prompt-input");
|
|
2664
|
+
var promptErrorEl = document.getElementById("prompt-error");
|
|
2665
|
+
var promptCancelBtn = document.getElementById("prompt-cancel");
|
|
2666
|
+
var promptConfirmBtn = document.getElementById("prompt-confirm");
|
|
2667
|
+
var promptCloseBtn = document.getElementById("prompt-close");
|
|
2668
|
+
var promptResolve = null;
|
|
2669
|
+
var promptValidate = null;
|
|
2670
|
+
|
|
2671
|
+
function closePromptDialog(result) {
|
|
2672
|
+
var resolve = promptResolve;
|
|
2673
|
+
promptResolve = null;
|
|
2674
|
+
promptValidate = null;
|
|
2675
|
+
promptDialogEl.close();
|
|
2676
|
+
if (resolve) resolve(result);
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
function promptDialog(opts) {
|
|
2680
|
+
opts = opts || {};
|
|
2681
|
+
return new Promise(function (resolve) {
|
|
2682
|
+
promptResolve = resolve;
|
|
2683
|
+
promptValidate = typeof opts.validate === "function" ? opts.validate : null;
|
|
2684
|
+
promptTitleEl.textContent = opts.title || "";
|
|
2685
|
+
promptMessageEl.textContent = opts.message || "";
|
|
2686
|
+
promptMessageEl.hidden = !opts.message;
|
|
2687
|
+
promptLabelEl.textContent = opts.label || "";
|
|
2688
|
+
promptLabelEl.hidden = !opts.label;
|
|
2689
|
+
promptInput.placeholder = opts.placeholder || "";
|
|
2690
|
+
promptInput.value = opts.value || "";
|
|
2691
|
+
promptConfirmBtn.textContent = opts.confirmLabel || "OK";
|
|
2692
|
+
promptErrorEl.hidden = true;
|
|
2693
|
+
promptErrorEl.textContent = "";
|
|
2694
|
+
promptDialogEl.showModal();
|
|
2695
|
+
setTimeout(function () { promptInput.focus(); promptInput.select(); }, 0);
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
function submitPromptDialog() {
|
|
2700
|
+
var value = promptInput.value;
|
|
2701
|
+
if (promptValidate) {
|
|
2702
|
+
var err = promptValidate(value);
|
|
2703
|
+
if (err) {
|
|
2704
|
+
promptErrorEl.textContent = err;
|
|
2705
|
+
promptErrorEl.hidden = false;
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
closePromptDialog(value);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
promptCancelBtn.addEventListener("click", function () { closePromptDialog(null); });
|
|
2713
|
+
promptCloseBtn.addEventListener("click", function () { closePromptDialog(null); });
|
|
2714
|
+
promptConfirmBtn.addEventListener("click", submitPromptDialog);
|
|
2715
|
+
promptInput.addEventListener("keydown", function (e) {
|
|
2716
|
+
if (e.key === "Enter") { e.preventDefault(); submitPromptDialog(); }
|
|
2717
|
+
});
|
|
2718
|
+
promptDialogEl.addEventListener("click", function (e) {
|
|
2719
|
+
if (e.target === promptDialogEl) closePromptDialog(null);
|
|
2720
|
+
});
|
|
2721
|
+
promptDialogEl.addEventListener("close", function () {
|
|
2722
|
+
if (promptResolve) { var resolve = promptResolve; promptResolve = null; resolve(null); }
|
|
2723
|
+
});
|
|
2724
|
+
|
|
2161
2725
|
function labelCells(tr, labels) {
|
|
2162
2726
|
for (var i = 0; i < tr.children.length; i++) tr.children[i].setAttribute("data-label", labels[i] || "");
|
|
2163
2727
|
}
|
|
@@ -2198,13 +2762,14 @@ const clientJs = `
|
|
|
2198
2762
|
|
|
2199
2763
|
var optionsTd = document.createElement("td");
|
|
2200
2764
|
var flags = [];
|
|
2201
|
-
if (row.insecure) flags.push(["insecure", "Upstream TLS certificate is not verified"]);
|
|
2202
|
-
if (row.cors) flags.push(["cors", "Origin/Referer rewritten to the target; any origin may call this host"]);
|
|
2765
|
+
if (row.insecure) flags.push(["insecure", "Upstream TLS certificate is not verified", "badge-warn"]);
|
|
2766
|
+
if (row.cors) flags.push(["cors", "Origin/Referer rewritten to the target; any origin may call this host", "badge-cors"]);
|
|
2767
|
+
if (row.allow && row.allow.length) flags.push(["allow " + row.allow.length, row.allow.join(", "), "badge-accent"]);
|
|
2203
2768
|
if (flags.length) {
|
|
2204
2769
|
optionsTd.className = "badges";
|
|
2205
2770
|
flags.forEach(function (flag) {
|
|
2206
2771
|
var badge = document.createElement("span");
|
|
2207
|
-
badge.className = "badge " +
|
|
2772
|
+
badge.className = "badge " + flag[2];
|
|
2208
2773
|
badge.textContent = flag[0];
|
|
2209
2774
|
badge.title = flag[1];
|
|
2210
2775
|
optionsTd.appendChild(badge);
|
|
@@ -2235,6 +2800,20 @@ const clientJs = `
|
|
|
2235
2800
|
var actionsWrap = document.createElement("div");
|
|
2236
2801
|
actionsWrap.className = "row-actions";
|
|
2237
2802
|
actionsTd.appendChild(actionsWrap);
|
|
2803
|
+
var accessBtn = document.createElement("button");
|
|
2804
|
+
accessBtn.type = "button";
|
|
2805
|
+
var allowCount = row.allow ? row.allow.length : 0;
|
|
2806
|
+
accessBtn.className = "btn btn-sm btn-icon" + (allowCount ? " is-set" : "");
|
|
2807
|
+
accessBtn.title = allowCount ? "Internal access: " + row.allow.join(", ") : "Internal access: let shared visitors reach addresses such as localhost:3000";
|
|
2808
|
+
accessBtn.setAttribute("aria-label", "Internal access for " + row.host);
|
|
2809
|
+
var accessSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2810
|
+
accessSvg.setAttribute("viewBox", "0 0 24 24");
|
|
2811
|
+
accessSvg.setAttribute("aria-hidden", "true");
|
|
2812
|
+
var accessPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
2813
|
+
accessPath.setAttribute("d", "M12 3l7 3v5c0 4.5-3 8.3-7 10-4-1.7-7-5.5-7-10V6l7-3zM9.5 12l1.8 1.8L15 10");
|
|
2814
|
+
accessSvg.appendChild(accessPath);
|
|
2815
|
+
accessBtn.appendChild(accessSvg);
|
|
2816
|
+
accessBtn.addEventListener("click", function () { openAllowDialog(row); });
|
|
2238
2817
|
var editBtn = document.createElement("button");
|
|
2239
2818
|
editBtn.type = "button";
|
|
2240
2819
|
editBtn.className = "btn btn-sm";
|
|
@@ -2245,20 +2824,23 @@ const clientJs = `
|
|
|
2245
2824
|
removeBtn.className = "btn btn-sm btn-danger";
|
|
2246
2825
|
removeBtn.textContent = "Remove";
|
|
2247
2826
|
removeBtn.addEventListener("click", function () {
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2827
|
+
(prefs.confirm ? confirmDialog({ title: "Remove host " + row.host + "?", message: "This can't be undone.", confirmLabel: "Remove host", danger: true }) : Promise.resolve(true)).then(function (ok) {
|
|
2828
|
+
if (!ok) return;
|
|
2829
|
+
removeBtn.disabled = true;
|
|
2830
|
+
apiFetch("/api/hosts/" + encodeURIComponent(row.host), { method: "DELETE" })
|
|
2831
|
+
.then(function () {
|
|
2832
|
+
showToast("Removed " + row.host, "success");
|
|
2833
|
+
return loadHosts();
|
|
2834
|
+
})
|
|
2835
|
+
.catch(function (err) {
|
|
2836
|
+
apiError(err, "Couldn't remove " + row.host);
|
|
2837
|
+
removeBtn.disabled = false;
|
|
2838
|
+
});
|
|
2839
|
+
});
|
|
2259
2840
|
});
|
|
2260
2841
|
if (!row.remote) {
|
|
2261
2842
|
actionsWrap.appendChild(shareButton(row));
|
|
2843
|
+
actionsWrap.appendChild(accessBtn);
|
|
2262
2844
|
actionsWrap.appendChild(editBtn);
|
|
2263
2845
|
}
|
|
2264
2846
|
actionsWrap.appendChild(removeBtn);
|
|
@@ -2424,13 +3006,15 @@ const clientJs = `
|
|
|
2424
3006
|
|
|
2425
3007
|
logsRefreshBtn.addEventListener("click", loadLogs);
|
|
2426
3008
|
logsClearBtn.addEventListener("click", function () {
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
3009
|
+
confirmDialog({ title: "Clear logs?", message: "This can't be undone.", confirmLabel: "Clear logs", danger: true }).then(function (ok) {
|
|
3010
|
+
if (!ok) return;
|
|
3011
|
+
apiFetch("/api/logs/clear", { method: "POST" })
|
|
3012
|
+
.then(function () {
|
|
3013
|
+
showToast("Logs cleared", "success");
|
|
3014
|
+
return loadLogs();
|
|
3015
|
+
})
|
|
3016
|
+
.catch(function (err) { apiError(err, "Couldn't clear logs"); });
|
|
3017
|
+
});
|
|
2434
3018
|
});
|
|
2435
3019
|
|
|
2436
3020
|
cliAddCopy.addEventListener("click", function () { copyText(cliAddPre.textContent, cliAddCopy); });
|
|
@@ -2856,7 +3440,8 @@ const clientJs = `
|
|
|
2856
3440
|
});
|
|
2857
3441
|
|
|
2858
3442
|
restartBtn.addEventListener("click", function () {
|
|
2859
|
-
|
|
3443
|
+
confirmDialog({ title: "Restart the proxy?", message: "Hosts are unavailable for a few seconds.", confirmLabel: "Restart", danger: true }).then(function (ok) {
|
|
3444
|
+
if (!ok) return;
|
|
2860
3445
|
var s = settingsLoaded;
|
|
2861
3446
|
var nextPort = s ? ((s.env && s.env.httpPort) ? s.httpPort : ((s.saved && s.saved.httpPort) || s.httpPort)) : null;
|
|
2862
3447
|
restartBtn.disabled = true;
|
|
@@ -2889,6 +3474,7 @@ const clientJs = `
|
|
|
2889
3474
|
restartBtn.disabled = false;
|
|
2890
3475
|
restartBtn.classList.remove("busy");
|
|
2891
3476
|
});
|
|
3477
|
+
});
|
|
2892
3478
|
});
|
|
2893
3479
|
|
|
2894
3480
|
document.getElementById("set-home-copy").addEventListener("click", function (e) { copyText(setHome.textContent, e.currentTarget); });
|
|
@@ -2962,6 +3548,7 @@ const clientJs = `
|
|
|
2962
3548
|
uiAuthSave.textContent = enabled ? "Change password" : "Set password";
|
|
2963
3549
|
uiAuthRemove.hidden = !enabled;
|
|
2964
3550
|
signOutBtn.hidden = !enabled;
|
|
3551
|
+
renderHubPanel(enabled && hubPanelSwitch.getAttribute("aria-checked") === "true");
|
|
2965
3552
|
}
|
|
2966
3553
|
renderUiAuth(UI_AUTH);
|
|
2967
3554
|
|
|
@@ -3314,6 +3901,13 @@ function renderPage(nonce, token, uiAuth = false) {
|
|
|
3314
3901
|
</div>
|
|
3315
3902
|
<button type="button" id="hub-localhost" class="switch" role="switch" aria-checked="true" aria-label="Let admins open this dashboard and any port on this machine"><span class="switch-knob"></span></button>
|
|
3316
3903
|
</div>
|
|
3904
|
+
<div id="hub-panel-row" class="toggle-row" hidden>
|
|
3905
|
+
<div>
|
|
3906
|
+
<div class="label">Share this dashboard</div>
|
|
3907
|
+
<div id="hub-panel-sub" class="tile-sub"></div>
|
|
3908
|
+
</div>
|
|
3909
|
+
<button type="button" id="hub-panel" class="switch" role="switch" aria-checked="false" aria-label="Share this dashboard at the public URL, behind the dashboard password"><span class="switch-knob"></span></button>
|
|
3910
|
+
</div>
|
|
3317
3911
|
</div>
|
|
3318
3912
|
|
|
3319
3913
|
<div id="hub-share-section" class="hub-section" hidden>
|
|
@@ -3611,6 +4205,104 @@ function renderPage(nonce, token, uiAuth = false) {
|
|
|
3611
4205
|
<a href="https://github.com/avinashid/locadot" target="_blank" rel="noopener noreferrer">github.com/avinashid/locadot</a>
|
|
3612
4206
|
</footer>
|
|
3613
4207
|
</div>
|
|
4208
|
+
<dialog id="allow-dialog">
|
|
4209
|
+
<div class="modal-head">
|
|
4210
|
+
<div>
|
|
4211
|
+
<h3 class="modal-title">Internal access</h3>
|
|
4212
|
+
<p id="allow-host" class="modal-sub mono"></p>
|
|
4213
|
+
</div>
|
|
4214
|
+
<button type="button" id="allow-close" class="btn btn-ghost modal-close" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
4215
|
+
</div>
|
|
4216
|
+
<div class="modal-body">
|
|
4217
|
+
<p class="modal-desc">People who open this mapping through a tunnel or remote access can only reach public hosts. Allow the internal addresses this app calls, such as its API on localhost.</p>
|
|
4218
|
+
<div id="allow-warn" class="modal-warn" hidden>Bypass CORS is off for this mapping. These addresses take effect once it's on, because the page reaches them through locadot's pass-through.</div>
|
|
4219
|
+
<div id="allow-list" class="allow-list"></div>
|
|
4220
|
+
<div class="allow-add-row">
|
|
4221
|
+
<input type="text" id="allow-input" class="input" placeholder="localhost:3000" aria-label="Address" autocomplete="off">
|
|
4222
|
+
<button type="button" id="allow-add" class="btn btn-sm">Add</button>
|
|
4223
|
+
</div>
|
|
4224
|
+
<p class="modal-hint">host:port, or a host alone for any port. locadot's own ports are always blocked.</p>
|
|
4225
|
+
<div id="allow-error" class="field-error" role="alert" hidden></div>
|
|
4226
|
+
</div>
|
|
4227
|
+
<div class="modal-foot">
|
|
4228
|
+
<button type="button" id="allow-cancel" class="btn">Cancel</button>
|
|
4229
|
+
<button type="button" id="allow-save" class="btn btn-primary" disabled>Save changes</button>
|
|
4230
|
+
</div>
|
|
4231
|
+
</dialog>
|
|
4232
|
+
<dialog id="confirm-dialog">
|
|
4233
|
+
<div class="modal-head">
|
|
4234
|
+
<div>
|
|
4235
|
+
<h3 class="modal-title" id="confirm-title"></h3>
|
|
4236
|
+
</div>
|
|
4237
|
+
<button type="button" id="confirm-close" class="btn btn-ghost modal-close" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
4238
|
+
</div>
|
|
4239
|
+
<div class="modal-body">
|
|
4240
|
+
<p class="modal-desc" id="confirm-message"></p>
|
|
4241
|
+
</div>
|
|
4242
|
+
<div class="modal-foot">
|
|
4243
|
+
<button type="button" id="confirm-cancel" class="btn">Cancel</button>
|
|
4244
|
+
<button type="button" id="confirm-ok" class="btn btn-primary">OK</button>
|
|
4245
|
+
</div>
|
|
4246
|
+
</dialog>
|
|
4247
|
+
<dialog id="prompt-dialog">
|
|
4248
|
+
<div class="modal-head">
|
|
4249
|
+
<div>
|
|
4250
|
+
<h3 class="modal-title" id="prompt-title"></h3>
|
|
4251
|
+
</div>
|
|
4252
|
+
<button type="button" id="prompt-close" class="btn btn-ghost modal-close" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
4253
|
+
</div>
|
|
4254
|
+
<div class="modal-body">
|
|
4255
|
+
<p class="modal-desc" id="prompt-message"></p>
|
|
4256
|
+
<div class="field">
|
|
4257
|
+
<label id="prompt-label" for="prompt-input"></label>
|
|
4258
|
+
<input type="text" id="prompt-input" class="input" autocomplete="off">
|
|
4259
|
+
</div>
|
|
4260
|
+
<div id="prompt-error" class="field-error" role="alert" hidden></div>
|
|
4261
|
+
</div>
|
|
4262
|
+
<div class="modal-foot">
|
|
4263
|
+
<button type="button" id="prompt-cancel" class="btn">Cancel</button>
|
|
4264
|
+
<button type="button" id="prompt-confirm" class="btn btn-primary">OK</button>
|
|
4265
|
+
</div>
|
|
4266
|
+
</dialog>
|
|
4267
|
+
<dialog id="share-dialog">
|
|
4268
|
+
<div class="modal-head">
|
|
4269
|
+
<div>
|
|
4270
|
+
<h3 class="modal-title">Share publicly</h3>
|
|
4271
|
+
<p id="share-host" class="modal-sub mono"></p>
|
|
4272
|
+
</div>
|
|
4273
|
+
<button type="button" id="share-close" class="btn btn-ghost modal-close" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
4274
|
+
</div>
|
|
4275
|
+
<div class="modal-body">
|
|
4276
|
+
<p class="modal-desc">Anyone with the link will be able to reach this host, without signing in.</p>
|
|
4277
|
+
<div id="share-options" class="share-options" role="radiogroup" aria-label="Sharing method">
|
|
4278
|
+
<button type="button" id="share-option-random" class="option-card" role="radio" aria-checked="false">
|
|
4279
|
+
<span class="option-dot" aria-hidden="true"></span>
|
|
4280
|
+
<span class="option-body">
|
|
4281
|
+
<span class="option-title">Random URL</span>
|
|
4282
|
+
<span class="option-desc">https://<random>.trycloudflare.com · No account needed. Changes each time sharing restarts.</span>
|
|
4283
|
+
</span>
|
|
4284
|
+
</button>
|
|
4285
|
+
<button type="button" id="share-option-custom" class="option-card" role="radio" aria-checked="false">
|
|
4286
|
+
<span class="option-dot" aria-hidden="true"></span>
|
|
4287
|
+
<span class="option-body">
|
|
4288
|
+
<span class="option-title">Custom domain</span>
|
|
4289
|
+
<span class="option-desc">Your own hostname on a domain in your Cloudflare account. Stable URL.</span>
|
|
4290
|
+
<span id="share-custom-extra" class="option-extra" hidden>
|
|
4291
|
+
<label for="share-domain-input" class="dim">Hostname</label>
|
|
4292
|
+
<input type="text" id="share-domain-input" class="input mono" placeholder="app.example.com" autocomplete="off">
|
|
4293
|
+
<div id="share-domain-error" class="field-error" role="alert" hidden></div>
|
|
4294
|
+
<p id="share-domain-hint" class="modal-hint"></p>
|
|
4295
|
+
</span>
|
|
4296
|
+
</span>
|
|
4297
|
+
</button>
|
|
4298
|
+
</div>
|
|
4299
|
+
<div id="share-error" class="field-error" role="alert" hidden></div>
|
|
4300
|
+
</div>
|
|
4301
|
+
<div class="modal-foot">
|
|
4302
|
+
<button type="button" id="share-cancel" class="btn">Cancel</button>
|
|
4303
|
+
<button type="button" id="share-submit" class="btn btn-primary">Start sharing</button>
|
|
4304
|
+
</div>
|
|
4305
|
+
</dialog>
|
|
3614
4306
|
<div id="toast-container" class="toast-container" aria-live="polite"></div>
|
|
3615
4307
|
<script nonce="${safeNonce}">${clientJs}</script>
|
|
3616
4308
|
</body>
|