main
ts 236 lines 5.42 KB
Raw
1 import { BROWSER_API_PATHS } from "@/lib/apiPaths";
2 import { resolveExposeName } from "@/lib/exposeName";
3
4 export type TunnelCommandOS = "unix" | "windows";
5
6 export interface TunnelCommandOptions {
7 currentOrigin: string;
8 target: string;
9 name: string;
10 nameSeed: string;
11 relayUrls: string[];
12 discovery: boolean;
13 thumbnailURL: string;
14 enableUDP?: boolean;
15 udpPort?: string;
16 os: TunnelCommandOS;
17 }
18
19
20 export function buildTunnelCommand({
21 currentOrigin,
22 discovery,
23 enableUDP = false,
24 name,
25 nameSeed,
26 os,
27 relayUrls,
28 target,
29 thumbnailURL,
30 udpPort = "",
31 }: TunnelCommandOptions): string {
32 const { installLine, exposeHead, exposeOptions } = buildTunnelCommandParts({
33 currentOrigin,
34 discovery,
35 enableUDP,
36 name,
37 nameSeed,
38 os,
39 relayUrls,
40 target,
41 thumbnailURL,
42 udpPort,
43 });
44
45 return joinTunnelCommand(installLine, exposeHead, exposeOptions);
46 }
47
48 export function buildTunnelDisplayCommand({
49 currentOrigin,
50 discovery,
51 enableUDP = false,
52 name,
53 nameSeed,
54 os,
55 relayUrls,
56 target,
57 thumbnailURL,
58 udpPort = "",
59 }: TunnelCommandOptions): string {
60 const { installLine, exposeHead, exposeOptions } = buildTunnelCommandParts({
61 currentOrigin,
62 discovery,
63 enableUDP,
64 name,
65 nameSeed,
66 os,
67 relayUrls,
68 target,
69 thumbnailURL,
70 udpPort,
71 });
72
73 return joinTunnelCommand(installLine, exposeHead, exposeOptions);
74 }
75
76 function buildTunnelCommandParts({
77 currentOrigin,
78 discovery,
79 enableUDP = false,
80 name,
81 nameSeed,
82 os,
83 relayUrls,
84 target,
85 thumbnailURL,
86 udpPort = "",
87 }: TunnelCommandOptions): {
88 installLine: string;
89 exposeHead: string;
90 exposeOptions: string[];
91 } {
92 const targetValue = target.trim() === "" ? "3000" : target.trim();
93 const nameValue = resolveExposeName(name, targetValue, nameSeed);
94 const relayURLValue =
95 relayUrls.length > 0 ? relayUrls.join(",") : currentOrigin;
96 const installScriptURL = new URL(
97 BROWSER_API_PATHS.install.shell,
98 currentOrigin
99 ).toString();
100 const installPowerShellURL = new URL(
101 BROWSER_API_PATHS.install.powershell,
102 currentOrigin
103 ).toString();
104
105 const exposeArgs: string[] = [];
106
107 exposeArgs.push(`--name ${formatToken(nameValue, os)}`);
108 if (relayUrls.length > 0) {
109 exposeArgs.push(`--relays ${formatToken(relayURLValue, os)}`);
110 }
111 if (!discovery) {
112 exposeArgs.push("--discovery=false");
113 }
114
115 const normalizedThumbnailURL = normalizeAbsoluteHTTPURL(thumbnailURL);
116 if (normalizedThumbnailURL !== "") {
117 exposeArgs.push(`--thumbnail ${formatToken(normalizedThumbnailURL, os)}`);
118 }
119 if (enableUDP) {
120 exposeArgs.push("--udp");
121 const normalizedUDPPort = udpPort.trim();
122 if (normalizedUDPPort !== "") {
123 exposeArgs.push(`--udp-addr ${formatToken(normalizedUDPPort, os)}`);
124 }
125 }
126
127 if (os === "windows") {
128 return {
129 installLine: [
130 `$ProgressPreference = 'SilentlyContinue'`,
131 `irm ${formatToken(installPowerShellURL, os)} | iex`,
132 ].join("\n"),
133 exposeHead: "portal expose",
134 exposeOptions: [formatToken(targetValue, os), ...exposeArgs],
135 };
136 }
137
138 const curlFlags = isLocalRelayOrigin(currentOrigin) ? "-ksSL" : "-sSL";
139 return {
140 installLine: `curl ${curlFlags} ${formatToken(installScriptURL, os)} | bash`,
141 exposeHead: "portal expose",
142 exposeOptions: [formatToken(targetValue, os), ...exposeArgs],
143 };
144 }
145
146 export function buildTunnelPreviewURL(
147 origin: string,
148 name: string,
149 target: string,
150 nameSeed: string
151 ): string {
152 const baseHost = getRelayOriginHost(origin);
153 const subdomain = resolveExposeName(name, target, nameSeed);
154 return `https://${subdomain}.${baseHost}`;
155 }
156
157 export function buildServiceStatusHostname(
158 origin: string,
159 name: string,
160 target: string,
161 nameSeed: string
162 ): string {
163 const relayHost = getRelayOriginHost(origin);
164 if (relayHost === "") {
165 return "";
166 }
167 const subdomain = resolveExposeName(name, target, nameSeed);
168 return `${subdomain}.${relayHost}`;
169 }
170
171 export function normalizeAbsoluteHTTPURL(raw: string): string {
172 const trimmed = raw.trim();
173 if (trimmed === "") {
174 return "";
175 }
176
177 try {
178 const parsed = new URL(trimmed);
179 if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
180 return "";
181 }
182 return parsed.toString();
183 } catch {
184 return "";
185 }
186 }
187
188 function getRelayOriginHost(origin: string): string {
189 try {
190 const parsed = new URL(origin);
191 return parsed.hostname.trim().toLowerCase();
192 } catch {
193 return "";
194 }
195 }
196
197 function isLocalRelayOrigin(origin: string): boolean {
198 try {
199 const parsed = new URL(origin);
200 return isLocalRelayHostname(parsed.hostname.trim().toLowerCase());
201 } catch {
202 return false;
203 }
204 }
205
206 function isLocalRelayHostname(hostname: string): boolean {
207 return (
208 hostname === "localhost" ||
209 hostname === "127.0.0.1" ||
210 hostname === "::1" ||
211 hostname.endsWith(".localhost")
212 );
213 }
214
215 function quoteShellValue(value: string): string {
216 return "'" + value.replace(/'/g, `'"'"'`) + "'";
217 }
218
219 function quotePowerShellValue(value: string): string {
220 return `'${value.replace(/'/g, "''")}'`;
221 }
222
223 function formatToken(value: string, os: TunnelCommandOS): string {
224 if (/^[A-Za-z0-9:/.=_-]+$/.test(value)) {
225 return value;
226 }
227 return os === "windows" ? quotePowerShellValue(value) : quoteShellValue(value);
228 }
229
230 function joinTunnelCommand(
231 installLine: string,
232 exposeHead: string,
233 exposeOptions: string[]
234 ): string {
235 return [installLine, [exposeHead, ...exposeOptions].join(" ")].join("\n");
236 }