POSA_LEAKSMS/firmware/common/httpapi.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

105 lines
4 KiB
C

/* =============================================================================
* httpapi.c - HTTP POST 작성/전송 (이식성: 전송은 transport 콜백에 위임)
* ===========================================================================*/
#include "httpapi.h"
#include "sig.h"
#include <string.h>
/* ── 경계 검사 어펜더 (httpapi 전용) ────────────────────────────────────── */
typedef struct { char *p; size_t cap; size_t len; int ok; } ha_buf;
static void ha_init(ha_buf *b, char *out, size_t cap)
{
b->p = out; b->cap = cap; b->len = 0; b->ok = (cap > 0);
if (b->ok) b->p[0] = '\0';
}
static void ha_putc(ha_buf *b, char c)
{
if (!b->ok) return;
if (b->len + 1 >= b->cap) { b->ok = 0; return; }
b->p[b->len++] = c; b->p[b->len] = '\0';
}
static void ha_raw(ha_buf *b, const char *s) { while (*s) ha_putc(b, *s++); }
static void ha_u(ha_buf *b, unsigned long v)
{
char tmp[20]; int n = 0;
if (v == 0) { ha_putc(b, '0'); return; }
while (v > 0 && n < (int)sizeof(tmp)) { tmp[n++] = (char)('0' + (v % 10)); v /= 10; }
while (n > 0) ha_putc(b, tmp[--n]);
}
int http_build_post(char *out, size_t cap,
const char *host, const char *path,
const char *device_id, const char *sig_hex,
const char *body)
{
ha_buf b;
ha_init(&b, out, cap);
ha_raw(&b, "POST "); ha_raw(&b, path); ha_raw(&b, " HTTP/1.1\r\n");
ha_raw(&b, "Host: "); ha_raw(&b, host); ha_raw(&b, "\r\n");
ha_raw(&b, "User-Agent: SHT30Sensor-STM32/" ); /* 버전은 본문 app_version 참고 */
ha_raw(&b, "1.0\r\n");
ha_raw(&b, "X-Device-Id: "); ha_raw(&b, device_id); ha_raw(&b, "\r\n");
ha_raw(&b, "X-Signature: "); ha_raw(&b, sig_hex); ha_raw(&b, "\r\n");
ha_raw(&b, "Content-Type: application/json\r\n");
ha_raw(&b, "Content-Length: "); ha_u(&b, (unsigned long)strlen(body)); ha_raw(&b, "\r\n");
ha_raw(&b, "Connection: close\r\n");
ha_raw(&b, "\r\n");
ha_raw(&b, body);
return b.ok ? (int)b.len : -1;
}
int http_parse_status(const uint8_t *resp, size_t len)
{
/* "HTTP/1.1 200 OK..." 에서 상태 코드 추출 */
size_t i = 0;
/* 첫 공백까지 스킵 (HTTP/1.x) */
while (i < len && resp[i] != ' ' && resp[i] != '\r' && resp[i] != '\n') i++;
while (i < len && resp[i] == ' ') i++;
if (i + 3 > len) return -1;
if (resp[i] < '0' || resp[i] > '9') return -1;
int code = 0, digits = 0;
while (i < len && resp[i] >= '0' && resp[i] <= '9' && digits < 3) {
code = code * 10 + (resp[i] - '0');
i++; digits++;
}
return (digits == 3) ? code : -1;
}
int api_post_once(transport_t *t, const char *host, uint16_t port,
const char *path, const char *device_id,
const char *api_key, const char *body, uint32_t timeout_ms)
{
char sig_hex[SIG_HEX_BUFSZ];
char req[1024];
uint8_t resp[256];
sig_raw_body(api_key, body, strlen(body), sig_hex);
int reqlen = http_build_post(req, sizeof(req), host, path, device_id, sig_hex, body);
if (reqlen < 0) return -1000; /* 요청 버퍼 부족 */
int rc = t->connect(t->ctx, host, port, timeout_ms);
if (rc < 0) return -2000 + rc;
int sent = t->send(t->ctx, (const uint8_t *)req, (size_t)reqlen, timeout_ms);
if (sent < 0) { t->close(t->ctx); return -3000 + sent; }
/* 상태 라인을 포함하는 첫 응답까지만 읽는다(Connection: close). */
size_t total = 0;
int have_line = 0;
while (total < sizeof(resp) - 1) {
int n = t->recv(t->ctx, resp + total, sizeof(resp) - 1 - total, timeout_ms);
if (n <= 0) break; /* 연결 종료 또는 오류 */
total += (size_t)n;
for (size_t k = 0; k < total; k++) { if (resp[k] == '\n') { have_line = 1; break; } }
if (have_line) break;
}
t->close(t->ctx);
if (total == 0) return -4000; /* 응답 없음 */
return http_parse_status(resp, total); /* HTTP 상태 코드 */
}