package com.emotion.controller; import com.emotion.common.Result; import com.emotion.dto.request.ApiTestProxyRequest; import com.emotion.dto.response.ApiTestProxyResponse; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import javax.validation.Valid; import java.net.SocketTimeoutException; import java.util.HashMap; import java.util.Map; /** * 接口代理测试控制器 * 仅允许转发到 /api/* 路径,避免 SSRF * * @author Peanut * @date 2026-05-23 */ @RestController @RequestMapping("/admin/endpoint") @Tag(name = "接口管理", description = "接口测试代理") public class ApiTestProxyController { private static final Logger log = LoggerFactory.getLogger(ApiTestProxyController.class); private static final int DEFAULT_TIMEOUT = 30; private static final int MAX_RAW_BODY_LENGTH = 2000; @Value("${server.port:19089}") private int serverPort; @Autowired private RestTemplate restTemplate; @Autowired private ObjectMapper objectMapper; @Operation(summary = "代理测试请求", description = "转发请求到本地后端并返回响应") @PostMapping("/test") public Result test(@Valid @RequestBody ApiTestProxyRequest request) { if (!request.getPath().startsWith("/api/")) { return Result.error("仅允许代理 /api/* 路径的请求"); } String url = "http://127.0.0.1:" + serverPort + request.getPath(); int timeout = request.getTimeoutSeconds() != null ? request.getTimeoutSeconds() : DEFAULT_TIMEOUT; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); if (request.getHeaders() != null) { for (Map.Entry entry : request.getHeaders().entrySet()) { headers.set(entry.getKey(), entry.getValue()); } } // Append query params to URL if present if (request.getParams() != null && !request.getParams().isEmpty()) { StringBuilder sb = new StringBuilder(url); sb.append("?"); for (Map.Entry entry : request.getParams().entrySet()) { sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } url = sb.substring(0, sb.length() - 1); } Object body = null; if (request.getBody() != null && !request.getBody().isBlank()) { try { body = objectMapper.readValue(request.getBody(), Object.class); } catch (Exception e) { return Result.error("请求体 JSON 格式错误: " + e.getMessage()); } } HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod().toUpperCase()); HttpEntity entity = new HttpEntity<>(body, headers); long startTime = System.currentTimeMillis(); ApiTestProxyResponse response = new ApiTestProxyResponse(); try { ResponseEntity rawResponse = restTemplate.exchange(url, httpMethod, entity, String.class); long duration = System.currentTimeMillis() - startTime; response.setStatus(rawResponse.getStatusCodeValue()); response.setDuration(duration); try { response.setBody(objectMapper.readValue(rawResponse.getBody(), Object.class)); } catch (Exception e) { String rawBody = rawResponse.getBody(); if (rawBody != null && rawBody.length() > MAX_RAW_BODY_LENGTH) { rawBody = rawBody.substring(0, MAX_RAW_BODY_LENGTH) + "\n... (已截断)"; } if (rawBody != null) { rawBody = rawBody.replace("<", "<").replace(">", ">"); } response.setRawBody(rawBody); } Map respHeaders = new HashMap<>(); for (String key : rawResponse.getHeaders().keySet()) { respHeaders.put(key, rawResponse.getHeaders().getFirst(key)); } response.setHeaders(respHeaders); return Result.success(response); } catch (ResourceAccessException e) { long duration = System.currentTimeMillis() - startTime; if (e.getCause() instanceof SocketTimeoutException) { return Result.error("代理请求超时(" + timeout + "s),目标接口可能响应过慢或不可达"); } return Result.error("代理请求失败: " + e.getMessage()); } catch (HttpClientErrorException | HttpServerErrorException e) { long duration = System.currentTimeMillis() - startTime; response.setStatus(e.getStatusCode().value()); response.setDuration(duration); String rawBody = e.getResponseBodyAsString(); if (rawBody.length() > MAX_RAW_BODY_LENGTH) { rawBody = rawBody.substring(0, MAX_RAW_BODY_LENGTH) + "\n... (已截断)"; } response.setRawBody(rawBody.replace("<", "<").replace(">", ">")); return Result.success(response); } catch (Exception e) { return Result.error("代理请求失败: " + e.getMessage()); } } }