Skip to content

Commit 39dc3bd

Browse files
committed
feat: service api on macOS
1 parent cb3309d commit 39dc3bd

File tree

2 files changed

+203
-0
lines changed

2 files changed

+203
-0
lines changed

src/lib.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
export { bin } from "./constants.js";
22
export { install } from "./install.js";
33
export { tunnel } from "./tunnel.js";
4+
export {
5+
service,
6+
identifier,
7+
MACOS_SERVICE_PATH,
8+
AlreadyInstalledError,
9+
NotInstalledError,
10+
} from "./service.js";
411
export type { Connection } from "./types.js";

src/service.ts

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import os from "node:os";
2+
import fs from "node:fs";
3+
import { spawnSync } from "node:child_process";
4+
import { bin } from "./constants.js";
5+
import { Connection } from "./types.js";
6+
7+
/**
8+
* Cloudflared launchd identifier.
9+
*/
10+
export const identifier = "com.cloudflare.cloudflared";
11+
12+
/**
13+
* Path of service related files.
14+
*/
15+
export const MACOS_SERVICE_PATH = {
16+
PLIST: is_root()
17+
? `/Library/LaunchDaemons/${identifier}.plist`
18+
: `${os.homedir()}/Library/LaunchAgents/${identifier}.plist`,
19+
OUT: is_root()
20+
? `/Library/Logs/${identifier}.out.log`
21+
: `${os.homedir()}/Library/Logs/${identifier}.out.log`,
22+
ERR: is_root()
23+
? `/Library/Logs/${identifier}.err.log`
24+
: `${os.homedir()}/Library/Logs/${identifier}.err.log`,
25+
};
26+
27+
/**
28+
* Cloudflared Service API.
29+
*/
30+
export const service = { install, uninstall, exists, log, err, current };
31+
32+
/**
33+
* Throw when service is already installed.
34+
*/
35+
export class AlreadyInstalledError extends Error {
36+
constructor() {
37+
super("service is already installed");
38+
}
39+
}
40+
41+
/**
42+
* Throw when service is not installed.
43+
*/
44+
export class NotInstalledError extends Error {
45+
constructor() {
46+
super("service is not installed");
47+
}
48+
}
49+
50+
/**
51+
* Install Cloudflared service.
52+
* @param token Tunnel service token.
53+
*/
54+
export function install(token: string): void {
55+
if (process.platform !== "darwin") {
56+
throw new Error(`Not Implemented on platform ${process.platform}`);
57+
}
58+
59+
if (exists()) {
60+
throw new AlreadyInstalledError();
61+
}
62+
63+
spawnSync(bin, ["service", "install", token]);
64+
}
65+
66+
/**
67+
* Uninstall Cloudflared service.
68+
*/
69+
export function uninstall(): void {
70+
if (process.platform !== "darwin") {
71+
throw new Error(`Not Implemented on platform ${process.platform}`);
72+
}
73+
74+
if (!exists()) {
75+
throw new NotInstalledError();
76+
}
77+
78+
spawnSync(bin, ["service", "uninstall"]);
79+
80+
fs.rmSync(MACOS_SERVICE_PATH.OUT);
81+
fs.rmSync(MACOS_SERVICE_PATH.ERR);
82+
}
83+
84+
/**
85+
* Get stdout log of cloudflared service. (Usually empty)
86+
* @returns stdout log of cloudflared service.
87+
*/
88+
export function log(): string {
89+
if (process.platform !== "darwin") {
90+
throw new Error(`Not Implemented on platform ${process.platform}`);
91+
}
92+
93+
if (!exists()) {
94+
throw new NotInstalledError();
95+
}
96+
97+
return fs.readFileSync(MACOS_SERVICE_PATH.OUT, "utf8");
98+
}
99+
100+
/**
101+
* Get stderr log of cloudflared service. (cloudflared print all things here)
102+
* @returns stderr log of cloudflared service.
103+
*/
104+
export function err(): string {
105+
if (process.platform !== "darwin") {
106+
throw new Error(`Not Implemented on platform ${process.platform}`);
107+
}
108+
109+
if (!exists()) {
110+
throw new NotInstalledError();
111+
}
112+
113+
return fs.readFileSync(MACOS_SERVICE_PATH.ERR, "utf8");
114+
}
115+
116+
/**
117+
* Get informations of current running cloudflared service.
118+
* @returns informations of current running cloudflared service.
119+
*/
120+
export function current(): {
121+
/** Tunnel ID */
122+
tunnelID: string;
123+
/** Connector ID */
124+
connectorID: string;
125+
/** The connections of the tunnel */
126+
connections: Connection[];
127+
/** Metrics Server Location */
128+
metrics: string;
129+
/** Tunnel Configuration */
130+
config: {
131+
ingress: { service: string; hostname?: string }[];
132+
[key: string]: unknown;
133+
};
134+
} {
135+
if (process.platform !== "darwin") {
136+
throw new Error(`Not Implemented on platform ${process.platform}`);
137+
}
138+
139+
if (!exists()) {
140+
throw new NotInstalledError();
141+
}
142+
143+
const error = err();
144+
145+
const regex = {
146+
tunnelID: /tunnelID=([0-9a-z-]+)/g,
147+
connectorID: /Connector ID: ([0-9a-z-]+)/g,
148+
connections:
149+
/Connection ([a-z0-9-]+) registered connIndex=(\d) ip=([0-9.]+) location=([A-Z]+)/g,
150+
metrics: /metrics server on ([0-9.:]+\/metrics)/g,
151+
config: /config="(.+[^\\])"/g,
152+
};
153+
154+
const match = {
155+
tunnelID: regex.tunnelID.exec(error),
156+
connectorID: regex.connectorID.exec(error),
157+
connections: error.matchAll(regex.connections),
158+
metrics: regex.metrics.exec(error),
159+
config: regex.config.exec(error),
160+
};
161+
162+
const config = (() => {
163+
try {
164+
return JSON.parse(match.config?.[1].replace(/\\/g, "") ?? "{}");
165+
} catch (e) {
166+
return {};
167+
}
168+
})();
169+
170+
return {
171+
tunnelID: match.tunnelID?.[1] ?? "",
172+
connectorID: match.connectorID?.[1] ?? "",
173+
connections:
174+
[...match.connections].map(([, id, , ip, location]) => ({
175+
id,
176+
ip,
177+
location,
178+
})) ?? [],
179+
metrics: match.metrics?.[1] ?? "",
180+
config,
181+
};
182+
}
183+
184+
/**
185+
* Check if cloudflared service is installed.
186+
* @returns true if service is installed, false otherwise.
187+
*/
188+
export function exists(): boolean {
189+
return is_root()
190+
? fs.existsSync(MACOS_SERVICE_PATH.PLIST)
191+
: fs.existsSync(MACOS_SERVICE_PATH.PLIST);
192+
}
193+
194+
function is_root(): boolean {
195+
return process.getuid?.() === 0;
196+
}

0 commit comments

Comments
 (0)