Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
## [Unreleased]

### Fixed
* HTTP 请求管线正确性与响应一致性(C458,GFX-1180,GitHub #926):ProtoBuf 成功响应写出前更正 Content-Type 为 `application/x-protobuf`(原恒为 JSON,按媒体类型分派的客户端/网关解析二进制体失败);参数字典改 OrdinalIgnoreCase 并同步把 Query 提取改 TryAdd——大小写差异同名键(Query↔Body、Query↔Query)一律显式 `ParamErr` 拒绝,消除「被校验/被签名的 Query 值被 Body 静默覆盖」窗口;非 JSON 对象 Body(null 字面量/数组/标量/空体)以参数错误拒绝(新键 `Network.Http.RequestBodyNotJsonObject`),不再落入顶层 catch 记服务器错误日志;非 GET 缺 Content-Type 改统一 JSON envelope;DataAnnotations 校验失败业务码由字面量 400 统一为 `GameHttpStatusCode.ParamErr`;Swagger 路由匹配改末段精确比较(互为后缀命令不再错配文档,驼峰路由可匹配)且 GET 参数必填性跟随 `RequiredAttribute`;AOP 拒绝且未自行写响应时管线回写校验失败 envelope(契约写入 `IHttpAopHandler.Run`);`CheckSign` 缺 sign/timestamp 改本地化消息。对外破坏性:依赖 `code == 400` 判定校验失败的客户端改判 `code == 13`;`HttpStatusMessage.Success`/`NotFoundCommand`/`CheckFailedCommand` 及孤儿键 `Network.Http.NotFoundCommand`/`CheckFailedCommand` 删除(公共 API,下游如有引用需迁移)。
* Utility 安全修复:配置凭据脱敏、解压炸弹上限、冲突异常 Secret 脱敏与压缩失败显式抛出(C433,GFX-1173,GitHub #919):①`AppSetting.ToString/ToFormatString` 经浅拷贝掩码后序列化——`DataBasePassword`/`HttpSignSecret`/`InterNodeAuthSecret`/`HttpsCertPassword` 非空值输出 `***`(null/空串原样保留区分「未配置」),四个凭据不再随启动/停止日志明文外发;②`CompressionHelper.Decompress` 新增 `maxOutputLength` 上限重载(旧签名转发 const 默认 64MB),膨胀过程内超限即抛 `InvalidDataException`——Network `DefaultMessageDecompressHandler` 增构造参数上限、RemoteMessaging `IMessageCompressionAlgorithm.Decompress` 接口加参(const 默认值,源码兼容;`DefaultMessageCodec` 显式传 `_maxDecompressedSize`),事后长度检查降级为兜底防线,1MB 高压缩比帧不再可膨胀至 GB 级托管内存;③`SettingFieldConflict` 敏感字段判定扩展 Secret/Token 子串——多 Role 配置冲突异常不再明文携带 `HttpSignSecret`/`InterNodeAuthSecret`;④`RandomHelper` 类文档声明双随机源分级(基础接口 `Random.Shared` 非加密 PRNG 禁用于令牌/验证码/防作弊场景,安全场景用权重系列 CSPRNG 或 `RandomNumberGenerator`);⑤落地已归档 C349 既定方案:`Compress` 异常原样抛出、`Decompress` 对非法/损坏/截断数据统一抛 `InvalidDataException`(含 SharpZipLib 截断流死循环主动判定),删除「失败静默返回原始字节」回退与 `CompressFailed` 本地化键;⑥`FP.Sqrt/Acos` 越界异常构造器实参换序(本地化消息进 `Message`、`"x"` 进 `ParamName`,修复前 `ParamName == null`)。公共 API 行为变更经本条披露;解压炸弹/脱敏/截断/参数名均有回归用例锁定。

* Core 回收链路数据与稳定性修复(C436,GFX-1176,GitHub #922):`Actor.OnRecycleAsync` 在回收回调后删除 `ScheduleIdSet` 中剩余的 Quartz 任务并清空集合,三条回收路径(空闲回收 / `RemoveAsync` / `RemoveAllAsync`)均覆盖;定时任务触发改为 `isNew: false` 反查,目标 Actor 不在内存时不再重建 Actor(消除周期任务复活已回收 Actor 的循环),记 Warning 并删除该任务。`BaseComponent.InactiveAsync` 以 try/finally 保证 `IsActive` 落为 false,`Actor.InactiveAsync` 逐组件隔离异常(Fatal 日志后继续),单个组件反激活失败不再让 Actor 永驻内存,也不再打断 `RemoveAllAsync` 关服流程。显式移除前对有未落库修改的 Actor 强制回存(失败记 Fatal 不阻断移除)。`StateComponent.TimerSaveAsync` 单个状态类型回存失败不再中断整轮,日志级别 Info → Error。`ActorManager.RemoveAsync` 返回真实完成任务(await 返回即回存 / 反激活 / 回收回调均已执行,隔离 Actor 返回异常任务)并同步删除 `ActiveTimeDic` 条目。行为变化:依赖「定时器唤醒离线 Actor」的业务需在触发前自行加载 Actor;组件反激活异常不再向 `RemoveAllAsync` 调用方抛出。回归 11 例(A/B:还原生产改动后 11/11 必败)。
Expand Down
27 changes: 9 additions & 18 deletions GameFrameX.Localization/Keys.NetworkHttp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,15 @@ public static class NetworkHttp
/// </remarks>
public const string DataVerificationFailed = "Network.Http.DataVerificationFailed";

/// <summary>
/// 请求体必须是 JSON 对象
/// </summary>
/// <remarks>
/// 键名: Network.Http.RequestBodyNotJsonObject
/// 用途: 当JSON请求体不是对象(null字面量/数组/标量/空体)时返回参数错误
/// </remarks>
public const string RequestBodyNotJsonObject = "Network.Http.RequestBodyNotJsonObject";

/// <summary>
/// http命令未包含验证参数{0} 和 {1}
/// </summary>
Expand All @@ -215,24 +224,6 @@ public static class NetworkHttp
/// </remarks>
public const string UndefinedCommand = "Network.Http.UndefinedCommand";

/// <summary>
/// not found command
/// </summary>
/// <remarks>
/// 键名: Network.Http.NotFoundCommand
/// 用途: 命令未找到时的错误消息
/// </remarks>
public const string NotFoundCommand = "Network.Http.NotFoundCommand";

/// <summary>
/// check failed command
/// </summary>
/// <remarks>
/// 键名: Network.Http.CheckFailedCommand
/// 用途: 命令验证失败时的错误消息
/// </remarks>
public const string CheckFailedCommand = "Network.Http.CheckFailedCommand";

/// <summary>
/// server error
/// </summary>
Expand Down
9 changes: 3 additions & 6 deletions GameFrameX.Localization/Localization/Messages/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -199,18 +199,15 @@
</data>
<data name="Network.Http.DataVerificationFailed" xml:space="preserve">
<value>data verification failed</value>
</data>
<data name="Network.Http.RequestBodyNotJsonObject" xml:space="preserve">
<value>request body must be a JSON object</value>
</data>
<data name="Network.Http.HttpCommandMissingValidationParameters" xml:space="preserve">
<value>http command missing validation parameters {0} and {1}</value>
</data>
<data name="Network.Http.UndefinedCommand" xml:space="preserve">
<value>undefined command</value>
</data>
<data name="Network.Http.NotFoundCommand" xml:space="preserve">
<value>not found command</value>
</data>
<data name="Network.Http.CheckFailedCommand" xml:space="preserve">
<value>check failed command</value>
</data>
<data name="Network.Http.ServerError" xml:space="preserve">
<value>server error</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,18 +217,15 @@
</data>
<data name="Network.Http.DataVerificationFailed" xml:space="preserve">
<value>数据验证失败</value>
</data>
<data name="Network.Http.RequestBodyNotJsonObject" xml:space="preserve">
<value>请求体必须是 JSON 对象</value>
</data>
<data name="Network.Http.HttpCommandMissingValidationParameters" xml:space="preserve">
<value>http命令未包含验证参数{0} 和 {1}</value>
</data>
<data name="Network.Http.UndefinedCommand" xml:space="preserve">
<value>未定义的命令</value>
</data>
<data name="Network.Http.NotFoundCommand" xml:space="preserve">
<value>未找到的命令</value>
</data>
<data name="Network.Http.CheckFailedCommand" xml:space="preserve">
<value>验证失败的命令</value>
</data>
<data name="Network.Http.ServerError" xml:space="preserve">
<value>服务器错误</value>
Expand Down
2 changes: 1 addition & 1 deletion GameFrameX.Network.HTTP/BaseHttpHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ public bool CheckSign(string command, Dictionary<string, object> paramMap, out s
// 内部验证
if (!paramMap.ContainsKey(GlobalConst.HttpSignKey) || !paramMap.ContainsKey(GlobalConst.HttpTimestampKey))
{
error = HttpJsonResultData<string>.ValidationErrorString();
error = HttpJsonResultData<string>.ValidationErrorString(HttpStatusMessage.ParamErrorMessage);
return false;
}

Expand Down
74 changes: 64 additions & 10 deletions GameFrameX.Network.HTTP/HttpHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,18 @@ public static async Task HandleRequestAsync(HttpContext context, Func<string, Ba

try
{
var paramMap = new Dictionary<string, object>();
var queryStringParamCount = ExtractQueryParameters(context, paramMap);
// 参数字典不区分大小写:与 JsonHelper 绑定(PropertyNameCaseInsensitive)口径一致,
// Query 与 Body 的大小写差异同名键一律视为重复参数显式拒绝,杜绝 Body 静默覆盖 Query
var paramMap = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

context.Response.Headers.ContentType = JsonContentType;
var queryExtraction = await ExtractQueryParametersAsync(context, paramMap);
if (queryExtraction.ShouldAbort)
{
return;
}

var queryStringParamCount = queryExtraction.Count;
var isGetRequest = HttpMethods.IsGet(context.Request.Method);

var parseResult = await TryParseRequestBodyAsync(context, paramMap, isGetRequest);
Expand Down Expand Up @@ -154,17 +162,24 @@ public static async Task HandleRequestAsync(HttpContext context, Func<string, Ba
}
}

// 从查询字符串中提取参数并返回参数个数 / Extract parameters from the query string and return the count.
private static int ExtractQueryParameters(HttpContext context, Dictionary<string, object> paramMap)
// 从查询字符串中提取参数并返回参数个数;键不区分大小写重复(如 ?a=1&A=2)时写出参数重复错误响应并返回 ShouldAbort=true。
// Extract parameters from the query string and return the count; a case-insensitive duplicate key (e.g. ?a=1&A=2) writes the duplicate-parameter error response and returns ShouldAbort=true.
private static async Task<(bool ShouldAbort, int Count)> ExtractQueryParametersAsync(HttpContext context, Dictionary<string, object> paramMap)
{
var queryStringParamCount = 0;
foreach (var keyValuePair in context.Request.Query)
{
paramMap.Add(keyValuePair.Key, keyValuePair.Value.ToString());
if (!paramMap.TryAdd(keyValuePair.Key, keyValuePair.Value.ToString()))
{
// 参数Key发生重复(不区分大小写)
await context.Response.WriteAsync(HttpJsonResultData<string>.ErrorString(GameHttpStatusCode.ParamErr, LocalizationService.GetString(Localization.Keys.NetworkHttp.ParameterDuplicate, keyValuePair.Key)));
return (true, queryStringParamCount);
}

queryStringParamCount++;
}

return queryStringParamCount;
return (false, queryStringParamCount);
}

// 解析请求体:GET 跳过 Body;ProtoBuf 反序列化;JSON 合并参数。返回解析结果,ShouldAbort=true 表示已写出错误响应、调用方应直接 return。
Expand All @@ -176,7 +191,7 @@ private static int ExtractQueryParameters(HttpContext context, Dictionary<string
// GET 请求允许没有 ContentType
if (contentType.IsNullOrWhiteSpace() && !isGetRequest)
{
await context.Response.WriteAsync(LocalizationService.GetString(Localization.Keys.NetworkHttp.HttpHeaderContentTypeNull));
await context.Response.WriteAsync(HttpJsonResultData<string>.ErrorString(GameHttpStatusCode.ParamErr, LocalizationService.GetString(Localization.Keys.NetworkHttp.HttpHeaderContentTypeNull)));
return (true, null, null, false);
}

Expand Down Expand Up @@ -220,7 +235,31 @@ private static int ExtractQueryParameters(HttpContext context, Dictionary<string
}

var jsonBody = Encoding.UTF8.GetString(jsonReadResult.Buffer);
var jsonKv = JsonHelper.Deserialize<Dictionary<string, object>>(jsonBody);
Dictionary<string, object> jsonKv;
try
{
jsonKv = JsonHelper.Deserialize<Dictionary<string, object>>(jsonBody);
}
catch (System.Text.Json.JsonException)
{
// 数组/标量/畸形 JSON:客户端格式错误按参数错误拒绝,不落入顶层 catch 记服务器错误日志
await WriteRequestBodyNotJsonObjectAsync(context);
return (true, null, null, false);
}
catch (ArgumentException)
{
// 空/空白请求体:JsonHelper 对空串先抛 ArgumentException
await WriteRequestBodyNotJsonObjectAsync(context);
return (true, null, null, false);
}

if (jsonKv == null)
{
// body 为 null 字面量时反序列化返回 null
await WriteRequestBodyNotJsonObjectAsync(context);
return (true, null, null, false);
}

foreach (var keyValuePair in jsonKv)
{
if (!paramMap.TryAdd(keyValuePair.Key, keyValuePair.Value))
Expand Down Expand Up @@ -273,6 +312,12 @@ private static bool RunAopHandlers(HttpContext httpContext, HttpActionContext co
// 执行AOP处理器
if (!RunAopHandlers(httpContext, context, aopHandlerTypes))
{
// AOP 拒绝且未自行写出响应时,回写统一校验失败信封,避免客户端收到空 200
if (!httpContext.Response.HasStarted)
{
await httpContext.Response.WriteAsync(HttpJsonResultData<string>.ValidationErrorString());
}

return (false, null);
}

Expand Down Expand Up @@ -326,6 +371,8 @@ private static async Task ExecuteProtoBufAsync(HttpContext httpContext, BaseHttp
ReadOnlyMemory<byte> body = ProtoBufSerializerHelper.Serialize(result);
var messageHttpObject = new MessageHttpObject { Id = MessageProtoHelper.GetMessageIdByType(result), UniqueId = context.MessageObject.UniqueId, Body = body.ToArray(), };
var resultResponse = ProtoBufSerializerHelper.Serialize(messageHttpObject);
// ProtoBuf 成功响应是二进制负载,写出前更正 Content-Type(入口处统一预设的是 JSON)
httpContext.Response.ContentType = ProtoBufContentType;
httpContext.Response.ContentLength = resultResponse.Length;
await httpContext.Response.BodyWriter.WriteAsync(resultResponse);
}
Expand Down Expand Up @@ -380,11 +427,11 @@ private static async Task ExecuteJsonMessageRequestAsync(HttpContext httpContext
{
if (validationResults.Count > 0)
{
await httpContext.Response.WriteAsync(HttpJsonResultData<string>.ErrorString(400, validationResults[0].ErrorMessage));
await httpContext.Response.WriteAsync(HttpJsonResultData<string>.ErrorString(GameHttpStatusCode.ParamErr, validationResults[0].ErrorMessage));
}
else
{
await httpContext.Response.WriteAsync(HttpJsonResultData<string>.ErrorString(400, LocalizationService.GetString(Localization.Keys.NetworkHttp.DataVerificationFailed)));
await httpContext.Response.WriteAsync(HttpJsonResultData<string>.ErrorString(GameHttpStatusCode.ParamErr, LocalizationService.GetString(Localization.Keys.NetworkHttp.DataVerificationFailed)));
}
}
}
Expand Down Expand Up @@ -460,4 +507,11 @@ private static Task WriteRequestBodyTooLargeAsync(HttpContext context, long maxB
context.Response.StatusCode = StatusCodes.Status413PayloadTooLarge;
return context.Response.WriteAsync(HttpJsonResultData<string>.ErrorString(StatusCodes.Status413PayloadTooLarge, $"HTTP request body is too large. Max allowed bytes: {maxBodyBytes}."));
}

// 写出"请求体必须是 JSON 对象"的参数错误响应 / Writes the "request body must be a JSON object" parameter-error response.
private static Task WriteRequestBodyNotJsonObjectAsync(HttpContext context)
{
// Localization: Network.Http.RequestBodyNotJsonObject - 请求体必须是 JSON 对象
return context.Response.WriteAsync(HttpJsonResultData<string>.ErrorString(GameHttpStatusCode.ParamErr, LocalizationService.GetString(Localization.Keys.NetworkHttp.RequestBodyNotJsonObject)));
}
}
32 changes: 0 additions & 32 deletions GameFrameX.Network.HTTP/HttpStatusMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,6 @@ namespace GameFrameX.Network.HTTP;
/// </remarks>
public static class HttpStatusMessage
{
/// <summary>
/// 成功状态消息常量。
/// </summary>
/// <remarks>
/// Success status message constant.
/// </remarks>
/// <value>成功消息字符串 / Success message string</value>
public const string Success = "ok";

/// <summary>
/// 获取未定义命令的错误消息。
Expand All @@ -61,30 +53,6 @@ public static string UndefinedCommand
get { return LocalizationService.GetString(Localization.Keys.NetworkHttp.UndefinedCommand); }
}

/// <summary>
/// 获取未找到命令的错误消息。
/// </summary>
/// <remarks>
/// Gets the error message for not found commands.
/// </remarks>
/// <value>未找到命令的本地化错误消息 / Localized error message for not found commands</value>
public static string NotFoundCommand
{
get { return LocalizationService.GetString(Localization.Keys.NetworkHttp.NotFoundCommand); }
}

/// <summary>
/// 获取验证失败的错误消息。
/// </summary>
/// <remarks>
/// Gets the error message for validation failures.
/// </remarks>
/// <value>验证失败的本地化错误消息 / Localized error message for validation failures</value>
public static string CheckFailedCommand
{
get { return LocalizationService.GetString(Localization.Keys.NetworkHttp.CheckFailedCommand); }
}

/// <summary>
/// 获取服务器错误的错误消息。
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions GameFrameX.Network.HTTP/IHttpAopHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public interface IHttpAopHandler
/// </summary>
/// <remarks>
/// Executes the interception processing logic.
/// 契约:返回 <c>false</c> 表示拒绝该请求并终止后续处理;若实现未自行写出响应,管线将回写统一校验失败响应(HttpJsonResultData 校验失败信封),客户端不会收到空响应。
/// Contract: returning <c>false</c> rejects the request and stops further processing; if the implementation has not written a response itself, the pipeline writes the unified validation-failure response (the HttpJsonResultData validation-failure envelope), so the client never receives an empty response.
/// </remarks>
/// <param name="httpContext">HTTP 上下文 / HTTP context</param>
/// <param name="context">HTTP 处理管线统一请求上下文 / Unified request context for the HTTP handling pipeline</param>
Expand Down
Loading
Loading