param( [ValidateSet('host', 'join', 'install')][string]$Mode = 'install', [string[]]$RendezvousArguments = @() ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest $Version = '0.2.19' $ReleaseOrigin = 'https://rendezvous.sh' $ReleaseSha256 = '8a8cb681cefbb6d609f2d3c2e3e191816eb18efcf7424286629b9b04b82dd788' $ReleaseParts = @('rendezvous-0.2.19-windows-x86_64.zip.part00', 'rendezvous-0.2.19-windows-x86_64.zip.part01') if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'This installer requires Windows.' } if (-not [Environment]::Is64BitOperatingSystem -or -not [Environment]::Is64BitProcess) { throw 'Use 64-bit PowerShell on Windows x64.' } if ($env:PROCESSOR_ARCHITECTURE -ne 'AMD64' -or $env:PROCESSOR_ARCHITEW6432) { throw 'This package requires Windows x64.' } if ([Environment]::OSVersion.Version.Build -lt 17763) { throw 'Windows 10 version 1809 or newer is required.' } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $UserSid = [Security.Principal.WindowsIdentity]::GetCurrent().User $SystemSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-18') $PrivateAcl = [Security.AccessControl.DirectorySecurity]::new() $PrivateAcl.SetOwner($UserSid) $PrivateAcl.SetAccessRuleProtection($true, $false) foreach ($Sid in @($UserSid, $SystemSid)) { $PrivateAcl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($Sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')) } function Assert-PrivateDirectory([string]$Path) { $Item = Get-Item -LiteralPath $Path -Force if (-not $Item.PSIsContainer -or ($Item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw "Unsafe installation directory: $Path" } $Acl = Get-Acl -LiteralPath $Path if ($Acl.GetOwner([Security.Principal.SecurityIdentifier]).Value -ne $UserSid.Value) { throw "Unexpected installation owner: $Path" } foreach ($Rule in $Acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) { if ($Rule.AccessControlType -ne 'Allow' -or $Rule.IdentityReference.Value -notin @($UserSid.Value, $SystemSid.Value)) { throw "Installation directory is not private: $Path" } } } function New-PrivateDirectory([string]$Path) { $Directory = [IO.DirectoryInfo]::new($Path) if (-not $Directory.Exists) { if ($PSVersionTable.PSVersion.Major -ge 7) { [IO.FileSystemAclExtensions]::Create($Directory, $PrivateAcl) } else { $Directory.Create($PrivateAcl) } } Assert-PrivateDirectory $Path } $Root = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Rendezvous' New-PrivateDirectory $Root $InstallLock = [IO.File]::Open((Join-Path $Root '.install-lock'), [IO.FileMode]::OpenOrCreate, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None) $Work = Join-Path $Root ('download-' + [Guid]::NewGuid().ToString('N')) $Runtime = $null try { New-PrivateDirectory $Work $CurrentFile = Join-Path $Root 'current.json' if (Test-Path -LiteralPath $CurrentFile) { if ((Get-Item -LiteralPath $CurrentFile -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Unsafe installation marker.' } $Current = Get-Content -LiteralPath $CurrentFile -Raw | ConvertFrom-Json if ($Current.version -eq $Version -and $Current.sha256 -eq $ReleaseSha256 -and $Current.runtime -match '^runtime-\d+\.\d+\.\d+\.[a-f0-9]+$') { $Candidate = Join-Path $Root $Current.runtime Assert-PrivateDirectory $Candidate if (Test-Path -LiteralPath (Join-Path $Candidate 'bin\rdzv.exe')) { $Runtime = $Candidate } } } if (-not $Runtime) { Write-Host "Downloading Rendezvous $Version..." $Archive = Join-Path $Work 'runtime.zip' $Output = [IO.File]::Open($Archive, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) try { foreach ($Part in $ReleaseParts) { if ($Part -notmatch '^rendezvous-\d+\.\d+\.\d+-windows-x86_64\.zip\.part\d{2}$') { throw 'Invalid release part.' } $PartPath = Join-Path $Work $Part Invoke-WebRequest -UseBasicParsing -Uri "$ReleaseOrigin/releases/$Version/$Part" -OutFile $PartPath $InputStream = [IO.File]::OpenRead($PartPath) try { $InputStream.CopyTo($Output) } finally { $InputStream.Dispose() } Remove-Item -LiteralPath $PartPath } } finally { $Output.Dispose() } if ((Get-FileHash -LiteralPath $Archive -Algorithm SHA256).Hash.ToLowerInvariant() -ne $ReleaseSha256) { throw 'Rendezvous archive checksum mismatch.' } $Runtime = Join-Path $Root ("runtime-$Version." + [Guid]::NewGuid().ToString('N')) New-PrivateDirectory $Runtime Add-Type -AssemblyName System.IO.Compression.FileSystem $Zip = [IO.Compression.ZipFile]::OpenRead($Archive) try { $Prefix = $Runtime + [IO.Path]::DirectorySeparatorChar $Seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $Total = [long]0 foreach ($Entry in $Zip.Entries) { $Name = $Entry.FullName if ($Name -match '(^[/\\]|[\\:]|(^|/)\.\.?(/|$)|[. ](/|$))' -or (($Entry.ExternalAttributes -shr 16) -band 0xF000) -eq 0xA000) { throw 'Unsafe release archive entry.' } $Target = [IO.Path]::GetFullPath((Join-Path $Runtime $Name)) if (-not $Target.StartsWith($Prefix, [StringComparison]::OrdinalIgnoreCase) -or -not $Seen.Add($Target)) { throw 'Invalid or duplicate release archive path.' } $Total += $Entry.Length if ($Total -gt 2GB -or $Seen.Count -gt 30000) { throw 'Release archive exceeds installation limits.' } if ($Name.EndsWith('/')) { [IO.Directory]::CreateDirectory($Target) | Out-Null; continue } [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($Target)) | Out-Null [IO.Compression.ZipFileExtensions]::ExtractToFile($Entry, $Target, $false) } } finally { $Zip.Dispose() } $Metadata = Get-Content -LiteralPath (Join-Path $Runtime 'BUILD.json') -Raw | ConvertFrom-Json if ($Metadata.version -ne $Version -or $Metadata.platform -ne 'windows-x86_64') { throw 'Release metadata does not match the installer.' } $Executable = Join-Path $Runtime 'bin\rdzv.exe' $ReportedVersion = & $Executable --version if ($LASTEXITCODE -ne 0 -or $ReportedVersion -ne "Rendezvous $Version") { throw 'Installed runtime failed its version check.' } $Record = @{ version = $Version; sha256 = $ReleaseSha256; runtime = [IO.Path]::GetFileName($Runtime) } | ConvertTo-Json $Pending = Join-Path $Work 'current.json' [IO.File]::WriteAllText($Pending, $Record, [Text.UTF8Encoding]::new($false)) if ([IO.File]::Exists($CurrentFile)) { [IO.File]::Replace($Pending, $CurrentFile, $null) } else { [IO.File]::Move($Pending, $CurrentFile) } } $Bin = Join-Path $Runtime 'bin' $Owned = [regex]::Escape($Root) + '\\runtime-\d+\.\d+\.\d+\.[a-f0-9]+\\bin\\?$' $UserPath = [Environment]::GetEnvironmentVariable('Path', 'User') $Entries = @($UserPath -split ';' | Where-Object { $_ -and $_ -notmatch ('^' + $Owned) }) [Environment]::SetEnvironmentVariable('Path', (@($Bin) + $Entries -join ';'), 'User') $env:Path = @($Bin) + @($env:Path -split ';' | Where-Object { $_ -and $_ -notmatch ('^' + $Owned) }) -join ';' $Executable = Join-Path $Bin 'rdzv.exe' & $Executable --prune-runtimes if ($LASTEXITCODE -ne 0) { throw 'Installed runtime cleanup failed.' } } finally { if (Test-Path -LiteralPath $Work) { Remove-Item -LiteralPath $Work -Recurse -Force } $InstallLock.Dispose() } if ($Mode -eq 'install') { Write-Host "Rendezvous $Version is installed. Run rdzv host or rdzv join." } else { & $Executable $Mode @RendezvousArguments }