2번 모듈 신규(raspberry_pi4_sht30/): SHT30 온습도 → cafe24, raw-body X-Signature HMAC, 폐쇄망 오프라인 설치(vendor/install), systemd, CRC/포맷 자가점검. firmware sht30_fw wire 포맷과 byte-parity 검증. 1번 모듈 통합(raspberry_pi4/): 누수+카메라를 동일 서버로. 레거시 sha256(key+json) body-sig → X-Signature HMAC 전환, 사진 canonical=device_id|sensor_id|log_id|timestamp. 서버 통합(php/): api/sensor_data.php 누수 이벤트 수신, api/photo_upload.php 신규(HMAC·finfo·서버생성 파일명·nonce 멱등·sensor 결속), photo.php 인증 사진서빙, dashboard.php 누수 패널+사진, sms_send.php 누수 SMS, config.php 태그/요약. DB: sql/migration_add_leak.sql(가산·멱등), schema_sht30.sql 통합 스키마. 보안 하드닝(외부 펜테스트 대비, 확정 CRITICAL/HIGH 0): cron_heartbeat api_key fail-closed, setup_mfa 부트스트랩 토큰 post-enrollment 잠금, var/ PHP 실행차단, HSTS. 문서/테스트: SYSTEM_OVERVIEW.md, security_hardening_test.php 갱신. 참고: 사용자 요청으로 세션 외 기존 미커밋 변경도 함께 포함. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
2.9 KiB
Python
65 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
# =============================================================================
|
|
# build_standalone.py — INSTALL_GUIDE.html 의 모든 로컬 이미지를 base64 로 인라인하여
|
|
# 단일 자체완결 파일(INSTALL_GUIDE.standalone.html)을 만든다.
|
|
#
|
|
# 사용:
|
|
# 1) docs/screenshots/ 에 캡처를 README 의 파일명대로 저장
|
|
# 2) python3 docs/build_standalone.py
|
|
# 3) 산출물: docs/INSTALL_GUIDE.standalone.html (이미지까지 한 파일에 내장 — 폴더 없이 전달 가능)
|
|
#
|
|
# 파일이 아직 없는 캡처는 상대경로(src="screenshots/..")로 남겨 두므로, 본문의
|
|
# onerror 자리표시가 그대로 동작한다. 캡처를 채운 뒤 다시 실행하면 모두 내장된다.
|
|
# 외부(http/https)·이미 data: 인 src 는 건드리지 않는다(폐쇄망 유지).
|
|
# =============================================================================
|
|
import base64, os, re, sys
|
|
|
|
BASE = os.path.dirname(os.path.abspath(__file__)) # docs/
|
|
SRC = os.path.join(BASE, 'INSTALL_GUIDE.html')
|
|
OUT = os.path.join(BASE, 'INSTALL_GUIDE.standalone.html')
|
|
|
|
MIME = {'.png':'image/png', '.svg':'image/svg+xml', '.jpg':'image/jpeg',
|
|
'.jpeg':'image/jpeg', '.gif':'image/gif', '.bmp':'image/bmp', '.webp':'image/webp'}
|
|
|
|
def to_data_uri(path: str) -> str:
|
|
ext = os.path.splitext(path)[1].lower()
|
|
mime = MIME.get(ext, 'application/octet-stream')
|
|
with open(path, 'rb') as f:
|
|
b64 = base64.b64encode(f.read()).decode('ascii')
|
|
return f'data:{mime};base64,{b64}'
|
|
|
|
def main() -> int:
|
|
if not os.path.isfile(SRC):
|
|
print(f'ERROR: {SRC} 없음', file=sys.stderr); return 1
|
|
html = open(SRC, encoding='utf-8').read()
|
|
|
|
embedded, missing, skipped = [], [], []
|
|
for src in sorted(set(re.findall(r'src="([^"]+)"', html))):
|
|
if src.startswith(('data:', 'http://', 'https://')):
|
|
skipped.append(src); continue
|
|
fp = os.path.normpath(os.path.join(BASE, src))
|
|
if os.path.isfile(fp):
|
|
html = html.replace(f'src="{src}"', f'src="{to_data_uri(fp)}"')
|
|
embedded.append(src)
|
|
else:
|
|
missing.append(src)
|
|
|
|
open(OUT, 'w', encoding='utf-8').write(html)
|
|
|
|
print(f'내장(embedded) {len(embedded)}개:')
|
|
for e in embedded: print(' +', e)
|
|
if missing:
|
|
print(f'미존재 — 상대경로 유지(캡처 추가 후 재실행) {len(missing)}개:')
|
|
for m in missing: print(' -', m)
|
|
if skipped:
|
|
print(f'건너뜀(외부/이미 data:) {len(skipped)}개')
|
|
size = os.path.getsize(OUT)
|
|
print(f'\n산출물: {OUT} ({size/1024:.0f} KB)')
|
|
if not missing:
|
|
print('→ 모든 이미지 내장 완료. 이 파일 하나만 전달하면 됩니다(폴더 불필요).')
|
|
else:
|
|
print('→ 일부 캡처 미제공: 해당 자리는 자리표시로 표시됩니다. 캡처 추가 후 다시 실행하세요.')
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|