@ystemsrx/cfshare 0.1.5 → 0.1.6
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/dist/src/cli.js +131 -1
- package/dist/src/manager.d.ts +15 -1
- package/dist/src/manager.d.ts.map +1 -1
- package/dist/src/manager.js +479 -110
- package/dist/src/tools.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +148 -1
- package/src/manager.ts +569 -115
- package/src/tools.ts +1 -1
package/dist/src/manager.js
CHANGED
|
@@ -184,6 +184,18 @@ function ensureString(input) {
|
|
|
184
184
|
const trimmed = input.trim();
|
|
185
185
|
return trimmed || undefined;
|
|
186
186
|
}
|
|
187
|
+
function asNumber(input) {
|
|
188
|
+
if (typeof input === "number" && Number.isFinite(input)) {
|
|
189
|
+
return Math.trunc(input);
|
|
190
|
+
}
|
|
191
|
+
if (typeof input === "string" && input.trim()) {
|
|
192
|
+
const parsed = Number(input);
|
|
193
|
+
if (Number.isFinite(parsed)) {
|
|
194
|
+
return Math.trunc(parsed);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
187
199
|
function buildContentDisposition(params) {
|
|
188
200
|
if (params.mode === "raw") {
|
|
189
201
|
return undefined;
|
|
@@ -558,6 +570,7 @@ export class CfshareManager {
|
|
|
558
570
|
workspaceRoot;
|
|
559
571
|
auditFile;
|
|
560
572
|
sessionsFile;
|
|
573
|
+
sessionsDir;
|
|
561
574
|
exportsDir;
|
|
562
575
|
initialized = false;
|
|
563
576
|
initializing;
|
|
@@ -577,6 +590,7 @@ export class CfshareManager {
|
|
|
577
590
|
this.workspaceRoot = path.join(this.stateDir, "workspaces");
|
|
578
591
|
this.auditFile = path.join(this.stateDir, "audit.jsonl");
|
|
579
592
|
this.sessionsFile = path.join(this.stateDir, "sessions.json");
|
|
593
|
+
this.sessionsDir = path.join(this.stateDir, "sessions");
|
|
580
594
|
this.exportsDir = path.join(this.stateDir, "exports");
|
|
581
595
|
this.cloudflaredPathInput = this.pluginConfig.cloudflaredPath ?? "cloudflared";
|
|
582
596
|
}
|
|
@@ -593,6 +607,7 @@ export class CfshareManager {
|
|
|
593
607
|
await mkdirp(this.stateDir);
|
|
594
608
|
await mkdirp(this.workspaceRoot);
|
|
595
609
|
await mkdirp(this.exportsDir);
|
|
610
|
+
await mkdirp(this.sessionsDir);
|
|
596
611
|
await this.reloadPolicy();
|
|
597
612
|
this.startGuard();
|
|
598
613
|
this.initialized = true;
|
|
@@ -622,16 +637,201 @@ export class CfshareManager {
|
|
|
622
637
|
this.logger.warn(`cfshare: failed to write audit event: ${String(error)}`);
|
|
623
638
|
}
|
|
624
639
|
}
|
|
625
|
-
|
|
626
|
-
|
|
640
|
+
sessionRecordPath(id) {
|
|
641
|
+
return path.join(this.sessionsDir, `${encodeURIComponent(id)}.json`);
|
|
642
|
+
}
|
|
643
|
+
toPersistedSessionRecord(session) {
|
|
644
|
+
return {
|
|
627
645
|
id: session.id,
|
|
628
646
|
type: session.type,
|
|
629
647
|
status: session.status,
|
|
648
|
+
createdAt: session.createdAt,
|
|
630
649
|
expiresAt: session.expiresAt,
|
|
650
|
+
localUrl: session.localUrl,
|
|
651
|
+
publicUrl: session.publicUrl,
|
|
652
|
+
sourcePort: session.sourcePort,
|
|
653
|
+
originPort: session.originPort,
|
|
654
|
+
tunnelPort: session.tunnelPort,
|
|
631
655
|
workspaceDir: session.workspaceDir,
|
|
632
656
|
processPid: session.process?.pid,
|
|
633
|
-
|
|
634
|
-
|
|
657
|
+
ownerPid: process.pid,
|
|
658
|
+
fileMode: session.fileMode,
|
|
659
|
+
filePresentation: session.filePresentation,
|
|
660
|
+
maxDownloads: session.maxDownloads,
|
|
661
|
+
manifest: session.manifest,
|
|
662
|
+
access: session.access,
|
|
663
|
+
stats: session.stats,
|
|
664
|
+
lastError: session.lastError,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
parsePersistedSessionRecord(raw) {
|
|
668
|
+
if (!raw || typeof raw !== "object") {
|
|
669
|
+
return undefined;
|
|
670
|
+
}
|
|
671
|
+
const row = raw;
|
|
672
|
+
const id = ensureString(row.id);
|
|
673
|
+
const type = ensureString(row.type);
|
|
674
|
+
const status = ensureString(row.status);
|
|
675
|
+
if (!id || (type !== "port" && type !== "files")) {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
const normalizedStatus = status === "starting" || status === "running" || status === "stopped" || status === "error" || status === "expired"
|
|
679
|
+
? status
|
|
680
|
+
: "error";
|
|
681
|
+
const accessRaw = (row.access ?? {});
|
|
682
|
+
const accessModeRaw = ensureString(accessRaw.mode);
|
|
683
|
+
const accessMode = accessModeRaw === "token" || accessModeRaw === "basic" || accessModeRaw === "none" ? accessModeRaw : "none";
|
|
684
|
+
const statsRaw = (row.stats ?? {});
|
|
685
|
+
return {
|
|
686
|
+
id,
|
|
687
|
+
type,
|
|
688
|
+
status: normalizedStatus,
|
|
689
|
+
createdAt: ensureString(row.createdAt) || nowIso(),
|
|
690
|
+
expiresAt: ensureString(row.expiresAt) || nowIso(),
|
|
691
|
+
localUrl: ensureString(row.localUrl) || "",
|
|
692
|
+
publicUrl: ensureString(row.publicUrl) || undefined,
|
|
693
|
+
sourcePort: asNumber(row.sourcePort),
|
|
694
|
+
originPort: asNumber(row.originPort) ?? 0,
|
|
695
|
+
tunnelPort: asNumber(row.tunnelPort) ?? 0,
|
|
696
|
+
workspaceDir: ensureString(row.workspaceDir) || undefined,
|
|
697
|
+
processPid: asNumber(row.processPid),
|
|
698
|
+
ownerPid: asNumber(row.ownerPid),
|
|
699
|
+
fileMode: ensureString(row.fileMode) === "zip" ? "zip" : "normal",
|
|
700
|
+
filePresentation: normalizeFilePresentation(ensureString(row.filePresentation) || "download"),
|
|
701
|
+
maxDownloads: asNumber(row.maxDownloads),
|
|
702
|
+
manifest: Array.isArray(row.manifest) ? row.manifest : undefined,
|
|
703
|
+
access: {
|
|
704
|
+
mode: accessMode,
|
|
705
|
+
protectOrigin: Boolean(accessRaw.protectOrigin),
|
|
706
|
+
allowlistPaths: normalizeAllowlistPaths(accessRaw.allowlistPaths),
|
|
707
|
+
token: ensureString(accessRaw.token) || undefined,
|
|
708
|
+
username: ensureString(accessRaw.username) || undefined,
|
|
709
|
+
password: ensureString(accessRaw.password) || undefined,
|
|
710
|
+
},
|
|
711
|
+
stats: {
|
|
712
|
+
requests: asNumber(statsRaw.requests) ?? 0,
|
|
713
|
+
downloads: asNumber(statsRaw.downloads) ?? 0,
|
|
714
|
+
bytesSent: asNumber(statsRaw.bytesSent) ?? 0,
|
|
715
|
+
lastAccessAt: ensureString(statsRaw.lastAccessAt) || undefined,
|
|
716
|
+
},
|
|
717
|
+
lastError: ensureString(row.lastError) || undefined,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
async readPersistedSessions() {
|
|
721
|
+
const out = new Map();
|
|
722
|
+
let sessionsDirMissing = false;
|
|
723
|
+
const entries = await fs.readdir(this.sessionsDir, { withFileTypes: true }).catch((error) => {
|
|
724
|
+
if (error?.code === "ENOENT") {
|
|
725
|
+
sessionsDirMissing = true;
|
|
726
|
+
}
|
|
727
|
+
return [];
|
|
728
|
+
});
|
|
729
|
+
for (const entry of entries) {
|
|
730
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const abs = path.join(this.sessionsDir, entry.name);
|
|
734
|
+
try {
|
|
735
|
+
const raw = await fs.readFile(abs, "utf8");
|
|
736
|
+
const record = this.parsePersistedSessionRecord(JSON.parse(raw));
|
|
737
|
+
if (record) {
|
|
738
|
+
out.set(record.id, record);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
// ignore malformed session files
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (out.size > 0 || !sessionsDirMissing) {
|
|
746
|
+
return out;
|
|
747
|
+
}
|
|
748
|
+
// Legacy fallback for snapshots generated before per-session persistence.
|
|
749
|
+
try {
|
|
750
|
+
const raw = await fs.readFile(this.sessionsFile, "utf8");
|
|
751
|
+
const rows = JSON.parse(raw);
|
|
752
|
+
if (!Array.isArray(rows)) {
|
|
753
|
+
return out;
|
|
754
|
+
}
|
|
755
|
+
for (const row of rows) {
|
|
756
|
+
const record = this.parsePersistedSessionRecord({
|
|
757
|
+
...row,
|
|
758
|
+
createdAt: row.createdAt ?? nowIso(),
|
|
759
|
+
localUrl: row.localUrl ?? "",
|
|
760
|
+
originPort: row.originPort ?? 0,
|
|
761
|
+
tunnelPort: row.tunnelPort ?? 0,
|
|
762
|
+
access: row.access ??
|
|
763
|
+
{
|
|
764
|
+
mode: "none",
|
|
765
|
+
protectOrigin: false,
|
|
766
|
+
allowlistPaths: [],
|
|
767
|
+
},
|
|
768
|
+
stats: row.stats ??
|
|
769
|
+
{
|
|
770
|
+
requests: 0,
|
|
771
|
+
downloads: 0,
|
|
772
|
+
bytesSent: 0,
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
if (record) {
|
|
776
|
+
out.set(record.id, record);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
catch {
|
|
781
|
+
// ignore missing legacy snapshot
|
|
782
|
+
}
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
async writePersistedSessionRecord(record) {
|
|
786
|
+
const target = this.sessionRecordPath(record.id);
|
|
787
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
788
|
+
await fs.writeFile(tmp, JSON.stringify(record, null, 2), "utf8");
|
|
789
|
+
await fs.rename(tmp, target);
|
|
790
|
+
}
|
|
791
|
+
async deletePersistedSessionRecord(id) {
|
|
792
|
+
await fs.rm(this.sessionRecordPath(id), { force: true });
|
|
793
|
+
}
|
|
794
|
+
isProcessAlive(pid) {
|
|
795
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
try {
|
|
799
|
+
process.kill(pid, 0);
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
catch {
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async persistSessionsSnapshot() {
|
|
807
|
+
const activeIds = new Set();
|
|
808
|
+
const records = Array.from(this.sessions.values()).map((session) => {
|
|
809
|
+
const record = this.toPersistedSessionRecord(session);
|
|
810
|
+
activeIds.add(record.id);
|
|
811
|
+
return record;
|
|
812
|
+
});
|
|
813
|
+
await Promise.all(records.map((record) => this.writePersistedSessionRecord(record)));
|
|
814
|
+
const persisted = await this.readPersistedSessions();
|
|
815
|
+
for (const record of persisted.values()) {
|
|
816
|
+
if (record.ownerPid !== process.pid || activeIds.has(record.id)) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
await this.deletePersistedSessionRecord(record.id);
|
|
820
|
+
}
|
|
821
|
+
const mergedRows = Array.from((await this.readPersistedSessions()).values())
|
|
822
|
+
.map((record) => ({
|
|
823
|
+
id: record.id,
|
|
824
|
+
type: record.type,
|
|
825
|
+
status: record.status,
|
|
826
|
+
expiresAt: record.expiresAt,
|
|
827
|
+
workspaceDir: record.workspaceDir,
|
|
828
|
+
processPid: record.processPid,
|
|
829
|
+
ownerPid: record.ownerPid,
|
|
830
|
+
publicUrl: record.publicUrl,
|
|
831
|
+
localUrl: record.localUrl,
|
|
832
|
+
}))
|
|
833
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
834
|
+
await fs.writeFile(this.sessionsFile, JSON.stringify(mergedRows, null, 2), "utf8");
|
|
635
835
|
}
|
|
636
836
|
makeAccessState(params) {
|
|
637
837
|
const allowlistPaths = normalizeAllowlistPaths(params.allowlistPaths);
|
|
@@ -658,24 +858,26 @@ export class CfshareManager {
|
|
|
658
858
|
allowlistPaths,
|
|
659
859
|
};
|
|
660
860
|
}
|
|
661
|
-
|
|
662
|
-
const base = session.publicUrl;
|
|
861
|
+
makeResponsePublicUrlFromValues(base, access) {
|
|
663
862
|
if (!base) {
|
|
664
863
|
return undefined;
|
|
665
864
|
}
|
|
666
|
-
if (
|
|
865
|
+
if (access.mode !== "token" || !access.token) {
|
|
667
866
|
return base;
|
|
668
867
|
}
|
|
669
868
|
try {
|
|
670
869
|
const out = new URL(base);
|
|
671
|
-
out.searchParams.set("token",
|
|
870
|
+
out.searchParams.set("token", access.token);
|
|
672
871
|
return out.toString();
|
|
673
872
|
}
|
|
674
873
|
catch {
|
|
675
874
|
const sep = base.includes("?") ? "&" : "?";
|
|
676
|
-
return `${base}${sep}token=${encodeURIComponent(
|
|
875
|
+
return `${base}${sep}token=${encodeURIComponent(access.token)}`;
|
|
677
876
|
}
|
|
678
877
|
}
|
|
878
|
+
makeResponsePublicUrl(session) {
|
|
879
|
+
return this.makeResponsePublicUrlFromValues(session.publicUrl, session.access);
|
|
880
|
+
}
|
|
679
881
|
buildRateLimiter(policy) {
|
|
680
882
|
if (!policy.enabled) {
|
|
681
883
|
return () => true;
|
|
@@ -1702,8 +1904,27 @@ export class CfshareManager {
|
|
|
1702
1904
|
throw error;
|
|
1703
1905
|
}
|
|
1704
1906
|
}
|
|
1705
|
-
exposureList() {
|
|
1706
|
-
|
|
1907
|
+
async exposureList() {
|
|
1908
|
+
await this.ensureInitialized();
|
|
1909
|
+
const persisted = await this.readPersistedSessions();
|
|
1910
|
+
const out = new Map();
|
|
1911
|
+
for (const session of this.sessions.values()) {
|
|
1912
|
+
out.set(session.id, this.makeExposureRecord(session));
|
|
1913
|
+
}
|
|
1914
|
+
for (const record of persisted.values()) {
|
|
1915
|
+
if (out.has(record.id)) {
|
|
1916
|
+
continue;
|
|
1917
|
+
}
|
|
1918
|
+
out.set(record.id, {
|
|
1919
|
+
id: record.id,
|
|
1920
|
+
type: record.type,
|
|
1921
|
+
status: record.status,
|
|
1922
|
+
public_url: this.makeResponsePublicUrlFromValues(record.publicUrl, record.access),
|
|
1923
|
+
local_url: record.localUrl,
|
|
1924
|
+
expires_at: record.expiresAt,
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
return Array.from(out.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
1707
1928
|
}
|
|
1708
1929
|
normalizeRequestedIds(rawIds) {
|
|
1709
1930
|
const out = [];
|
|
@@ -1721,50 +1942,95 @@ export class CfshareManager {
|
|
|
1721
1942
|
}
|
|
1722
1943
|
return out;
|
|
1723
1944
|
}
|
|
1724
|
-
|
|
1945
|
+
matchesExposureFilterByValues(type, status, filter) {
|
|
1725
1946
|
if (!filter) {
|
|
1726
1947
|
return true;
|
|
1727
1948
|
}
|
|
1728
|
-
if (filter.status &&
|
|
1949
|
+
if (filter.status && status !== filter.status) {
|
|
1729
1950
|
return false;
|
|
1730
1951
|
}
|
|
1731
|
-
if (filter.type &&
|
|
1952
|
+
if (filter.type && type !== filter.type) {
|
|
1732
1953
|
return false;
|
|
1733
1954
|
}
|
|
1734
1955
|
return true;
|
|
1735
1956
|
}
|
|
1736
|
-
|
|
1957
|
+
matchesExposureFilter(session, filter) {
|
|
1958
|
+
return this.matchesExposureFilterByValues(session.type, session.status, filter);
|
|
1959
|
+
}
|
|
1960
|
+
async loadSessionLookupMap() {
|
|
1961
|
+
const out = new Map();
|
|
1962
|
+
for (const session of this.sessions.values()) {
|
|
1963
|
+
out.set(session.id, { live: session });
|
|
1964
|
+
}
|
|
1965
|
+
const persisted = await this.readPersistedSessions();
|
|
1966
|
+
for (const [id, record] of persisted.entries()) {
|
|
1967
|
+
const current = out.get(id);
|
|
1968
|
+
if (current) {
|
|
1969
|
+
current.persisted = record;
|
|
1970
|
+
}
|
|
1971
|
+
else {
|
|
1972
|
+
out.set(id, { persisted: record });
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return out;
|
|
1976
|
+
}
|
|
1977
|
+
resolveExposureSelection(query, lookupMap) {
|
|
1737
1978
|
const explicitIds = this.normalizeRequestedIds([
|
|
1738
1979
|
...(typeof query.id === "string" ? [query.id] : []),
|
|
1739
1980
|
...((query.ids ?? []).filter((value) => typeof value === "string")),
|
|
1740
1981
|
]);
|
|
1741
1982
|
const hasAll = explicitIds.includes("all");
|
|
1742
|
-
const
|
|
1983
|
+
const allIds = Array.from(lookupMap.keys());
|
|
1743
1984
|
if (hasAll) {
|
|
1744
|
-
const selectedIds =
|
|
1745
|
-
|
|
1746
|
-
|
|
1985
|
+
const selectedIds = allIds.filter((id) => {
|
|
1986
|
+
const lookup = lookupMap.get(id);
|
|
1987
|
+
if (!lookup) {
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
if (lookup.live) {
|
|
1991
|
+
return this.matchesExposureFilter(lookup.live, query.filter);
|
|
1992
|
+
}
|
|
1993
|
+
if (lookup.persisted) {
|
|
1994
|
+
return this.matchesExposureFilterByValues(lookup.persisted.type, lookup.persisted.status, query.filter);
|
|
1995
|
+
}
|
|
1996
|
+
return false;
|
|
1997
|
+
});
|
|
1747
1998
|
return { selectorUsed: true, selectedIds, missingIds: [] };
|
|
1748
1999
|
}
|
|
1749
2000
|
if (explicitIds.length > 0) {
|
|
1750
2001
|
const selectedIds = [];
|
|
1751
2002
|
const missingIds = [];
|
|
1752
2003
|
for (const id of explicitIds) {
|
|
1753
|
-
const
|
|
1754
|
-
if (!
|
|
2004
|
+
const lookup = lookupMap.get(id);
|
|
2005
|
+
if (!lookup) {
|
|
1755
2006
|
missingIds.push(id);
|
|
1756
2007
|
continue;
|
|
1757
2008
|
}
|
|
1758
|
-
if (this.matchesExposureFilter(
|
|
2009
|
+
if (lookup.live && this.matchesExposureFilter(lookup.live, query.filter)) {
|
|
2010
|
+
selectedIds.push(id);
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
if (lookup.persisted &&
|
|
2014
|
+
this.matchesExposureFilterByValues(lookup.persisted.type, lookup.persisted.status, query.filter)) {
|
|
1759
2015
|
selectedIds.push(id);
|
|
1760
2016
|
}
|
|
1761
2017
|
}
|
|
1762
2018
|
return { selectorUsed: true, selectedIds, missingIds };
|
|
1763
2019
|
}
|
|
1764
2020
|
if (query.filter) {
|
|
1765
|
-
const selectedIds =
|
|
1766
|
-
|
|
1767
|
-
|
|
2021
|
+
const selectedIds = allIds.filter((id) => {
|
|
2022
|
+
const lookup = lookupMap.get(id);
|
|
2023
|
+
if (!lookup) {
|
|
2024
|
+
return false;
|
|
2025
|
+
}
|
|
2026
|
+
if (lookup.live) {
|
|
2027
|
+
return this.matchesExposureFilter(lookup.live, query.filter);
|
|
2028
|
+
}
|
|
2029
|
+
if (lookup.persisted) {
|
|
2030
|
+
return this.matchesExposureFilterByValues(lookup.persisted.type, lookup.persisted.status, query.filter);
|
|
2031
|
+
}
|
|
2032
|
+
return false;
|
|
2033
|
+
});
|
|
1768
2034
|
return { selectorUsed: true, selectedIds, missingIds: [] };
|
|
1769
2035
|
}
|
|
1770
2036
|
return { selectorUsed: false, selectedIds: [], missingIds: [] };
|
|
@@ -1774,31 +2040,9 @@ export class CfshareManager {
|
|
|
1774
2040
|
const originAlive = session.type === "port"
|
|
1775
2041
|
? await probeLocalPort(session.sourcePort ?? session.originPort)
|
|
1776
2042
|
: Boolean(session.originServer?.listening);
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
const probeUrl = new URL(session.publicUrl);
|
|
1781
|
-
if (session.access.mode === "token" && session.access.token) {
|
|
1782
|
-
probeUrl.searchParams.set("token", session.access.token);
|
|
1783
|
-
}
|
|
1784
|
-
const controller = new AbortController();
|
|
1785
|
-
const timer = setTimeout(() => controller.abort(), DEFAULT_PROBE_TIMEOUT_MS);
|
|
1786
|
-
const headers = {};
|
|
1787
|
-
if (session.access.mode === "basic" && session.access.username && session.access.password) {
|
|
1788
|
-
headers.authorization = `Basic ${Buffer.from(`${session.access.username}:${session.access.password}`).toString("base64")}`;
|
|
1789
|
-
}
|
|
1790
|
-
const response = await fetch(probeUrl.toString(), {
|
|
1791
|
-
method: "HEAD",
|
|
1792
|
-
signal: controller.signal,
|
|
1793
|
-
headers,
|
|
1794
|
-
});
|
|
1795
|
-
clearTimeout(timer);
|
|
1796
|
-
publicProbe = { ok: response.ok, status: response.status };
|
|
1797
|
-
}
|
|
1798
|
-
catch (error) {
|
|
1799
|
-
publicProbe = { ok: false, error: String(error) };
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
2043
|
+
const publicProbe = opts?.probe_public
|
|
2044
|
+
? await this.probePublicEndpoint(session.publicUrl, session.access)
|
|
2045
|
+
: undefined;
|
|
1802
2046
|
const fileSharing = session.type === "files"
|
|
1803
2047
|
? {
|
|
1804
2048
|
mode: session.fileMode ?? "normal",
|
|
@@ -1837,6 +2081,78 @@ export class CfshareManager {
|
|
|
1837
2081
|
}
|
|
1838
2082
|
return detail;
|
|
1839
2083
|
}
|
|
2084
|
+
async probePublicEndpoint(publicUrl, access) {
|
|
2085
|
+
if (!publicUrl) {
|
|
2086
|
+
return undefined;
|
|
2087
|
+
}
|
|
2088
|
+
try {
|
|
2089
|
+
const probeUrl = new URL(publicUrl);
|
|
2090
|
+
if (access.mode === "token" && access.token) {
|
|
2091
|
+
probeUrl.searchParams.set("token", access.token);
|
|
2092
|
+
}
|
|
2093
|
+
const controller = new AbortController();
|
|
2094
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_PROBE_TIMEOUT_MS);
|
|
2095
|
+
const headers = {};
|
|
2096
|
+
if (access.mode === "basic" && access.username && access.password) {
|
|
2097
|
+
headers.authorization = `Basic ${Buffer.from(`${access.username}:${access.password}`).toString("base64")}`;
|
|
2098
|
+
}
|
|
2099
|
+
const response = await fetch(probeUrl.toString(), {
|
|
2100
|
+
method: "HEAD",
|
|
2101
|
+
signal: controller.signal,
|
|
2102
|
+
headers,
|
|
2103
|
+
});
|
|
2104
|
+
clearTimeout(timer);
|
|
2105
|
+
return { ok: response.ok, status: response.status };
|
|
2106
|
+
}
|
|
2107
|
+
catch (error) {
|
|
2108
|
+
return { ok: false, error: String(error) };
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
async buildPersistedExposureDetail(record, opts) {
|
|
2112
|
+
const tunnelAlive = this.isProcessAlive(record.processPid);
|
|
2113
|
+
const probePort = record.type === "port" ? (record.sourcePort ?? record.originPort) : record.originPort;
|
|
2114
|
+
const originAlive = probePort > 0 ? await probeLocalPort(probePort) : false;
|
|
2115
|
+
const publicProbe = opts?.probe_public
|
|
2116
|
+
? await this.probePublicEndpoint(record.publicUrl, record.access)
|
|
2117
|
+
: undefined;
|
|
2118
|
+
const fileSharing = record.type === "files"
|
|
2119
|
+
? {
|
|
2120
|
+
mode: record.fileMode ?? "normal",
|
|
2121
|
+
presentation: record.filePresentation ?? "download",
|
|
2122
|
+
}
|
|
2123
|
+
: undefined;
|
|
2124
|
+
const includeManifest = Boolean(opts?.include_manifest);
|
|
2125
|
+
const manifestBundle = record.type === "files" ? this.makeManifestResponse(record.manifest, opts?.manifest_limit) : undefined;
|
|
2126
|
+
const detail = {
|
|
2127
|
+
id: record.id,
|
|
2128
|
+
type: record.type,
|
|
2129
|
+
created_at: record.createdAt,
|
|
2130
|
+
status: {
|
|
2131
|
+
state: record.status,
|
|
2132
|
+
tunnel_alive: tunnelAlive,
|
|
2133
|
+
origin_alive: originAlive,
|
|
2134
|
+
public_probe: publicProbe,
|
|
2135
|
+
},
|
|
2136
|
+
port: {
|
|
2137
|
+
source_port: record.sourcePort,
|
|
2138
|
+
origin_port: record.originPort,
|
|
2139
|
+
tunnel_port: record.tunnelPort,
|
|
2140
|
+
},
|
|
2141
|
+
public_url: this.makeResponsePublicUrlFromValues(record.publicUrl, record.access),
|
|
2142
|
+
expires_at: record.expiresAt,
|
|
2143
|
+
local_url: record.localUrl,
|
|
2144
|
+
stats: record.stats,
|
|
2145
|
+
file_sharing: fileSharing,
|
|
2146
|
+
last_error: record.lastError,
|
|
2147
|
+
};
|
|
2148
|
+
if (manifestBundle) {
|
|
2149
|
+
detail.manifest_meta = manifestBundle.manifest_meta;
|
|
2150
|
+
if (includeManifest) {
|
|
2151
|
+
detail.manifest = manifestBundle.manifest;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
return detail;
|
|
2155
|
+
}
|
|
1840
2156
|
projectExposureDetail(detail, fields) {
|
|
1841
2157
|
if (!fields || fields.length === 0) {
|
|
1842
2158
|
return detail;
|
|
@@ -1864,8 +2180,39 @@ export class CfshareManager {
|
|
|
1864
2180
|
}
|
|
1865
2181
|
return out;
|
|
1866
2182
|
}
|
|
2183
|
+
async stopPersistedSession(record, opts, cleaned) {
|
|
2184
|
+
const uniquePids = Array.from(new Set([record.ownerPid, record.processPid].filter((pid) => Boolean(pid && pid > 0))));
|
|
2185
|
+
for (const pid of uniquePids) {
|
|
2186
|
+
try {
|
|
2187
|
+
process.kill(pid, 0);
|
|
2188
|
+
process.kill(pid, "SIGTERM");
|
|
2189
|
+
}
|
|
2190
|
+
catch (error) {
|
|
2191
|
+
const errno = error;
|
|
2192
|
+
if (errno?.code !== "ESRCH") {
|
|
2193
|
+
throw error;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
if (record.workspaceDir && (await fileExists(record.workspaceDir))) {
|
|
2198
|
+
await fs.rm(record.workspaceDir, { recursive: true, force: true });
|
|
2199
|
+
cleaned.push(record.workspaceDir);
|
|
2200
|
+
}
|
|
2201
|
+
await this.deletePersistedSessionRecord(record.id);
|
|
2202
|
+
await this.writeAudit({
|
|
2203
|
+
ts: nowIso(),
|
|
2204
|
+
event: opts?.expired ? "exposure_expired" : "exposure_stopped",
|
|
2205
|
+
id: record.id,
|
|
2206
|
+
type: record.type,
|
|
2207
|
+
details: {
|
|
2208
|
+
reason: opts?.reason,
|
|
2209
|
+
public_url: record.publicUrl,
|
|
2210
|
+
},
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
1867
2213
|
async exposureGet(params) {
|
|
1868
2214
|
await this.ensureInitialized();
|
|
2215
|
+
const lookupMap = await this.loadSessionLookupMap();
|
|
1869
2216
|
const fields = Array.isArray(params.fields) ? this.normalizeRequestedIds(params.fields) : undefined;
|
|
1870
2217
|
const typedFields = fields;
|
|
1871
2218
|
const legacySingle = Boolean(params.id) && params.id !== "all" && !params.ids && !params.filter && !params.fields;
|
|
@@ -1873,17 +2220,24 @@ export class CfshareManager {
|
|
|
1873
2220
|
id: params.id,
|
|
1874
2221
|
ids: params.ids,
|
|
1875
2222
|
filter: params.filter,
|
|
1876
|
-
});
|
|
2223
|
+
}, lookupMap);
|
|
1877
2224
|
if (!selection.selectorUsed) {
|
|
1878
2225
|
throw new Error("id, ids, or filter is required");
|
|
1879
2226
|
}
|
|
1880
2227
|
if (legacySingle) {
|
|
1881
2228
|
const legacyId = params.id;
|
|
1882
|
-
const
|
|
1883
|
-
if (!
|
|
2229
|
+
const lookup = lookupMap.get(legacyId);
|
|
2230
|
+
if (!lookup) {
|
|
1884
2231
|
return { id: legacyId, status: "not_found" };
|
|
1885
2232
|
}
|
|
1886
|
-
|
|
2233
|
+
if (lookup.live) {
|
|
2234
|
+
return await this.buildExposureDetail(lookup.live, {
|
|
2235
|
+
probe_public: params.opts?.probe_public,
|
|
2236
|
+
include_manifest: true,
|
|
2237
|
+
manifest_limit: MAX_RESPONSE_MANIFEST_ITEMS,
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
return await this.buildPersistedExposureDetail(lookup.persisted, {
|
|
1887
2241
|
probe_public: params.opts?.probe_public,
|
|
1888
2242
|
include_manifest: true,
|
|
1889
2243
|
manifest_limit: MAX_RESPONSE_MANIFEST_ITEMS,
|
|
@@ -1897,16 +2251,22 @@ export class CfshareManager {
|
|
|
1897
2251
|
const selectedIdsTruncated = selection.selectedIds.length > responseSelectedIds.length;
|
|
1898
2252
|
const items = [];
|
|
1899
2253
|
for (const id of responseSelectedIds) {
|
|
1900
|
-
const
|
|
1901
|
-
if (!
|
|
2254
|
+
const lookup = lookupMap.get(id);
|
|
2255
|
+
if (!lookup) {
|
|
1902
2256
|
items.push(this.makeExposureGetNotFound(id, typedFields));
|
|
1903
2257
|
continue;
|
|
1904
2258
|
}
|
|
1905
|
-
const detail =
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2259
|
+
const detail = lookup.live
|
|
2260
|
+
? await this.buildExposureDetail(lookup.live, {
|
|
2261
|
+
probe_public: params.opts?.probe_public,
|
|
2262
|
+
include_manifest: manifestRequested,
|
|
2263
|
+
manifest_limit: manifestLimit,
|
|
2264
|
+
})
|
|
2265
|
+
: await this.buildPersistedExposureDetail(lookup.persisted, {
|
|
2266
|
+
probe_public: params.opts?.probe_public,
|
|
2267
|
+
include_manifest: manifestRequested,
|
|
2268
|
+
manifest_limit: manifestLimit,
|
|
2269
|
+
});
|
|
1910
2270
|
items.push(this.projectExposureDetail(detail, typedFields));
|
|
1911
2271
|
}
|
|
1912
2272
|
for (const missingId of selection.missingIds) {
|
|
@@ -1925,6 +2285,7 @@ export class CfshareManager {
|
|
|
1925
2285
|
}
|
|
1926
2286
|
async stopExposure(idOrIds, opts) {
|
|
1927
2287
|
await this.ensureInitialized();
|
|
2288
|
+
const lookupMap = await this.loadSessionLookupMap();
|
|
1928
2289
|
const requested = this.normalizeRequestedIds(Array.isArray(idOrIds) ? idOrIds : [idOrIds]);
|
|
1929
2290
|
if (requested.length === 0) {
|
|
1930
2291
|
return { stopped: [], failed: [{ id: "unknown", error: "id or ids is required" }], cleaned: [] };
|
|
@@ -1933,14 +2294,14 @@ export class CfshareManager {
|
|
|
1933
2294
|
const ids = [];
|
|
1934
2295
|
const failed = [];
|
|
1935
2296
|
if (includeAll) {
|
|
1936
|
-
ids.push(...Array.from(
|
|
2297
|
+
ids.push(...Array.from(lookupMap.keys()));
|
|
1937
2298
|
if (ids.length === 0) {
|
|
1938
2299
|
return { stopped: [], failed: [{ id: "all", error: "not_found" }], cleaned: [] };
|
|
1939
2300
|
}
|
|
1940
2301
|
}
|
|
1941
2302
|
else {
|
|
1942
2303
|
for (const id of requested) {
|
|
1943
|
-
if (
|
|
2304
|
+
if (lookupMap.has(id)) {
|
|
1944
2305
|
ids.push(id);
|
|
1945
2306
|
}
|
|
1946
2307
|
else {
|
|
@@ -1955,43 +2316,53 @@ export class CfshareManager {
|
|
|
1955
2316
|
const stopped = [];
|
|
1956
2317
|
const cleaned = [];
|
|
1957
2318
|
for (const id of stopIds) {
|
|
1958
|
-
const
|
|
1959
|
-
if (!
|
|
2319
|
+
const lookup = lookupMap.get(id);
|
|
2320
|
+
if (!lookup) {
|
|
1960
2321
|
failed.push({ id, error: "not_found" });
|
|
1961
2322
|
continue;
|
|
1962
2323
|
}
|
|
2324
|
+
const session = lookup.live;
|
|
1963
2325
|
try {
|
|
1964
|
-
if (session
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
2326
|
+
if (session) {
|
|
2327
|
+
if (session.timeoutHandle) {
|
|
2328
|
+
clearTimeout(session.timeoutHandle);
|
|
2329
|
+
session.timeoutHandle = undefined;
|
|
2330
|
+
}
|
|
2331
|
+
await this.terminateProcess(session.process);
|
|
2332
|
+
if (session.proxyServer?.listening) {
|
|
2333
|
+
await new Promise((resolve) => session.proxyServer?.close(() => resolve()));
|
|
2334
|
+
}
|
|
2335
|
+
if (session.originServer?.listening) {
|
|
2336
|
+
await new Promise((resolve) => session.originServer?.close(() => resolve()));
|
|
2337
|
+
}
|
|
2338
|
+
if (session.workspaceDir && (await fileExists(session.workspaceDir))) {
|
|
2339
|
+
await fs.rm(session.workspaceDir, { recursive: true, force: true });
|
|
2340
|
+
cleaned.push(session.workspaceDir);
|
|
2341
|
+
}
|
|
2342
|
+
session.status = opts?.expired ? "expired" : "stopped";
|
|
2343
|
+
if (opts?.reason) {
|
|
2344
|
+
session.lastError = opts.reason;
|
|
2345
|
+
this.appendLog(session, "manager", `stop reason: ${opts.reason}`);
|
|
2346
|
+
}
|
|
2347
|
+
stopped.push(id);
|
|
2348
|
+
await this.writeAudit({
|
|
2349
|
+
ts: nowIso(),
|
|
2350
|
+
event: opts?.expired ? "exposure_expired" : "exposure_stopped",
|
|
2351
|
+
id: session.id,
|
|
2352
|
+
type: session.type,
|
|
2353
|
+
details: {
|
|
2354
|
+
reason: opts?.reason,
|
|
2355
|
+
public_url: session.publicUrl,
|
|
2356
|
+
},
|
|
2357
|
+
});
|
|
1974
2358
|
}
|
|
1975
|
-
if (
|
|
1976
|
-
await
|
|
1977
|
-
|
|
2359
|
+
else if (lookup.persisted) {
|
|
2360
|
+
await this.stopPersistedSession(lookup.persisted, opts, cleaned);
|
|
2361
|
+
stopped.push(id);
|
|
1978
2362
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
session.lastError = opts.reason;
|
|
1982
|
-
this.appendLog(session, "manager", `stop reason: ${opts.reason}`);
|
|
2363
|
+
else {
|
|
2364
|
+
failed.push({ id, error: "not_found" });
|
|
1983
2365
|
}
|
|
1984
|
-
stopped.push(id);
|
|
1985
|
-
await this.writeAudit({
|
|
1986
|
-
ts: nowIso(),
|
|
1987
|
-
event: opts?.expired ? "exposure_expired" : "exposure_stopped",
|
|
1988
|
-
id: session.id,
|
|
1989
|
-
type: session.type,
|
|
1990
|
-
details: {
|
|
1991
|
-
reason: opts?.reason,
|
|
1992
|
-
public_url: session.publicUrl,
|
|
1993
|
-
},
|
|
1994
|
-
});
|
|
1995
2366
|
}
|
|
1996
2367
|
catch (error) {
|
|
1997
2368
|
failed.push({ id, error: String(error) });
|
|
@@ -2143,9 +2514,11 @@ export class CfshareManager {
|
|
|
2143
2514
|
async runGc() {
|
|
2144
2515
|
const removedWorkspaces = [];
|
|
2145
2516
|
const killedPids = [];
|
|
2146
|
-
const
|
|
2147
|
-
|
|
2148
|
-
.
|
|
2517
|
+
const persisted = await this.readPersistedSessions();
|
|
2518
|
+
const activeWorkspaces = new Set([
|
|
2519
|
+
...Array.from(this.sessions.values()).map((session) => session.workspaceDir),
|
|
2520
|
+
...Array.from(persisted.values()).map((session) => session.workspaceDir),
|
|
2521
|
+
].filter((entry) => Boolean(entry)));
|
|
2149
2522
|
const workspaces = await fs.readdir(this.workspaceRoot, { withFileTypes: true }).catch(() => []);
|
|
2150
2523
|
for (const entry of workspaces) {
|
|
2151
2524
|
if (!entry.isDirectory()) {
|
|
@@ -2158,26 +2531,22 @@ export class CfshareManager {
|
|
|
2158
2531
|
await fs.rm(abs, { recursive: true, force: true });
|
|
2159
2532
|
removedWorkspaces.push(abs);
|
|
2160
2533
|
}
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
}
|
|
2534
|
+
for (const record of persisted.values()) {
|
|
2535
|
+
if (this.sessions.has(record.id)) {
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
const pids = Array.from(new Set([record.ownerPid, record.processPid].filter((pid) => Boolean(pid && pid > 0))));
|
|
2539
|
+
for (const pid of pids) {
|
|
2168
2540
|
try {
|
|
2169
|
-
process.kill(
|
|
2170
|
-
process.kill(
|
|
2171
|
-
killedPids.push(
|
|
2541
|
+
process.kill(pid, 0);
|
|
2542
|
+
process.kill(pid, "SIGTERM");
|
|
2543
|
+
killedPids.push(pid);
|
|
2172
2544
|
}
|
|
2173
2545
|
catch {
|
|
2174
|
-
// ignore
|
|
2546
|
+
// ignore dead pid
|
|
2175
2547
|
}
|
|
2176
2548
|
}
|
|
2177
2549
|
}
|
|
2178
|
-
catch {
|
|
2179
|
-
// ignore missing snapshot
|
|
2180
|
-
}
|
|
2181
2550
|
await this.persistSessionsSnapshot();
|
|
2182
2551
|
await this.writeAudit({
|
|
2183
2552
|
ts: nowIso(),
|