[CmdletBinding()] param( [string]$SourceDirectory = "", [string]$InstallDirectory = (Join-Path $env:ProgramFiles "Scert"), [string]$DataDirectory = (Join-Path $env:ProgramData "Scert"), [string]$CertificateDirectory = (Join-Path $env:ProgramData "Scert\ssl"), [ValidatePattern("^[A-Za-z0-9_.-]+$")] [string]$ServiceName = "Scert", [ValidatePattern("^[A-Za-z0-9_.-]+$")] [string]$AdminServiceName = "Scert-admin" ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Definition if ([string]::IsNullOrWhiteSpace($SourceDirectory)) { $SourceDirectory = $scriptDirectory } function Test-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Resolve-RequiredDirectory { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Label ) if ([string]::IsNullOrWhiteSpace($Path)) { throw "$Label 不能为空。" } $resolved = [System.IO.Path]::GetFullPath($Path) $root = [System.IO.Path]::GetPathRoot($resolved) if ($resolved -eq $root) { throw "$Label 不能使用磁盘根目录:$resolved" } return $resolved.TrimEnd([System.IO.Path]::DirectorySeparatorChar) } function Invoke-Sc { param( [Parameter(Mandatory = $true)] [string[]]$Arguments, [switch]$AllowFailure ) $output = & sc.exe @Arguments 2>&1 if ($LASTEXITCODE -ne 0 -and -not $AllowFailure) { throw "Windows 服务命令执行失败:sc.exe $($Arguments -join ' ')`n$($output -join "`n")" } return $output } function Test-ServiceExists { param([Parameter(Mandatory = $true)][string]$Name) & sc.exe query $Name *> $null return $LASTEXITCODE -eq 0 } function Install-OrUpdateService { param( [Parameter(Mandatory = $true)][string]$Name, [Parameter(Mandatory = $true)][string]$DisplayName, [Parameter(Mandatory = $true)][string]$Description, [Parameter(Mandatory = $true)][string]$BinaryPath, [Parameter(Mandatory = $true)][ValidateSet("auto", "demand")][string]$StartMode ) if (Test-ServiceExists -Name $Name) { Invoke-Sc -Arguments @("config", $Name, "binPath=", $BinaryPath, "start=", $StartMode, "obj=", "NT AUTHORITY\LocalService", "DisplayName=", $DisplayName) | Out-Null } else { Invoke-Sc -Arguments @("create", $Name, "binPath=", $BinaryPath, "start=", $StartMode, "obj=", "NT AUTHORITY\LocalService", "DisplayName=", $DisplayName) | Out-Null } Invoke-Sc -Arguments @("description", $Name, $Description) | Out-Null } function Set-DirectoryAcl { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][ValidateSet("read", "modify")][string]$Mode ) $localServicePermission = if ($Mode -eq "modify") { "(OI)(CI)(M)" } else { "(OI)(CI)(RX)" } & icacls.exe $Path /inheritance:r /grant:r ` "BUILTIN\Administrators:(OI)(CI)(F)" ` "NT AUTHORITY\SYSTEM:(OI)(CI)(F)" ` "NT AUTHORITY\LOCAL SERVICE:$localServicePermission" *> $null if ($LASTEXITCODE -ne 0) { throw "设置目录访问控制失败:$Path" } } if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw "该安装器只能在 Windows 上运行。" } if (-not (Test-Administrator)) { throw "请使用管理员权限运行 Scert Windows 安装器。" } if (-not (Get-Command sc.exe -ErrorAction SilentlyContinue)) { throw "系统中没有找到 sc.exe,无法注册 Windows 服务。" } if (-not (Get-Command icacls.exe -ErrorAction SilentlyContinue)) { throw "系统中没有找到 icacls.exe,无法设置安全权限。" } $source = Resolve-RequiredDirectory -Path $SourceDirectory -Label "安装产物目录" $install = Resolve-RequiredDirectory -Path $InstallDirectory -Label "程序安装目录" $data = Resolve-RequiredDirectory -Path $DataDirectory -Label "数据目录" $certificate = Resolve-RequiredDirectory -Path $CertificateDirectory -Label "证书目录" $sourceScert = Join-Path $source "scert.exe" $sourceAdmin = Join-Path $source "scert-admin.exe" if (-not (Test-Path -LiteralPath $sourceScert -PathType Leaf)) { throw "安装产物目录缺少 scert.exe:$sourceScert" } if (-not (Test-Path -LiteralPath $sourceAdmin -PathType Leaf)) { throw "安装产物目录缺少 scert-admin.exe:$sourceAdmin" } $configDirectory = Join-Path $data "config" $databaseDirectory = Join-Path $data "data" $logDirectory = Join-Path $data "logs" $backupDirectory = Join-Path $data "backups" $docsDirectory = Join-Path $data "docs" $runtimeDirectory = Join-Path $data "runtime" foreach ($directory in @($install, $data, $configDirectory, $databaseDirectory, $logDirectory, $backupDirectory, $docsDirectory, $runtimeDirectory, $certificate)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } $installedScert = Join-Path $install "scert.exe" $installedAdmin = Join-Path $install "scert-admin.exe" Copy-Item -LiteralPath $sourceScert -Destination $installedScert -Force Copy-Item -LiteralPath $sourceAdmin -Destination $installedAdmin -Force Set-DirectoryAcl -Path $install -Mode read foreach ($directory in @($data, $configDirectory, $databaseDirectory, $logDirectory, $backupDirectory, $runtimeDirectory, $certificate)) { Set-DirectoryAcl -Path $directory -Mode modify } Set-DirectoryAcl -Path $docsDirectory -Mode read $configurationPath = Join-Path $configDirectory "scert.json" $resetCodePath = Join-Path $runtimeDirectory "reset-code" $mainBinaryPath = '"{0}" --config "{1}" serve' -f $installedScert, $configurationPath $adminBinaryPath = '"{0}" --config "{1}" --code-output "{2}"' -f $installedAdmin, $configurationPath, $resetCodePath Install-OrUpdateService ` -Name $ServiceName ` -DisplayName "Scert SSL 证书管理服务" ` -Description "Scert SSL 证书申请、续期和部署服务" ` -BinaryPath $mainBinaryPath ` -StartMode auto Install-OrUpdateService ` -Name $AdminServiceName ` -DisplayName "Scert-admin 临时密码重置服务" ` -Description "仅在管理员手动启动时临时激活 Scert 密码重置入口" ` -BinaryPath $adminBinaryPath ` -StartMode demand $metadata = [ordered]@{ installed_at = [DateTimeOffset]::Now.ToString("o") install_directory = $install data_directory = $data config_directory = $configDirectory certificate_root = $certificate service_name = $ServiceName admin_service_name = $AdminServiceName service_account = "NT AUTHORITY\LocalService" } $metadataPath = Join-Path $configDirectory "installation.json" $metadata | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $metadataPath -Encoding UTF8 Write-Host "Scert Windows 文件安装和服务注册完成。" Write-Host "核心程序:$installedScert" Write-Host "密码重置程序:$installedAdmin" Write-Host "配置目录:$configDirectory" Write-Host "数据目录:$databaseDirectory" Write-Host "日志目录:$logDirectory" Write-Host "备份目录:$backupDirectory" Write-Host "证书目录:$certificate" Write-Host "安装记录:$metadataPath" Write-Host "Scert 主服务已注册为自动启动,但本安装器没有启动或重启服务。" Write-Host "Scert-admin 已注册为手动启动,本安装器不会启动它,也不会创建公网防火墙规则。" Write-Host "首次初始化时请使用配置文件路径:$configurationPath"