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 제외.
105 lines
4 KiB
C
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 상태 코드 */
|
|
}
|