[CmdletBinding()] param([switch]$CheckOnly,[switch]$Install,[string]$PlayerRoaming='',[string]$PlayerSid='',[string]$RenderPreview='') $ErrorActionPreference='Stop' # Public connection settings only. No game files, passwords or private keys are included. $ServerAddress='foreverwow.servegame.com' $Portal='trinity.actual.battle.net' $PublicKey='02596F0D0C061A8B30745988FD72C59E29EC367FB0F341F28E0F08D037BAFC69' $ExpectedCaSha256='1EDA827C3CB1216CCC29FB8B164BEE92EA1D0668597AA49FEB9670B74E944CFD' $ExpectedThumbprint='B85187CCA9B97C41AA408CA695CF6AC66D5FD908' $Website='https://foreverwow.servegame.com' $Utf8=New-Object Text.UTF8Encoding($false) [Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 function Get-BytesHash([byte[]]$Bytes){$sha=[Security.Cryptography.SHA256]::Create();try{return ([BitConverter]::ToString($sha.ComputeHash($Bytes))).Replace('-','')}finally{$sha.Dispose()}} function Write-AtomicText([string]$Path,[string]$Text){ $temp=$Path+'.foreverwow-'+[guid]::NewGuid().ToString('N')+'.tmp' try{[IO.File]::WriteAllText($temp,$Text,$Utf8);if([IO.File]::Exists($Path)){[IO.File]::Replace($temp,$Path,[NullString]::Value)}else{[IO.File]::Move($temp,$Path)}}finally{if([IO.File]::Exists($temp)){[IO.File]::Delete($temp)}} } function Test-GamePort([string]$Address,[int]$Port){ $client=New-Object Net.Sockets.TcpClient try{$task=$client.ConnectAsync($Address,$Port);if(-not $task.Wait(4000)){return $false};return $client.Connected}catch{return $false}finally{$client.Dispose()} } function Get-UpdatedHosts([string[]]$ExistingLines,[string]$Address,[string]$GamePortal){ $result=New-Object 'System.Collections.Generic.List[string]' foreach($line in $ExistingLines){ $parts=(($line -split '#',2)[0].Trim() -split '\s+') if($parts.Count -gt 1 -and @($parts[1..($parts.Count-1)] | Where-Object {$_ -ieq $GamePortal}).Count -gt 0){$others=@($parts[1..($parts.Count-1)] | Where-Object {$_ -ine $GamePortal});if($others.Count -gt 0){$result.Add($parts[0]+"`t"+($others -join ' '))}}else{$result.Add($line)} } $result.Add($Address+"`t"+$GamePortal+' # ForeverWoW player connection') return ,$result.ToArray() } function Get-UpdatedLauncherConfig([string]$ExistingJson){ if($ExistingJson){$config=$ExistingJson | ConvertFrom-Json;if(-not $config.PSObject.Properties['servers'] -or $config.servers -is [string]){throw 'The existing servers.json has an unknown format. No configuration was changed.'}}else{$config=[pscustomobject]@{servers=@()}} $entry=[pscustomobject]@{name='WoW Forever';portal=$Portal;publicKey=$PublicKey;regionGroup=8;loginPort=1119;worldPort=8085;website=$Website;statusUrl=''} $config.servers=@(@($config.servers | Where-Object {$_.name -ne 'WoW Forever'})+@($entry)) return $config } function Get-ConnectionCheck { $resolved=@([Net.Dns]::GetHostAddresses($ServerAddress) | Where-Object AddressFamily -eq InterNetwork | ForEach-Object IPAddressToString) if($resolved.Count -eq 0){throw 'The No-IP hostname has no IPv4 address.'} $web=New-Object Net.WebClient try{$caBytes=$web.DownloadData($Website+'/download/connection/forever_root_ca.crt')}finally{$web.Dispose()} if((Get-BytesHash $caBytes) -ne $ExpectedCaSha256){throw 'The downloaded server certificate does not match the pinned SHA256. Nothing was installed.'} $certificate=[Security.Cryptography.X509Certificates.X509Certificate2]::new($caBytes) if($certificate.Thumbprint -ne $ExpectedThumbprint -or $certificate.NotAfter -lt (Get-Date) -or $certificate.NotBefore -gt (Get-Date)){throw 'Invalid or expired game server certificate.'} $basic=$certificate.Extensions | Where-Object {$_.Oid.Value -eq '2.5.29.19'} $constraints=$certificate.Extensions | Where-Object {$_.Oid.Value -eq '2.5.29.30'} if(-not $basic.CertificateAuthority -or -not $constraints.Critical){throw 'The pinned CA is missing its CA flag or critical DNS name constraints.'} $ports=@(1119,8081,8085 | ForEach-Object {[pscustomobject]@{Port=$_;Reachable=(Test-GamePort $resolved[0] $_)}}) return [pscustomobject]@{Server=$ServerAddress;IPv4=$resolved[0];WebsiteHttpsValidated=$true;CertificateSha256=$ExpectedCaSha256;CertificateThumbprint=$certificate.Thumbprint;Ports=$ports;CaBytes=$caBytes} } if($CheckOnly){ try{$check=Get-ConnectionCheck;$check | Select-Object Server,IPv4,WebsiteHttpsValidated,CertificateSha256,CertificateThumbprint,Ports | ConvertTo-Json -Depth 4;if(@($check.Ports | Where-Object {-not $_.Reachable}).Count -gt 0){exit 2};exit 0}catch{Write-Error $_;exit 1} } Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $French=([Globalization.CultureInfo]::CurrentUICulture.TwoLetterISOLanguageName -eq 'fr') function L([string]$En,[string]$Fr){if($French){$Fr}else{$En}} if(-not $PlayerRoaming){$PlayerRoaming=[Environment]::GetFolderPath('ApplicationData')} if(-not $PlayerSid){$PlayerSid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value} $form=New-Object Windows.Forms.Form $form.Text='WoW Forever | Player connection';$form.Size=New-Object Drawing.Size(730,590);$form.MinimumSize=$form.Size;$form.StartPosition='CenterScreen';$form.BackColor=[Drawing.Color]::FromArgb(14,20,29);$form.ForeColor=[Drawing.Color]::Gainsboro;$form.Font=New-Object Drawing.Font('Segoe UI',10) $title=New-Object Windows.Forms.Label;$title.Text='WoW Forever';$title.Font=New-Object Drawing.Font('Segoe UI Semibold',23);$title.ForeColor=[Drawing.Color]::FromArgb(230,207,153);$title.Location=New-Object Drawing.Point(26,20);$title.AutoSize=$true;$form.Controls.Add($title) $intro=New-Object Windows.Forms.Label;$intro.Location=New-Object Drawing.Point(28,76);$intro.Size=New-Object Drawing.Size(655,85);$intro.Text=L 'Connect your official WoW Classic Beta client to foreverwow.servegame.com. This assistant verifies the public server certificate, backs up your connection files and adds this server to ForeverLauncher. It does not install the game.' 'Connectez votre client officiel WoW Classic Beta à foreverwow.servegame.com. Cet assistant vérifie le certificat public, sauvegarde vos fichiers de connexion et ajoute le serveur à ForeverLauncher. Il n''installe pas le jeu.';$form.Controls.Add($intro) $output=New-Object Windows.Forms.TextBox;$output.Multiline=$true;$output.ReadOnly=$true;$output.ScrollBars='Vertical';$output.Location=New-Object Drawing.Point(28,170);$output.Size=New-Object Drawing.Size(655,205);$output.BackColor=[Drawing.Color]::FromArgb(8,13,20);$output.ForeColor=[Drawing.Color]::FromArgb(191,205,221);$output.BorderStyle='FixedSingle';$form.Controls.Add($output) $progress=New-Object Windows.Forms.ProgressBar;$progress.Location=New-Object Drawing.Point(28,391);$progress.Size=New-Object Drawing.Size(655,12);$form.Controls.Add($progress) function Status([string]$Text,[int]$Percent){$output.AppendText($Text+[Environment]::NewLine);$progress.Value=$Percent;$form.Refresh()} $buttons=@() function Add-Action([string]$Text,[int]$X,[int]$Y,[scriptblock]$Handler){$button=New-Object Windows.Forms.Button;$button.Text=$Text;$button.Location=New-Object Drawing.Point($X,$Y);$button.Size=New-Object Drawing.Size(210,36);$button.FlatStyle='Flat';$button.ForeColor=[Drawing.Color]::FromArgb(230,207,153);$button.Add_Click($Handler);$form.Controls.Add($button);$script:buttons+=@($button);return $button} function Configure-Player { $isAdmin=([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if(-not $isAdmin){ if($PlayerRoaming.IndexOfAny([char[]]@('"',"`r","`n")) -ge 0 -or $PlayerSid -notmatch '^S-1-5-[0-9-]+$'){throw 'Invalid player profile arguments.'} $arguments='-NoProfile -ExecutionPolicy Bypass -File "'+$PSCommandPath+'" -Install -PlayerRoaming "'+$PlayerRoaming+'" -PlayerSid "'+$PlayerSid+'"' Start-Process -FilePath "$env:windir\System32\WindowsPowerShell\v1.0\powershell.exe" -ArgumentList $arguments -Verb RunAs -WindowStyle Hidden | Out-Null $form.Close();return } $profileKey=Get-ItemProperty -LiteralPath ('HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\'+$PlayerSid) $playerHome=[Environment]::ExpandEnvironmentVariables($profileKey.ProfileImagePath) $expectedRoaming=[IO.Path]::GetFullPath((Join-Path $playerHome 'AppData\Roaming')) if(-not [IO.Path]::GetFullPath($PlayerRoaming).Equals($expectedRoaming,[StringComparison]::OrdinalIgnoreCase)){throw 'Unexpected player profile location. No configuration was changed.'} if(Get-Process -Name WowB,ForeverLauncher,bnetserver -ErrorAction SilentlyContinue){throw (L 'Close the game and ForeverLauncher first. If this PC hosts bnetserver, use ForeverHub instead of this external-player installer.' 'Fermez le jeu et ForeverLauncher. Si ce PC héberge bnetserver, utilisez ForeverHub plutôt que cet assistant pour joueurs externes.')} Status (L '1/4 Checking No-IP, HTTPS and game ports...' '1/4 Vérification No-IP, HTTPS et ports du jeu…') 10 $check=Get-ConnectionCheck if(@($check.Ports | Where-Object {-not $_.Reachable}).Count -gt 0){throw (L 'One or more game ports cannot be reached. The realm may be offline; contact the server owner. No settings were changed.' 'Un ou plusieurs ports du jeu sont inaccessibles. Le royaume est peut-être hors ligne ; contactez l''équipe. Aucun paramètre n''a été modifié.')} $message=(L 'Trust this private GAME server CA? This is NOT the website HTTPS certificate and is NOT an official Blizzard certificate.' 'Faire confiance au certificat privé du serveur de JEU ? Ce n''est PAS le certificat HTTPS du site, ni un certificat officiel Blizzard.')+"`r`n`r`n$ServerAddress ($($check.IPv4))`r`nSHA256: $ExpectedCaSha256`r`n`r`n"+(L 'This will add the pinned CA to Local Computer Trusted Roots, map the technical game portal in hosts and merge a server entry into your ForeverLauncher settings. Backups will be kept. Never use your Blizzard password here.' 'Cela ajoute le certificat vérifié aux racines de confiance de l''ordinateur, configure le portail technique dans hosts et ajoute une entrée dans ForeverLauncher. Des sauvegardes sont conservées. N''utilisez jamais votre mot de passe Blizzard ici.') if([Windows.Forms.MessageBox]::Show($form,$message,'WoW Forever - trust confirmation','YesNo','Warning') -ne 'Yes'){return} Status (L '2/4 Backing up current connection files...' '2/4 Sauvegarde des fichiers actuels…') 35 $backup=Join-Path $playerHome ('AppData\Local\ForeverWoW\connection-backups\'+(Get-Date -Format 'yyyyMMdd-HHmmss-fff'));New-Item -ItemType Directory -Path $backup | Out-Null $hosts=Join-Path $env:windir 'System32\drivers\etc\hosts';$originalHosts=[IO.File]::ReadAllBytes($hosts);[IO.File]::WriteAllBytes((Join-Path $backup 'hosts'),$originalHosts) $launcherFolder=Join-Path $PlayerRoaming 'ForeverLauncher';$launcherConfig=Join-Path $launcherFolder 'servers.json';$originalConfig=$null if(Test-Path -LiteralPath $launcherConfig){$originalConfig=[IO.File]::ReadAllBytes($launcherConfig);[IO.File]::WriteAllBytes((Join-Path $backup 'servers.json'),$originalConfig);$config=Get-UpdatedLauncherConfig (Get-Content -Raw -LiteralPath $launcherConfig)}else{$config=Get-UpdatedLauncherConfig ''} $lines=Get-UpdatedHosts ([IO.File]::ReadAllLines($hosts)) $check.IPv4 $Portal $caFile=Join-Path $backup 'forever_root_ca.crt';[IO.File]::WriteAllBytes($caFile,$check.CaBytes) $trustedPath='Cert:\LocalMachine\Root\'+$ExpectedThumbprint;$alreadyTrusted=Test-Path -LiteralPath $trustedPath;$hostsWritten=$false;$configWritten=$false try { Status (L '3/4 Applying certificate and connection settings...' '3/4 Application du certificat et de la connexion…') 65 if((Get-BytesHash ([IO.File]::ReadAllBytes($hosts))) -ne (Get-BytesHash $originalHosts)){throw 'The hosts file changed during setup; retry after reviewing it.'} if(-not $alreadyTrusted){Import-Certificate -FilePath $caFile -CertStoreLocation Cert:\LocalMachine\Root | Out-Null} Write-AtomicText $hosts (($lines -join "`r`n")+"`r`n");$hostsWritten=$true New-Item -ItemType Directory -Path $launcherFolder -Force | Out-Null if($null -ne $originalConfig -and (Get-BytesHash ([IO.File]::ReadAllBytes($launcherConfig))) -ne (Get-BytesHash $originalConfig)){throw 'The launcher configuration changed during setup; retry after reviewing it.'} if($null -eq $originalConfig -and (Test-Path -LiteralPath $launcherConfig)){throw 'A launcher configuration appeared during setup; retry after reviewing it.'} Write-AtomicText $launcherConfig ($config | ConvertTo-Json -Depth 20);$configWritten=$true Clear-DnsClientCache if(-not(Test-Path -LiteralPath $trustedPath)){throw 'Certificate import could not be verified.'} if(@([Net.Dns]::GetHostAddresses($Portal) | ForEach-Object IPAddressToString) -notcontains $check.IPv4){throw 'The game portal mapping could not be verified.'} Status (L '4/4 Connection prepared. Open ForeverLauncher, select WoW Forever and choose your _classic_beta_ folder (build 70374). Use your server account, never your Blizzard credentials.' '4/4 Connexion préparée. Ouvrez ForeverLauncher, choisissez WoW Forever puis le dossier _classic_beta_ (build 70374). Utilisez votre compte serveur, jamais vos identifiants Blizzard.') 100 Status ('Backups: '+$backup) 100 Status (L 'Run this assistant again if the No-IP IP address changes. It preserves other server entries and unrelated hosts lines.' 'Relancez cet assistant si l''adresse IP No-IP change. Les autres serveurs et les lignes hosts sans rapport sont conservés.') 100 } catch { if($hostsWritten){[IO.File]::WriteAllBytes($hosts,$originalHosts)} if($configWritten -and $null -ne $originalConfig){[IO.File]::WriteAllBytes($launcherConfig,$originalConfig)}elseif($configWritten -and (Test-Path -LiteralPath $launcherConfig)){Move-Item -LiteralPath $launcherConfig -Destination (Join-Path $backup 'failed-servers.json')} if(-not $alreadyTrusted -and (Test-Path -LiteralPath $trustedPath)){Remove-Item -LiteralPath $trustedPath} Clear-DnsClientCache;throw } } $checkButton=Add-Action (L 'Check connection' 'Vérifier la connexion') 28 423 {try{foreach($b in $buttons){$b.Enabled=$false};Status 'Checking DNS, HTTPS certificate and game ports...' 15;$result=Get-ConnectionCheck;Status ($result.IPv4+' | HTTPS valid | certificate SHA256 verified') 70;foreach($port in $result.Ports){Status ('TCP '+$port.Port+': '+$port.Reachable) 100}}catch{Status $_.Exception.Message 0}finally{foreach($b in $buttons){$b.Enabled=$true}}} $installButton=Add-Action (L 'Configure this player PC' 'Configurer ce PC joueur') 250 423 {try{foreach($b in $buttons){$b.Enabled=$false};Configure-Player}catch{Status $_.Exception.Message 0;[Windows.Forms.MessageBox]::Show($form,$_.Exception.Message,'WoW Forever','OK','Error') | Out-Null}finally{foreach($b in $buttons){$b.Enabled=$true}}} $launcherButton=Add-Action 'ForeverLauncher' 473 423 {Start-Process 'https://github.com/advocaite/foreverlauncher-releases/releases/latest' | Out-Null} $footer=New-Object Windows.Forms.Label;$footer.Location=New-Object Drawing.Point(28,475);$footer.Size=New-Object Drawing.Size(655,60);$footer.Text=L 'Website HTTPS needs no manual certificate. This assistant installs only the pinned game CA after your confirmation. No firewall, antivirus or game binary is modified. Made with love by FilinoDZ.' 'Le site HTTPS ne nécessite aucun certificat manuel. Cet assistant installe uniquement le certificat de jeu vérifié, après votre accord. Aucun pare-feu, antivirus ou fichier du jeu n''est modifié. Made with love by FilinoDZ.';$footer.ForeColor=[Drawing.Color]::FromArgb(150,165,185);$form.Controls.Add($footer) if($Install -and -not $RenderPreview){$form.Add_Shown({$installButton.PerformClick()})} Status (L 'Start with Check connection. Configure this player PC requires your Windows approval and a separate certificate confirmation.' 'Commencez par Vérifier la connexion. La configuration demande votre autorisation Windows et une confirmation du certificat.') 0 if($RenderPreview){$form.Opacity=0;$form.ShowInTaskbar=$false;$form.Show();[Windows.Forms.Application]::DoEvents();$bitmap=New-Object Drawing.Bitmap($form.Width,$form.Height);try{$form.DrawToBitmap($bitmap,(New-Object Drawing.Rectangle(0,0,$form.Width,$form.Height)));$bitmap.Save($RenderPreview,[Drawing.Imaging.ImageFormat]::Png)}finally{$bitmap.Dispose();$form.Close();$form.Dispose()};return} [void]$form.ShowDialog()