feat(tunnel): add PowerShell script support for Windows tunnel setup
snowmerak committed
Dec 20, 2025 at 23:59 UTC
fa3768d7702bd58b3ccb8c7304fcc138cc18972b
1 file changed
+81
-7
cmd/relay-server/tunnel.go
+81
-7
@@ -61,6 +61,56 @@ echo "Starting portal-tunnel..." >&2
61
exec "$@"
62
`
63
64
+const tunnelPowerShellScriptTemplate = `$ErrorActionPreference = "Stop"
65
+
66
+$BaseUrl = if ($env:BASE_URL) { $env:BASE_URL } else { "%s" }
67
+$RelayUrl = if ($env:RELAY_URL) { $env:RELAY_URL } else { $BaseUrl }
68
+
69
+$Arch = $env:PROCESSOR_ARCHITECTURE
70
+if ($Arch -eq "AMD64") {
71
+ $TunnelArch = "amd64"
72
+} elseif ($Arch -eq "ARM64") {
73
+ $TunnelArch = "arm64"
74
+} else {
75
+ Write-Error "Unsupported architecture: $Arch"
76
+ exit 1
77
+}
78
+
79
+$BinUrl = if ($env:BIN_URL) { $env:BIN_URL } else { "$BaseUrl/tunnel/bin/windows-$TunnelArch" }
80
+
81
+$WorkDir = Join-Path $env:TEMP ("portal-tunnel-" + [Guid]::NewGuid().ToString())
82
+New-Item -ItemType Directory -Force -Path $WorkDir | Out-Null
83
+$BinPath = Join-Path $WorkDir "portal-tunnel.exe"
84
+
85
+try {
86
+ Write-Host "Downloading portal-tunnel (windows/$TunnelArch)..."
87
+ Invoke-WebRequest -Uri $BinUrl -OutFile $BinPath
88
+} catch {
89
+ Write-Error "Failed to download portal-tunnel: $_"
90
+ Remove-Item -Recurse -Force $WorkDir
91
+ exit 1
92
+}
93
+
94
+$ArgsList = @("--relay", $RelayUrl)
95
+
96
+if ($env:HOST) { $ArgsList += "--host", $env:HOST } else { $ArgsList += "--host", "localhost:3000" }
97
+if ($env:NAME) { $ArgsList += "--name", $env:NAME }
98
+if ($env:DESCRIPTION) { $ArgsList += "--description", $env:DESCRIPTION }
99
+if ($env:TAGS) { $ArgsList += "--tags", $env:TAGS }
100
+if ($env:THUMBNAIL) { $ArgsList += "--thumbnail", $env:THUMBNAIL }
101
+if ($env:OWNER) { $ArgsList += "--owner", $env:OWNER }
102
+if ($env:HIDE -eq "1" -or $env:HIDE -eq "true") { $ArgsList += "--hide" }
103
+
104
+Write-Host "Starting portal-tunnel..."
105
+try {
106
+ & $BinPath $ArgsList
107
+} finally {
108
+ if (Test-Path $WorkDir) {
109
+ Remove-Item -Recurse -Force $WorkDir
110
+ }
111
+}
112
+`
113
+
114
func serveTunnelScript(w http.ResponseWriter, r *http.Request) {
115
utils.SetCORSHeaders(w)
116
if r.Method != http.MethodGet && r.Method != http.MethodHead {
@@ -69,11 +119,33 @@ func serveTunnelScript(w http.ResponseWriter, r *http.Request) {
119
return
120
}
121
72
- script := fmt.Sprintf(tunnelScriptTemplate, flagPortalURL)
122
+ targetOS := r.URL.Query().Get("os")
123
+ isWindows := false
124
+ if targetOS != "" {
125
+ isWindows = strings.EqualFold(targetOS, "windows")
126
+ } else {
127
+ // Fallback: check User-Agent
128
+ ua := strings.ToLower(r.UserAgent())
129
+ isWindows = strings.Contains(ua, "windows")
130
+ }
131
+
132
+ var script string
133
+ var contentType string
134
+ var filename string
135
+
136
+ if isWindows {
137
+ script = fmt.Sprintf(tunnelPowerShellScriptTemplate, flagPortalURL)
138
+ contentType = "text/plain" // or application/x-powershell
139
+ filename = "tunnel.ps1"
140
+ } else {
141
+ script = fmt.Sprintf(tunnelScriptTemplate, flagPortalURL)
142
+ contentType = "text/x-shellscript"
143
+ filename = "tunnel.sh"
144
+ }
145
74
- w.Header().Set("Content-Type", "text/x-shellscript")
146
+ w.Header().Set("Content-Type", contentType)
147
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
76
- w.Header().Set("Content-Disposition", "inline; filename=\"tunnel.sh\"")
148
+ w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
149
w.WriteHeader(http.StatusOK)
150
if r.Method == http.MethodGet {
151
w.Write([]byte(script))
@@ -91,10 +163,12 @@ func serveTunnelBinary(w http.ResponseWriter, r *http.Request) {
163
slug := strings.TrimPrefix(r.URL.Path, "/tunnel/bin/")
164
slug = strings.Trim(slug, "/")
165
path, ok := map[string]string{
94
- "linux-amd64": "dist/tunnel/portal-tunnel-linux-amd64",
95
- "linux-arm64": "dist/tunnel/portal-tunnel-linux-arm64",
96
- "darwin-amd64": "dist/tunnel/portal-tunnel-darwin-amd64",
97
- "darwin-arm64": "dist/tunnel/portal-tunnel-darwin-arm64",
166
+ "linux-amd64": "dist/tunnel/portal-tunnel-linux-amd64",
167
+ "linux-arm64": "dist/tunnel/portal-tunnel-linux-arm64",
168
+ "darwin-amd64": "dist/tunnel/portal-tunnel-darwin-amd64",
169
+ "darwin-arm64": "dist/tunnel/portal-tunnel-darwin-arm64",
170
+ "windows-amd64": "dist/tunnel/portal-tunnel-windows-amd64.exe",
171
+ "windows-arm64": "dist/tunnel/portal-tunnel-windows-arm64.exe",
172
}[slug]
173
if !ok {
174
http.NotFound(w, r)