Files
FlyShotHost/tests/Publishing/Publish-Obfuscated.Tests.ps1
yunxiao.zhu 070d2ba5c0 ✨ feat(server): 增加混淆发布链路与调试页静态说明
- 新增 Obfuscar 混淆发布脚本、发布测试与产物隐私检查
- 调整 VS Code 发布任务和宿主发布配置,统一走混淆流程
- 为 debug 页面内嵌静态接口说明,并补充对应集成测试
- 更新 README 与设计计划文档,记录跨平台发布约束
2026-06-22 15:55:34 +08:00

253 lines
9.0 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<#
.SYNOPSIS
验证混淆发布脚本生成的 Obfuscar 配置满足仓库发布契约。
.DESCRIPTION
该测试使用临时空文件模拟发布目录,仅验证模块筛选、绝对路径和失败语义,
不会执行 dotnet publish,也不会要求测试环境安装 Obfuscar。
#>
$ErrorActionPreference = 'Stop'
$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$publishScriptPath = Join-Path $repositoryRoot 'scripts\Publish-Obfuscated.ps1'
$publishProfilePath = Join-Path $repositoryRoot 'src\Flyshot.Server.Host\Properties\PublishProfiles\FolderProfile.pubxml'
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) "flyshot-obfuscation-test-$([guid]::NewGuid().ToString('N'))"
$inputDirectory = Join-Path $temporaryRoot 'input'
$outputDirectory = Join-Path $temporaryRoot 'obfuscated'
$configPath = Join-Path $temporaryRoot 'obfuscar.xml'
$expectedAssemblies = @(
'Flyshot.Server.Host.dll'
'Flyshot.ControllerClientCompat.dll'
'Flyshot.Core.Config.dll'
'Flyshot.Core.Domain.dll'
'Flyshot.Core.Planning.dll'
'Flyshot.Core.Triggering.dll'
'Flyshot.Runtime.Fanuc.dll'
'Flyshot.Runtime.Common.dll'
)
$futureAssemblyName = 'Flyshot.Future.Module.dll'
try
{
$publishScriptContent = Get-Content -LiteralPath $publishScriptPath -Raw
$publishScriptTokens = $null
$publishScriptErrors = $null
$publishScriptAst = [System.Management.Automation.Language.Parser]::ParseInput(
$publishScriptContent,
[ref]$publishScriptTokens,
[ref]$publishScriptErrors)
if ($publishScriptErrors.Count -ne 0)
{
throw "Publish-Obfuscated.ps1 存在 PowerShell 语法错误:$($publishScriptErrors[0].Message)"
}
$scriptParameters = @($publishScriptAst.ParamBlock.Parameters | ForEach-Object {
$_.Name.VariablePath.UserPath
})
if ('RuntimeIdentifiers' -in $scriptParameters -or 'ExistingRuntimeIdentifier' -in $scriptParameters)
{
throw '发布脚本不应再暴露运行平台或 RID 参数。'
}
if ($publishScriptContent -match "'-r'|--self-contained'\s*,?\s*'true'|win-x64|linux-x64")
{
throw '发布脚本默认发布流程不得再指定 RID 或生成自包含平台包。'
}
if ($publishScriptContent -notmatch '-p:UseAppHost=false')
{
throw '框架依赖发布必须关闭平台相关 apphost,只保留跨平台 DLL 入口。'
}
[xml]$publishProfile = Get-Content -LiteralPath $publishProfilePath -Raw
$publishNamespace = [System.Xml.XmlNamespaceManager]::new($publishProfile.NameTable)
$publishNamespace.AddNamespace('msb', $publishProfile.Project.NamespaceURI)
$useAppHost = $publishProfile.SelectSingleNode(
'/msb:Project/msb:PropertyGroup/msb:UseAppHost',
$publishNamespace)
if ($null -eq $useAppHost -or $useAppHost.InnerText -ne 'false')
{
throw 'FolderProfile.pubxml 必须关闭平台相关 apphost。'
}
$afterPublishTarget = $publishProfile.SelectSingleNode(
'/msb:Project/msb:Target[@Name="ObfuscatePublishedFlyshotAssemblies"]',
$publishNamespace)
if ($null -eq $afterPublishTarget -or $afterPublishTarget.AfterTargets -ne 'Publish')
{
throw 'FolderProfile.pubxml 必须注册 Publish 完成后的混淆 Target。'
}
$publishExec = $afterPublishTarget.SelectSingleNode('msb:Exec', $publishNamespace)
if (
$null -eq $publishExec -or
$publishExec.Command -notmatch '-ObfuscateExistingPublish' -or
$publishExec.Command -notmatch '\$\(PublishDir\)\.' -or
$publishExec.Command -match 'RuntimeIdentifier'
)
{
throw 'Visual Studio 发布 Target 必须把 PublishDir 以安全的非反斜杠结尾形式交给混淆模式。'
}
New-Item -ItemType Directory -Path $inputDirectory -Force | Out-Null
foreach ($assemblyName in $expectedAssemblies)
{
New-Item -ItemType File -Path (Join-Path $inputDirectory $assemblyName) -Force | Out-Null
}
# 模拟未来新增模块,证明脚本会自动保护所有 Flyshot.* 程序集,而不是只处理当前固定清单。
New-Item -ItemType File -Path (Join-Path $inputDirectory $futureAssemblyName) -Force | Out-Null
# 第三方程序集用于证明配置生成器只选择 Flyshot 自研模块。
New-Item -ItemType File -Path (Join-Path $inputDirectory 'NLog.dll') -Force | Out-Null
& pwsh -NoProfile -File $publishScriptPath `
-GenerateConfigOnly `
-InputDirectory $inputDirectory `
-ObfuscatedDirectory $outputDirectory `
-ConfigPath $configPath
if ($LASTEXITCODE -ne 0)
{
throw "配置生成命令退出码不是 0:$LASTEXITCODE"
}
[xml]$configuration = Get-Content -LiteralPath $configPath -Raw
$modulePaths = @($configuration.Obfuscator.Module | ForEach-Object { [string]$_.file })
$allExpectedAssemblies = @($expectedAssemblies) + $futureAssemblyName
if ($modulePaths.Count -ne $allExpectedAssemblies.Count)
{
throw "预期生成 $($allExpectedAssemblies.Count) 个模块,实际为 $($modulePaths.Count) 个。"
}
foreach ($assemblyName in $allExpectedAssemblies)
{
$expectedPath = [System.IO.Path]::GetFullPath((Join-Path $inputDirectory $assemblyName))
if ($expectedPath -notin $modulePaths)
{
throw "Obfuscar 配置缺少自研程序集:$assemblyName"
}
}
if ($modulePaths | Where-Object { [System.IO.Path]::GetFileName($_) -eq 'NLog.dll' })
{
throw '第三方程序集 NLog.dll 不应进入 Obfuscar 模块列表。'
}
foreach ($path in $modulePaths)
{
if (-not [System.IO.Path]::IsPathFullyQualified($path))
{
throw "Obfuscar 模块路径不是绝对路径:$path"
}
}
foreach ($module in @($configuration.Obfuscator.Module))
{
$anonymousTypeRule = @($module.SkipType | Where-Object {
[string]$_.name -eq '^<>f__AnonymousType.*'
})
if ($anonymousTypeRule.Count -ne 1)
{
throw "模块 $($module.file) 必须显式保留匿名类型元数据。"
}
if (
[string]$anonymousTypeRule[0].skipMethods -ne 'true' -or
[string]$anonymousTypeRule[0].skipFields -ne 'true' -or
[string]$anonymousTypeRule[0].skipProperties -ne 'true' -or
[string]$anonymousTypeRule[0].skipEvents -ne 'true'
)
{
throw "模块 $($module.file) 的匿名类型排除规则不完整。"
}
}
$assemblySearchPaths = @($configuration.Obfuscator.AssemblySearchPath | ForEach-Object {
[string]$_.path
})
if ($assemblySearchPaths.Count -lt 2)
{
throw 'Obfuscar 配置必须包含 .NETCore.App 与 ASP.NET Core 共享框架搜索目录。'
}
foreach ($assemblySearchPath in $assemblySearchPaths)
{
if (-not [System.IO.Path]::IsPathFullyQualified($assemblySearchPath))
{
throw "共享框架搜索目录不是绝对路径:$assemblySearchPath"
}
}
if (-not ($assemblySearchPaths | Where-Object { $_ -match 'Microsoft\.NETCore\.App' }))
{
throw 'Obfuscar 配置缺少 Microsoft.NETCore.App 搜索目录。'
}
if (-not ($assemblySearchPaths | Where-Object { $_ -match 'Microsoft\.AspNetCore\.App' }))
{
throw 'Obfuscar 配置缺少 Microsoft.AspNetCore.App 搜索目录。'
}
$variables = @{}
foreach ($variable in $configuration.Obfuscator.Var)
{
$variables[[string]$variable.name] = [string]$variable.value
}
if ($variables['KeepPublicApi'] -ne 'true')
{
throw 'KeepPublicApi 必须为 true,以保护 ASP.NET Core 和公共契约。'
}
if ($variables['HidePrivateApi'] -ne 'true')
{
throw 'HidePrivateApi 必须为 true,以混淆内部实现。'
}
if ($variables['RenameProperties'] -ne 'false' -or $variables['RenameEvents'] -ne 'false')
{
throw '属性和事件重命名必须关闭,以保护配置绑定及序列化契约。'
}
if ($variables['SkipGenerated'] -ne 'true')
{
throw '编译器生成类型必须跳过混淆,以保护匿名 JSON 响应和异步状态机。'
}
if ($variables['SkipSpecialName'] -ne 'true')
{
throw 'SpecialName 成员必须跳过混淆,以保护属性、事件和框架反射约定。'
}
Remove-Item -LiteralPath (Join-Path $inputDirectory 'Flyshot.Runtime.Common.dll') -Force
$missingAssemblyConfigPath = Join-Path $temporaryRoot 'missing-assembly.xml'
& pwsh -NoProfile -File $publishScriptPath `
-GenerateConfigOnly `
-InputDirectory $inputDirectory `
-ObfuscatedDirectory $outputDirectory `
-ConfigPath $missingAssemblyConfigPath *> $null
if ($LASTEXITCODE -eq 0)
{
throw '缺少预期的 Flyshot 程序集时,配置生成必须失败。'
}
Write-Host 'Publish-Obfuscated.Tests.ps1: PASS'
}
finally
{
if (Test-Path -LiteralPath $temporaryRoot)
{
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force
}
}