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 제외.
162 lines
7.4 KiB
C
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);
|
|
}
|