diff --git a/.gitignore b/.gitignore index ab67f4b..c34fc0d 100644 --- a/.gitignore +++ b/.gitignore @@ -48,11 +48,6 @@ Desktop.ini # Portable USB bundle — track LAUNCH.bat only; runtime output stays local /usb/* !/usb/LAUNCH.bat -!/usb/cloudflare/ -/usb/cloudflare/* -!/usb/cloudflare/SETUP.txt -!/usb/cloudflare/start-tunnel.ps1 -!/usb/cloudflare/tunnel-config.json *.msi /cloudflared-windows-amd64.msi diff --git a/LAUNCH.bat b/LAUNCH.bat index 12162c1..21b4e28 100644 --- a/LAUNCH.bat +++ b/LAUNCH.bat @@ -170,35 +170,19 @@ if not exist "%ROOT%\data\blueprints" mkdir "%ROOT%\data\blueprints" if not exist "%ROOT%\data\preps" mkdir "%ROOT%\data\preps" :: ---------------------------------------------------------------- -:: 5. Cloudflare Tunnel - start in background window + 60s watchdog +:: 5. Detect LAN IP for display :: ---------------------------------------------------------------- set "SERVER_PORT=8989" -set "CF_HOSTNAME=aether.thetempleofdoom.com" -set "CF_READY=0" - -if not exist "%ROOT%\cloudflare\start-tunnel.ps1" ( - echo [CF] WARNING: cloudflare\start-tunnel.ps1 missing - tunnel skipped. - goto cf_done +for /f "tokens=2 delims=:" %%I in ('ipconfig ^| findstr /i "IPv4" ^| findstr /v "127.0.0.1"') do ( + set "LAN_IP=%%I" + goto lan_done ) - -echo. -echo [CF] Starting Cloudflare tunnel for https://%CF_HOSTNAME% ... -echo [CF] Log: %ROOT%\data\logs\tunnel.log -start "AetherForge Tunnel" powershell -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\cloudflare\start-tunnel.ps1" -Root "%ROOT%" - -:: Give the tunnel script 8 seconds to start before we open the server -ping -n 9 127.0.0.1 >nul -set "CF_READY=1" - -:cf_done - -:: ---------------------------------------------------------------- -:: 6. Detect LAN IP for display -:: ---------------------------------------------------------------- set "LAN_IP=localhost" +:lan_done +set "LAN_IP=%LAN_IP: =%" :: ---------------------------------------------------------------- -:: 7. Kill any stale server process +:: 6. Kill any stale server process :: ---------------------------------------------------------------- taskkill /F /IM AetherForge.exe >nul 2>nul ping -n 2 127.0.0.1 >nul @@ -207,15 +191,11 @@ echo. echo ================================================================ echo STARTING CONTROL DECK echo ================================================================ -echo Local: http://localhost:%SERVER_PORT% -echo LAN: http://%LAN_IP%:%SERVER_PORT% -if "!CF_READY!"=="1" ( - echo Public: https://!CF_HOSTNAME! - echo Dropper PS: iex -irm https://!CF_HOSTNAME!/install.ps1 -) -echo Data: %ROOT%\data\ +echo Local: http://localhost:%SERVER_PORT% +echo LAN: http://%LAN_IP%:%SERVER_PORT% +echo Data: %ROOT%\data\ echo. -echo Dashboard login printed below each start. +echo Login credentials printed below. echo Press Ctrl+C to stop. echo ================================================================ echo. @@ -235,8 +215,6 @@ if "!EC!"=="0" ( echo If port %SERVER_PORT% is in use, close other AetherForge windows and retry. ) -:: Cloudflare tunnel runs as a Windows service — stays up after LAUNCH.bat exits - echo. pause endlocal diff --git a/cloudflare/SETUP.bat b/cloudflare/SETUP.bat deleted file mode 100644 index b5e4a90..0000000 --- a/cloudflare/SETUP.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -echo Tunnel token is baked into LAUNCH.bat — no JSON setup needed. -echo Go up one folder and run LAUNCH.bat as Administrator once. -pause diff --git a/cloudflare/SETUP.txt b/cloudflare/SETUP.txt deleted file mode 100644 index f48ccc1..0000000 --- a/cloudflare/SETUP.txt +++ /dev/null @@ -1,47 +0,0 @@ -======================================================== - AetherForge — Cloudflare Tunnel - https://killa.thetempleofdoom.com -======================================================== - -FIRST RUN (once per PC) ------------------------ -1. Double-click LAUNCH.bat -2. Approve the UAC prompt when asked — tunnel install needs Administrator -3. LAUNCH.bat installs cloudflared to: - C:\ProgramData\AetherForge\bin\cloudflared.exe - (stable path on the PC, not on the USB drive) -4. Registers the Windows service "cloudflared" with your tunnel token -5. Starts the service — connector shows Healthy in Cloudflare Zero Trust - -You can also run manually (as Administrator): - cloudflare\install-tunnel.bat - -VERIFY ON THE PC ----------------- - sc query cloudflared - -Look for STATE : 4 RUNNING - -Cloudflare dashboard: - Zero Trust → Networks → Tunnels → your tunnel → Healthy connector - -Public hostname route (required): - killa.thetempleofdoom.com → http://localhost:8989 - -End-to-end test (AetherForge must be running on :8989): - https://killa.thetempleofdoom.com - -TROUBLESHOOTING ---------------- -- No [CF] lines in LAUNCH? Old USB copy — re-copy usb\ from pack-usb.bat -- sc query shows nothing / service missing? Run LAUNCH.bat as Administrator once -- Service exists but not RUNNING? net start cloudflared -- Public URL 502? AetherForge not listening on localhost:8989 -- Tunnel Healthy but no public URL? Add hostname route in Cloudflare dashboard - -Dropper (after tunnel + server are up): - iex (irm 'https://killa.thetempleofdoom.com/install.ps1') - -credentials.json is NOT used — token is in install-tunnel.bat / LAUNCH.bat. - -======================================================== diff --git a/cloudflare/credentials.json b/cloudflare/credentials.json deleted file mode 100644 index 89cc299..0000000 --- a/cloudflare/credentials.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "__SETUP_REQUIRED__": "Replace this file with your real tunnel credentials JSON.", - "__INSTRUCTIONS__": "See SETUP.txt in this folder.", - "AccountTag": "", - "TunnelSecret": "", - "TunnelID": "" -} diff --git a/cloudflare/install-tunnel.bat b/cloudflare/install-tunnel.bat deleted file mode 100644 index 499e199..0000000 --- a/cloudflare/install-tunnel.bat +++ /dev/null @@ -1,163 +0,0 @@ -@echo off -setlocal EnableExtensions EnableDelayedExpansion -title AetherForge — Cloudflare Tunnel Setup - -:: Must run elevated (Windows service install requires Administrator). -net session >nul 2>&1 -if errorlevel 1 ( - echo. - echo [CF] ERROR: Administrator rights required. - echo Right-click LAUNCH.bat and choose "Run as administrator" once. - echo. - exit /b 1 -) - -set "ROOT=%~1" -if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" -if "%ROOT%"=="" ( - echo [CF] ERROR: Missing deck root argument. - exit /b 1 -) - -set "CF_CONFIG_JSON=%ROOT%\cloudflare\tunnel-config.json" -set "CF_MSI=%ROOT%\cloudflare\cloudflared-windows-amd64.msi" -set "CF_PORTABLE=%ROOT%\cloudflare\cloudflared.exe" -set "CF_DIR=%ProgramData%\AetherForge\.cloudflared" -set "DEST_DIR=%ProgramData%\AetherForge\bin" -set "CF_EXE=%DEST_DIR%\cloudflared.exe" -set "CF_CONFIG=%CF_DIR%\config.yml" -set "CF_WRITER=%ROOT%\cloudflare\write-tunnel-config.ps1" - -echo. -echo ================================================================ -echo AetherForge — Cloudflare Tunnel Install -echo ================================================================ -echo Deck: %ROOT% -echo Config: %CF_CONFIG% -echo ================================================================ -echo. - -if not exist "%CF_CONFIG_JSON%" ( - echo [CF] ERROR: Missing %CF_CONFIG_JSON% - exit /b 1 -) -if not exist "%CF_WRITER%" ( - echo [CF] ERROR: Missing %CF_WRITER% - exit /b 1 -) - -if not exist "%DEST_DIR%" mkdir "%DEST_DIR%" - -:: Obtain cloudflared.exe — stable path on this PC, not the USB drive. -if exist "%CF_EXE%" goto have_bin - -if exist "%CF_PORTABLE%" ( - echo [CF] Copying bundled cloudflared.exe to ProgramData... - copy /y "%CF_PORTABLE%" "%CF_EXE%" >nul - if exist "%CF_EXE%" goto have_bin -) - -if exist "%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" ( - echo [CF] Copying installed cloudflared to ProgramData... - copy /y "%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" "%CF_EXE%" >nul - if exist "%CF_EXE%" goto have_bin -) - -if exist "%CF_MSI%" ( - echo [CF] Installing cloudflared from MSI... - msiexec /i "%CF_MSI%" /quiet /norestart - echo [CF] Waiting for MSI to finish... - ping -n 15 127.0.0.1 >nul - if exist "%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" ( - copy /y "%ProgramFiles%\cloudflare\cloudflared\cloudflared.exe" "%CF_EXE%" >nul - if exist "%CF_EXE%" goto have_bin - ) -) - -echo [CF] ERROR: cloudflared binary not found. -exit /b 1 - -:have_bin -echo [CF] Binary ready: %CF_EXE% - -:: Write credentials.json + config.yml with explicit local ingress route. -echo [CF] Writing local tunnel config: hostname to origin... - -powershell -NoProfile -ExecutionPolicy Bypass -File "%CF_WRITER%" -ConfigJson "%CF_CONFIG_JSON%" -CloudflaredDir "%CF_DIR%" > "%TEMP%\af-cf-writer.out" 2>&1 -for /f "usebackq tokens=1,* delims==" %%A in ("%TEMP%\af-cf-writer.out") do ( - if /i "%%A"=="TUNNEL_ID" set "TUNNEL_ID=%%B" - if /i "%%A"=="HOSTNAME" set "CF_HOSTNAME=%%B" - if /i "%%A"=="ORIGIN" set "CF_ORIGIN=%%B" -) - -if not exist "%CF_CONFIG%" ( - echo [CF] ERROR: Failed to write %CF_CONFIG% - exit /b 1 -) - -echo [CF] Route: https://!CF_HOSTNAME! -^> !CF_ORIGIN! -echo [CF] Config: %CF_CONFIG% -type "%CF_CONFIG%" -echo. - -echo [CF] Validating ingress rules... -"%CF_EXE%" --config "%CF_CONFIG%" tunnel ingress validate -if errorlevel 1 ( - echo [CF] WARNING: ingress validation failed - check config.yml -) - -:: Re-register service so it uses config.yml - not token-only remote routing. -sc query cloudflared >nul 2>&1 -if errorlevel 1 goto cf_register_service -echo [CF] Removing old cloudflared service - switching to local config routing... -net stop cloudflared >nul 2>&1 -sc delete cloudflared >nul 2>&1 -ping -n 3 127.0.0.1 >nul - -:cf_register_service -echo [CF] Registering Windows service with local config.yml... -"%CF_EXE%" --config "%CF_CONFIG%" service install -if errorlevel 1 ( - echo [CF] ERROR: service install failed. - exit /b 1 -) -ping -n 3 127.0.0.1 >nul - -echo [CF] Ensuring DNS route for !CF_HOSTNAME!... -"%CF_EXE%" --config "%CF_CONFIG%" tunnel route dns !TUNNEL_ID! !CF_HOSTNAME! -if errorlevel 1 goto cf_dns_manual -goto cf_dns_done -:cf_dns_manual -set "CF_CNAME_TARGET=%TUNNEL_ID%.cfargotunnel.com" -echo [CF] NOTE: DNS route command failed - add CNAME in Cloudflare DNS manually: -echo %CF_HOSTNAME% CNAME %CF_CNAME_TARGET% -:cf_dns_done - -sc query cloudflared | findstr /I "RUNNING" >nul 2>&1 -if errorlevel 1 goto cf_start_service -goto show_status -:cf_start_service -echo [CF] Starting cloudflared service... -net start cloudflared - -:show_status -echo. -echo --- cloudflared service status --- -sc query cloudflared -echo -------------------------------- -echo. -echo --- expected ingress - local config --- -type "%CF_CONFIG%" -echo ------------------------------------- -echo. - -sc query cloudflared | findstr /I "RUNNING" >nul 2>&1 -if errorlevel 1 ( - echo [CF] Tunnel connector is NOT running on this PC. - exit /b 1 -) - -echo [CF] Tunnel connector running with LOCAL routing config. -echo [CF] Public URL when AetherForge is on !CF_ORIGIN!: https://!CF_HOSTNAME! -echo [CF] Run cloudflare\verify-tunnel.bat to test DNS + local + public reachability. -exit /b 0 diff --git a/cloudflare/repair-tunnel.ps1 b/cloudflare/repair-tunnel.ps1 deleted file mode 100644 index 133b8f3..0000000 --- a/cloudflare/repair-tunnel.ps1 +++ /dev/null @@ -1,115 +0,0 @@ -param( - [Parameter(Mandatory = $true)][string]$Root, - [ValidateRange(1, 4)][int]$Level = 1 -) - -$ErrorActionPreference = 'Continue' -$Root = $Root.TrimEnd('\') -$configJson = Join-Path $Root 'cloudflare\tunnel-config.json' -$installBat = Join-Path $Root 'cloudflare\install-tunnel.bat' -$writer = Join-Path $Root 'cloudflare\write-tunnel-config.ps1' -$cfDir = Join-Path $env:ProgramData 'AetherForge\.cloudflared' -$cfConfig = Join-Path $cfDir 'config.yml' -$cfExe = Join-Path $env:ProgramData 'AetherForge\bin\cloudflared.exe' - -function Write-RepairLog([string]$Message) { - $line = "{0} [CF-HEAL-L{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message - Write-Output $line -} - -function Get-ServiceRunning { - $q = sc.exe query cloudflared 2>&1 | Out-String - return ($q -match 'STATE\s+:\s+\d+\s+RUNNING') -} - -function Start-CloudflaredService { - $null = net.exe start cloudflared 2>&1 - Start-Sleep -Seconds 3 - return (Get-ServiceRunning) -} - -function Stop-CloudflaredService { - $null = net.exe stop cloudflared 2>&1 - Start-Sleep -Seconds 2 -} - -function Restart-CloudflaredService { - Stop-CloudflaredService - return (Start-CloudflaredService) -} - -Write-RepairLog "Repair level $Level starting..." - -switch ($Level) { - 1 { - Write-RepairLog "Action: net start cloudflared" - if (Start-CloudflaredService) { - Write-RepairLog "Service is RUNNING" - exit 0 - } - Write-RepairLog "net start failed" - exit 1 - } - 2 { - Write-RepairLog "Action: restart cloudflared service" - if (Restart-CloudflaredService) { - Write-RepairLog "Service restarted OK" - exit 0 - } - Write-RepairLog "Restart failed" - exit 1 - } - 3 { - Write-RepairLog "Action: rewrite config.yml + restart + DNS route" - if (-not (Test-Path $configJson)) { - Write-RepairLog "Missing tunnel-config.json" - exit 1 - } - if (-not (Test-Path $writer)) { - Write-RepairLog "Missing write-tunnel-config.ps1" - exit 1 - } - $tunnelId = $null - $hostname = $null - & $writer -ConfigJson $configJson -CloudflaredDir $cfDir | ForEach-Object { - if ($_ -match '^TUNNEL_ID=(.+)$') { $tunnelId = $Matches[1] } - if ($_ -match '^HOSTNAME=(.+)$') { $hostname = $Matches[1] } - Write-RepairLog $_ - } - if (-not (Test-Path $cfConfig)) { - Write-RepairLog "config.yml write failed" - exit 1 - } - Restart-CloudflaredService | Out-Null - if ($cfExe -and (Test-Path $cfExe) -and $tunnelId -and $hostname) { - Write-RepairLog "Action: tunnel route dns $hostname" - & $cfExe --config $cfConfig tunnel route dns $tunnelId $hostname 2>&1 | ForEach-Object { Write-RepairLog $_ } - } - if (Get-ServiceRunning) { - Write-RepairLog "Config refresh + service RUNNING" - exit 0 - } - Write-RepairLog "Still not RUNNING after config refresh" - exit 1 - } - 4 { - Write-RepairLog "Action: full tunnel reinstall (install-tunnel.bat)" - if (-not (Test-Path $installBat)) { - Write-RepairLog "Missing install-tunnel.bat" - exit 1 - } - $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( - [Security.Principal.WindowsBuiltInRole]::Administrator - ) - if ($isAdmin) { - & cmd.exe /c "`"$installBat`" `"$Root`"" - exit $LASTEXITCODE - } - Write-RepairLog "Requesting elevated reinstall (UAC)..." - $p = Start-Process -FilePath $installBat -ArgumentList "`"$Root`"" -Verb RunAs -Wait -PassThru - exit $p.ExitCode - } -} - -Write-RepairLog "Unknown level" -exit 1 diff --git a/cloudflare/start-tunnel.ps1 b/cloudflare/start-tunnel.ps1 deleted file mode 100644 index 877409d..0000000 --- a/cloudflare/start-tunnel.ps1 +++ /dev/null @@ -1,302 +0,0 @@ -#Requires -Version 5 -param([string]$Root) -$ErrorActionPreference = 'Continue' -$Root = ($Root -replace '\\$','') - -# --- Config (overridden by tunnel-config.json if present) -------------------- -$TOKEN = 'eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9' -$HOSTNAME = 'killa.thetempleofdoom.com' -$ORIGIN = 'http://127.0.0.1:8989' -$SVC = 'cloudflared' -$DESTDIR = "$env:ProgramData\AetherForge\bin" -$LOGDIR = "$Root\data\logs" -$LOGFILE = "$LOGDIR\tunnel.log" -$PIDFILE = "$env:ProgramData\AetherForge\tunnel.pid" -$INTERVAL = 60 - -$cfgPath = Join-Path $PSScriptRoot 'tunnel-config.json' -if (Test-Path $cfgPath) { - try { - $c = Get-Content $cfgPath -Raw | ConvertFrom-Json - if ($c.tunnel_token) { $TOKEN = $c.tunnel_token } - if ($c.hostname) { $HOSTNAME = $c.hostname } - if ($c.origin) { $ORIGIN = $c.origin } - if ($c.watch_interval_seconds){ $INTERVAL = [int]$c.watch_interval_seconds } - } catch {} -} - -# --- Helpers ----------------------------------------------------------------- -function Log([string]$msg, [string]$tag = 'INFO') { - $line = '{0} [TUNNEL][{1}] {2}' -f (Get-Date -Format 'HH:mm:ss'), $tag, $msg - Write-Host $line - try { Add-Content -LiteralPath $LOGFILE -Value $line -Encoding UTF8 } catch {} -} - -function SvcState { - $q = (& sc.exe query $SVC 2>&1) -join ' ' - if ($q -match '1060|does not exist') { return 'MISSING' } - if ($q -match 'RUNNING') { return 'RUNNING' } - if ($q -match 'STOPPED') { return 'STOPPED' } - return 'UNKNOWN' -} - -function IsAdmin { - ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() - ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -} - -function HttpGet([string]$url, [int]$sec = 10) { - try { - $r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec $sec - return [pscustomobject]@{ Ok=$true; Code=[int]$r.StatusCode; Err=$null } - } catch { - $code = $null - if ($_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode } - return [pscustomobject]@{ Ok=$false; Code=$code; Err=$_.Exception.Message } - } -} - -# --- Locate cloudflared ------------------------------------------------------- -function FindCloudflared { - # 1. Already staged in ProgramData - if (Test-Path "$DESTDIR\cloudflared.exe") { return "$DESTDIR\cloudflared.exe" } - - New-Item -ItemType Directory -Force -Path $DESTDIR | Out-Null - - # 2. Bundled next to this script on the USB - $bundled = Join-Path $PSScriptRoot 'cloudflared.exe' - if (Test-Path $bundled) { - Copy-Item $bundled "$DESTDIR\cloudflared.exe" -Force - return "$DESTDIR\cloudflared.exe" - } - - # 3. MSI-installed on this PC - foreach ($dir in @("$env:ProgramFiles\cloudflare\cloudflared", - "${env:ProgramFiles(x86)}\cloudflare\cloudflared")) { - if (Test-Path "$dir\cloudflared.exe") { - Copy-Item "$dir\cloudflared.exe" "$DESTDIR\cloudflared.exe" -Force - return "$DESTDIR\cloudflared.exe" - } - } - - # 4. Run the MSI if bundled - $msi = Join-Path $PSScriptRoot 'cloudflared-windows-amd64.msi' - if (Test-Path $msi) { - Log 'Installing cloudflared MSI...' 'SETUP' - Start-Process msiexec -ArgumentList "/i `"$msi`" /quiet /norestart" -Wait - Start-Sleep -Seconds 6 - if (Test-Path "$env:ProgramFiles\cloudflare\cloudflared\cloudflared.exe") { - Copy-Item "$env:ProgramFiles\cloudflare\cloudflared\cloudflared.exe" "$DESTDIR\cloudflared.exe" -Force - return "$DESTDIR\cloudflared.exe" - } - } - - # 5. Download from GitHub as last resort - Log 'Downloading cloudflared from GitHub...' 'SETUP' - try { - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe' - Invoke-WebRequest -Uri $url -OutFile "$DESTDIR\cloudflared.exe" -UseBasicParsing - if (Test-Path "$DESTDIR\cloudflared.exe") { return "$DESTDIR\cloudflared.exe" } - } catch { Log "Download failed: $_" 'ERROR' } - - return $null -} - -# ============================================================================= -# MAIN -# ============================================================================= -New-Item -ItemType Directory -Force -Path $LOGDIR | Out-Null -New-Item -ItemType Directory -Force -Path $DESTDIR | Out-Null - -Log "Tunnel setup hostname=$HOSTNAME origin=$ORIGIN" 'SETUP' - -$CF = FindCloudflared -if (-not $CF) { - Log 'Cannot locate cloudflared.exe - tunnel skipped.' 'ERROR' - exit 1 -} -Log "Using: $CF" 'SETUP' - -# --- Install + start as Windows service (admin path) ------------------------- -$asService = $false - -if (IsAdmin) { - # Stop and remove any existing service - if ((SvcState) -ne 'MISSING') { - Log 'Removing existing cloudflared service...' 'SETUP' - & net.exe stop $SVC 2>&1 | Out-Null - Start-Sleep -Seconds 2 - & sc.exe delete $SVC 2>&1 | Out-Null - Start-Sleep -Seconds 2 - } - - # Clear stale EventLog registry key left by previous installs. - # cloudflared rolls back the entire service install if this key already exists. - $evtKey = 'HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\Application\Cloudflared' - if (Test-Path $evtKey) { - Log 'Removing stale EventLog registry key...' 'SETUP' - Remove-Item -LiteralPath $evtKey -Force -ErrorAction SilentlyContinue - Start-Sleep -Seconds 1 - } - - Log 'Installing cloudflared Windows service...' 'SETUP' - # Capture output without triggering PS NativeCommandError decorations - $installOut = (& $CF service install $TOKEN 2>&1) -join "`n" - Log $installOut.Trim() 'SETUP' - Start-Sleep -Seconds 3 - - Log 'Starting service...' 'SETUP' - & net.exe start $SVC 2>&1 | Out-Null - Start-Sleep -Seconds 6 - - if ((SvcState) -eq 'RUNNING') { - Log 'Service RUNNING' 'OK' - $asService = $true - } else { - Log "Service not RUNNING after install (state=$(SvcState)) - falling back to background process." 'WARN' - } -} else { - Log 'Not running as Administrator.' 'WARN' - Log 'For a persistent Windows service: right-click LAUNCH.bat > Run as administrator.' 'WARN' - if ((SvcState) -eq 'RUNNING') { - Log 'Existing cloudflared service is RUNNING - using it.' 'OK' - $asService = $true - } -} - -# --- Run directly as background process (non-admin fallback) ----------------- -$bgProc = $null -if (-not $asService) { - Log 'Starting cloudflared as background process...' 'SETUP' - - # Kill leftover from previous run - if (Test-Path $PIDFILE) { - $oldpid = [int](Get-Content $PIDFILE -Raw -ErrorAction SilentlyContinue) - if ($oldpid) { Stop-Process -Id $oldpid -Force -ErrorAction SilentlyContinue } - } - - $si = New-Object System.Diagnostics.ProcessStartInfo - $si.FileName = $CF - $si.Arguments = "tunnel run --token $TOKEN" - $si.UseShellExecute = $false - $si.CreateNoWindow = $true - - $bgProc = [System.Diagnostics.Process]::Start($si) - Set-Content -LiteralPath $PIDFILE -Value $bgProc.Id -Encoding ASCII - Log "cloudflared running (PID $($bgProc.Id))" 'SETUP' - Start-Sleep -Seconds 5 -} - -# --- Wait up to 20s for cloudflared to establish connection ------------------ -# NOTE: We do NOT check the public URL here because AetherForge hasn't started -# yet (LAUNCH.bat starts it after this script returns). A 502 at this stage is -# normal - it just means the tunnel is connected but origin isn't up yet. -# The watchdog (below) will report OK once AetherForge is running. -Log "Giving cloudflared 20s to connect to Cloudflare..." 'SETUP' -Start-Sleep -Seconds 20 -$finalState = SvcState -if ($asService) { - if ($finalState -eq 'RUNNING') { - Log "Tunnel connector ready (service=$finalState). AetherForge starting next." 'OK' - } else { - Log "Service state=$finalState after 20s - may still be connecting." 'WARN' - } -} else { - if ($bgProc -and -not $bgProc.HasExited) { - Log "Tunnel connector ready (background PID $($bgProc.Id)). AetherForge starting next." 'OK' - } else { - Log "cloudflared process exited unexpectedly." 'ERROR' - } -} -Log "Public URL will be live once AetherForge starts: https://$HOSTNAME" 'SETUP' - -# ============================================================================= -# WATCHDOG - runs forever; auto-restarts cloudflared if it goes down -# ============================================================================= -Log "Watchdog started: checking every ${INTERVAL}s. Log: $LOGFILE" 'WATCH' -$fails = 0 - -while ($true) { - Start-Sleep -Seconds $INTERVAL - - $svc = SvcState - $pub = HttpGet "https://$HOSTNAME/" 12 - $procOk = ($bgProc -ne $null) -and (-not $bgProc.HasExited) - - # In background-process mode the service will always be MISSING - that is fine. - $connectorOk = ($asService -and $svc -eq 'RUNNING') -or (-not $asService -and $procOk) - $ok = $pub.Ok -and $connectorOk - - if ($ok) { - if ($fails -gt 0) { Log "Recovered after $fails failure(s)." 'OK' } - else { Log "OK svc=$svc public=HTTP $($pub.Code)" 'WATCH' } - $fails = 0 - continue - } - - $fails++ - $pubLabel = if ($pub.Ok) { "HTTP $($pub.Code)" } else { $pub.Err } - $connLabel = if ($asService) { "svc=$svc" } else { "proc=$(if($procOk){'alive'}else{'dead'})" } - Log "DEGRADED #$fails $connLabel public=$pubLabel" 'WARN' - - if ($asService) { - # --- Service mode healing --- - if ($svc -ne 'RUNNING') { - Log 'Restart: net start cloudflared' 'HEAL' - & net.exe start $SVC 2>&1 | Out-Null - Start-Sleep -Seconds 5 - if ((SvcState) -eq 'RUNNING') { - Log 'Service restarted OK.' 'HEAL' - $fails = 0 - } elseif ($fails -ge 3 -and (IsAdmin)) { - Log 'Reinstalling cloudflared service after 3 failures...' 'HEAL' - & net.exe stop $SVC 2>&1 | Out-Null - & sc.exe delete $SVC 2>&1 | Out-Null - Start-Sleep -Seconds 2 - & $CF service install $TOKEN 2>&1 | Out-Null - Start-Sleep -Seconds 2 - & net.exe start $SVC 2>&1 | Out-Null - Start-Sleep -Seconds 5 - if ((SvcState) -eq 'RUNNING') { - Log 'Service reinstalled and RUNNING.' 'HEAL' - $fails = 0 - } else { - Log 'Reinstall failed.' 'ERROR' - } - } - } else { - # Service running but public URL unreachable - Log "Service OK but public URL unreachable. Check Cloudflare dashboard route and that AetherForge is on port 8989." 'WARN' - } - } else { - # --- Background process mode healing --- - if ($bgProc -and $bgProc.HasExited) { - Log "cloudflared process exited - restarting..." 'HEAL' - $si2 = New-Object System.Diagnostics.ProcessStartInfo - $si2.FileName = $CF - $si2.Arguments = "tunnel run --token $TOKEN" - $si2.UseShellExecute = $false - $si2.CreateNoWindow = $true - $bgProc = [System.Diagnostics.Process]::Start($si2) - Set-Content -LiteralPath $PIDFILE -Value $bgProc.Id -Encoding ASCII - Log "cloudflared restarted (PID $($bgProc.Id))" 'HEAL' - $fails = 0 - Start-Sleep -Seconds 8 - } elseif ($fails -ge 3) { - Log "3 failures, killing and restarting cloudflared..." 'HEAL' - if ($bgProc -and -not $bgProc.HasExited) { $bgProc.Kill() } - Start-Sleep -Seconds 2 - $si3 = New-Object System.Diagnostics.ProcessStartInfo - $si3.FileName = $CF - $si3.Arguments = "tunnel run --token $TOKEN" - $si3.UseShellExecute = $false - $si3.CreateNoWindow = $true - $bgProc = [System.Diagnostics.Process]::Start($si3) - Set-Content -LiteralPath $PIDFILE -Value $bgProc.Id -Encoding ASCII - Log "cloudflared restarted (PID $($bgProc.Id))" 'HEAL' - $fails = 0 - Start-Sleep -Seconds 8 - } - } -} diff --git a/cloudflare/tunnel-config.json b/cloudflare/tunnel-config.json deleted file mode 100644 index 2ee2679..0000000 --- a/cloudflare/tunnel-config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "tunnel_token": "eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9", - "hostname": "killa.thetempleofdoom.com", - "origin": "http://127.0.0.1:8989", - "watch_interval_seconds": 60 -} diff --git a/cloudflare/tunnel-watch.ps1 b/cloudflare/tunnel-watch.ps1 deleted file mode 100644 index 43ff06d..0000000 --- a/cloudflare/tunnel-watch.ps1 +++ /dev/null @@ -1,158 +0,0 @@ -param( - [Parameter(Mandatory = $true)][string]$Root -) - -$ErrorActionPreference = 'Continue' -$Root = $Root.TrimEnd('\') - -$configPath = Join-Path $Root 'cloudflare\tunnel-config.json' -$repairScript = Join-Path $Root 'cloudflare\repair-tunnel.ps1' -$logDir = Join-Path $Root 'data\logs' -$logFile = Join-Path $logDir 'tunnel-watch.log' -$stateFile = Join-Path $env:ProgramData 'AetherForge\tunnel-watch-state.json' -$lockFile = Join-Path $env:ProgramData 'AetherForge\tunnel-watch.pid' - -if (-not (Test-Path $configPath)) { - Write-Host "[CF-WATCH] ERROR: missing $configPath" - exit 1 -} - -$cfg = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json -$hostname = [string]$cfg.hostname -$origin = [string]$cfg.origin -$interval = if ($cfg.watch_interval_seconds) { [int]$cfg.watch_interval_seconds } else { 60 } -$publicTimeout = if ($cfg.public_check_timeout_seconds) { [int]$cfg.public_check_timeout_seconds } else { 20 } -$originTimeout = if ($cfg.origin_check_timeout_seconds) { [int]$cfg.origin_check_timeout_seconds } else { 8 } -$healCfg = $cfg.heal_after_failures -$lvlRestart = if ($healCfg.restart_service) { [int]$healCfg.restart_service } else { 1 } -$lvlRewrite = if ($healCfg.rewrite_config_and_restart) { [int]$healCfg.rewrite_config_and_restart } else { 2 } -$lvlReinstall = if ($healCfg.full_reinstall) { [int]$healCfg.full_reinstall } else { 4 } - -New-Item -ItemType Directory -Force -Path $logDir | Out-Null -New-Item -ItemType Directory -Force -Path (Split-Path $stateFile) | Out-Null -Set-Content -LiteralPath $lockFile -Value $PID -Encoding ASCII - -function Write-WatchLog([string]$Message, [string]$Level = 'INFO') { - $line = '{0} [CF-WATCH] [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message - Write-Host $line - Add-Content -LiteralPath $logFile -Value $line -Encoding UTF8 -} - -function Get-State { - if (Test-Path $stateFile) { - try { return Get-Content $stateFile -Raw | ConvertFrom-Json } catch { } - } - return [pscustomobject]@{ - consecutive_failures = 0 - last_heal_level = 0 - last_heal_at = $null - last_ok = $null - } -} - -function Set-State($state) { - $state | ConvertTo-Json | Set-Content -LiteralPath $stateFile -Encoding UTF8 -} - -function Test-ServiceRunning { - $q = sc.exe query cloudflared 2>&1 | Out-String - if ($q -match '1060|does not exist') { return 'MISSING' } - if ($q -match 'STATE\s+:\s+\d+\s+RUNNING') { return 'RUNNING' } - if ($q -match 'STATE\s+:\s+\d+\s+STOPPED') { return 'STOPPED' } - return 'UNKNOWN' -} - -function Test-Http([string]$Url, [int]$TimeoutSec) { - try { - $r = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec $TimeoutSec - return [pscustomobject]@{ Ok = $true; Code = [int]$r.StatusCode; Error = $null } - } catch { - $code = $null - if ($_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode } - return [pscustomobject]@{ Ok = $false; Code = $code; Error = $_.Exception.Message } - } -} - -function Invoke-Heal([int]$Level) { - Write-WatchLog "Self-heal level $Level triggered" 'HEAL' - if (-not (Test-Path $repairScript)) { - Write-WatchLog "repair-tunnel.ps1 missing — cannot heal" 'ERROR' - return $false - } - $out = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $repairScript -Root $Root -Level $Level 2>&1 - foreach ($line in $out) { Write-WatchLog $line 'HEAL' } - return ($LASTEXITCODE -eq 0) -} - -Write-WatchLog "Watchdog started — interval ${interval}s | public=https://$hostname/ | origin=$origin" -Write-WatchLog "Log file: $logFile" - -while ($true) { - $state = Get-State - $svc = Test-ServiceRunning - $originCheck = Test-Http ($origin.TrimEnd('/') + '/') $originTimeout - $publicCheck = Test-Http ("https://$hostname/") $publicTimeout - - $originLabel = if ($originCheck.Ok) { "OK $($originCheck.Code)" } else { "FAIL $($originCheck.Error)" } - $publicLabel = if ($publicCheck.Ok) { "OK $($publicCheck.Code)" } else { "FAIL $($publicCheck.Error)" } - - $allOk = ($svc -eq 'RUNNING') -and $originCheck.Ok -and $publicCheck.Ok - - if ($allOk) { - if ($state.consecutive_failures -gt 0) { - Write-WatchLog "Recovered — service=$svc origin=$originLabel public=$publicLabel" 'OK' - } else { - Write-WatchLog "OK service=$svc origin=$originLabel public=$publicLabel" 'OK' - } - Set-State ([pscustomobject]@{ - consecutive_failures = 0 - last_heal_level = 0 - last_heal_at = $null - last_ok = (Get-Date).ToString('o') - }) - } else { - $state.consecutive_failures = [int]$state.consecutive_failures + 1 - $fail = $state.consecutive_failures - Write-WatchLog "DEGRADED (#$fail) service=$svc origin=$originLabel public=$publicLabel" 'WARN' - - $healLevel = 0 - if ($svc -eq 'MISSING' -or $fail -ge $lvlReinstall) { - $healLevel = 4 - } elseif ($svc -ne 'RUNNING' -and $fail -ge $lvlRestart) { - $healLevel = if ($fail -ge $lvlRewrite) { 3 } else { 1 } - } elseif ($svc -eq 'RUNNING' -and $originCheck.Ok -and -not $publicCheck.Ok) { - if ($fail -ge $lvlReinstall) { $healLevel = 4 } - elseif ($fail -ge $lvlRewrite) { $healLevel = 3 } - elseif ($fail -ge $lvlRestart) { $healLevel = 2 } - } elseif ($svc -eq 'RUNNING' -and -not $originCheck.Ok) { - Write-WatchLog "Origin down — start/repair AetherForge on $origin (tunnel cannot heal dead origin)" 'WARN' - } - - if ($healLevel -gt 0) { - $skipHeal = $false - if ($healLevel -eq 4 -and [int]$state.last_heal_level -eq 4 -and $state.last_heal_at) { - try { - $elapsed = (Get-Date) - [datetime]$state.last_heal_at - if ($elapsed.TotalMinutes -lt 10) { - Write-WatchLog "Reinstall cooldown (10 min) — skipping level 4 retry" 'WARN' - $skipHeal = $true - } - } catch { } - } - if (-not $skipHeal) { - $healed = Invoke-Heal $healLevel - $state.last_heal_level = $healLevel - $state.last_heal_at = (Get-Date).ToString('o') - if ($healed) { - Write-WatchLog "Heal level $healLevel completed — recheck next interval" 'HEAL' - } else { - Write-WatchLog "Heal level $healLevel failed — will retry on next check" 'ERROR' - } - } - } - - Set-State $state - } - - Start-Sleep -Seconds $interval -} diff --git a/cloudflare/verify-tunnel.bat b/cloudflare/verify-tunnel.bat deleted file mode 100644 index 1aba605..0000000 --- a/cloudflare/verify-tunnel.bat +++ /dev/null @@ -1,52 +0,0 @@ -@echo off -setlocal EnableExtensions EnableDelayedExpansion -title AetherForge — Tunnel Verify - -set "ROOT=%~dp0.." -if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" - -set "CF_CONFIG_JSON=%ROOT%\cloudflare\tunnel-config.json" -set "CF_HOSTNAME=killa.thetempleofdoom.com" -set "CF_ORIGIN=http://127.0.0.1:8989" - -if exist "%CF_CONFIG_JSON%" ( - for /f "usebackq delims=" %%L in (`powershell -NoProfile -Command "(Get-Content '%CF_CONFIG_JSON%' -Raw | ConvertFrom-Json).hostname"`) do set "CF_HOSTNAME=%%L" - for /f "usebackq delims=" %%L in (`powershell -NoProfile -Command "(Get-Content '%CF_CONFIG_JSON%' -Raw | ConvertFrom-Json).origin"`) do set "CF_ORIGIN=%%L" -) - -echo. -echo ================================================================ -echo AetherForge — Tunnel Verification -echo ================================================================ -echo Hostname: https://%CF_HOSTNAME% -echo Origin: %CF_ORIGIN% -echo ================================================================ -echo. - -echo [1] cloudflared service -sc query cloudflared 2>nul -if errorlevel 1 echo FAIL: cloudflared service not installed -echo. - -echo [2] DNS for %CF_HOSTNAME% -nslookup %CF_HOSTNAME% 2>nul -echo NOTE: URL must be killa.thetempleofdoom.com (two l's), not kill. -echo. - -echo [3] Local AetherForge (%CF_ORIGIN%) -powershell -NoProfile -Command "try { $r = Invoke-WebRequest -Uri '%CF_ORIGIN%/' -UseBasicParsing -TimeoutSec 5; Write-Host (' OK HTTP ' + $r.StatusCode) } catch { Write-Host (' FAIL: ' + $_.Exception.Message) }" -echo. - -echo [4] Public tunnel https://%CF_HOSTNAME%/ -powershell -NoProfile -Command "try { $r = Invoke-WebRequest -Uri 'https://%CF_HOSTNAME%/' -UseBasicParsing -TimeoutSec 15; Write-Host (' OK HTTP ' + $r.StatusCode) } catch { Write-Host (' FAIL: ' + $_.Exception.Message) }" -echo. - -echo [5] Local config on this PC -if exist "%ProgramData%\AetherForge\.cloudflared\config.yml" ( - type "%ProgramData%\AetherForge\.cloudflared\config.yml" -) else ( - echo FAIL: %ProgramData%\AetherForge\.cloudflared\config.yml not found - echo Run LAUNCH.bat as Administrator once. -) -echo. -pause diff --git a/cloudflare/write-tunnel-config.ps1 b/cloudflare/write-tunnel-config.ps1 deleted file mode 100644 index 84ef769..0000000 --- a/cloudflare/write-tunnel-config.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -param( - [Parameter(Mandatory = $true)][string]$ConfigJson, - [Parameter(Mandatory = $true)][string]$CloudflaredDir -) - -$ErrorActionPreference = 'Stop' -$cfg = Get-Content -LiteralPath $ConfigJson -Raw | ConvertFrom-Json - -if (-not $cfg.hostname) { throw 'tunnel-config.json: hostname is required' } -if (-not $cfg.origin) { throw 'tunnel-config.json: origin is required (e.g. http://127.0.0.1:8989)' } -if (-not $cfg.tunnel_token) { throw 'tunnel-config.json: tunnel_token is required' } - -$tokenJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($cfg.tunnel_token)) | ConvertFrom-Json -if (-not $tokenJson.a -or -not $tokenJson.t -or -not $tokenJson.s) { - throw 'tunnel_token is not a valid Cloudflare tunnel token' -} - -New-Item -ItemType Directory -Force -Path $CloudflaredDir | Out-Null - -$credPath = Join-Path $CloudflaredDir 'credentials.json' -$configPath = Join-Path $CloudflaredDir 'config.yml' - -$credentials = [ordered]@{ - AccountTag = [string]$tokenJson.a - TunnelSecret = [string]$tokenJson.s - TunnelID = [string]$tokenJson.t -} -($credentials | ConvertTo-Json) | Set-Content -LiteralPath $credPath -Encoding UTF8 - -$credPathYaml = ($credPath -replace '\\', '/') -$config = @" -# AetherForge local tunnel routing (written by install-tunnel.bat) -tunnel: $($tokenJson.t) -credentials-file: $credPathYaml - -ingress: - - hostname: $($cfg.hostname) - service: $($cfg.origin) - - service: http_status:404 -"@ -Set-Content -LiteralPath $configPath -Value $config -Encoding UTF8 - -Write-Output "TUNNEL_ID=$($tokenJson.t)" -Write-Output "HOSTNAME=$($cfg.hostname)" -Write-Output "ORIGIN=$($cfg.origin)" -Write-Output "CONFIG=$configPath" -Write-Output "CREDENTIALS=$credPath" diff --git a/server/internal/api/router.go b/server/internal/api/router.go index 092550b..55b93c0 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -279,12 +279,11 @@ func reconcileLoginSidecar(dataDir, sidecarPath string, users map[string]string) log.Printf("[Auth] Regenerated admin password (login-credentials.json was missing)") } -// generateRandomPassword returns a 20-character hex string suitable for use -// as an initial admin password. +// generateRandomPassword returns an 8-character password (4 random bytes as hex). func generateRandomPassword() string { - b := make([]byte, 10) + b := make([]byte, 4) if _, err := rand.Read(b); err != nil { - return "CHANGE-ME-NOW-12345" + return "aether1!" } return hex.EncodeToString(b) } diff --git a/usb/LAUNCH.bat b/usb/LAUNCH.bat index 12162c1..21b4e28 100644 --- a/usb/LAUNCH.bat +++ b/usb/LAUNCH.bat @@ -170,35 +170,19 @@ if not exist "%ROOT%\data\blueprints" mkdir "%ROOT%\data\blueprints" if not exist "%ROOT%\data\preps" mkdir "%ROOT%\data\preps" :: ---------------------------------------------------------------- -:: 5. Cloudflare Tunnel - start in background window + 60s watchdog +:: 5. Detect LAN IP for display :: ---------------------------------------------------------------- set "SERVER_PORT=8989" -set "CF_HOSTNAME=aether.thetempleofdoom.com" -set "CF_READY=0" - -if not exist "%ROOT%\cloudflare\start-tunnel.ps1" ( - echo [CF] WARNING: cloudflare\start-tunnel.ps1 missing - tunnel skipped. - goto cf_done +for /f "tokens=2 delims=:" %%I in ('ipconfig ^| findstr /i "IPv4" ^| findstr /v "127.0.0.1"') do ( + set "LAN_IP=%%I" + goto lan_done ) - -echo. -echo [CF] Starting Cloudflare tunnel for https://%CF_HOSTNAME% ... -echo [CF] Log: %ROOT%\data\logs\tunnel.log -start "AetherForge Tunnel" powershell -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\cloudflare\start-tunnel.ps1" -Root "%ROOT%" - -:: Give the tunnel script 8 seconds to start before we open the server -ping -n 9 127.0.0.1 >nul -set "CF_READY=1" - -:cf_done - -:: ---------------------------------------------------------------- -:: 6. Detect LAN IP for display -:: ---------------------------------------------------------------- set "LAN_IP=localhost" +:lan_done +set "LAN_IP=%LAN_IP: =%" :: ---------------------------------------------------------------- -:: 7. Kill any stale server process +:: 6. Kill any stale server process :: ---------------------------------------------------------------- taskkill /F /IM AetherForge.exe >nul 2>nul ping -n 2 127.0.0.1 >nul @@ -207,15 +191,11 @@ echo. echo ================================================================ echo STARTING CONTROL DECK echo ================================================================ -echo Local: http://localhost:%SERVER_PORT% -echo LAN: http://%LAN_IP%:%SERVER_PORT% -if "!CF_READY!"=="1" ( - echo Public: https://!CF_HOSTNAME! - echo Dropper PS: iex -irm https://!CF_HOSTNAME!/install.ps1 -) -echo Data: %ROOT%\data\ +echo Local: http://localhost:%SERVER_PORT% +echo LAN: http://%LAN_IP%:%SERVER_PORT% +echo Data: %ROOT%\data\ echo. -echo Dashboard login printed below each start. +echo Login credentials printed below. echo Press Ctrl+C to stop. echo ================================================================ echo. @@ -235,8 +215,6 @@ if "!EC!"=="0" ( echo If port %SERVER_PORT% is in use, close other AetherForge windows and retry. ) -:: Cloudflare tunnel runs as a Windows service — stays up after LAUNCH.bat exits - echo. pause endlocal diff --git a/usb/cloudflare/start-tunnel.ps1 b/usb/cloudflare/start-tunnel.ps1 deleted file mode 100644 index 9ea93f8..0000000 --- a/usb/cloudflare/start-tunnel.ps1 +++ /dev/null @@ -1,302 +0,0 @@ -#Requires -Version 5 -param([string]$Root) -$ErrorActionPreference = 'Continue' -$Root = ($Root -replace '\\$','') - -# --- Config (overridden by tunnel-config.json if present) -------------------- -$TOKEN = 'eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9' -$HOSTNAME = 'aether.thetempleofdoom.com' -$ORIGIN = 'http://127.0.0.1:8989' -$SVC = 'cloudflared' -$DESTDIR = "$env:ProgramData\AetherForge\bin" -$LOGDIR = "$Root\data\logs" -$LOGFILE = "$LOGDIR\tunnel.log" -$PIDFILE = "$env:ProgramData\AetherForge\tunnel.pid" -$INTERVAL = 60 - -$cfgPath = Join-Path $PSScriptRoot 'tunnel-config.json' -if (Test-Path $cfgPath) { - try { - $c = Get-Content $cfgPath -Raw | ConvertFrom-Json - if ($c.tunnel_token) { $TOKEN = $c.tunnel_token } - if ($c.hostname) { $HOSTNAME = $c.hostname } - if ($c.origin) { $ORIGIN = $c.origin } - if ($c.watch_interval_seconds){ $INTERVAL = [int]$c.watch_interval_seconds } - } catch {} -} - -# --- Helpers ----------------------------------------------------------------- -function Log([string]$msg, [string]$tag = 'INFO') { - $line = '{0} [TUNNEL][{1}] {2}' -f (Get-Date -Format 'HH:mm:ss'), $tag, $msg - Write-Host $line - try { Add-Content -LiteralPath $LOGFILE -Value $line -Encoding UTF8 } catch {} -} - -function SvcState { - $q = (& sc.exe query $SVC 2>&1) -join ' ' - if ($q -match '1060|does not exist') { return 'MISSING' } - if ($q -match 'RUNNING') { return 'RUNNING' } - if ($q -match 'STOPPED') { return 'STOPPED' } - return 'UNKNOWN' -} - -function IsAdmin { - ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() - ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -} - -function HttpGet([string]$url, [int]$sec = 10) { - try { - $r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec $sec - return [pscustomobject]@{ Ok=$true; Code=[int]$r.StatusCode; Err=$null } - } catch { - $code = $null - if ($_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode } - return [pscustomobject]@{ Ok=$false; Code=$code; Err=$_.Exception.Message } - } -} - -# --- Locate cloudflared ------------------------------------------------------- -function FindCloudflared { - # 1. Already staged in ProgramData - if (Test-Path "$DESTDIR\cloudflared.exe") { return "$DESTDIR\cloudflared.exe" } - - New-Item -ItemType Directory -Force -Path $DESTDIR | Out-Null - - # 2. Bundled next to this script on the USB - $bundled = Join-Path $PSScriptRoot 'cloudflared.exe' - if (Test-Path $bundled) { - Copy-Item $bundled "$DESTDIR\cloudflared.exe" -Force - return "$DESTDIR\cloudflared.exe" - } - - # 3. MSI-installed on this PC - foreach ($dir in @("$env:ProgramFiles\cloudflare\cloudflared", - "${env:ProgramFiles(x86)}\cloudflare\cloudflared")) { - if (Test-Path "$dir\cloudflared.exe") { - Copy-Item "$dir\cloudflared.exe" "$DESTDIR\cloudflared.exe" -Force - return "$DESTDIR\cloudflared.exe" - } - } - - # 4. Run the MSI if bundled - $msi = Join-Path $PSScriptRoot 'cloudflared-windows-amd64.msi' - if (Test-Path $msi) { - Log 'Installing cloudflared MSI...' 'SETUP' - Start-Process msiexec -ArgumentList "/i `"$msi`" /quiet /norestart" -Wait - Start-Sleep -Seconds 6 - if (Test-Path "$env:ProgramFiles\cloudflare\cloudflared\cloudflared.exe") { - Copy-Item "$env:ProgramFiles\cloudflare\cloudflared\cloudflared.exe" "$DESTDIR\cloudflared.exe" -Force - return "$DESTDIR\cloudflared.exe" - } - } - - # 5. Download from GitHub as last resort - Log 'Downloading cloudflared from GitHub...' 'SETUP' - try { - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe' - Invoke-WebRequest -Uri $url -OutFile "$DESTDIR\cloudflared.exe" -UseBasicParsing - if (Test-Path "$DESTDIR\cloudflared.exe") { return "$DESTDIR\cloudflared.exe" } - } catch { Log "Download failed: $_" 'ERROR' } - - return $null -} - -# ============================================================================= -# MAIN -# ============================================================================= -New-Item -ItemType Directory -Force -Path $LOGDIR | Out-Null -New-Item -ItemType Directory -Force -Path $DESTDIR | Out-Null - -Log "Tunnel setup hostname=$HOSTNAME origin=$ORIGIN" 'SETUP' - -$CF = FindCloudflared -if (-not $CF) { - Log 'Cannot locate cloudflared.exe - tunnel skipped.' 'ERROR' - exit 1 -} -Log "Using: $CF" 'SETUP' - -# --- Install + start as Windows service (admin path) ------------------------- -$asService = $false - -if (IsAdmin) { - # Stop and remove any existing service - if ((SvcState) -ne 'MISSING') { - Log 'Removing existing cloudflared service...' 'SETUP' - & net.exe stop $SVC 2>&1 | Out-Null - Start-Sleep -Seconds 2 - & sc.exe delete $SVC 2>&1 | Out-Null - Start-Sleep -Seconds 2 - } - - # Clear stale EventLog registry key left by previous installs. - # cloudflared rolls back the entire service install if this key already exists. - $evtKey = 'HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\Application\Cloudflared' - if (Test-Path $evtKey) { - Log 'Removing stale EventLog registry key...' 'SETUP' - Remove-Item -LiteralPath $evtKey -Force -ErrorAction SilentlyContinue - Start-Sleep -Seconds 1 - } - - Log 'Installing cloudflared Windows service...' 'SETUP' - # Capture output without triggering PS NativeCommandError decorations - $installOut = (& $CF service install $TOKEN 2>&1) -join "`n" - Log $installOut.Trim() 'SETUP' - Start-Sleep -Seconds 3 - - Log 'Starting service...' 'SETUP' - & net.exe start $SVC 2>&1 | Out-Null - Start-Sleep -Seconds 6 - - if ((SvcState) -eq 'RUNNING') { - Log 'Service RUNNING' 'OK' - $asService = $true - } else { - Log "Service not RUNNING after install (state=$(SvcState)) - falling back to background process." 'WARN' - } -} else { - Log 'Not running as Administrator.' 'WARN' - Log 'For a persistent Windows service: right-click LAUNCH.bat > Run as administrator.' 'WARN' - if ((SvcState) -eq 'RUNNING') { - Log 'Existing cloudflared service is RUNNING - using it.' 'OK' - $asService = $true - } -} - -# --- Run directly as background process (non-admin fallback) ----------------- -$bgProc = $null -if (-not $asService) { - Log 'Starting cloudflared as background process...' 'SETUP' - - # Kill leftover from previous run - if (Test-Path $PIDFILE) { - $oldpid = [int](Get-Content $PIDFILE -Raw -ErrorAction SilentlyContinue) - if ($oldpid) { Stop-Process -Id $oldpid -Force -ErrorAction SilentlyContinue } - } - - $si = New-Object System.Diagnostics.ProcessStartInfo - $si.FileName = $CF - $si.Arguments = "tunnel run --token $TOKEN" - $si.UseShellExecute = $false - $si.CreateNoWindow = $true - - $bgProc = [System.Diagnostics.Process]::Start($si) - Set-Content -LiteralPath $PIDFILE -Value $bgProc.Id -Encoding ASCII - Log "cloudflared running (PID $($bgProc.Id))" 'SETUP' - Start-Sleep -Seconds 5 -} - -# --- Wait up to 20s for cloudflared to establish connection ------------------ -# NOTE: We do NOT check the public URL here because AetherForge hasn't started -# yet (LAUNCH.bat starts it after this script returns). A 502 at this stage is -# normal - it just means the tunnel is connected but origin isn't up yet. -# The watchdog (below) will report OK once AetherForge is running. -Log "Giving cloudflared 20s to connect to Cloudflare..." 'SETUP' -Start-Sleep -Seconds 20 -$finalState = SvcState -if ($asService) { - if ($finalState -eq 'RUNNING') { - Log "Tunnel connector ready (service=$finalState). AetherForge starting next." 'OK' - } else { - Log "Service state=$finalState after 20s - may still be connecting." 'WARN' - } -} else { - if ($bgProc -and -not $bgProc.HasExited) { - Log "Tunnel connector ready (background PID $($bgProc.Id)). AetherForge starting next." 'OK' - } else { - Log "cloudflared process exited unexpectedly." 'ERROR' - } -} -Log "Public URL will be live once AetherForge starts: https://$HOSTNAME" 'SETUP' - -# ============================================================================= -# WATCHDOG - runs forever; auto-restarts cloudflared if it goes down -# ============================================================================= -Log "Watchdog started: checking every ${INTERVAL}s. Log: $LOGFILE" 'WATCH' -$fails = 0 - -while ($true) { - Start-Sleep -Seconds $INTERVAL - - $svc = SvcState - $pub = HttpGet "https://$HOSTNAME/" 12 - $procOk = ($bgProc -ne $null) -and (-not $bgProc.HasExited) - - # In background-process mode the service will always be MISSING - that is fine. - $connectorOk = ($asService -and $svc -eq 'RUNNING') -or (-not $asService -and $procOk) - $ok = $pub.Ok -and $connectorOk - - if ($ok) { - if ($fails -gt 0) { Log "Recovered after $fails failure(s)." 'OK' } - else { Log "OK svc=$svc public=HTTP $($pub.Code)" 'WATCH' } - $fails = 0 - continue - } - - $fails++ - $pubLabel = if ($pub.Ok) { "HTTP $($pub.Code)" } else { $pub.Err } - $connLabel = if ($asService) { "svc=$svc" } else { "proc=$(if($procOk){'alive'}else{'dead'})" } - Log "DEGRADED #$fails $connLabel public=$pubLabel" 'WARN' - - if ($asService) { - # --- Service mode healing --- - if ($svc -ne 'RUNNING') { - Log 'Restart: net start cloudflared' 'HEAL' - & net.exe start $SVC 2>&1 | Out-Null - Start-Sleep -Seconds 5 - if ((SvcState) -eq 'RUNNING') { - Log 'Service restarted OK.' 'HEAL' - $fails = 0 - } elseif ($fails -ge 3 -and (IsAdmin)) { - Log 'Reinstalling cloudflared service after 3 failures...' 'HEAL' - & net.exe stop $SVC 2>&1 | Out-Null - & sc.exe delete $SVC 2>&1 | Out-Null - Start-Sleep -Seconds 2 - & $CF service install $TOKEN 2>&1 | Out-Null - Start-Sleep -Seconds 2 - & net.exe start $SVC 2>&1 | Out-Null - Start-Sleep -Seconds 5 - if ((SvcState) -eq 'RUNNING') { - Log 'Service reinstalled and RUNNING.' 'HEAL' - $fails = 0 - } else { - Log 'Reinstall failed.' 'ERROR' - } - } - } else { - # Service running but public URL unreachable - Log "Service OK but public URL unreachable. Check Cloudflare dashboard route and that AetherForge is on port 8989." 'WARN' - } - } else { - # --- Background process mode healing --- - if ($bgProc -and $bgProc.HasExited) { - Log "cloudflared process exited - restarting..." 'HEAL' - $si2 = New-Object System.Diagnostics.ProcessStartInfo - $si2.FileName = $CF - $si2.Arguments = "tunnel run --token $TOKEN" - $si2.UseShellExecute = $false - $si2.CreateNoWindow = $true - $bgProc = [System.Diagnostics.Process]::Start($si2) - Set-Content -LiteralPath $PIDFILE -Value $bgProc.Id -Encoding ASCII - Log "cloudflared restarted (PID $($bgProc.Id))" 'HEAL' - $fails = 0 - Start-Sleep -Seconds 8 - } elseif ($fails -ge 3) { - Log "3 failures, killing and restarting cloudflared..." 'HEAL' - if ($bgProc -and -not $bgProc.HasExited) { $bgProc.Kill() } - Start-Sleep -Seconds 2 - $si3 = New-Object System.Diagnostics.ProcessStartInfo - $si3.FileName = $CF - $si3.Arguments = "tunnel run --token $TOKEN" - $si3.UseShellExecute = $false - $si3.CreateNoWindow = $true - $bgProc = [System.Diagnostics.Process]::Start($si3) - Set-Content -LiteralPath $PIDFILE -Value $bgProc.Id -Encoding ASCII - Log "cloudflared restarted (PID $($bgProc.Id))" 'HEAL' - $fails = 0 - Start-Sleep -Seconds 8 - } - } -} diff --git a/usb/cloudflare/tunnel-config.json b/usb/cloudflare/tunnel-config.json deleted file mode 100644 index a16fb6a..0000000 --- a/usb/cloudflare/tunnel-config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "tunnel_token": "eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWFhYzYzMTctMzgyYS00OTM3LTgxY2YtYjM2ZjVkNjZjYTU4IiwicyI6Ik1XRXlaV0ZqTlRndE5XTTRPUzAwT0RCa0xXRTNaR010WkdRNU56UTJZMlJoTmpNMiJ9", - "hostname": "aether.thetempleofdoom.com", - "origin": "http://127.0.0.1:8989", - "watch_interval_seconds": 60 -}