repoburg 1.3.217 → 1.3.219
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/agent-pane/frontend/dist/assets/index-BUkOj2Cb.js +392 -0
- package/agent-pane/frontend/dist/assets/index-gAFaOmjN.css +1 -0
- package/agent-pane/frontend/dist/index.html +19 -0
- package/daemon/dist/agentPane.js +13 -8
- package/daemon/dist/auth/authManager.js +9 -0
- package/daemon/dist/index.js +32 -5
- package/daemon/dist/middleware/checkRemoteAuth.js +72 -0
- package/daemon/dist/middleware/checkRemoteAuth.spec.js +117 -0
- package/daemon/dist/middleware/corsOrigin.js +53 -0
- package/daemon/dist/middleware/corsOrigin.spec.js +46 -0
- package/daemon/dist/remote/access-jwt.js +67 -0
- package/daemon/dist/remote/access-jwt.spec.js +94 -0
- package/daemon/dist/remote/cloudflared.js +239 -0
- package/daemon/dist/remote/cloudflared.spec.js +117 -0
- package/daemon/dist/remote/orchestrator.js +245 -0
- package/daemon/dist/remote/orchestrator.spec.js +192 -0
- package/daemon/dist/remote/probes.js +59 -0
- package/daemon/dist/remote/provisioner.js +86 -0
- package/daemon/dist/remote/provisioner.spec.js +104 -0
- package/daemon/dist/remote/routes.js +155 -0
- package/daemon/dist/remote/routes.spec.js +245 -0
- package/daemon/dist/remote/tunnel-state.js +54 -0
- package/daemon/dist/remote/tunnel-state.spec.js +40 -0
- package/daemon/dist/staticServing.js +44 -0
- package/daemon/dist/staticServing.spec.js +85 -0
- package/package.json +2 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const access_jwt_1 = require("./access-jwt");
|
|
37
|
+
const jose = __importStar(require("jose"));
|
|
38
|
+
/**
|
|
39
|
+
* Real-crypto tests: sign actual Access-style JWTs with an injected HMAC key
|
|
40
|
+
* and verify the email-pinning logic on top of signature/iss/aud checks.
|
|
41
|
+
*/
|
|
42
|
+
const SECRET = new TextEncoder().encode('test-secret-for-specs-only');
|
|
43
|
+
const CONFIG = { teamDomain: 'team.cloudflareaccess.com', aud: 'aud-tag' };
|
|
44
|
+
const jwks = (async () => SECRET);
|
|
45
|
+
async function makeAssertion(claims) {
|
|
46
|
+
return new jose.SignJWT(claims)
|
|
47
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
48
|
+
.setIssuer('https://team.cloudflareaccess.com')
|
|
49
|
+
.setAudience('aud-tag')
|
|
50
|
+
.setExpirationTime('2h')
|
|
51
|
+
.sign(SECRET);
|
|
52
|
+
}
|
|
53
|
+
describe('verifyAccessJwt', () => {
|
|
54
|
+
it('accepts a valid token signed by the team', async () => {
|
|
55
|
+
const assertion = await makeAssertion({ common_name: 'owner@repoburg.com' });
|
|
56
|
+
await expect((0, access_jwt_1.verifyAccessJwt)(assertion, CONFIG, { jwks })).resolves.toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
it('accepts any identity when no owner email is pinned (optional check)', async () => {
|
|
59
|
+
const assertion = await makeAssertion({ common_name: 'someone-else@evil.com' });
|
|
60
|
+
await expect((0, access_jwt_1.verifyAccessJwt)(assertion, CONFIG, { jwks })).resolves.toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
it('rejects a wrong-email token when the owner email is pinned', async () => {
|
|
63
|
+
const assertion = await makeAssertion({ common_name: 'hacker@evil.com' });
|
|
64
|
+
await expect((0, access_jwt_1.verifyAccessJwt)(assertion, CONFIG, { jwks, ownerEmail: 'owner@repoburg.com' })).rejects.toThrow(/does not match the machine owner/);
|
|
65
|
+
});
|
|
66
|
+
it('accepts the pinned owner email', async () => {
|
|
67
|
+
const assertion = await makeAssertion({ common_name: 'owner@repoburg.com' });
|
|
68
|
+
await expect((0, access_jwt_1.verifyAccessJwt)(assertion, CONFIG, { jwks, ownerEmail: 'owner@repoburg.com' })).resolves.toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
it('matches the pinned owner email via the email claim (real CF Access format)', async () => {
|
|
71
|
+
// Live-verified 2026-09: CF Access JWTs carry `email`, NOT `common_name`.
|
|
72
|
+
const assertion = await makeAssertion({ email: 'owner@repoburg.com' });
|
|
73
|
+
await expect((0, access_jwt_1.verifyAccessJwt)(assertion, CONFIG, { jwks, ownerEmail: 'owner@repoburg.com' })).resolves.toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
it('rejects a wrong email claim when the owner email is pinned', async () => {
|
|
76
|
+
const assertion = await makeAssertion({ email: 'hacker@evil.com' });
|
|
77
|
+
await expect((0, access_jwt_1.verifyAccessJwt)(assertion, CONFIG, { jwks, ownerEmail: 'owner@repoburg.com' })).rejects.toThrow(/does not match the machine owner/);
|
|
78
|
+
});
|
|
79
|
+
it('rejects a token with neither email nor common_name when pinned', async () => {
|
|
80
|
+
const assertion = await makeAssertion({ sub: '99dd1943-5978-589e-9dd0-884f0c79e926' });
|
|
81
|
+
await expect((0, access_jwt_1.verifyAccessJwt)(assertion, CONFIG, { jwks, ownerEmail: 'owner@repoburg.com' })).rejects.toThrow(/does not match the machine owner/);
|
|
82
|
+
});
|
|
83
|
+
it('rejects tokens with a wrong issuer or audience', async () => {
|
|
84
|
+
const wrongIss = await new jose.SignJWT({ common_name: 'owner@repoburg.com' })
|
|
85
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
86
|
+
.setIssuer('https://other-team.cloudflareaccess.com')
|
|
87
|
+
.setAudience('aud-tag')
|
|
88
|
+
.setExpirationTime('2h')
|
|
89
|
+
.sign(SECRET);
|
|
90
|
+
await expect((0, access_jwt_1.verifyAccessJwt)(wrongIss, CONFIG, { jwks })).rejects.toThrow();
|
|
91
|
+
const wrongAud = await makeAssertion({ common_name: 'owner@repoburg.com' });
|
|
92
|
+
await expect((0, access_jwt_1.verifyAccessJwt)(wrongAud, { teamDomain: 'team.cloudflareaccess.com', aud: 'other-aud' }, { jwks })).rejects.toThrow();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.defaultExecutor = exports.CLOUDFLARED_METRICS_PORT = void 0;
|
|
7
|
+
exports.detectPlatform = detectPlatform;
|
|
8
|
+
exports.assetName = assetName;
|
|
9
|
+
exports.installDir = installDir;
|
|
10
|
+
exports.cloudflaredBinaryPath = cloudflaredBinaryPath;
|
|
11
|
+
exports.isInstalled = isInstalled;
|
|
12
|
+
exports.selectChecksum = selectChecksum;
|
|
13
|
+
exports.downloadCloudflared = downloadCloudflared;
|
|
14
|
+
exports.cloudflaredMetricsUrl = cloudflaredMetricsUrl;
|
|
15
|
+
exports.systemdUnitContent = systemdUnitContent;
|
|
16
|
+
exports.launchdPlistContent = launchdPlistContent;
|
|
17
|
+
exports.systemdUnitPath = systemdUnitPath;
|
|
18
|
+
exports.launchdPlistPath = launchdPlistPath;
|
|
19
|
+
exports.installService = installService;
|
|
20
|
+
exports.removeService = removeService;
|
|
21
|
+
exports.isServiceRunning = isServiceRunning;
|
|
22
|
+
/**
|
|
23
|
+
* cloudflared installer + system service management (user-level, no root).
|
|
24
|
+
*
|
|
25
|
+
* Download spec: latest GitHub release asset per platform, verified against
|
|
26
|
+
* the release's checksums.txt. macOS binaries are shipped as .tgz; the
|
|
27
|
+
* fallback handles both plain and archived assets.
|
|
28
|
+
*/
|
|
29
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
30
|
+
const path_1 = __importDefault(require("path"));
|
|
31
|
+
const os_1 = __importDefault(require("os"));
|
|
32
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
33
|
+
const child_process_1 = require("child_process");
|
|
34
|
+
const util_1 = require("util");
|
|
35
|
+
const exec = (0, util_1.promisify)(child_process_1.execFile);
|
|
36
|
+
const RELEASE_BASE = 'https://github.com/cloudflare/cloudflared/releases/latest/download';
|
|
37
|
+
function detectPlatform() {
|
|
38
|
+
const platform = os_1.default.platform();
|
|
39
|
+
const arch = os_1.default.arch();
|
|
40
|
+
if (platform !== 'linux' && platform !== 'darwin') {
|
|
41
|
+
throw new Error(`Unsupported platform for cloudflared: ${platform}`);
|
|
42
|
+
}
|
|
43
|
+
if (arch !== 'x64' && arch !== 'arm64') {
|
|
44
|
+
throw new Error(`Unsupported architecture for cloudflared: ${arch}`);
|
|
45
|
+
}
|
|
46
|
+
return { platform, arch };
|
|
47
|
+
}
|
|
48
|
+
/** Release asset name for a platform (plain binary). */
|
|
49
|
+
function assetName({ platform, arch }) {
|
|
50
|
+
const osPart = platform === 'darwin' ? 'darwin' : 'linux';
|
|
51
|
+
const archPart = arch === 'x64' ? 'amd64' : 'arm64';
|
|
52
|
+
return `cloudflared-${osPart}-${archPart}`;
|
|
53
|
+
}
|
|
54
|
+
function installDir(home = os_1.default.homedir()) {
|
|
55
|
+
return path_1.default.join(home, '.repoburg', 'bin');
|
|
56
|
+
}
|
|
57
|
+
function cloudflaredBinaryPath(home = os_1.default.homedir()) {
|
|
58
|
+
return path_1.default.join(installDir(home), 'cloudflared');
|
|
59
|
+
}
|
|
60
|
+
function isInstalled(home = os_1.default.homedir()) {
|
|
61
|
+
return fs_extra_1.default.existsSync(cloudflaredBinaryPath(home));
|
|
62
|
+
}
|
|
63
|
+
/** Pick the sha256 from the checksums.txt content for an asset. */
|
|
64
|
+
function selectChecksum(checksumsTxt, asset) {
|
|
65
|
+
const line = checksumsTxt.split('\n').find((entry) => entry.trim().endsWith(asset));
|
|
66
|
+
const match = line?.match(/^([0-9a-f]{64})\s+\S+$/);
|
|
67
|
+
return match ? match[1] : null;
|
|
68
|
+
}
|
|
69
|
+
async function fetchText(url) {
|
|
70
|
+
const response = await fetch(url);
|
|
71
|
+
if (!response.ok)
|
|
72
|
+
throw new Error(`Download failed (${response.status}): ${url}`);
|
|
73
|
+
return response.text();
|
|
74
|
+
}
|
|
75
|
+
async function fetchBuffer(url) {
|
|
76
|
+
const response = await fetch(url);
|
|
77
|
+
if (!response.ok)
|
|
78
|
+
throw new Error(`Download failed (${response.status}): ${url}`);
|
|
79
|
+
return Buffer.from(await response.arrayBuffer());
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Download cloudflared into ~/.repoburg/bin, sha256-verified.
|
|
83
|
+
* Throws when the checksum does not match — never install an unverified binary.
|
|
84
|
+
*/
|
|
85
|
+
async function downloadCloudflared(options = {}) {
|
|
86
|
+
const home = options.home ?? os_1.default.homedir();
|
|
87
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
88
|
+
const platformInfo = options.platform ?? detectPlatform();
|
|
89
|
+
const asset = assetName(platformInfo);
|
|
90
|
+
const checksumsTxt = await (async (url) => {
|
|
91
|
+
const response = await fetchImpl(url);
|
|
92
|
+
if (!response.ok)
|
|
93
|
+
throw new Error(`Checksums download failed (${response.status})`);
|
|
94
|
+
return response.text();
|
|
95
|
+
})(`${RELEASE_BASE}/cloudflared_checksums.txt`);
|
|
96
|
+
const expected = selectChecksum(checksumsTxt, asset);
|
|
97
|
+
if (!expected)
|
|
98
|
+
throw new Error(`No checksum found for asset ${asset}`);
|
|
99
|
+
const dir = installDir(home);
|
|
100
|
+
fs_extra_1.default.ensureDirSync(dir);
|
|
101
|
+
let binary;
|
|
102
|
+
const binaryUrl = `${RELEASE_BASE}/${asset}`;
|
|
103
|
+
const binaryResponse = await fetchImpl(binaryUrl);
|
|
104
|
+
if (binaryResponse.ok) {
|
|
105
|
+
binary = Buffer.from(await binaryResponse.arrayBuffer());
|
|
106
|
+
}
|
|
107
|
+
else if (platformInfo.platform === 'darwin') {
|
|
108
|
+
// macOS releases ship as .tgz; extract with the system tar.
|
|
109
|
+
const tgz = Buffer.from(await (await fetchImpl(`${binaryUrl}.tgz`)).arrayBuffer());
|
|
110
|
+
const tgzPath = path_1.default.join(dir, `${asset}.tgz`);
|
|
111
|
+
fs_extra_1.default.writeFileSync(tgzPath, tgz);
|
|
112
|
+
await exec('tar', ['-xzf', tgzPath, '-C', dir]);
|
|
113
|
+
fs_extra_1.default.removeSync(tgzPath);
|
|
114
|
+
const binaryPath = cloudflaredBinaryPath(home);
|
|
115
|
+
if (!fs_extra_1.default.existsSync(binaryPath))
|
|
116
|
+
throw new Error('cloudflared extraction produced no binary');
|
|
117
|
+
return binaryPath;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
throw new Error(`Binary download failed (${binaryResponse.status})`);
|
|
121
|
+
}
|
|
122
|
+
const actual = crypto_1.default.createHash('sha256').update(binary).digest('hex');
|
|
123
|
+
if (actual !== expected) {
|
|
124
|
+
throw new Error(`Checksum mismatch for ${asset}: expected ${expected}, got ${actual}`);
|
|
125
|
+
}
|
|
126
|
+
const binaryPath = cloudflaredBinaryPath(home);
|
|
127
|
+
fs_extra_1.default.writeFileSync(binaryPath, binary);
|
|
128
|
+
fs_extra_1.default.chmodSync(binaryPath, 0o755);
|
|
129
|
+
return binaryPath;
|
|
130
|
+
}
|
|
131
|
+
// --- System service files (user-level, no root required) ---
|
|
132
|
+
const SERVICE_NAME_LINUX = 'repoburg-cloudflared';
|
|
133
|
+
const SERVICE_ID_DARWIN = 'com.repoburg.cloudflared';
|
|
134
|
+
/** Loopback metrics endpoint (tunnel connectivity is probed from here). */
|
|
135
|
+
exports.CLOUDFLARED_METRICS_PORT = 20241;
|
|
136
|
+
function cloudflaredMetricsUrl() {
|
|
137
|
+
return `http://127.0.0.1:${exports.CLOUDFLARED_METRICS_PORT}/metrics`;
|
|
138
|
+
}
|
|
139
|
+
function systemdUnitContent(binaryPath, tunnelToken) {
|
|
140
|
+
return ` [Unit]
|
|
141
|
+
Description=Repoburg remote access tunnel (cloudflared)
|
|
142
|
+
After=network-online.target
|
|
143
|
+
|
|
144
|
+
[Service]
|
|
145
|
+
ExecStart=${binaryPath} tunnel --metrics 127.0.0.1:${exports.CLOUDFLARED_METRICS_PORT} run --token ${tunnelToken}
|
|
146
|
+
Restart=always
|
|
147
|
+
RestartSec=5
|
|
148
|
+
|
|
149
|
+
[Install]
|
|
150
|
+
WantedBy=default.target
|
|
151
|
+
`.trimStart();
|
|
152
|
+
}
|
|
153
|
+
function launchdPlistContent(binaryPath, tunnelToken) {
|
|
154
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
155
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
156
|
+
<plist version="1.0">
|
|
157
|
+
<dict>
|
|
158
|
+
<key>Label</key><string>${SERVICE_ID_DARWIN}</string>
|
|
159
|
+
<key>ProgramArguments</key>
|
|
160
|
+
<array>
|
|
161
|
+
<string>${binaryPath}</string>
|
|
162
|
+
<string>tunnel</string>
|
|
163
|
+
<string>--metrics</string>
|
|
164
|
+
<string>127.0.0.1:${exports.CLOUDFLARED_METRICS_PORT}</string>
|
|
165
|
+
<string>run</string>
|
|
166
|
+
<string>--token</string>
|
|
167
|
+
<string>${tunnelToken}</string>
|
|
168
|
+
</array>
|
|
169
|
+
<key>RunAtLoad</key><true/>
|
|
170
|
+
<key>KeepAlive</key><true/>
|
|
171
|
+
<key>StandardOutPath</key><string>${installDir()}/cloudflared.log</string>
|
|
172
|
+
<key>StandardErrorPath</key><string>${installDir()}/cloudflared.log</string>
|
|
173
|
+
</dict>
|
|
174
|
+
</plist>
|
|
175
|
+
`;
|
|
176
|
+
}
|
|
177
|
+
function systemdUnitPath(home = os_1.default.homedir()) {
|
|
178
|
+
return path_1.default.join(home, '.config', 'systemd', 'user', `${SERVICE_NAME_LINUX}.service`);
|
|
179
|
+
}
|
|
180
|
+
function launchdPlistPath(home = os_1.default.homedir()) {
|
|
181
|
+
return path_1.default.join(home, 'Library', 'LaunchAgents', `${SERVICE_ID_DARWIN}.plist`);
|
|
182
|
+
}
|
|
183
|
+
exports.defaultExecutor = exec;
|
|
184
|
+
/** Write the service file and enable it (idempotent). Returns the service file path. */
|
|
185
|
+
async function installService(tunnelToken, options = {}) {
|
|
186
|
+
const home = options.home ?? os_1.default.homedir();
|
|
187
|
+
const platform = options.platform ?? os_1.default.platform();
|
|
188
|
+
const executor = options.executor ?? exports.defaultExecutor;
|
|
189
|
+
const binaryPath = cloudflaredBinaryPath(home);
|
|
190
|
+
if (platform === 'darwin') {
|
|
191
|
+
const plistPath = launchdPlistPath(home);
|
|
192
|
+
fs_extra_1.default.ensureDirSync(path_1.default.dirname(plistPath));
|
|
193
|
+
fs_extra_1.default.writeFileSync(plistPath, launchdPlistContent(binaryPath, tunnelToken));
|
|
194
|
+
await executor('launchctl', ['unload', plistPath]).catch(() => undefined);
|
|
195
|
+
await executor('launchctl', ['load', plistPath]);
|
|
196
|
+
return plistPath;
|
|
197
|
+
}
|
|
198
|
+
const unitPath = systemdUnitPath(home);
|
|
199
|
+
fs_extra_1.default.ensureDirSync(path_1.default.dirname(unitPath));
|
|
200
|
+
fs_extra_1.default.writeFileSync(unitPath, systemdUnitContent(binaryPath, tunnelToken));
|
|
201
|
+
await executor('systemctl', ['--user', 'daemon-reload']);
|
|
202
|
+
await executor('systemctl', ['--user', 'enable', '--now', SERVICE_NAME_LINUX]);
|
|
203
|
+
return unitPath;
|
|
204
|
+
}
|
|
205
|
+
async function removeService(options = {}) {
|
|
206
|
+
const home = options.home ?? os_1.default.homedir();
|
|
207
|
+
const platform = options.platform ?? os_1.default.platform();
|
|
208
|
+
const executor = options.executor ?? exports.defaultExecutor;
|
|
209
|
+
if (platform === 'darwin') {
|
|
210
|
+
const plistPath = launchdPlistPath(home);
|
|
211
|
+
if (fs_extra_1.default.existsSync(plistPath)) {
|
|
212
|
+
await executor('launchctl', ['unload', plistPath]).catch(() => undefined);
|
|
213
|
+
fs_extra_1.default.removeSync(plistPath);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await executor('systemctl', ['--user', 'disable', '--now', SERVICE_NAME_LINUX]).catch(() => undefined);
|
|
218
|
+
fs_extra_1.default.removeSync(systemdUnitPath(home));
|
|
219
|
+
}
|
|
220
|
+
async function isServiceRunning(options = {}) {
|
|
221
|
+
const platform = options.platform ?? os_1.default.platform();
|
|
222
|
+
const executor = options.executor ?? exports.defaultExecutor;
|
|
223
|
+
if (platform === 'darwin') {
|
|
224
|
+
try {
|
|
225
|
+
const { stdout } = await executor('launchctl', ['list']);
|
|
226
|
+
return stdout.includes(SERVICE_ID_DARWIN);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const { stdout } = await executor('systemctl', ['--user', 'is-active', SERVICE_NAME_LINUX]);
|
|
234
|
+
return stdout.trim() === 'active';
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const cloudflared_1 = require("./cloudflared");
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const tmpHome = fs_extra_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'cloudflared-'));
|
|
11
|
+
describe('cloudflared', () => {
|
|
12
|
+
describe('assetName', () => {
|
|
13
|
+
it('maps platforms to release asset names', () => {
|
|
14
|
+
expect((0, cloudflared_1.assetName)({ platform: 'darwin', arch: 'x64' })).toBe('cloudflared-darwin-amd64');
|
|
15
|
+
expect((0, cloudflared_1.assetName)({ platform: 'darwin', arch: 'arm64' })).toBe('cloudflared-darwin-arm64');
|
|
16
|
+
expect((0, cloudflared_1.assetName)({ platform: 'linux', arch: 'x64' })).toBe('cloudflared-linux-amd64');
|
|
17
|
+
expect((0, cloudflared_1.assetName)({ platform: 'linux', arch: 'arm64' })).toBe('cloudflared-linux-arm64');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('selectChecksum', () => {
|
|
21
|
+
it('finds the sha256 for an asset in checksums.txt', () => {
|
|
22
|
+
const txt = [
|
|
23
|
+
`${'a'.repeat(64)} cloudflared-linux-amd64`,
|
|
24
|
+
`${'b'.repeat(64)} cloudflared-darwin-arm64`,
|
|
25
|
+
].join('\n');
|
|
26
|
+
expect((0, cloudflared_1.selectChecksum)(txt, 'cloudflared-darwin-arm64')).toBe('b'.repeat(64));
|
|
27
|
+
});
|
|
28
|
+
it('returns null for unknown assets', () => {
|
|
29
|
+
expect((0, cloudflared_1.selectChecksum)('aaa111 other-binary', 'cloudflared-linux-amd64')).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('service file content', () => {
|
|
33
|
+
it('builds a systemd unit running the tunnel with the token and metrics flag', () => {
|
|
34
|
+
const content = (0, cloudflared_1.systemdUnitContent)('/bin/cloudflared', 'TOKEN123');
|
|
35
|
+
// --metrics is a TUNNEL-level flag — it must come BEFORE the run
|
|
36
|
+
// subcommand (verified live: `tunnel run --metrics` is rejected).
|
|
37
|
+
expect(content).toContain('ExecStart=/bin/cloudflared tunnel --metrics 127.0.0.1:20241 run --token TOKEN123');
|
|
38
|
+
expect(content).toContain('WantedBy=default.target');
|
|
39
|
+
});
|
|
40
|
+
it('builds a launchd plist running the tunnel with the token and metrics flag', () => {
|
|
41
|
+
const content = (0, cloudflared_1.launchdPlistContent)('/bin/cloudflared', 'TOKEN123');
|
|
42
|
+
const args = content.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('<string>'));
|
|
43
|
+
// Metrics flag must precede the `run` subcommand.
|
|
44
|
+
expect(args.indexOf('<string>tunnel</string>')).toBeLessThan(args.indexOf('<string>run</string>'));
|
|
45
|
+
expect(content).toContain('<string>--metrics</string>');
|
|
46
|
+
expect(content).toContain('<string>127.0.0.1:20241</string>');
|
|
47
|
+
expect(content).toContain('<string>--token</string>');
|
|
48
|
+
expect(content).toContain('<string>TOKEN123</string>');
|
|
49
|
+
expect(content).toContain('<key>KeepAlive</key><true/>');
|
|
50
|
+
});
|
|
51
|
+
it('exposes the metrics URL on loopback', () => {
|
|
52
|
+
expect((0, cloudflared_1.cloudflaredMetricsUrl)()).toBe('http://127.0.0.1:20241/metrics');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('downloadCloudflared', () => {
|
|
56
|
+
const linux = { platform: 'linux', arch: 'x64' };
|
|
57
|
+
const makeFetch = (sha) => jest.fn(async (url) => {
|
|
58
|
+
const href = String(url);
|
|
59
|
+
if (href.endsWith('cloudflared_checksums.txt')) {
|
|
60
|
+
return { ok: true, text: async () => `${sha} cloudflared-linux-amd64`, status: 200 };
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, arrayBuffer: async () => new Uint8Array([1, 2, 3, 4]), status: 200 };
|
|
63
|
+
});
|
|
64
|
+
it('downloads, verifies the checksum, and writes the binary', async () => {
|
|
65
|
+
const sha = require('crypto').createHash('sha256').update(Buffer.from([1, 2, 3, 4])).digest('hex');
|
|
66
|
+
const binaryPath = await (0, cloudflared_1.downloadCloudflared)({ home: tmpHome, platform: linux, fetchImpl: makeFetch(sha) });
|
|
67
|
+
expect(binaryPath).toBe((0, cloudflared_1.cloudflaredBinaryPath)(tmpHome));
|
|
68
|
+
expect(fs_extra_1.default.existsSync(binaryPath)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
it('rejects a binary whose checksum does not match', async () => {
|
|
71
|
+
await expect((0, cloudflared_1.downloadCloudflared)({ home: tmpHome, platform: linux, fetchImpl: makeFetch('0'.repeat(64)) })).rejects.toThrow(/Checksum mismatch/);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('system service', () => {
|
|
75
|
+
const calls = [];
|
|
76
|
+
const executor = jest.fn(async (cmd, args) => {
|
|
77
|
+
calls.push([cmd, ...args]);
|
|
78
|
+
return { stdout: 'active\n' };
|
|
79
|
+
});
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
calls.length = 0;
|
|
82
|
+
executor.mockClear();
|
|
83
|
+
});
|
|
84
|
+
it('writes a systemd unit and enables it on linux', async () => {
|
|
85
|
+
const unitPath = await (0, cloudflared_1.installService)('TOKEN', {
|
|
86
|
+
home: tmpHome,
|
|
87
|
+
platform: 'linux',
|
|
88
|
+
executor: executor,
|
|
89
|
+
});
|
|
90
|
+
expect(unitPath).toBe((0, cloudflared_1.systemdUnitPath)(tmpHome));
|
|
91
|
+
expect(fs_extra_1.default.existsSync(unitPath)).toBe(true);
|
|
92
|
+
expect(calls[0]).toEqual(['systemctl', '--user', 'daemon-reload']);
|
|
93
|
+
expect(calls[1]).toEqual(['systemctl', '--user', 'enable', '--now', 'repoburg-cloudflared']);
|
|
94
|
+
});
|
|
95
|
+
it('writes a launchd plist and loads it on darwin', async () => {
|
|
96
|
+
const plistPath = await (0, cloudflared_1.installService)('TOKEN', {
|
|
97
|
+
home: tmpHome,
|
|
98
|
+
platform: 'darwin',
|
|
99
|
+
executor: executor,
|
|
100
|
+
});
|
|
101
|
+
expect(plistPath).toBe((0, cloudflared_1.launchdPlistPath)(tmpHome));
|
|
102
|
+
expect(fs_extra_1.default.existsSync(plistPath)).toBe(true);
|
|
103
|
+
expect(calls.at(-1)).toEqual(['launchctl', 'load', plistPath]);
|
|
104
|
+
});
|
|
105
|
+
it('reports the systemd service as running when active', async () => {
|
|
106
|
+
executor.mockResolvedValue({ stdout: 'active\n' });
|
|
107
|
+
await expect((0, cloudflared_1.isServiceRunning)({ platform: 'linux', executor: executor })).resolves.toBe(true);
|
|
108
|
+
executor.mockResolvedValue({ stdout: 'inactive\n' });
|
|
109
|
+
await expect((0, cloudflared_1.isServiceRunning)({ platform: 'linux', executor: executor })).resolves.toBe(false);
|
|
110
|
+
});
|
|
111
|
+
it('removes the systemd unit on linux', async () => {
|
|
112
|
+
await (0, cloudflared_1.installService)('TOKEN', { home: tmpHome, platform: 'linux', executor: executor });
|
|
113
|
+
await (0, cloudflared_1.removeService)({ home: tmpHome, platform: 'linux', executor: executor });
|
|
114
|
+
expect(fs_extra_1.default.existsSync((0, cloudflared_1.systemdUnitPath)(tmpHome))).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|