main
ts 162 lines 4.57 KB
Raw
1 import { resolveExposeName } from './expose-name';
2
3 /** Hardcoded relay origin for the static docs site */
4 export const RELAY_ORIGIN = 'https://portal.suda.me';
5
6 export type TunnelCommandOS = 'unix' | 'windows';
7
8 export interface TunnelCommandOptions {
9 currentOrigin: string;
10 target: string;
11 name: string;
12 nameSeed: string;
13 relayUrls: string[];
14 discovery: boolean;
15 thumbnailURL: string;
16 enableUDP?: boolean;
17 udpPort?: string;
18 os: TunnelCommandOS;
19 }
20
21 export function buildTunnelDisplayCommand(opts: TunnelCommandOptions): string {
22 const { installLine, exposeHead, exposeOptions } = buildTunnelCommandParts(opts);
23 return joinTunnelDisplayCommand(installLine, exposeHead, exposeOptions);
24 }
25
26 export function buildTunnelPreviewURL(
27 origin: string,
28 name: string,
29 target: string,
30 nameSeed: string
31 ): string {
32 const baseHost = getRelayOriginHost(origin);
33 const subdomain = resolveExposeName(name, target, nameSeed);
34 return `https://${subdomain}.${baseHost}`;
35 }
36
37 function buildTunnelCommandParts({
38 currentOrigin,
39 discovery,
40 enableUDP = false,
41 name,
42 nameSeed,
43 os,
44 relayUrls,
45 target,
46 thumbnailURL,
47 udpPort = ''
48 }: TunnelCommandOptions): {
49 installLine: string;
50 exposeHead: string;
51 exposeOptions: string[];
52 } {
53 const targetValue = target.trim() === '' ? '3000' : target.trim();
54 const nameValue = resolveExposeName(name, targetValue, nameSeed);
55 const relayURLValue = relayUrls.length > 0 ? relayUrls.join(',') : currentOrigin;
56
57 // Inlined install paths — no apiPaths dependency
58 const installScriptURL = new URL('/api/install.sh', currentOrigin).toString();
59 const installPowerShellURL = new URL('/api/install.ps1', currentOrigin).toString();
60
61 const exposeArgs: string[] = [];
62 exposeArgs.push(`--name ${formatToken(nameValue, os)}`);
63 if (relayUrls.length > 0) {
64 exposeArgs.push(`--relays ${formatToken(relayURLValue, os)}`);
65 }
66 if (!discovery) {
67 exposeArgs.push('--discovery=false');
68 }
69
70 const normalizedThumbnailURL = normalizeAbsoluteHTTPURL(thumbnailURL);
71 if (normalizedThumbnailURL !== '') {
72 exposeArgs.push(`--thumbnail ${formatToken(normalizedThumbnailURL, os)}`);
73 }
74 if (enableUDP) {
75 exposeArgs.push('--udp');
76 const normalizedUDPPort = udpPort.trim();
77 if (normalizedUDPPort !== '') {
78 exposeArgs.push(`--udp-addr ${formatToken(normalizedUDPPort, os)}`);
79 }
80 }
81
82 if (os === 'windows') {
83 return {
84 installLine: [
85 `$ProgressPreference = 'SilentlyContinue'`,
86 `irm ${formatToken(installPowerShellURL, os)} | iex`
87 ].join('\n'),
88 exposeHead: 'portal expose',
89 exposeOptions: [formatToken(targetValue, os), ...exposeArgs]
90 };
91 }
92
93 const curlFlags = isLocalRelayOrigin(currentOrigin) ? '-ksSL' : '-sSL';
94 return {
95 installLine: `curl ${curlFlags} ${formatToken(installScriptURL, os)} | bash`,
96 exposeHead: 'portal expose',
97 exposeOptions: [formatToken(targetValue, os), ...exposeArgs]
98 };
99 }
100
101 function normalizeAbsoluteHTTPURL(raw: string): string {
102 const trimmed = raw.trim();
103 if (trimmed === '') return '';
104 try {
105 const parsed = new URL(trimmed);
106 if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return '';
107 return parsed.toString();
108 } catch { return ''; }
109 }
110
111 function getRelayOriginHost(origin: string): string {
112 try {
113 return new URL(origin).hostname.trim().toLowerCase();
114 } catch { return ''; }
115 }
116
117 function isLocalRelayOrigin(origin: string): boolean {
118 try {
119 const hostname = new URL(origin).hostname.trim().toLowerCase();
120 return (
121 hostname === 'localhost' ||
122 hostname === '127.0.0.1' ||
123 hostname === '::1' ||
124 hostname.endsWith('.localhost')
125 );
126 } catch { return false; }
127 }
128
129 function quoteShellValue(value: string): string {
130 return "'" + value.replace(/'/g, `'"'"'`) + "'";
131 }
132
133 function quotePowerShellValue(value: string): string {
134 return `'${value.replace(/'/g, "''")}'`;
135 }
136
137 function formatToken(value: string, os: TunnelCommandOS): string {
138 if (/^[A-Za-z0-9:/.=_-]+$/.test(value)) return value;
139 return os === 'windows' ? quotePowerShellValue(value) : quoteShellValue(value);
140 }
141
142 function joinTunnelCommand(
143 installLine: string,
144 exposeHead: string,
145 exposeOptions: string[]
146 ): string {
147 return [installLine, [exposeHead, ...exposeOptions].join(' ')].join('\n');
148 }
149
150 function joinTunnelDisplayCommand(
151 installLine: string,
152 exposeHead: string,
153 exposeOptions: string[]
154 ): string {
155 const relayIndex = exposeOptions.findIndex((option) => option.startsWith('--relays '));
156 if (relayIndex < 0) return joinTunnelCommand(installLine, exposeHead, exposeOptions);
157 const exposeLines = [
158 [exposeHead, ...exposeOptions.slice(0, relayIndex)].join(' '),
159 exposeOptions.slice(relayIndex).join(' ')
160 ];
161 return [installLine, exposeLines.join('\n')].join('\n');
162 }