| 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 | } |