# WinPanel installer. # # irm https://wpinstall.dzbilisim.com.tr | iex # # Run it in an elevated PowerShell on a clean Windows Server (2019 or newer). It downloads the newest signed release from the # update address, checks the publisher's signature and the file's hash, installs what WinPanel needs (IIS, SQL Server Express, # the .NET Hosting Bundle, URL Rewrite, the FTP and DNS roles), then WinPanel itself. Nothing is run before the signature has been checked. # # Options are given as environment variables before the command (a piped script cannot take parameters): # $env:WPINSTALL_HOSTNAME the server's public name (default: this computer's name) # $env:WPINSTALL_PORT the panel's HTTPS port (default 3241) # $env:WPINSTALL_WITH extra parts, comma separated: clamav,spamassassin,roundcube,adminer (default: none) # $env:WPINSTALL_SKIP_SQL "1" to use an SQL Server that is there already instead of installing Express # $env:WPINSTALL_MAILENABLE path of the MailEnable installer (it is not redistributed); without it mail is left out # $env:WPINSTALL_DRYRUN "1" to show what would be done without changing anything & { $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $updateUrl = 'https://wpupdate.dzbilisim.com.tr/latest.json' $publicKeyPem = @' -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFLauI1x3yegUMrPxRH0CO+hLym9B 0PGi6fBqFxU3GrehiSj9HRfSaJm8pf7vBdDeXozw4R5SCeVCoQYLVN0s2A== -----END PUBLIC KEY----- '@ function Fail([string]$Message) { Write-Host "ERROR: $Message" -ForegroundColor Red; throw $Message } function Step([string]$Message) { Write-Host "==> $Message" -ForegroundColor Cyan } $principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Fail 'Open PowerShell as Administrator and run the command again.' } if ($PSVersionTable.PSVersion.Major -lt 5) { Fail 'Windows PowerShell 5.1 or newer is required.' } if (-not [Environment]::Is64BitOperatingSystem) { Fail 'A 64-bit Windows is required.' } $os = Get-CimInstance Win32_OperatingSystem if ([version]$os.Version -lt [version]'10.0.17763') { Fail "Windows Server 2019 or newer is required (this is $($os.Caption))." } # ---- the signed release ---------------------------------------------------------------------------------------- Step "Reading the newest release from $updateUrl" $raw = (Invoke-WebRequest -Uri $updateUrl -UseBasicParsing -TimeoutSec 60).Content if ($raw.Length -gt 65536) { Fail 'The release information is larger than allowed.' } $signed = $raw | ConvertFrom-Json $payload = [Convert]::FromBase64String($signed.payload) $signature = [Convert]::FromBase64String($signed.signature) # The public key is a SubjectPublicKeyInfo of a P-256 key: its last 65 bytes are 0x04 followed by X and Y. $der = [Convert]::FromBase64String((($publicKeyPem -split "`n") | Where-Object { $_ -notmatch '-----' } | ForEach-Object { $_.Trim() }) -join '') $point = $der[($der.Length - 64)..($der.Length - 1)] $blob = [byte[]](0x45, 0x43, 0x53, 0x31, 0x20, 0x00, 0x00, 0x00) + $point $cng = [Security.Cryptography.CngKey]::Import($blob, [Security.Cryptography.CngKeyBlobFormat]::EccPublicBlob) $ecdsa = New-Object Security.Cryptography.ECDsaCng($cng) if (-not $ecdsa.VerifyData($payload, $signature, [Security.Cryptography.HashAlgorithmName]::SHA256)) { Fail 'The release is NOT signed by the publisher. Nothing was installed.' } $manifest = [Text.Encoding]::UTF8.GetString($payload) | ConvertFrom-Json if ($manifest.product -ne 'WinPanel' -or $manifest.version -notmatch '^\d{1,4}\.\d{1,4}\.\d{1,4}$' -or $manifest.zipFile -notmatch '^[A-Za-z0-9._-]{1,100}\.zip$' -or $manifest.zipSha256 -notmatch '^[0-9a-f]{64}$') { Fail 'The release information is incomplete.' } Write-Host "Signature checked: WinPanel $($manifest.version)" # ---- download and unpack ------------------------------------------------------------------------------------------- $setup = Join-Path $env:ProgramData 'WinPanel\setup' New-Item -ItemType Directory -Force $setup | Out-Null $zip = Join-Path $setup $manifest.zipFile $staged = Join-Path $setup $manifest.version $dry = $env:WPINSTALL_DRYRUN -eq '1' Step "Downloading $($manifest.zipFile) ($([math]::Round($manifest.zipBytes / 1MB)) MB)" if (-not $dry) { Invoke-WebRequest -Uri ([Uri]::new([Uri]$updateUrl, $manifest.zipFile)) -OutFile $zip -UseBasicParsing -TimeoutSec 3600 if ((Get-Item $zip).Length -ne $manifest.zipBytes) { Fail 'The downloaded file has the wrong size.' } if ((Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant() -ne $manifest.zipSha256) { Fail 'The downloaded file does not match the signed release (its hash differs).' } if (Test-Path $staged) { cmd.exe /c rmdir /s /q "`"$staged`"" } New-Item -ItemType Directory -Force $staged | Out-Null Step 'Unpacking' tar.exe -xf $zip -C $staged if ($LASTEXITCODE -ne 0) { Fail 'The package could not be unpacked.' } } # ---- prerequisites and WinPanel --------------------------------------------------------------------------------- $hostName = if ($env:WPINSTALL_HOSTNAME) { $env:WPINSTALL_HOSTNAME } else { [Net.Dns]::GetHostName() } $port = if ($env:WPINSTALL_PORT) { [int]$env:WPINSTALL_PORT } else { 3241 } $with = if ($env:WPINSTALL_WITH) { $env:WPINSTALL_WITH.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ } } else { @() } $prerequisites = Join-Path $staged 'deploy\install-prerequisites.ps1' $arguments = @{ With = $with } if ($env:WPINSTALL_SKIP_SQL -eq '1') { $arguments.SkipSql = $true } if ($env:WPINSTALL_MAILENABLE) { $arguments.MailEnableInstaller = $env:WPINSTALL_MAILENABLE } if ($dry) { $arguments.DryRun = $true } Step 'Installing what WinPanel needs' if ($dry) { Write-Host "[dry-run] $prerequisites" } else { & $prerequisites @arguments } Step "Installing WinPanel $($manifest.version)" $install = Join-Path $staged 'deploy\install.ps1' if ($dry) { Write-Host "[dry-run] $install -HostName $hostName -Port $port" } else { & $install -HostName $hostName -Port $port } if (-not $dry) { foreach ($part in $with) { $script = Join-Path $staged "deploy\install-$part.ps1" if (Test-Path $script) { Step "Installing $part"; & $script } else { Write-Host "Unknown part '$part' skipped." -ForegroundColor Yellow } } } Write-Host '' if ($dry) { Write-Host 'Dry run finished: nothing was installed.'; return } Write-Host "WinPanel $($manifest.version) is installed. Open https://${hostName}:${port} and create the first administrator." -ForegroundColor Green Write-Host 'From now on this server checks the update address by itself and installs new releases in the quiet hours (Updates page).' }