Files
confirms/worker/api_receiver.php

318 lines
11 KiB
PHP

<?php
/**
* [A 작업] 네이버 검증 요청 실시간 수신 리시버
* - 프레임워크를 로드하지 않아 매우 빠르고 안전함
* - 받은 데이터를 Redis 큐에 넣고 즉시 응답 테스트
*/
// .env 파일 로드 함수 (프레임워크 미사용 환경)
function loadEnvFile($filePath) {
if (!file_exists($filePath)) {
return;
}
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
// 주석 제거
if (strpos(trim($line), '#') === 0) {
continue;
}
// KEY = VALUE 파싱
if (strpos($line, '=') !== false) {
list($key, $value) = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
// 따옴표 제거
$value = trim($value, '"\'');
// 환경변수 설정 (이미 있으면 덮어쓰지 않음)
if (!getenv($key)) {
putenv("$key=$value");
}
}
}
}
// .env 파일 로드 (한 단계 상위 디렉토리)
loadEnvFile(__DIR__ . '/../.env');
/**
* X-Forwarded-For 헤더에서 실제 클라이언트 IP 추출
* @return string 클라이언트 IP
*/
function getRealClientIP() {
// 1. X-Forwarded-For 헤더 확인 (여러 프록시를 거친 경우)
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
// "123.123.123.123, 192.168.10.1" 같은 형식에서 첫 번째 IP 추출
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$clientIP = trim($ips[0]); // 첫 번째 = 원래 클라이언트
// IP 형식 검증
if (filter_var($clientIP, FILTER_VALIDATE_IP)) {
return $clientIP;
}
}
// 2. X-Real-IP 헤더 확인 (단일 프록시)
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
if (filter_var($_SERVER['HTTP_X_REAL_IP'], FILTER_VALIDATE_IP)) {
return $_SERVER['HTTP_X_REAL_IP'];
}
}
// 3. CF-Connecting-IP (Cloudflare)
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
if (filter_var($_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP)) {
return $_SERVER['HTTP_CF_CONNECTING_IP'];
}
}
// 4. 기본값: REMOTE_ADDR (nginx가 set_real_ip_from으로 이미 변경함)
return $_SERVER['REMOTE_ADDR'] ?? '';
}
/**
* 날짜별 로그 기록 함수
* @param string $message 로그 내용
* @param string $level 로그 레벨 (INFO, ERROR, DEBUG 등)
*/
function writeLog($message, $level = 'ERROR') {
try {
// 1. 로그 저장 경로 설정 (프로젝트 루트의 logs 폴더)
$logBaseDir = __DIR__ . '/logs';
$Dir = $logBaseDir . '/';
// 2. 폴더가 없으면 생성
if (!is_dir($Dir)) {
if (!mkdir($Dir, 0777, true) && !is_dir($Dir)) {
error_log("Failed to create log directory: $Dir");
return;
}
chmod($Dir, 0777); // 권한 명시적 설정
}
// 3. 파일명 결정
$logFile = $Dir . date('Y-m-d') . '.log';
// 4. 로그 포맷팅 (시간 [레벨] 메시지)
$timestamp = date('Y-m-d H:i:s');
$singleLineMessage = str_replace(["\r", "\n", "\t"], " ", $message);
$formattedMessage = "[$timestamp] [$level] $singleLineMessage" . PHP_EOL;
// 5. 파일 기록
$result = file_put_contents($logFile, $formattedMessage, FILE_APPEND | LOCK_EX);
if ($result === false) {
error_log("Failed to write log file: $logFile");
} else {
chmod($logFile, 0666); // 로그 파일 권한 설정
}
} catch (Exception $e) {
error_log("writeLog Exception: " . $e->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'
]);
}