$bypass 프록시 예외 목록 * @return array{success: bool, message_key: string, egress_ip: string|null, elapsed_ms: int, error: string|null} 검사 결과 */ public function test(string $proxyUrl, array $bypass = []): array { // 적용 시와 같은 조립·정규화를 거친다 — 여기서 손으로 배열을 만들면 저장 전에 // 확인한 구성과 저장 후 실제로 적용되는 구성이 달라진다. $proxyOptions = OutboundProxy::options($proxyUrl, $bypass); if ($proxyOptions === null) { return $this->result(false, 'settings.outbound_proxy_test_invalid_url', null, 0); } $urls = (array) config('core.outbound_proxy.egress_lookup_urls', []); if ($urls === []) { return $this->result(false, 'settings.outbound_proxy_test_no_lookup_url', null, 0); } $timeout = (int) config('core.outbound_proxy.test_timeout_seconds', 10); $startedAt = microtime(true); $lastError = null; foreach ($urls as $url) { try { $response = Http::withOptions(['proxy' => $proxyOptions]) ->withHeaders(['User-Agent' => 'curl/8']) ->timeout($timeout) ->get($url); if (! $response->successful()) { $lastError = 'HTTP '.$response->status(); continue; } $ip = trim($response->body()); if (filter_var($ip, FILTER_VALIDATE_IP) === false) { $lastError = 'unexpected response body'; continue; } return $this->result(true, 'settings.outbound_proxy_test_success', $ip, $this->elapsedMs($startedAt)); } catch (\Throwable $e) { // 개별 조회처 실패는 다음 후보로 넘어간다 — 한 곳이 죽어 있다고 프록시가 // 잘못됐다고 단정할 수 없기 때문이다. $lastError = $e->getMessage(); } } Log::warning('아웃바운드 프록시 연결 테스트 실패', ['error' => $lastError]); return $this->result(false, 'settings.outbound_proxy_test_failed', null, $this->elapsedMs($startedAt), $lastError); } /** * 경과 시간을 밀리초로 환산합니다. * * @param float $startedAt 시작 시각 (microtime) * @return int 경과 밀리초 */ private function elapsedMs(float $startedAt): int { return (int) round((microtime(true) - $startedAt) * 1000); } /** * 결과 배열을 구성합니다. * * @param bool $success 성공 여부 * @param string $messageKey 다국어 메시지 키 * @param string|null $egressIp 프록시를 거친 출발지 IP * @param int $elapsedMs 경과 밀리초 * @param string|null $error 실패 원인 원문 (관리자 진단용) * @return array{success: bool, message_key: string, egress_ip: string|null, elapsed_ms: int, error: string|null} */ private function result(bool $success, string $messageKey, ?string $egressIp, int $elapsedMs, ?string $error = null): array { return [ 'success' => $success, 'message_key' => $messageKey, 'egress_ip' => $egressIp, 'elapsed_ms' => $elapsedMs, 'error' => $error, ]; } }