wpmind/modules/api-gateway/includes/Pipeline/GatewayRequestContext.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

339 lines
No EOL
7.1 KiB
PHP

<?php
/**
* Gateway Request Context
*
* Core DTO that flows through every pipeline stage.
*
* @package WPMind\Modules\ApiGateway\Pipeline
* @since 1.0.0
*/
declare(strict_types=1);
namespace WPMind\Modules\ApiGateway\Pipeline;
/**
* Class GatewayRequestContext
*
* Immutable-ish value object carrying all state for a single
* API gateway request through the middleware pipeline.
*/
final class GatewayRequestContext {
private string $operation;
private string $request_id;
private \WP_REST_Request $rest_request;
private ?object $auth_result = null;
private ?array $internal_payload = null;
private mixed $internal_result = null;
private ?\WP_REST_Response $rest_response = null;
private ?\WP_Error $error = null;
private ?\Throwable $exception = null;
private ?string $client_ip = null;
private array $response_headers = [];
private int $retry_after_sec = 0;
private float $start_time;
private function __construct() {}
/**
* Create context from a WP REST request.
*
* @param string $operation Operation type (e.g. 'chat.completions').
* @param \WP_REST_Request $request Original REST request.
* @return self
*/
public static function from_rest_request( string $operation, \WP_REST_Request $request ): self {
$ctx = new self();
$ctx->operation = $operation;
$ctx->rest_request = $request;
$ctx->start_time = microtime( true );
// Generate UUID v4.
$data = random_bytes( 16 );
$data[6] = chr( ord( $data[6] ) & 0x0f | 0x40 );
$data[8] = chr( ord( $data[8] ) & 0x3f | 0x80 );
$ctx->request_id = vsprintf(
'%s%s-%s-%s-%s-%s%s%s',
str_split( bin2hex( $data ), 4 )
);
return $ctx;
}
/**
* Get the operation type.
*
* @return string
*/
public function operation(): string {
return $this->operation;
}
/**
* Get the unique request ID (UUID v4).
*
* @return string
*/
public function request_id(): string {
return $this->request_id;
}
/**
* Get the original WP REST request.
*
* @return \WP_REST_Request
*/
public function rest_request(): \WP_REST_Request {
return $this->rest_request;
}
/**
* Get the raw request body.
*
* @return string
*/
public function raw_body(): string {
return $this->rest_request->get_body();
}
/**
* Get the key_id from auth result.
*
* @return string|null
*/
public function key_id(): ?string {
return $this->auth_result?->key_id ?? null;
}
/**
* Set the resolved client IP address.
*
* @param string $ip Client IP address.
*/
public function set_client_ip( string $ip ): void {
$this->client_ip = $ip;
}
/**
* Get the resolved client IP address.
*
* @return string|null
*/
public function client_ip(): ?string {
return $this->client_ip;
}
/**
* Set the authentication result.
*
* @param object $result Auth result object.
*/
public function set_auth_result( object $result ): void {
$this->auth_result = $result;
}
/**
* Get the authentication result.
*
* @return object|null
*/
public function auth_result(): ?object {
return $this->auth_result;
}
/**
* Set the internal payload (WPMind format).
*
* @param array $payload Transformed payload.
*/
public function set_internal_payload( array $payload ): void {
$this->internal_payload = $payload;
}
/**
* Get the internal payload.
*
* @return array|null
*/
public function get_internal_payload(): ?array {
return $this->internal_payload;
}
/**
* Set the internal result from PublicAPI.
*
* @param mixed $result Result data.
*/
public function set_internal_result( mixed $result ): void {
$this->internal_result = $result;
}
/**
* Get the internal result.
*
* @return mixed
*/
public function get_internal_result(): mixed {
return $this->internal_result;
}
/**
* Set an error on the context.
*
* @param \WP_Error $error WordPress error.
*/
public function set_error( \WP_Error $error ): void {
$this->error = $error;
}
/**
* Check if the context has an error.
*
* @return bool
*/
public function has_error(): bool {
return $this->error !== null;
}
/**
* Get the error.
*
* @return \WP_Error|null
*/
public function error(): ?\WP_Error {
return $this->error;
}
/**
* Set an exception on the context.
*
* @param \Throwable $e The exception.
*/
public function set_exception( \Throwable $e ): void {
$this->exception = $e;
}
/**
* Check if the context has an exception.
*
* @return bool
*/
public function has_exception(): bool {
return $this->exception !== null;
}
/**
* Get the exception.
*
* @return \Throwable|null
*/
public function exception(): ?\Throwable {
return $this->exception;
}
/**
* Set a response header.
*
* @param string $name Header name.
* @param string $value Header value.
*/
public function set_response_header( string $name, string $value ): void {
$this->response_headers[ $name ] = $value;
}
/**
* Get all response headers.
*
* @return array<string, string>
*/
public function get_response_headers(): array {
return $this->response_headers;
}
/**
* Set retry-after seconds for rate limiting.
*
* @param int $seconds Seconds to wait.
*/
public function set_retry_after( int $seconds ): void {
$this->retry_after_sec = $seconds;
}
/**
* Get retry-after seconds.
*
* @return int
*/
public function retry_after_sec(): int {
return $this->retry_after_sec;
}
/**
* Set the final REST response.
*
* @param \WP_REST_Response $response REST response.
*/
public function set_rest_response( \WP_REST_Response $response ): void {
$this->rest_response = $response;
}
/**
* Build and return the final REST response.
*
* If a rest_response was explicitly set, returns it.
* If an error exists, converts it to a REST response.
* Otherwise builds a 200 response from internal_result.
*
* @return \WP_REST_Response
*/
public function to_rest_response(): \WP_REST_Response {
if ( $this->rest_response !== null ) {
$response = $this->rest_response;
} elseif ( $this->error !== null ) {
$data = $this->error->get_error_data();
$status = is_array( $data ) && isset( $data['status'] ) ? (int) $data['status'] : 500;
$response = new \WP_REST_Response(
[
'error' => [
'message' => $this->error->get_error_message(),
'type' => $this->error->get_error_code(),
],
],
$status
);
} else {
$response = new \WP_REST_Response( $this->internal_result, 200 );
}
// Apply collected headers.
foreach ( $this->response_headers as $name => $value ) {
$response->header( $name, $value );
}
// Always include request ID.
$response->header( 'X-Request-Id', $this->request_id );
return $response;
}
/**
* Get elapsed time in milliseconds since request start.
*
* @return int
*/
public function elapsed_ms(): int {
return (int) round( ( microtime( true ) - $this->start_time ) * 1000 );
}
/**
* Check if this is a management route (e.g. status, models).
*
* Management routes may skip budget/quota checks.
*
* @return bool
*/
public function is_management_route(): bool {
return in_array( $this->operation, [ 'status', 'models' ], true );
}
}