$debugSettings debug 카테고리 설정 배열 * @return array{http: string, https: string, no: array}|null 적용할 프록시 옵션 (미적용 시 null) */ public static function resolve(array $debugSettings): ?array { // 게이트: 디버그 모드가 꺼져 있으면 저장값이 있어도 적용하지 않는다. if (empty($debugSettings['mode'])) { return null; } return self::options( $debugSettings['outbound_proxy'] ?? null, $debugSettings['outbound_proxy_bypass'] ?? [] ); } /** * 프록시 주소와 예외 목록을 Guzzle 의 `proxy` 옵션 형태로 조립합니다. * * 게이트(디버그 모드)는 보지 않습니다 — 저장 전 연결 테스트처럼 아직 적용 대상이 아닌 * 값을 그대로 검사해야 하는 경로가 있기 때문입니다. 게이트 판정은 `resolve()` 가 맡습니다. * * 조립을 이 한 곳에 모으는 이유는 정규화 때문입니다. 테스트가 손으로 배열을 만들면 예외 * 목록의 공백·빈 항목·중복 처리가 실제 적용분과 어긋나, 저장 전에 확인한 구성과 저장 후 * 적용되는 구성이 달라집니다. * * @param mixed $url 프록시 주소 * @param mixed $bypass 예외 목록 * @return array{http: string, https: string, no: array}|null 조립된 옵션 (주소가 부적합하면 null) */ public static function options(mixed $url, mixed $bypass = []): ?array { $normalized = self::normalizeUrl($url); if ($normalized === null) { return null; } return [ 'http' => $normalized, 'https' => $normalized, 'no' => self::normalizeBypass($bypass), ]; } /** * 프록시 URL 이 적용 가능한 형태인지 판정합니다. * * @param mixed $value 검사할 값 * @return bool 허용 스킴과 호스트를 갖춘 URL 이면 true */ public static function isValidUrl(mixed $value): bool { return self::normalizeUrl($value) !== null; } /** * 현재 적용 중인 프록시를 curl 옵션 형태로 돌려줍니다. * * `Http::` 파사드를 쓰지 못하는 호출 지점(외부 SDK 규약상 curl 핸들을 직접 다뤄야 하는 * 경우 등)이 같은 프록시를 타도록 하기 위한 통로입니다. 판정은 여기서 다시 하지 않고 * 이미 주입된 `g7.outbound_proxy` 를 읽습니다 — 게이트는 한 곳에만 둔다. * * 적용 대상이 없으면 빈 배열이므로 `curl_setopt_array()` 에 그대로 넘겨도 무해합니다. * * @return array curl 옵션 배열 (미적용 시 빈 배열) */ public static function curlOptions(): array { $proxy = config('g7.outbound_proxy'); if (! is_array($proxy) || empty($proxy['https'])) { return []; } $options = [CURLOPT_PROXY => $proxy['https']]; if (! empty($proxy['no']) && is_array($proxy['no'])) { $options[CURLOPT_NOPROXY] = implode(',', $proxy['no']); } return $options; } /** * 프록시 URL 을 정규화합니다. * * 허용 스킴과 호스트를 모두 갖추지 못한 값은 null 을 돌려줍니다. * * @param mixed $value 원본 값 * @return string|null 정규화된 URL (부적합 시 null) */ private static function normalizeUrl(mixed $value): ?string { if (! is_string($value)) { return null; } $url = trim($value); if ($url === '') { return null; } $parts = parse_url($url); if ($parts === false || empty($parts['host'])) { return null; } $scheme = strtolower($parts['scheme'] ?? ''); if (! in_array($scheme, self::ALLOWED_SCHEMES, true)) { return null; } return $url; } /** * 프록시 예외 목록을 정규화합니다. * * 빈 항목과 중복을 걸러내고 순번을 다시 매깁니다 — 비연속 키는 JSON 직렬화 시 객체가 되어 * Guzzle 이 목록으로 읽지 못합니다. * * 목록 맨 앞에는 사이트 자기 호스트와 루프백이 항상 들어갑니다 (selfHosts 참조). * * @param mixed $value 원본 예외 목록 * @return array 정규화된 호스트 목록 */ private static function normalizeBypass(mixed $value): array { $hosts = self::selfHosts(); if (is_array($value)) { foreach ($value as $host) { if (! is_string($host)) { continue; } $host = trim($host); if ($host !== '') { $hosts[] = $host; } } } return array_values(array_unique($hosts)); } /** * 프록시를 거치지 않아야 하는 자기 자신 호스트 목록을 돌려줍니다. * * 아웃바운드 프록시는 **바깥으로 나가는** 트래픽의 출발지를 지정하려는 장치입니다. 그런데 * 사이트는 자기 자신에게도 HTTP 를 겁니다 — SEO 렌더러가 데이터소스를 부를 때, API 문서 * 생성기가 엔드포인트를 탐침할 때가 그렇습니다. 그 요청까지 프록시로 내보내면 프록시가 * 응답하지 않을 때 호출마다 연결 실패 시각까지 매달리고, 실패는 폴백으로 삼켜지므로 * 예외도 오류 화면도 없이 저장 요청만 느려집니다. 운영자에게는 원인을 알 단서가 없습니다. * * 자기 자신으로 가는 요청을 프록시로 보내는 것은 어떤 구성에서도 의도가 아니므로, 운영자가 * 예외 목록에 적었는지와 무관하게 항상 제외합니다. * * @return array 항상 우회할 호스트 목록 */ private static function selfHosts(): array { $hosts = ['localhost', '127.0.0.1', '::1']; $appHost = parse_url((string) config('app.url'), PHP_URL_HOST); if (is_string($appHost) && $appHost !== '') { $hosts[] = $appHost; } return $hosts; } }