✨ feat(server): 增加混淆发布链路与调试页静态说明
- 新增 Obfuscar 混淆发布脚本、发布测试与产物隐私检查 - 调整 VS Code 发布任务和宿主发布配置,统一走混淆流程 - 为 debug 页面内嵌静态接口说明,并补充对应集成测试 - 更新 README 与设计计划文档,记录跨平台发布约束
This commit is contained in:
@@ -33,6 +33,9 @@ public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IC
|
||||
|
||||
Assert.Contains("Flyshot Replacement 接口调试", html, StringComparison.Ordinal);
|
||||
Assert.Contains("id=\"debug-console-app\"", html, StringComparison.Ordinal);
|
||||
Assert.Contains("id=\"debug-static-docs\"", html, StringComparison.Ordinal);
|
||||
Assert.Contains("POST /move_pose/", html, StringComparison.Ordinal);
|
||||
Assert.Contains("POST /upload_flyshot/", html, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("__SWAGGER_JSON_URL__", html, StringComparison.Ordinal);
|
||||
Assert.Contains("/assets/debug.css", html, StringComparison.Ordinal);
|
||||
Assert.Contains("/assets/debug.js", html, StringComparison.Ordinal);
|
||||
@@ -66,6 +69,45 @@ public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IC
|
||||
Assert.Contains("r: 0.0", script, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调试脚本应当优先读取静态说明并在缺失时回退到 OpenAPI / “暂无说明”。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetDebugScript_ContainsStaticDocumentationMergeLogic()
|
||||
{
|
||||
using var configuredFactory = CreateFactoryWithSwaggerEnabled(true);
|
||||
using var client = configuredFactory.CreateClient();
|
||||
|
||||
using var scriptResponse = await client.GetAsync("/assets/debug.js");
|
||||
Assert.Equal(HttpStatusCode.OK, scriptResponse.StatusCode);
|
||||
|
||||
var script = await scriptResponse.Content.ReadAsStringAsync();
|
||||
Assert.Contains("debug-static-docs", script, StringComparison.Ordinal);
|
||||
Assert.Contains("op.method + \" \" + op.path", script, StringComparison.Ordinal);
|
||||
Assert.Contains("mergedSummary", script, StringComparison.Ordinal);
|
||||
Assert.Contains("mergedDescription", script, StringComparison.Ordinal);
|
||||
Assert.Contains("requestBodyFieldDocs", script, StringComparison.Ordinal);
|
||||
Assert.Contains("暂无说明", script, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调试卡片默认应直接展开,确保静态说明和字段表在初始页面就可见。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetDebugScript_DefaultsCardsToExpandedForVisibleDocumentation()
|
||||
{
|
||||
using var configuredFactory = CreateFactoryWithSwaggerEnabled(true);
|
||||
using var client = configuredFactory.CreateClient();
|
||||
|
||||
using var scriptResponse = await client.GetAsync("/assets/debug.js");
|
||||
Assert.Equal(HttpStatusCode.OK, scriptResponse.StatusCode);
|
||||
|
||||
var script = await scriptResponse.Content.ReadAsStringAsync();
|
||||
Assert.DoesNotContain("card.className = \"card collapsed\"", script, StringComparison.Ordinal);
|
||||
Assert.Contains("card.className = \"card\"", script, StringComparison.Ordinal);
|
||||
Assert.Contains("toggle.textContent = \"收起 ▴\"", script, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当 Swagger 启用时,调试配置 API 应当返回实际 Swagger JSON 地址。
|
||||
/// </summary>
|
||||
|
||||
252
tests/Publishing/Publish-Obfuscated.Tests.ps1
Normal file
252
tests/Publishing/Publish-Obfuscated.Tests.ps1
Normal file
@@ -0,0 +1,252 @@
|
||||
<#
|
||||
.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
|
||||
}
|
||||
}
|
||||
35
tests/Publishing/Test-PublishedArtifactPrivacy.ps1
Normal file
35
tests/Publishing/Test-PublishedArtifactPrivacy.ps1
Normal file
@@ -0,0 +1,35 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
验证混淆发布包不会泄露 Flyshot 源码文档或调试符号。
|
||||
|
||||
.PARAMETER PublishDirectory
|
||||
需要检查的框架依赖混淆发布目录。
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$PublishDirectory = (Join-Path $PSScriptRoot '..\..\outputs\obfuscated\framework-dependent')
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$absolutePublishDirectory = [System.IO.Path]::GetFullPath($PublishDirectory)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $absolutePublishDirectory -PathType Container))
|
||||
{
|
||||
throw "混淆发布目录不存在:$absolutePublishDirectory"
|
||||
}
|
||||
|
||||
$sensitiveFiles = @(
|
||||
Get-ChildItem -LiteralPath $absolutePublishDirectory -File |
|
||||
Where-Object {
|
||||
$_.Name -like 'Flyshot*.xml' -or
|
||||
$_.Name -like 'Flyshot*.pdb'
|
||||
}
|
||||
)
|
||||
|
||||
if ($sensitiveFiles.Count -gt 0)
|
||||
{
|
||||
throw "混淆发布包仍包含源码文档或调试符号:$($sensitiveFiles.Name -join ', ')"
|
||||
}
|
||||
|
||||
Write-Host 'Test-PublishedArtifactPrivacy.ps1: PASS'
|
||||
Reference in New Issue
Block a user