getMessage()); } } // 도우미 함수 정의 function safeJsonEncode($value, $flags = 0, $depth = 512) { try { return json_encode($value, $flags | JSON_THROW_ON_ERROR, $depth); } catch (JsonException $e) { return false; } } function apiResponse($error = null) { $encoded = safeJsonEncode($error, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($encoded === false) { writeLog('JSON_ENCODE_FAIL | apiResponse encoding failed', 'ERROR'); return '{"code":"E999","message":"Internal server error"}'; } return $encoded; } // 1. 응답 헤더 설정 (JSON) header('Content-Type: application/json; charset=utf-8'); // ===== 최우선: 모든 호출 정보를 로그에 저장 (보안 키 체크 전) ===== $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http"; $fullUrl = $protocol . "://" . ($_SERVER['HTTP_HOST'] ?? 'localhost') . ($_SERVER['REQUEST_URI'] ?? ''); $rawData = file_get_contents('php://input'); $rawDataLog = safeJsonEncode($rawData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($rawDataLog === false) { writeLog('JSON_ENCODE_FAIL | failed to encode raw request data for logging', 'WARNING'); $rawDataLog = '"json_encode_failed"'; } writeLog("REQUEST_INFO | " . $rawDataLog, 'INFO'); $requestInfo = [ 'timestamp' => date('Y-m-d H:i:s'), 'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN', 'full_url' => $fullUrl, 'request_uri' => $_SERVER['REQUEST_URI'] ?? '', 'query_string' => $_SERVER['QUERY_STRING'] ?? '', 'get_params' => $_GET, 'post_data' => $rawData, 'client_ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'real_ip' => getRealClientIP(), 'x_forwarded_for' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'referer' => $_SERVER['HTTP_REFERER'] ?? '', 'content_type' => $_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? '', 'content_length' => $_SERVER['CONTENT_LENGTH'] ?? '', 'accept' => $_SERVER['HTTP_ACCEPT'] ?? '', 'host' => $_SERVER['HTTP_HOST'] ?? '', 'server_protocol' => $_SERVER['SERVER_PROTOCOL'] ?? '', ]; $requestInfoLog = safeJsonEncode($requestInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($requestInfoLog === false) { writeLog('JSON_ENCODE_FAIL | failed to encode request metadata for logging', 'WARNING'); $requestInfoLog = '"json_encode_failed"'; } writeLog("REQUEST_INFO | " . $requestInfoLog, 'INFO'); // ================================================================ // 2. 보안 키 체크 (URL 파라미터 key=값) $configKey = getenv('API_RECEIVER_KEY') ?: "7EE868F4B36D36B3D86736828F4729EAC4992083"; $receivedKey = $_GET['key'] ?? ''; if ($receivedKey !== $configKey) { writeLog("SECURITY_FAIL | Invalid key: $receivedKey", 'WARNING'); http_response_code(403); echo apiResponse([ 'code' => 'E403', 'message' => 'Unregistered key' ]); exit; } try { // 3. 데이터 수신 및 검증 (POST JSON) if (empty($rawData)) { throw new Exception("No data received"); } $data = null; $contentType = $_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? ''; // 1차 시도: JSON으로 직접 파싱 $data = json_decode($rawData, true); // JSON 파싱 성공 if ($data !== null && json_last_error() === JSON_ERROR_NONE) { // 성공 } // JSON 파싱 실패 → form-urlencoded 시도 else if (strpos($contentType, 'application/x-www-form-urlencoded') !== false || empty($contentType)) { parse_str($rawData, $postData); // post_data 키가 있으면 그 값을 JSON으로 파싱 if (isset($postData['post_data'])) { $data = json_decode($postData['post_data'], true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception("Invalid JSON format in post_data: " . json_last_error_msg()); } } else { // post_data 키가 없으면 form 데이터 자체를 사용 $data = $postData; } } else { throw new Exception("Invalid JSON format: " . json_last_error_msg() . " | Data: " . substr($rawData, 0, 200)); } if (empty($data)) { throw new Exception("Empty data received"); } // 4. 페이로드 준비 $payload = [ 'request_data' => $data, 'received_at' => date('Y-m-d H:i:s'), 'client_ip' => getRealClientIP() ]; // 5. Redis 연결 시도 및 폴백 처리 $redisSuccess = false; $redis = new Redis(); try { // .env 환경변수에서 Redis 설정 읽기 $redisHost = getenv('REDIS_HOST') ?: '127.0.0.1'; $redisPort = getenv('REDIS_PORT') ?: 6379; $redisDatabase = getenv('REDIS_DATABASE') ?: 9; $success = $redis->connect($redisHost, (int)$redisPort, 2.5); // 2.5초 타임아웃 if (!$success) { throw new Exception("Could not connect to Redis at {$redisHost}:{$redisPort}"); } $redis->select((int)$redisDatabase); // 'naver:raw_queue'라는 이름의 리스트에 저장 $encodedPayload = safeJsonEncode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($encodedPayload === false) { throw new Exception('Failed to encode payload for Redis queue'); } $pushResult = $redis->lPush('naver:raw_queue', $encodedPayload); if (!$pushResult) { throw new Exception("Failed to push data to Redis queue"); } $redisSuccess = true; writeLog("SUCCESS | Redis queue length: {$pushResult} | IP: " . ($payload['client_ip'] ?? 'unknown'), 'INFO'); } catch (Exception $redisError) { // Redis 실패 시 파일 폴백 writeLog("REDIS_FAIL | " . $redisError->getMessage() . " | Using file fallback", 'WARNING'); // 폴백 디렉토리 생성 및 권한 설정 $fallbackDir = __DIR__ . '/fallback_queue'; if (!is_dir($fallbackDir)) { if (!mkdir($fallbackDir, 0777, true) && !is_dir($fallbackDir)) { throw new Exception("Failed to create fallback directory"); } chmod($fallbackDir, 0777); } // 파일로 저장 (타임스탬프 + 유니크ID) $filename = $fallbackDir . '/' . date('YmdHis') . '_' . uniqid() . '.json'; $encodedPayload = safeJsonEncode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($encodedPayload === false) { throw new Exception('Failed to encode payload for fallback file'); } $writeResult = file_put_contents($filename, $encodedPayload, LOCK_EX); if ($writeResult === false) { throw new Exception("Failed to write fallback file"); } chmod($filename, 0666); writeLog("FALLBACK_SUCCESS | File: " . basename($filename) . " | IP: " . ($payload['client_ip'] ?? 'unknown'), 'INFO'); } // 6. 성공 응답 (Redis 또는 Fallback 성공) http_response_code(200); echo apiResponse([ 'code' => 'success', 'message' => '' ]); } catch (Exception $e) { // 7. 완전 장애 발생 시 (Redis도 실패, File도 실패) writeLog('CRITICAL_ERROR: ' . $e->getMessage() . ' | Data: ' . substr($rawData, 0, 200), 'ERROR'); http_response_code(500); echo apiResponse([ 'code' => 'E999', 'message' => 'Internal server error' ]); }