& { <# Yedek Client Bu sunucudaki yedek klasörünü, merkezi yedek sunucusundaki FTP Senkronizasyon programının indirebilmesi için Windows'un kendi OpenSSH sunucusuyla SFTP üzerinden salt okunur olarak paylaşır ve kendini yedek sunucusuna kaydeder. Kurulumun yaptıkları: - OpenSSH Server Windows özelliğini kurar (yoksa) - "yedek" adında yerel bir kullanıcı oluşturur; şifreyle girişi yoktur, sadece yedek sunucusunun SSH anahtarıyla ve sadece yedek sunucusunun IP'lerinden bağlanabilir - Bu kullanıcıyı paylaşılan klasöre kilitler (chroot) ve salt okunur SFTP ile sınırlar (kabuk, port yönlendirme vb. kapalı); klasörde sadece okuma izni verir - Güvenlik duvarında SSH portunu sadece izin verilen IP'lere açar - Bağlantı bilgilerini eşleştirme koduyla imzalayıp yedek sunucusuna gönderir - SSH sunucusunu açılışta otomatik başlayan Windows servisi olarak ayarlar - Otomatik güncelleyici kurar: SYSTEM olarak açılışta ve her gün yeni sürümü kontrol eder, sadece yedek sunucusunun yayın anahtarıyla imzalanmış sürümü çalıştırır - Önceki sürümün FTPS servisi (YedekFtp) kuruluysa kayıttan sonra kaldırır OpenSSH bu sunucuda zaten kullanılıyorsa genel ayarlarına (port, şifreli giriş) dokunulmaz; sadece "yedek" kullanıcısına ait blok eklenir. Kurulum (yönetici PowerShell): irm https://backup.dzbilisim.com | iex Fonksiyonları test etmek için: $YedekClientNoGui = $true; . .\YedekClient.ps1 #> $ErrorActionPreference = 'Stop' # Yedek sunucusu bu dosyayı yayınlarken kendi adresini yazar; yükseltilmiş yeniden başlatmada kullanılır $InstallUrl = 'https://backup.dzbilisim.com' $ClientVersion = '2026.10.27.2' # yayınlanırken yedek sunucusu yazar (YYYY.AA.GG.sıra) $SftpUser = 'yedek' $SshDir = Join-Path $env:ProgramData 'ssh' $SshdConfig = Join-Path $SshDir 'sshd_config' $SshdExe = Join-Path $env:windir 'System32\OpenSSH\sshd.exe' $DataDir = Join-Path $env:ProgramData 'YedekClient' $AuthorizedKeys = Join-Path $DataDir 'authorized_keys' $StatePath = Join-Path $DataDir 'state.json' # gizli bilgi içermez; arayüzü doldurmak için $UpdaterPath = Join-Path $DataDir 'updater.ps1' $UpdateConfigPath = Join-Path $DataDir 'update.json' $UpdateLogPath = Join-Path $DataDir 'update.log' $UpdateTaskName = 'YedekClient Guncelleme' $FirewallRuleName = 'YedekSftp' $DefaultSshRule = 'OpenSSH-Server-In-TCP' # OpenSSH kurulumunun eklediği, 22'yi herkese açan kural $BlockStart = '# >>> YedekClient' $BlockEnd = '# <<< YedekClient' $GlobalStart = '# >>> YedekClient-genel' $GlobalEnd = '# <<< YedekClient-genel' $SidSystem = '*S-1-5-18' $SidAdministrators = '*S-1-5-32-544' # Önceki sürüm (FTPS) kalıntıları $LegacyService = 'YedekFtp' $LegacyInstallDir = Join-Path $env:ProgramFiles 'YedekClient' $LegacyFirewallRule = 'YedekFtp' # --- yardımcılar ---------------------------------------------------------------- function ConvertTo-Base64Url([byte[]]$Bytes) { [Convert]::ToBase64String($Bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_') } function ConvertFrom-Base64Url([string]$Text) { $t = $Text.Replace('-', '+').Replace('_', '/') switch ($t.Length % 4) { 2 { $t += '==' } 3 { $t += '=' } 1 { throw 'Geçersiz base64 verisi' } } return , [Convert]::FromBase64String($t) } function Get-Hex([byte[]]$Bytes) { -join ($Bytes | ForEach-Object { $_.ToString('x2') }) } function Get-RandomBytes([int]$Count) { $bytes = New-Object byte[] $Count $rng = [Security.Cryptography.RandomNumberGenerator]::Create() try { $rng.GetBytes($bytes) } finally { $rng.Dispose() } return , $bytes } function Get-Hmac([byte[]]$Key, [string]$Text) { $h = [Security.Cryptography.HMACSHA256]::new($Key) try { return , $h.ComputeHash([Text.Encoding]::UTF8.GetBytes($Text)) } finally { $h.Dispose() } } # Hata koduyla dönen komut satırı araçlarını (icacls, sc) istisnaya çevirir function Invoke-Native([string]$Exe, [string[]]$Arguments) { $out = & $Exe @Arguments 2>&1 | Out-String if ($LASTEXITCODE -ne 0) { throw "$Exe $($Arguments -join ' ') başarısız: $($out.Trim())" } } function Read-PairingCode([string]$Code) { $c = $Code -replace '\s', '' if ($c.StartsWith('YDK1.')) { throw 'Bu kod eski sürüm (FTPS) yedek sunucusu programından. Yedek sunucusundaki programı güncelleyip yeni kod oluşturun.' } if (-not $c.StartsWith('YDK2.')) { throw 'Eşleştirme kodu geçersiz: "YDK2." ile başlamalı.' } try { $o = [Text.Encoding]::UTF8.GetString((ConvertFrom-Base64Url $c.Substring(5))) | ConvertFrom-Json $key = ConvertFrom-Base64Url $o.k $uri = [Uri]$o.u } catch { throw 'Eşleştirme kodu çözülemedi; eksik kopyalanmış olabilir.' } if (-not $o.i -or $key.Length -ne 32) { throw 'Eşleştirme kodu eksik veya bozuk.' } if (-not (Test-SshPublicKey ([string]$o.p))) { throw 'Eşleştirme kodundaki SSH anahtarı geçersiz.' } $ips = @($o.a | Where-Object { $_ -and (Test-IpRule $_) }) $release = if ($o.r -and $o.r.n -and $o.r.e) { [pscustomobject]@{ n = [string]$o.r.n; e = [string]$o.r.e } } else { $null } $lan = @($o.l | Where-Object { $_ -and (Test-IpRule $_) -and -not $_.Contains('/') }) [pscustomobject]@{ Url = $o.u.TrimEnd('/'); Id = [string]$o.i; Key = $key; Host = $uri.Host PublicKey = [string]$o.p; AllowedIps = $ips; ReleaseKey = $release; LanIps = $lan } } function ConvertTo-UInt32Ip([string]$Ip) { $b = ([Net.IPAddress]::Parse($Ip)).GetAddressBytes() [Array]::Reverse($b) return [uint64][BitConverter]::ToUInt32($b, 0) } # Yedek sunucusu bu sunucuyla aynı yerel ağdaysa (aynı NAT arkasında) eşleşen IP çiftini döndürür. # Bu durumda yedek sunucusu dış IP yerine yerel ağdan bağlanır. function Find-SharedSubnet([string[]]$ServerIps) { $locals = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*' -and $_.PrefixLength -gt 0 } foreach ($local in $locals) { $mask = [uint64]([math]::Pow(2, 32) - [math]::Pow(2, 32 - $local.PrefixLength)) $mine = ConvertTo-UInt32Ip $local.IPAddress foreach ($server in $ServerIps) { if ($server -eq $local.IPAddress) { continue } if (((ConvertTo-UInt32Ip $server) -band $mask) -eq ($mine -band $mask)) { return [pscustomobject]@{ ServerIp = $server; MyIp = $local.IPAddress } } } } return $null } # "tür base64 [yorum]" biçiminde ve base64 içindeki tür baştakiyle aynı mı (SSH kablo biçimi). # Not: harf aralıklı -match kullanılmaz; Türkçe sistemlerde büyük/küçük "I" eşleşmez. function Test-SshPublicKey([string]$Line) { $parts = $Line.Trim().Split(' ') $types = 'ssh-ed25519', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-rsa' if ($parts.Count -lt 2 -or $types -cnotcontains $parts[0]) { return $false } try { $blob = [Convert]::FromBase64String($parts[1]) } catch { return $false } $type = $parts[0] if ($blob.Length -lt 4 + $type.Length) { return $false } $len = ([int]$blob[0] -shl 24) -bor ([int]$blob[1] -shl 16) -bor ([int]$blob[2] -shl 8) -bor [int]$blob[3] return $len -eq $type.Length -and [Text.Encoding]::ASCII.GetString($blob, 4, $len) -ceq $type } # "ssh-ed25519 SHA256:..." (ssh-keygen -lf ile aynı biçim) function Get-PublicKeyFingerprint([string]$PubLine) { $type, $b64 = $PubLine.Trim().Split(' ')[0, 1] $sha = [Security.Cryptography.SHA256]::Create() try { $digest = $sha.ComputeHash([Convert]::FromBase64String($b64)) } finally { $sha.Dispose() } return "$type SHA256:" + [Convert]::ToBase64String($digest).TrimEnd('=') } function Resolve-ServerIps([string]$HostName) { try { @([Net.Dns]::GetHostAddresses($HostName) | Where-Object { $_.AddressFamily -eq 'InterNetwork' } | ForEach-Object { $_.ToString() }) } catch { @() } } # Eşleştirme kodundaki IP'ler (yedek sunucusunun çıkış IP'leri), yoksa adresin DNS karşılığı function Get-DefaultAllowedIps($Pairing) { if ($Pairing.AllowedIps.Count) { return $Pairing.AllowedIps } return Resolve-ServerIps $Pairing.Host } function Test-IpRule([string]$Rule) { if ($Rule -eq '*') { return $true } $ip = $Rule; $bits = $null if ($Rule.Contains('/')) { $ip, $bits = $Rule.Split('/', 2) $n = 0 if (-not [int]::TryParse($bits, [ref]$n) -or $n -lt 0 -or $n -gt 128) { return $false } } $parsed = $null if (-not [Net.IPAddress]::TryParse($ip, [ref]$parsed)) { return $false } if ($null -ne $bits -and $parsed.AddressFamily -eq 'InterNetwork' -and [int]$bits -gt 32) { return $false } return $true } # Yedek sunucusunda yayındaki sürüm (sadece bilgi amaçlı; güncellemenin güvenliği imzaya dayanır) function Get-LatestVersion([string]$Url) { if (-not $Url) { return $null } try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $req = [Net.HttpWebRequest]::Create("$($Url.TrimEnd('/'))/version.txt") $req.Timeout = 5000 $resp = $req.GetResponse() try { return (New-Object IO.StreamReader($resp.GetResponseStream())).ReadToEnd().Trim() } finally { $resp.Close() } } catch { return $null } } function Read-State { if (Test-Path $StatePath) { try { return Get-Content -Raw -Encoding UTF8 $StatePath | ConvertFrom-Json } catch { } } return $null } function Save-State($State) { $State | ConvertTo-Json | Set-Content -Encoding UTF8 $StatePath } # --- yapılandırma metinleri (sistemi değiştirmez, test edilebilir) ---------------- function New-AuthorizedKeyLine([string]$PublicKey, [string[]]$AllowedIps) { $type, $b64 = $PublicKey.Trim().Split(' ')[0, 1] # restrict: kabuk/pty/yönlendirme kapalı; from: sadece yedek sunucusunun IP'lerinden $options = 'restrict' if ($AllowedIps -notcontains '*') { $options += ',from="' + ($AllowedIps -join ',') + '"' } return "$options $type $b64 yedek-sunucusu" } function Remove-ManagedBlocks([string]$Text) { foreach ($pair in @(@($GlobalStart, $GlobalEnd), @($BlockStart, $BlockEnd))) { $pattern = '(?ms)^' + [regex]::Escape($pair[0]) + '\b.*?^' + [regex]::Escape($pair[1]) + '\b[^\r\n]*(\r?\n)?' $Text = [regex]::Replace($Text, $pattern, '') } return $Text } function Get-SshdPort([string]$Text) { $m = [regex]::Match($Text, '(?m)^\s*Port\s+(\d+)', 'IgnoreCase, CultureInvariant') if ($m.Success) { return [int]$m.Groups[1].Value } return 22 } function Get-UpdatedSshdConfig([string]$Text, [string]$Root, [int]$Port, [bool]$Owned) { $Text = (Remove-ManagedBlocks $Text).TrimEnd() $chroot = $Root.Replace('\', '/') $match = @( "$BlockStart (Yedek Client tarafindan yonetilir, elle degistirmeyin)" "Match User $SftpUser" " AuthorizedKeysFile __PROGRAMDATA__/YedekClient/authorized_keys" " ChrootDirectory `"$chroot`"" " ForceCommand internal-sftp -R" " PasswordAuthentication no" " PubkeyAuthentication yes" " AllowTcpForwarding no" " AllowAgentForwarding no" " AllowStreamLocalForwarding no" " PermitTTY no" " PermitTunnel no" " X11Forwarding no" " GatewayPorts no" $BlockEnd ) -join "`r`n" if ($Owned) { # sshd başka amaçla kullanılmıyor: sadece yedek kullanıcısı, sadece anahtarla. # sshd'de ilk geçen değer geçerli olduğundan bu blok en başa konur. $global = @( "$GlobalStart (Yedek Client tarafindan yonetilir)" "Port $Port" "PasswordAuthentication no" "AllowUsers $SftpUser" $GlobalEnd ) -join "`r`n" $Text = $global + "`r`n`r`n" + $Text } # Match blokları dosyanın sonunda olmalı return $Text + "`r`n`r`n" + $match + "`r`n" } # --- kurulum adımları ----------------------------------------------------------- # sshd başka amaçla kullanılıyor mu? (kullanılmıyorsa genel ayarları biz yönetiriz) function Test-SshdUnused { $svc = Get-Service sshd -ErrorAction SilentlyContinue return (-not $svc) -or ($svc.Status -ne 'Running' -and $svc.StartType -ne 'Automatic') } function Install-OpenSshServer([scriptblock]$Log) { $cap = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' | Select-Object -First 1 if (-not $cap) { throw 'Bu Windows sürümünde OpenSSH Server özelliği yok (Windows Server 2019 veya üstü gerekir).' } if ($cap.State -ne 'Installed') { & $Log 'OpenSSH Server kuruluyor (Windows özelliği, birkaç dakika sürebilir)' try { Add-WindowsCapability -Online -Name $cap.Name | Out-Null } catch { throw ("OpenSSH Server kurulamadı: $($_.Exception.Message)`r`nSunucunun Windows Update erişimi yoksa " + "özelliği 'Features on Demand' kaynağından kurup tekrar deneyin.") } } # İlk çalıştırmada sshd anahtarlarını ve varsayılan sshd_config'i oluşturur if (-not (Test-Path $SshdConfig) -or -not (Test-Path (Join-Path $SshDir 'ssh_host_ed25519_key.pub'))) { Start-Service sshd (Get-Service sshd).WaitForStatus('Running', [TimeSpan]::FromSeconds(30)) Stop-Service sshd } } function Initialize-SftpUser { $user = Get-LocalUser -Name $SftpUser -ErrorAction SilentlyContinue if (-not $user) { # Rastgele ve hiçbir yere kaydedilmeyen şifre: şifreyle giriş zaten kapalı $pw = ConvertTo-SecureString ((ConvertTo-Base64Url (Get-RandomBytes 32)) + 'aA1!') -AsPlainText -Force New-LocalUser -Name $SftpUser -Password $pw -PasswordNeverExpires -UserMayNotChangePassword ` -AccountNeverExpires -Description 'Yedek SFTP (salt okunur, sadece anahtar)' # en fazla 48 karakter | Out-Null } elseif (-not $user.Enabled) { Enable-LocalUser -Name $SftpUser } } function Set-AuthorizedKey([string]$Line) { New-Item -ItemType Directory -Force $DataDir | Out-Null # Klasör ve anahtar dosyası sadece SYSTEM ve Administrators'a açık (sshd bunu şart koşar) Invoke-Native icacls.exe @($DataDir, '/inheritance:r', '/grant:r', "${SidSystem}:(OI)(CI)F", "${SidAdministrators}:(OI)(CI)F") [IO.File]::WriteAllText($AuthorizedKeys, $Line + "`n", (New-Object Text.UTF8Encoding($false))) Invoke-Native icacls.exe @($AuthorizedKeys, '/setowner', $SidAdministrators) } # Değişiklik yaptıysa $true döner (sshd'nin yeniden başlatılması gerekir) function Set-SshdConfig([string]$Root, [int]$Port, [bool]$Owned) { $current = if (Test-Path $SshdConfig) { [IO.File]::ReadAllText($SshdConfig) } else { '' } $updated = Get-UpdatedSshdConfig $current $Root $Port $Owned if ($updated -ceq $current) { return $false } $backup = "$SshdConfig.yedekclient.bak" if (-not (Test-Path $backup)) { Copy-Item $SshdConfig $backup } [IO.File]::WriteAllText($SshdConfig, $updated, (New-Object Text.UTF8Encoding($false))) $out = & $SshdExe -t 2>&1 | Out-String if ($LASTEXITCODE -ne 0) { [IO.File]::WriteAllText($SshdConfig, $current, (New-Object Text.UTF8Encoding($false))) throw "sshd_config doğrulanamadı, eski hali geri yüklendi: $($out.Trim())" } return $true } # Bilgisayar açıldığında otomatik başlar; bu kurulum yönetiyorsa çökerse de yeniden başlatılır function Set-SshdService([bool]$Owned) { Set-Service sshd -StartupType Automatic if ($Owned) { Invoke-Native sc.exe @('failure', 'sshd', 'reset=', '86400', 'actions=', 'restart/5000/restart/30000/restart/60000') } } # --- otomatik güncelleme ---------------------------------------------------------- # Kaynak sunucuda SYSTEM olarak çalışan küçük güncelleyici. Yedek sunucusundaki client'ı indirir, # kurulumda eşleştirme koduyla gelen yayın anahtarıyla imzasını doğrular; imza tutmazsa hiçbir şey # çalıştırmaz. Yeni sürümse onu güncelleme modunda çalıştırır (Update-Agent). function Get-UpdaterScript { return @' # Yedek Client otomatik güncelleyici (Yedek Client kurulumu oluşturur; elle değiştirmeyin) $ErrorActionPreference = 'Stop' $dir = Join-Path $env:ProgramData 'YedekClient' $logPath = Join-Path $dir 'update.log' function Write-UpdateLog([string]$Text) { $lines = @(if (Test-Path $logPath) { Get-Content $logPath -Encoding UTF8 | Select-Object -Last 300 }) $lines += (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + ' ' + $Text Set-Content -Path $logPath -Value $lines -Encoding UTF8 } try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $cfg = Get-Content -Raw -Encoding UTF8 (Join-Path $dir 'update.json') | ConvertFrom-Json $wc = New-Object Net.WebClient $bytes = $wc.DownloadData("$($cfg.Url)/client.ps1") $sig = [Convert]::FromBase64String($wc.DownloadString("$($cfg.Url)/client.ps1.sig").Trim()) $key = New-Object Security.Cryptography.RSAParameters $key.Modulus = [Convert]::FromBase64String($cfg.KeyN) $key.Exponent = [Convert]::FromBase64String($cfg.KeyE) $rsa = New-Object Security.Cryptography.RSACng $rsa.ImportParameters($key) if (-not $rsa.VerifyData($bytes, $sig, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1)) { throw 'İmza doğrulanamadı; indirilen dosya çalıştırılmadı' } $sha = [Security.Cryptography.SHA256]::Create() $YedekClientHash = -join ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) if ($YedekClientHash -eq $cfg.InstalledHash) { Write-UpdateLog 'Güncel'; return } Write-UpdateLog "Yeni sürüm bulundu ($($YedekClientHash.Substring(0, 12)))" $YedekClientMode = 'update' & ([scriptblock]::Create([Text.Encoding]::UTF8.GetString($bytes))) } catch { Write-UpdateLog "HATA: $($_.Exception.Message)" } '@ } function Install-Updater([string]$Url, $ReleaseKey, [string]$InstalledHash) { [ordered]@{ Url = $Url.TrimEnd('/'); KeyN = $ReleaseKey.n; KeyE = $ReleaseKey.e; InstalledHash = $InstalledHash } | ConvertTo-Json | Set-Content -Encoding UTF8 $UpdateConfigPath [IO.File]::WriteAllText($UpdaterPath, (Get-UpdaterScript), (New-Object Text.UTF8Encoding($true))) $action = New-ScheduledTaskAction -Execute 'powershell.exe' ` -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$UpdaterPath`"" $boot = New-ScheduledTaskTrigger -AtStartup $boot.Delay = 'PT5M' $daily = New-ScheduledTaskTrigger -Daily -At '04:00' -RandomDelay (New-TimeSpan -Hours 2) $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries ` -ExecutionTimeLimit (New-TimeSpan -Minutes 30) Register-ScheduledTask -TaskName $UpdateTaskName -Action $action -Trigger @($boot, $daily) -Principal $principal ` -Settings $settings -Description 'Yedek Client otomatik güncelleme (açılışta ve her gün)' -Force | Out-Null } function Remove-Updater { Unregister-ScheduledTask -TaskName $UpdateTaskName -Confirm:$false -ErrorAction SilentlyContinue Remove-Item -Force $UpdaterPath, $UpdateConfigPath -ErrorAction SilentlyContinue } # Eşleştirme gerektirmeden kurulumu yeni sürümle yeniden uygular (güncelleyici çağırır) function Update-Agent([scriptblock]$Log, [string]$Hash) { $state = Read-State if (-not $state -or -not $state.PSObject.Properties['HostKey']) { & $Log 'Kurulum bulunamadı; güncelleme atlandı'; return } $owned = [bool]$state.SshdOwned $port = [int]$state.Port $allowed = @($state.AllowedIps -split '[,; ]+' | Where-Object { $_ }) Install-OpenSshServer $Log Initialize-SftpUser Invoke-Native icacls.exe @($state.Root, '/grant', "$env:COMPUTERNAME\${SftpUser}:(OI)(CI)RX") $changed = Set-SshdConfig $state.Root $port $owned Set-SftpFirewall $port $allowed $owned $Log Set-SshdService $owned # Süren bir yedek aktarımı kesilmesin diye sadece gerekirse yeniden başlatılır if ($changed) { Restart-Service sshd } elseif ((Get-Service sshd).Status -ne 'Running') { Start-Service sshd } $cfg = Get-Content -Raw -Encoding UTF8 $UpdateConfigPath | ConvertFrom-Json Install-Updater $cfg.Url ([pscustomobject]@{ n = $cfg.KeyN; e = $cfg.KeyE }) $Hash $state | Add-Member -NotePropertyName Version -NotePropertyValue $ClientVersion -Force Save-State $state & $Log ("Güncelleme uygulandı: sürüm $ClientVersion" + $(if ($changed) { ' (SSH yapılandırması değişti, servis yeniden başlatıldı)' } else { '' })) } function Set-SftpFirewall([int]$Port, [string[]]$AllowedIps, [bool]$Owned, [scriptblock]$Log) { Remove-NetFirewallRule -Name $FirewallRuleName -ErrorAction SilentlyContinue $remote = if ($AllowedIps -contains '*') { 'Any' } else { $AllowedIps } New-NetFirewallRule -Name $FirewallRuleName -DisplayName 'Yedek SFTP' -Direction Inbound -Action Allow ` -Protocol TCP -LocalPort $Port -RemoteAddress $remote -Program $SshdExe -Profile Any | Out-Null $default = Get-NetFirewallRule -Name $DefaultSshRule -ErrorAction SilentlyContinue if ($default -and $default.Enabled -eq 'True') { if ($Owned) { Disable-NetFirewallRule -Name $DefaultSshRule & $Log "OpenSSH'nin portu herkese açan varsayılan güvenlik duvarı kuralı kapatıldı" } else { & $Log "UYARI: '$DefaultSshRule' kuralı SSH portunu her IP'ye açıyor; SSH başka amaçla kullanıldığı için dokunulmadı." } } } function Test-SshListening([int]$Port) { $tcp = New-Object Net.Sockets.TcpClient try { $tcp.Connect('127.0.0.1', $Port) $stream = $tcp.GetStream() $stream.ReadTimeout = 5000 return (New-Object IO.StreamReader($stream)).ReadLine() -like 'SSH-2.0*' } catch { return $false } finally { $tcp.Close() } } function Register-Agent($Pairing, [string]$Name, [string]$ExternalHost, [int]$Port, [string]$HostKey) { $ts = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() $nonce = Get-Hex (Get-RandomBytes 16) # Alan sırası ftp_register.FIELDS ile aynı olmalı $fields = @($Pairing.Id, $Name, 'sftp', $ExternalHost, $Port, $HostKey, $SftpUser, $ts, $nonce) $body = [ordered]@{ id = $Pairing.Id; name = $Name; protocol = 'sftp'; host = $ExternalHost; port = $Port; host_key = $HostKey user = $SftpUser; ts = $ts; nonce = $nonce; mac = Get-Hex (Get-Hmac $Pairing.Key ($fields -join "`n")) } | ConvertTo-Json -Compress $bytes = [Text.Encoding]::UTF8.GetBytes($body) # Windows PowerShell 5.1 varsayılan olarak TLS 1.2'yi açmayabilir [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $resp = $null try { $req = [Net.HttpWebRequest]::Create("$($Pairing.Url)/register") $req.Method = 'POST' $req.ContentType = 'application/json; charset=utf-8' $req.Timeout = 60000 $req.ContentLength = $bytes.Length $s = $req.GetRequestStream() $s.Write($bytes, 0, $bytes.Length) $s.Close() $resp = $req.GetResponse() } catch { $ex = $_.Exception while ($ex -and -not ($ex -is [Net.WebException])) { $ex = $ex.InnerException } if ($ex -and $ex.Response) { $resp = $ex.Response } else { throw "Yedek sunucusuna ulaşılamadı ($($Pairing.Url)): $($_.Exception.Message)" } } try { $reader = New-Object IO.StreamReader($resp.GetResponseStream(), [Text.Encoding]::UTF8) $o = $reader.ReadToEnd() | ConvertFrom-Json } finally { $resp.Close() } if (-not $o.ok) { throw "Yedek sunucusu kaydı reddetti: $($o.message)" } if ($o.mac -ne (Get-Hex (Get-Hmac $Pairing.Key "ok`n$nonce"))) { throw 'Yedek sunucusunun yanıtı doğrulanamadı (yanlış sunucu veya araya giren biri olabilir).' } return $o.message } function Remove-LegacyFtpAgent($OldState, [scriptblock]$Log) { $svc = Get-Service $LegacyService -ErrorAction SilentlyContinue if (-not $svc -and -not (Test-Path $LegacyInstallDir)) { return } & $Log 'Önceki sürümün FTPS servisi kaldırılıyor' if ($svc) { if ($svc.Status -ne 'Stopped') { Stop-Service $LegacyService -Force } if ($OldState -and $OldState.Root -and (Test-Path $OldState.Root)) { & icacls.exe $OldState.Root /remove:g "NT SERVICE\$LegacyService" 2>&1 | Out-Null # servis silinmeden önce } Invoke-Native sc.exe @('delete', $LegacyService) } Remove-NetFirewallRule -Name $LegacyFirewallRule -ErrorAction SilentlyContinue if ($OldState -and $OldState.PSObject.Properties['CertThumbprint'] -and $OldState.CertThumbprint -and (Test-Path "Cert:\LocalMachine\My\$($OldState.CertThumbprint)")) { Remove-Item "Cert:\LocalMachine\My\$($OldState.CertThumbprint)" -DeleteKey } Remove-Item -Recurse -Force $LegacyInstallDir -ErrorAction SilentlyContinue } function Install-Agent($Options, [scriptblock]$Log) { & $Log 'Eşleştirme kodu okunuyor' $pairing = Read-PairingCode $Options.Code $root = $Options.Root.Trim().TrimEnd('\') if (-not $root -or -not (Test-Path -PathType Container $root)) { throw "Paylaşılacak klasör bulunamadı: $root" } $root = (Resolve-Path $root).ProviderPath.TrimEnd('\') if ($root -cmatch '^[A-Za-z]:$' -or $root.StartsWith('\\')) { throw 'Sürücünün tamamı veya ağ yolu paylaşılamaz; yerel bir yedek klasörü seçin.' } $name = $Options.Name.Trim() if (-not $name) { throw 'Bağlantı adı boş olamaz.' } $port = [int]$Options.Port $allowed = @($Options.AllowedIps -split '[,; ]+' | Where-Object { $_ }) if (-not $allowed) { $allowed = Get-DefaultAllowedIps $pairing } if (-not $allowed) { throw "Yedek sunucusunun IP adresi bulunamadı ($($pairing.Host)); İzin verilen IP'ler alanını doldurun." } foreach ($ip in $allowed) { if (-not (Test-IpRule $ip)) { throw "Geçersiz IP: $ip" } } $externalHost = $Options.ExternalHost.Trim() $shared = Find-SharedSubnet $pairing.LanIps if ($shared) { # Aynı ağda: yedek sunucusu dış IP'ye değil yerel IP'ye bağlanır ve bize yerel IP'sinden gelir & $Log "Yedek sunucusu aynı yerel ağda ($($shared.ServerIp)); bağlantı yerel ağ üzerinden yapılacak" if ($allowed -notcontains $shared.ServerIp -and $allowed -notcontains '*') { $allowed += $shared.ServerIp } if (-not $externalHost) { $externalHost = $shared.MyIp } } $oldState = Read-State $owned = if ($oldState -and $oldState.PSObject.Properties['SshdOwned']) { [bool]$oldState.SshdOwned } else { Test-SshdUnused } & $Log $(if ($owned) { 'SSH sunucusu bu kurulum tarafından yönetilecek' } else { 'SSH sunucusu zaten kullanımda; genel ayarlarına dokunulmayacak' }) Install-OpenSshServer $Log if (-not $owned) { $actual = Get-SshdPort ([IO.File]::ReadAllText($SshdConfig)) if ($actual -ne $port) { & $Log "SSH sunucusu $actual portunu kullanıyor; $port yerine o kullanılacak"; $port = $actual } } & $Log "'$SftpUser' kullanıcısı ve yetkili anahtar hazırlanıyor" Initialize-SftpUser Set-AuthorizedKey (New-AuthorizedKeyLine $pairing.PublicKey $allowed) & $Log 'Klasör izni veriliyor (sadece okuma)' Invoke-Native icacls.exe @($root, '/grant', "$env:COMPUTERNAME\${SftpUser}:(OI)(CI)RX") if ($oldState -and $oldState.Root -and $oldState.Root -ne $root -and (Test-Path $oldState.Root)) { & icacls.exe $oldState.Root /remove:g "$env:COMPUTERNAME\$SftpUser" 2>&1 | Out-Null } & $Log 'SSH sunucusu yapılandırılıyor' [void](Set-SshdConfig $root $port $owned) & $Log "Güvenlik duvarı ayarlanıyor (port $port, izinli: $($allowed -join ', '))" Set-SftpFirewall $port $allowed $owned $Log Set-SshdService $owned Restart-Service sshd (Get-Service sshd).WaitForStatus('Running', [TimeSpan]::FromSeconds(30)) Start-Sleep -Milliseconds 500 if (-not (Test-SshListening $port)) { throw "SSH sunucusu çalışıyor ama $port portundan yanıt vermiyor." } if ((Get-Service sshd).StartType -ne 'Automatic') { throw 'SSH sunucusu otomatik başlangıca ayarlanamadı.' } & $Log 'SSH sunucusu Windows servisi olarak çalışıyor; bilgisayar açıldığında otomatik başlar' $hostKey = Get-PublicKeyFingerprint (Get-Content (Join-Path $SshDir 'ssh_host_ed25519_key.pub') -Raw) $state = [ordered]@{ Root = $root; Port = $port; AllowedIps = ($allowed -join ', '); Name = $name ExternalHost = $externalHost; HostKey = $hostKey; SshdOwned = $owned ServerUrl = $pairing.Url; RegisteredAt = $null; Version = $ClientVersion } Save-State $state & $Log "Yedek sunucusuna kaydediliyor ($($pairing.Url))" $message = Register-Agent $pairing $name $externalHost $port $hostKey $state.RegisteredAt = (Get-Date).ToString('yyyy-MM-dd HH:mm') Save-State $state if ($pairing.ReleaseKey) { $url = if ($InstallUrl) { $InstallUrl } else { "https://$($pairing.Host)" } Install-Updater $url $pairing.ReleaseKey '' & $Log 'Otomatik güncelleme kuruldu (açılışta ve her gün kontrol edilir)' } else { & $Log 'UYARI: eşleştirme kodu güncelleme anahtarı içermiyor; otomatik güncelleme kurulmadı' } Remove-LegacyFtpAgent $oldState $Log & $Log "Tamamlandı: $message" } function Uninstall-Agent([scriptblock]$Log) { $state = Read-State $owned = $state -and $state.PSObject.Properties['SshdOwned'] -and [bool]$state.SshdOwned if (Test-Path $SshdConfig) { & $Log 'SSH yapılandırmasından Yedek Client blokları kaldırılıyor' $text = [IO.File]::ReadAllText($SshdConfig) [IO.File]::WriteAllText($SshdConfig, (Remove-ManagedBlocks $text), (New-Object Text.UTF8Encoding($false))) } if (Get-Service sshd -ErrorAction SilentlyContinue) { if ($owned) { & $Log 'SSH sunucusu durduruluyor ve devre dışı bırakılıyor' Stop-Service sshd -ErrorAction SilentlyContinue Set-Service sshd -StartupType Disabled } elseif ((Get-Service sshd).Status -eq 'Running') { Restart-Service sshd } } Remove-NetFirewallRule -Name $FirewallRuleName -ErrorAction SilentlyContinue if ($state -and $state.Root -and (Test-Path $state.Root)) { & $Log 'Klasör izni geri alınıyor' & icacls.exe $state.Root /remove:g "$env:COMPUTERNAME\$SftpUser" 2>&1 | Out-Null } $user = Get-LocalUser -Name $SftpUser -ErrorAction SilentlyContinue if ($user) { & $Log "'$SftpUser' kullanıcısı siliniyor" $sid = $user.SID.Value Remove-LocalUser -Name $SftpUser Get-CimInstance Win32_UserProfile | Where-Object { $_.SID -eq $sid } | Remove-CimInstance } Remove-Updater Remove-LegacyFtpAgent $state $Log Remove-Item -Recurse -Force $DataDir -ErrorAction SilentlyContinue & $Log 'Kaldırıldı. Yedek sunucusundaki bağlantıyı da silmeyi unutmayın.' } function Get-VersionText($State, [string]$Latest) { $installed = if ($State.PSObject.Properties['Version'] -and $State.Version) { $State.Version } else { 'bilinmiyor (eski kurulum)' } if (-not $Latest) { return "kurulu $installed (yayındaki sürüm okunamadı)" } if ($installed -eq $Latest) { return "$installed - güncel" } return "kurulu $installed, yayında $Latest - YENİ SÜRÜM VAR (otomatik güncellenecek; hemen için 'Şimdi Güncelle')" } function Get-AgentStatusText([string]$Latest) { $svc = Get-Service sshd -ErrorAction SilentlyContinue $state = Read-State if (-not $state -or -not $state.PSObject.Properties['HostKey']) { if (Get-Service $LegacyService -ErrorAction SilentlyContinue) { return 'Önceki sürüm (FTPS) kurulu. Yeni bir eşleştirme koduyla kurulum yaparak SFTP''ye geçin.' } return "Kurulu değil. Aşağıdan kurulum yapın. (Bu araç: sürüm $ClientVersion)" } $st = if (-not $svc) { 'kurulu değil' } else { $run = switch ($svc.Status) { 'Running' { 'Çalışıyor' } 'Stopped' { 'Durduruldu' } default { "$($svc.Status)" } } $boot = if ($svc.StartType -eq 'Automatic') { 'açılışta otomatik başlar' } else { "UYARI: açılışta başlamaz ($($svc.StartType))" } "$run, $boot" } $mode = if ($state.SshdOwned) { 'bu kurulum yönetiyor' } else { 'başka amaçla da kullanılıyor' } $reg = if ($state.RegisteredAt) { "kaydedildi $($state.RegisteredAt)" } else { 'KAYIT TAMAMLANMADI' } $upd = if (-not (Get-ScheduledTask -TaskName $UpdateTaskName -ErrorAction SilentlyContinue)) { 'kurulu değil' } elseif (Test-Path $UpdateLogPath) { 'açık; son kayıt: ' + (Get-Content $UpdateLogPath -Encoding UTF8 -Tail 1) } else { 'açık; henüz kontrol yapılmadı' } return @( "Sürüm: $(Get-VersionText $state $Latest)" "SSH sunucusu: $st ($mode)" "Otomatik güncelleme: $upd" "Paylaşılan klasör: $($state.Root)" "SSH portu: $($state.Port), izinli IP'ler: $($state.AllowedIps)" "Yedek sunucusu: $($state.ServerUrl) - $reg (bağlantı adı: $($state.Name))" "Sunucu anahtarı: $($state.HostKey)" ) -join "`r`n" } # --- arayüz ----------------------------------------------------------------------- function Show-ClientGui { Add-Type -AssemblyName System.Windows.Forms, System.Drawing [Windows.Forms.Application]::EnableVisualStyles() $form = New-Object Windows.Forms.Form $form.Text = "Yedek Client - sürüm $ClientVersion" $version = @{ Latest = Get-LatestVersion $InstallUrl } # olay işleyicileri değiştirebilsin diye hashtable $form.StartPosition = 'CenterScreen' $form.AutoScaleMode = 'Dpi' $form.Font = New-Object Drawing.Font('Segoe UI', 9) $form.ClientSize = New-Object Drawing.Size(760, 660) $form.MinimumSize = New-Object Drawing.Size(700, 560) $layout = New-Object Windows.Forms.TableLayoutPanel $layout.Dock = 'Fill' $layout.Padding = New-Object Windows.Forms.Padding(10) $layout.ColumnCount = 1 $layout.RowCount = 3 [void]$layout.RowStyles.Add((New-Object Windows.Forms.RowStyle('AutoSize'))) [void]$layout.RowStyles.Add((New-Object Windows.Forms.RowStyle('AutoSize'))) [void]$layout.RowStyles.Add((New-Object Windows.Forms.RowStyle('Percent', 100))) $form.Controls.Add($layout) # Durum $statusBox = New-Object Windows.Forms.GroupBox $statusBox.Text = 'Durum' $statusBox.Dock = 'Fill' $statusBox.AutoSize = $true $statusPanel = New-Object Windows.Forms.FlowLayoutPanel $statusPanel.Dock = 'Fill' $statusPanel.AutoSize = $true $statusPanel.FlowDirection = 'TopDown' $statusLabel = New-Object Windows.Forms.Label $statusLabel.AutoSize = $true $statusLabel.MaximumSize = New-Object Drawing.Size(720, 0) $statusPanel.Controls.Add($statusLabel) $statusButtons = New-Object Windows.Forms.FlowLayoutPanel $statusButtons.AutoSize = $true $btnStart = New-Object Windows.Forms.Button -Property @{ Text = 'SSH Sunucusunu Başlat'; AutoSize = $true } $btnStop = New-Object Windows.Forms.Button -Property @{ Text = 'SSH Sunucusunu Durdur'; AutoSize = $true } $btnLog = New-Object Windows.Forms.Button -Property @{ Text = 'SSH Olay Günlüğü'; AutoSize = $true } $btnUpdate = New-Object Windows.Forms.Button -Property @{ Text = 'Şimdi Güncelle'; AutoSize = $true } $btnRemove = New-Object Windows.Forms.Button -Property @{ Text = 'Kaldır'; AutoSize = $true } $statusButtons.Controls.AddRange(@($btnStart, $btnStop, $btnLog, $btnUpdate, $btnRemove)) $statusPanel.Controls.Add($statusButtons) $statusBox.Controls.Add($statusPanel) $layout.Controls.Add($statusBox, 0, 0) # Kurulum $setupBox = New-Object Windows.Forms.GroupBox $setupBox.Text = 'Kurulum ve Eşleştirme' $setupBox.Dock = 'Fill' $setupBox.AutoSize = $true $grid = New-Object Windows.Forms.TableLayoutPanel $grid.Dock = 'Fill' $grid.AutoSize = $true $grid.ColumnCount = 3 [void]$grid.ColumnStyles.Add((New-Object Windows.Forms.ColumnStyle('AutoSize'))) [void]$grid.ColumnStyles.Add((New-Object Windows.Forms.ColumnStyle('Percent', 100))) [void]$grid.ColumnStyles.Add((New-Object Windows.Forms.ColumnStyle('AutoSize'))) $setupBox.Controls.Add($grid) $layout.Controls.Add($setupBox, 0, 1) $script:row = 0 function Add-Row([string]$Label, $Control, $Extra) { $l = New-Object Windows.Forms.Label -Property @{ Text = $Label; AutoSize = $true; Anchor = 'Left' } $grid.Controls.Add($l, 0, $script:row) $Control.Anchor = if ($Control -is [Windows.Forms.TextBox]) { 'Left, Right' } else { 'Left' } $grid.Controls.Add($Control, 1, $script:row) if ($Extra) { $grid.Controls.Add($Extra, 2, $script:row) } $script:row++ } function New-Hint([string]$Text) { New-Object Windows.Forms.Label -Property @{ Text = $Text; AutoSize = $true; ForeColor = [Drawing.Color]::Gray; Anchor = 'Left' } } $state = Read-State $txtCode = New-Object Windows.Forms.TextBox -Property @{ Multiline = $true; Height = 60; Font = New-Object Drawing.Font('Consolas', 9) } Add-Row 'Eşleştirme kodu:' $txtCode (New-Hint 'Yedek sunucusunda "Client ile Ekle"') $txtRoot = New-Object Windows.Forms.TextBox $btnBrowse = New-Object Windows.Forms.Button -Property @{ Text = 'Gözat…'; AutoSize = $true } Add-Row 'Paylaşılacak klasör:' $txtRoot $btnBrowse $txtName = New-Object Windows.Forms.TextBox -Property @{ Text = $env:COMPUTERNAME } Add-Row 'Bağlantı adı:' $txtName (New-Hint 'Yedek sunucusunda görünecek ad') $txtHost = New-Object Windows.Forms.TextBox Add-Row 'Dış adres:' $txtHost (New-Hint 'Boş = yedek sunucusu algılar') $numPort = New-Object Windows.Forms.NumericUpDown -Property @{ Minimum = 1; Maximum = 65535; Value = 22; Width = 80 } Add-Row 'SSH portu:' $numPort (New-Hint 'SSH zaten kullanılıyorsa mevcut port') $txtIps = New-Object Windows.Forms.TextBox Add-Row "İzin verilen IP'ler:" $txtIps (New-Hint 'Boş = koddaki yedek sunucusu IP''leri') $btnInstall = New-Object Windows.Forms.Button -Property @{ Text = 'Kur ve Kaydet'; AutoSize = $true } $grid.Controls.Add($btnInstall, 1, $script:row) if ($state) { if ($state.Root) { $txtRoot.Text = $state.Root } if ($state.Name) { $txtName.Text = $state.Name } if ($state.PSObject.Properties['ExternalHost'] -and $state.ExternalHost) { $txtHost.Text = $state.ExternalHost } if ($state.PSObject.Properties['HostKey'] -and $state.Port) { $numPort.Value = $state.Port } } $logBox = New-Object Windows.Forms.TextBox -Property @{ Multiline = $true; ReadOnly = $true; ScrollBars = 'Vertical'; Dock = 'Fill' Font = New-Object Drawing.Font('Consolas', 9); BackColor = [Drawing.Color]::White } $layout.Controls.Add($logBox, 0, 2) $log = { param([string]$Text) $logBox.AppendText((Get-Date).ToString('HH:mm:ss') + ' ' + $Text + "`r`n") [Windows.Forms.Application]::DoEvents() } $refresh = { $statusLabel.Text = Get-AgentStatusText $version.Latest $btnUpdate.Enabled = [bool](Get-ScheduledTask -TaskName $UpdateTaskName -ErrorAction SilentlyContinue) $svc = Get-Service sshd -ErrorAction SilentlyContinue $btnStart.Enabled = [bool]($svc -and $svc.Status -eq 'Stopped') $btnStop.Enabled = [bool]($svc -and $svc.Status -eq 'Running') $btnRemove.Enabled = (Test-Path $DataDir) -or [bool](Get-Service $LegacyService -ErrorAction SilentlyContinue) } $busy = { param([bool]$On) $form.UseWaitCursor = $On foreach ($b in @($btnInstall, $btnStart, $btnStop, $btnUpdate, $btnRemove)) { $b.Enabled = -not $On } [Windows.Forms.Application]::DoEvents() } $txtCode.Add_Leave({ # Kod yapıştırılınca izinli IP'leri koddan doldur if ($txtIps.Text.Trim() -or -not $txtCode.Text.Trim()) { return } try { $txtIps.Text = (Get-DefaultAllowedIps (Read-PairingCode $txtCode.Text)) -join ', ' } catch { } }) $btnBrowse.Add_Click({ $dlg = New-Object Windows.Forms.FolderBrowserDialog $dlg.Description = 'Yedek sunucusunun indireceği klasörü seçin' if ($txtRoot.Text -and (Test-Path $txtRoot.Text)) { $dlg.SelectedPath = $txtRoot.Text } if ($dlg.ShowDialog($form) -eq 'OK') { $txtRoot.Text = $dlg.SelectedPath } }) $btnInstall.Add_Click({ & $busy $true try { Install-Agent ([pscustomobject]@{ Code = $txtCode.Text; Root = $txtRoot.Text; Name = $txtName.Text; ExternalHost = $txtHost.Text Port = $numPort.Value; AllowedIps = $txtIps.Text }) $log $txtCode.Clear() [void][Windows.Forms.MessageBox]::Show($form, 'Kurulum tamamlandı ve yedek sunucusuna kaydedildi.', 'Yedek Client', 'OK', 'Information') } catch { & $log "HATA: $($_.Exception.Message)" [void][Windows.Forms.MessageBox]::Show($form, $_.Exception.Message, 'Yedek Client', 'OK', 'Error') } finally { & $busy $false & $refresh } }) $btnStart.Add_Click({ try { Start-Service sshd; & $log 'SSH sunucusu başlatıldı' } catch { & $log "HATA: $($_.Exception.Message)" } & $refresh }) $btnStop.Add_Click({ try { Stop-Service sshd; & $log 'SSH sunucusu durduruldu' } catch { & $log "HATA: $($_.Exception.Message)" } & $refresh }) $btnLog.Add_Click({ Start-Process eventvwr.msc '/c:OpenSSH/Operational' }) $btnUpdate.Add_Click({ # Güncelleyiciyi (SYSTEM görevi) hemen çalıştırır ve bitmesini bekler & $busy $true try { & $log 'Güncelleme kontrolü başlatıldı' Start-ScheduledTask -TaskName $UpdateTaskName $deadline = (Get-Date).AddMinutes(10) Start-Sleep -Seconds 2 while ((Get-ScheduledTask -TaskName $UpdateTaskName).State -eq 'Running' -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 500 [Windows.Forms.Application]::DoEvents() } if (Test-Path $UpdateLogPath) { & $log ('Güncelleyici: ' + (Get-Content $UpdateLogPath -Encoding UTF8 -Tail 1)) } $version.Latest = Get-LatestVersion $InstallUrl } catch { & $log "HATA: $($_.Exception.Message)" } finally { & $busy $false & $refresh } }) $btnRemove.Add_Click({ $answer = [Windows.Forms.MessageBox]::Show($form, ("'$SftpUser' kullanıcısı, SSH yapılandırma bloğu, güvenlik duvarı kuralı ve klasör izni kaldırılacak.`r`n" + "SSH sunucusunu bu kurulum yönetiyorsa durdurulup devre dışı bırakılır.`r`n" + "Paylaşılan klasördeki yedek dosyalarına dokunulmaz.`r`n`r`nDevam edilsin mi?"), 'Yedek Client', 'YesNo', 'Warning', 'Button2') if ($answer -ne 'Yes') { return } & $busy $true try { Uninstall-Agent $log } catch { & $log "HATA: $($_.Exception.Message)" } finally { & $busy $false; & $refresh } }) & $refresh [void]$form.ShowDialog() } # --- giriş ------------------------------------------------------------------------ if ($InstallUrl -like '__*') { $InstallUrl = '' } if ($ClientVersion -like '__*') { $ClientVersion = 'yerel kopya' } if ($YedekClientMode -eq 'update') { # Güncelleyici, imzası doğrulanmış yeni sürümü bu modda çalıştırır (SYSTEM, arayüzsüz) $updateLog = { param([string]$Text) Add-Content -Path $UpdateLogPath -Encoding UTF8 -Value ((Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + ' ' + $Text) } try { Update-Agent $updateLog $YedekClientHash } catch { & $updateLog "HATA: $($_.Exception.Message)" } return } if ($YedekClientNoGui -or $MyInvocation.InvocationName -eq '.') { return } $identity = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() if (-not $identity.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { # Yönetici olarak yeniden aç: "irm | iex" ile geldiyse aynı adresten tekrar indirerek if ($InstallUrl) { $cmd = "[Net.ServicePointManager]::SecurityProtocol = 'Tls12'; irm '$InstallUrl' | iex" Start-Process powershell.exe -Verb RunAs -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', $cmd) } elseif ($PSCommandPath) { Start-Process powershell.exe -Verb RunAs -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"") } else { Write-Warning 'Yedek Client yönetici yetkisi gerektirir. PowerShell''i "Yönetici olarak çalıştır" ile açıp tekrar deneyin.' } return } Show-ClientGui }