wpmind/modules/api-gateway/includes/Pipeline/QuotaMiddleware.php
LinuxJoy 36b69d300d security: API Gateway 全量代码审计修复 (59 issues)
Codex 审计发现 124 个问题,修复 59 个 P0-P2 级别问题:

P0 (CRITICAL):
- TOCTOU 竞态: TransientRateStore/SseConcurrencyGuard 改用 wp_cache_add 原子锁
- REST 路由: __return_true → check_bearer_present 轻量鉴权
- Schema 验证: messages/tools/tool_choice 添加完整 validate_callback
- SQL 列名不匹配: upsert 查询对齐 SchemaManager 定义
- SQL 表名插值: 统一使用 %i 占位符

P1 (HIGH):
- IP hash 加盐 (HMAC) + IP 解析统一 (context 共享)
- Header 注入防护 (X-Request-Id/响应头 \r\n 过滤)
- 数值参数范围校验 (temperature/top_p/penalties/max_tokens)
- insert_key 错误处理 + revoke_key 审计日志
- Pipeline finally 保护 + RateLimiter rollback 双 store
- wp_unslash 补全 + strtotime 返回值检查
- RedisRateStore $rid 冒号验证
- UpstreamStreamClient catch \Throwable

P2 (MEDIUM):
- AuditLogRepository DRY (提取 build_where_clause)
- Token 估算 CJK 优化 (mb_strlen)
- ResponseTransformMiddleware null 检查
- 嵌入响应 usage 数据修复
- SchemaManager autoload 优化

25 files changed, ~530 insertions, ~190 deletions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 01:14:12 +08:00

89 lines
No EOL
2.2 KiB
PHP

<?php
/**
* Quota Middleware
*
* Pipeline stage that enforces RPM and TPM rate limits per API key.
*
* @package WPMind\Modules\ApiGateway\Pipeline
* @since 1.0.0
*/
declare(strict_types=1);
namespace WPMind\Modules\ApiGateway\Pipeline;
use WPMind\Modules\ApiGateway\RateLimit\RateLimiter;
/**
* Class QuotaMiddleware
*
* Checks requests-per-minute and tokens-per-minute limits using
* the RateLimiter. Sets appropriate rate-limit response headers.
*/
final class QuotaMiddleware implements GatewayStageInterface {
/**
* {@inheritDoc}
*/
public function process( GatewayRequestContext $context ): void {
if ( $context->is_management_route() ) {
return;
}
if ( $context->has_error() ) {
return;
}
$auth_result = $context->auth_result();
if ( $auth_result === null ) {
return;
}
$estimated_tokens = $this->estimate_tokens( $context->raw_body() );
$limiter = RateLimiter::create();
$result = $limiter->check_and_consume(
$auth_result->key_id,
$context->request_id(),
$auth_result->rpm_limit,
$auth_result->tpm_limit,
$estimated_tokens
);
$reset_seconds = max( 0, $result->reset_epoch - time() );
$context->set_response_header( 'x-ratelimit-limit-requests', (string) $auth_result->rpm_limit );
$context->set_response_header( 'x-ratelimit-remaining-requests', (string) max( 0, $result->remaining ) );
$context->set_response_header( 'x-ratelimit-reset-requests', $reset_seconds . 's' );
if ( ! $result->allowed ) {
$retry_after = max( 1, $reset_seconds );
$context->set_response_header( 'Retry-After', (string) $retry_after );
$context->set_retry_after( $retry_after );
$context->set_error(
new \WP_Error(
'rate_limit_exceeded',
'Rate limit exceeded. Please retry after ' . $retry_after . ' seconds.',
[ 'status' => 429 ]
)
);
}
}
/**
* Estimate token count from the raw request body.
*
* Uses a simple heuristic of ~4 characters per token.
*
* @param string $body Raw request body.
* @return int Estimated token count.
*/
private function estimate_tokens( string $body ): int {
$length = mb_strlen( $body, 'UTF-8' );
return max( 1, (int) ( $length / 3 ) );
}
}