POSA_LEAKSMS/firmware/common/reporter.c
유창욱 90f121e14c chore: import codebase with security hardening
SHT30 온습도 모니터링 시스템 전체 소스(서버 PHP, STM32 펌웨어, SQL, 테스트).
전체 코드리뷰에서 도출된 보안 하드닝 10건 반영:
- 요청 서명 HMAC-SHA256 전환(펌웨어 sig.c/서버 config.php/호스트 패리티 동시)
- 재전송 방어 + 기본 API_KEY fail-closed + 디바이스 문자열 정제(api/sensor_data.php)
- 오프라인 SMS 중복 발송 경합 제거(cron_heartbeat.php, 원자적 선점)
- CSV 수식 주입 방지(monthly_report.php), 감사로그 회전 락(retention_cleanup.php)
- 브루트포스 카운터 원자화(login.php), 예시 TOTP 비밀키 무효화, 마이그레이션 멱등화

_backup/(하드코딩 실 비밀값 포함)·config.local.php·런타임 상태는 .gitignore 제외.
2026-06-20 09:37:40 +09:00

162 lines
7.4 KiB
C

/* =============================================================================
* reporter.c - 이벤트 보고 헬퍼 구현 (본문 생성 -> 서명 -> TLS POST + 재시도)
*
* SHT30 온습도 보드가 사용한다. RPi 의 post_reading() 의
* "본문 구성 -> requests.post -> 실패 시 재시도" 흐름을 그대로 옮긴다.
*
* 동작 개요(report_sht30_event):
* 1) jsonbody.c 로 JSON 본문 생성 (BOARD_* 식별값 + timesync_now() timestamp)
* 2) tls_transport_init() 로 TLS 전송(transport_t) 획득
* 3) httpapi.c::api_post_once() 로 1회 POST (서명/연결/송수신/상태파싱/종료 포함)
* 4) HTTP 200 이면 성공(0), 아니면 APP_HTTP_RETRY_DELAY_MS 대기 후 재시도
* (총 시도 = 1 + APP_HTTP_RETRY_COUNT)
*
* 동시성:
* 현재 보고는 측정 태스크 단독에서 호출되지만, 향후 다중 호출자에 대비해
* mbedTLS 컨텍스트/LwIP 소켓 세션을 정적 뮤텍스로 직렬화한다(한 번에 하나의
* TLS 세션만 수립).
* ===========================================================================*/
#include "reporter.h"
#include "app_config.h" /* APP_API_HOST/PORT/PATH, APP_HTTP_*, APP_VERSION */
#include "board_config.h" /* BOARD_DEVICE_ID/LOCATION/SENSOR_ID/SENSOR_NAME */
#include "jsonbody.h" /* jb_sht30_event */
#include "httpapi.h" /* api_post_once */
#include "tls.h" /* tls_transport_init */
#include "timesync.h" /* timesync_now / timesync_is_set */
#include "applog.h" /* LOGI / LOGW / LOGE */
#include "FreeRTOS.h"
#include "task.h" /* vTaskDelay, xTaskGetSchedulerState */
#include "semphr.h" /* xSemaphoreCreateMutexStatic 등 */
/* ── 상수 ───────────────────────────────────────────────────────────────── */
#define REPORT_BODY_BUFSZ 512u /* 본문 버퍼 (SHT30 본문에 충분) */
#define HTTP_STATUS_OK 200 /* 성공으로 간주하는 HTTP 상태 코드 */
/* ── TLS 세션 직렬화용 정적 뮤텍스 ─────────────────────────────────────────
* 정적 할당(xSemaphoreCreateMutexStatic)을 사용해 힙/초기화 순서 의존성을
* 없앤다. report_init() 호출 없이도 첫 사용 시 lazy 생성한다. */
static StaticSemaphore_t s_tls_mutex_storage;
static SemaphoreHandle_t s_tls_mutex = NULL;
/* 뮤텍스 lazy 초기화. 스케줄러 시작 전 호출되어도 안전하도록 임계구역으로
* 보호한다(동시 생성 방지). 생성 실패는 NULL 로 남으며, 이후 락 단계에서
* "락 없이 진행"으로 폴백한다. */
static SemaphoreHandle_t reporter_mutex(void)
{
if (s_tls_mutex == NULL) {
taskENTER_CRITICAL();
if (s_tls_mutex == NULL) {
s_tls_mutex = xSemaphoreCreateMutexStatic(&s_tls_mutex_storage);
}
taskEXIT_CRITICAL();
}
return s_tls_mutex;
}
/* 보고 절차 진입: 뮤텍스 확보. 스케줄러가 동작 중일 때만 블로킹 take 한다.
* (스케줄러 시작 전 부팅 단계 보고에서는 동시성이 없으므로 락을 건너뛴다.) */
static void reporter_lock(void)
{
SemaphoreHandle_t m = reporter_mutex();
if (m != NULL && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) {
(void)xSemaphoreTake(m, portMAX_DELAY);
}
}
static void reporter_unlock(void)
{
SemaphoreHandle_t m = s_tls_mutex;
if (m != NULL && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) {
(void)xSemaphoreGive(m);
}
}
/* ── 본문 1개를 재시도 포함하여 POST ───────────────────────────────────────
* body 는 이미 완성된 JSON 본문. what 은 로그용 라벨(예: "sht30/periodic").
* 반환: HTTP 200 이면 0, 그 외/통신 실패는 음수. */
static int report_post_body(const char *what, const char *body)
{
/* 총 시도 횟수 = 최초 1회 + 재시도 APP_HTTP_RETRY_COUNT (RPi 재시도 루프와 동일) */
const unsigned max_attempts = 1u + APP_HTTP_RETRY_COUNT;
int last_rc = -1;
reporter_lock();
for (unsigned attempt = 1u; attempt <= max_attempts; ++attempt) {
/* 매 시도마다 새 transport 를 바인딩한다(connect 시 핸드셰이크). */
transport_t t;
int init_rc = tls_transport_init(&t);
if (init_rc != 0) {
last_rc = init_rc;
LOGE("report %s: TLS init 실패 rc=%d (시도 %u/%u)",
what, init_rc, attempt, max_attempts);
} else {
int code = api_post_once(&t, APP_API_HOST, APP_API_PORT, APP_API_PATH,
BOARD_DEVICE_ID, APP_API_KEY, body,
APP_HTTP_TIMEOUT_MS);
if (code == HTTP_STATUS_OK) {
LOGI("report %s: HTTP 200 OK (시도 %u/%u)", what, attempt, max_attempts);
reporter_unlock();
return 0; /* 성공 */
}
if (code > 0) {
/* 서버 응답은 받았으나 비-200 (예: 4xx/5xx) */
last_rc = -code;
LOGW("report %s: HTTP %d (시도 %u/%u)", what, code, attempt, max_attempts);
} else {
/* 전송 계층 오류(연결/송수신/응답없음) */
last_rc = code;
LOGW("report %s: 전송 오류 rc=%d (시도 %u/%u)",
what, code, attempt, max_attempts);
}
}
/* 마지막 시도가 아니면 대기 후 재시도 */
if (attempt < max_attempts) {
vTaskDelay(pdMS_TO_TICKS(APP_HTTP_RETRY_DELAY_MS));
}
}
reporter_unlock();
LOGE("report %s: 모든 시도 실패 (last rc=%d)", what, last_rc);
return (last_rc < 0) ? last_rc : -1;
}
/* 현재 timestamp 획득. 미동기 시 0 이며 경고를 남기되 그대로 전송한다
* (서버가 수신 시각으로 보정할 수 있도록). */
static uint32_t reporter_timestamp(const char *what)
{
if (!timesync_is_set()) {
LOGW("report %s: 시간 미동기(timestamp=0) 상태로 전송", what);
}
return timesync_now();
}
/* ── 공개 API ───────────────────────────────────────────────────────────── */
int report_sht30_event(const char *event_type, double temp_c, double rh,
const char *metric_status)
{
char body[REPORT_BODY_BUFSZ];
uint32_t ts = reporter_timestamp("sht30");
/* SHT30 본문에는 app_version(APP_VERSION) 을 포함한다. */
int n = jb_sht30_event(body, sizeof(body),
BOARD_DEVICE_ID, BOARD_DEVICE_LOCATION,
BOARD_SENSOR_ID, BOARD_SENSOR_NAME,
event_type, ts, temp_c, rh,
metric_status, APP_VERSION);
if (n < 0) {
LOGE("report sht30: 본문 버퍼 부족(event=%s)", event_type ? event_type : "?");
return -1;
}
LOGI("report sht30: event=%s status=%s ts=%lu (%d bytes)",
event_type ? event_type : "?",
metric_status ? metric_status : "?",
(unsigned long)ts, n);
return report_post_body(event_type ? event_type : "sht30", body);
}