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

199 lines
5.5 KiB
PHP

<?php
/**
* Route Middleware
*
* Pipeline stage that dispatches requests to WPMind's PublicAPI.
*
* @package WPMind\Modules\ApiGateway\Pipeline
* @since 1.0.0
*/
declare(strict_types=1);
namespace WPMind\Modules\ApiGateway\Pipeline;
use WPMind\Modules\ApiGateway\Transform\ModelMapper;
use WPMind\Modules\ApiGateway\Transform\ResponseTransformer;
use WPMind\Modules\ApiGateway\Stream\SseStreamController;
use WPMind\Modules\ApiGateway\SchemaManager;
/**
* Class RouteMiddleware
*
* Reads the internal payload from the context and calls the
* appropriate PublicAPI method based on the operation type.
*/
final class RouteMiddleware implements GatewayStageInterface {
/**
* Process the gateway request context.
*
* @param GatewayRequestContext $context Shared request context.
*/
public function process( GatewayRequestContext $context ): void {
if ( $context->has_error() ) {
return;
}
$payload = $context->get_internal_payload();
$operation = $payload['operation'] ?? $context->operation();
match ( $operation ) {
'chat.completions' => $this->handle_chat( $context, $payload ),
'embeddings' => $this->handle_embeddings( $context, $payload ),
'responses' => $this->handle_chat( $context, $payload ),
'models' => $this->handle_models( $context ),
'model_detail' => $this->handle_model_detail( $context ),
'status' => $this->handle_status( $context ),
default => $context->set_error(
new \WP_Error(
'unsupported_operation',
sprintf( 'Unsupported operation: %s', sanitize_text_field( $operation ) ),
[ 'status' => 400 ]
)
),
};
}
/**
* Handle a chat completions request.
*
* @param GatewayRequestContext $context Pipeline context.
* @param array $payload Internal payload.
*/
private function handle_chat( GatewayRequestContext $context, array $payload ): void {
$messages = $payload['messages'] ?? [];
$options = $payload['options'] ?? [];
$stream = $payload['stream'] ?? false;
// For 'auto' provider, omit provider from options to let WPMind route.
if ( ( $options['provider'] ?? '' ) === 'auto' ) {
unset( $options['provider'], $options['model'] );
}
if ( $stream ) {
// Phase 6: SSE streaming via SseStreamController.
// If slot acquisition fails, the controller sets an error
// on the context and returns, allowing the pipeline to
// produce a normal JSON error response.
$controller = new SseStreamController();
$controller->serve_chat_stream( $context );
// If we reach here, slot acquisition failed and the error
// is already set on the context. The pipeline continues.
return;
}
$api = \WPMind\API\PublicAPI::instance();
/** @var array|\WP_Error $result */
$result = $api->chat( $messages, $options );
if ( is_wp_error( $result ) ) {
$context->set_error( $result );
return;
}
$context->set_internal_result( $result );
}
/**
* Handle an embeddings request.
*
* @param GatewayRequestContext $context Pipeline context.
* @param array $payload Internal payload.
*/
private function handle_embeddings( GatewayRequestContext $context, array $payload ): void {
$input = $payload['input'] ?? '';
$options = $payload['options'] ?? [];
// For 'auto' provider, omit provider from options.
if ( ( $options['provider'] ?? '' ) === 'auto' ) {
unset( $options['provider'], $options['model'] );
}
$api = \WPMind\API\PublicAPI::instance();
/** @var array|\WP_Error $result */
$result = $api->embed( $input, $options );
if ( is_wp_error( $result ) ) {
$context->set_error( $result );
return;
}
$context->set_internal_result( $result );
}
/**
* Handle a models list request.
*
* @param GatewayRequestContext $context Pipeline context.
*/
private function handle_models( GatewayRequestContext $context ): void {
$models = ModelMapper::get_available_models();
$transformer = new ResponseTransformer();
$data = $transformer->transform_models( $models );
$context->set_internal_result( $data );
}
/**
* Handle a single model detail request.
*
* @param GatewayRequestContext $context Pipeline context.
*/
private function handle_model_detail( GatewayRequestContext $context ): void {
$model_id = $context->rest_request()->get_param( 'model_id' );
if ( $model_id === null || $model_id === '' ) {
$context->set_error( new \WP_Error(
'model_not_found',
'Model ID is required.',
[ 'status' => 400 ]
) );
return;
}
$resolved = ModelMapper::resolve( (string) $model_id );
if ( $resolved === null ) {
$context->set_error( new \WP_Error(
'model_not_found',
sprintf( 'Model "%s" is not available.', sanitize_text_field( (string) $model_id ) ),
[ 'status' => 404 ]
) );
return;
}
$context->set_internal_result( [
'id' => (string) $model_id,
'object' => 'model',
'created' => 0,
'owned_by' => 'wpmind',
] );
}
/**
* Handle a gateway status request.
*
* Returns basic gateway health and configuration info.
*
* @param GatewayRequestContext $context Pipeline context.
*/
private function handle_status( GatewayRequestContext $context ): void {
$context->set_internal_result( [
'status' => 'ok',
'version' => '1.0.0',
'endpoints' => [
'/mind/v1/chat/completions',
'/mind/v1/embeddings',
'/mind/v1/responses',
'/mind/v1/models',
'/mind/v1/models/{model_id}',
'/mind/v1/status',
],
'schema_version' => SchemaManager::get_schema_version(),
] );
}
}