|
@@ -17,6 +17,8 @@ from urllib.parse import urlparse
|
|
|
|
|
|
|
|
import aiohttp
|
|
import aiohttp
|
|
|
|
|
|
|
|
|
|
+from backend.app.core.logging_filters import redact_url_credentials
|
|
|
|
|
+
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
@@ -195,9 +197,15 @@ async def capture_frame(
|
|
|
JPEG bytes or None on failure
|
|
JPEG bytes or None on failure
|
|
|
"""
|
|
"""
|
|
|
if snapshot_url:
|
|
if snapshot_url:
|
|
|
- logger.debug("capture_frame using snapshot override url=%s...", snapshot_url[:50])
|
|
|
|
|
|
|
+ # Redact before truncating — slicing first can cut the URL short of the
|
|
|
|
|
+ # ``@`` the pattern anchors on and leave the password in the log.
|
|
|
|
|
+ logger.debug("capture_frame using snapshot override url=%s...", redact_url_credentials(snapshot_url)[:50])
|
|
|
return await _capture_snapshot(snapshot_url, timeout)
|
|
return await _capture_snapshot(snapshot_url, timeout)
|
|
|
- logger.debug("capture_frame called: type=%s, url=%s...", camera_type, url[:50] if url else "None")
|
|
|
|
|
|
|
+ logger.debug(
|
|
|
|
|
+ "capture_frame called: type=%s, url=%s...",
|
|
|
|
|
+ camera_type,
|
|
|
|
|
+ redact_url_credentials(url)[:50] if url else "None",
|
|
|
|
|
+ )
|
|
|
if camera_type == "mjpeg":
|
|
if camera_type == "mjpeg":
|
|
|
return await _capture_mjpeg_frame(url, timeout)
|
|
return await _capture_mjpeg_frame(url, timeout)
|
|
|
elif camera_type == "rtsp":
|
|
elif camera_type == "rtsp":
|
|
@@ -311,7 +319,7 @@ async def _capture_mjpeg_frame(url: str, timeout: int) -> bytes | None:
|
|
|
"""
|
|
"""
|
|
|
safe_url = _sanitize_camera_url(url, ("http", "https"))
|
|
safe_url = _sanitize_camera_url(url, ("http", "https"))
|
|
|
if not safe_url:
|
|
if not safe_url:
|
|
|
- logger.error("Invalid MJPEG URL format: %s...", url[:50])
|
|
|
|
|
|
|
+ logger.error("Invalid MJPEG URL format: %s...", redact_url_credentials(url)[:50])
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
jpeg_start = b"\xff\xd8"
|
|
jpeg_start = b"\xff\xd8"
|
|
@@ -438,7 +446,8 @@ async def _capture_rtsp_frame(url: str, timeout: int) -> bytes | None:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
if process.returncode != 0:
|
|
if process.returncode != 0:
|
|
|
- logger.error("ffmpeg RTSP capture failed: %s", stderr.decode()[:200])
|
|
|
|
|
|
|
+ # ffmpeg echoes the RTSP input URL, which carries the camera password.
|
|
|
|
|
+ logger.error("ffmpeg RTSP capture failed: %s", redact_url_credentials(stderr.decode())[:200])
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
if not stdout or len(stdout) < 100:
|
|
if not stdout or len(stdout) < 100:
|
|
@@ -504,7 +513,7 @@ async def _capture_snapshot(url: str, timeout: int) -> bytes | None:
|
|
|
# Sanitize URL - returns reconstructed URL from validated components
|
|
# Sanitize URL - returns reconstructed URL from validated components
|
|
|
safe_url = _sanitize_camera_url(url, ("http", "https"))
|
|
safe_url = _sanitize_camera_url(url, ("http", "https"))
|
|
|
if not safe_url:
|
|
if not safe_url:
|
|
|
- logger.error("Invalid snapshot URL format: %s...", url[:50])
|
|
|
|
|
|
|
+ logger.error("Invalid snapshot URL format: %s...", redact_url_credentials(url)[:50])
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
@@ -559,7 +568,7 @@ async def test_connection(url: str, camera_type: str) -> dict:
|
|
|
Returns:
|
|
Returns:
|
|
|
Dict with {success: bool, error?: str, resolution?: str}
|
|
Dict with {success: bool, error?: str, resolution?: str}
|
|
|
"""
|
|
"""
|
|
|
- logger.info("Testing camera connection: type=%s, url=%s...", camera_type, url[:50])
|
|
|
|
|
|
|
+ logger.info("Testing camera connection: type=%s, url=%s...", camera_type, redact_url_credentials(url)[:50])
|
|
|
try:
|
|
try:
|
|
|
frame = await capture_frame(url, camera_type, timeout=10)
|
|
frame = await capture_frame(url, camera_type, timeout=10)
|
|
|
logger.info("Capture result: %s bytes", len(frame) if frame else 0)
|
|
logger.info("Capture result: %s bytes", len(frame) if frame else 0)
|
|
@@ -700,7 +709,7 @@ async def _stream_mjpeg(url: str) -> AsyncGenerator[bytes, None]:
|
|
|
# Sanitize URL - returns reconstructed URL from validated components
|
|
# Sanitize URL - returns reconstructed URL from validated components
|
|
|
safe_url = _sanitize_camera_url(url, ("http", "https"))
|
|
safe_url = _sanitize_camera_url(url, ("http", "https"))
|
|
|
if not safe_url:
|
|
if not safe_url:
|
|
|
- logger.error("Invalid MJPEG stream URL: %s...", url[:50])
|
|
|
|
|
|
|
+ logger.error("Invalid MJPEG stream URL: %s...", redact_url_credentials(url)[:50])
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
@@ -837,7 +846,8 @@ async def _stream_rtsp(
|
|
|
await asyncio.sleep(0.1)
|
|
await asyncio.sleep(0.1)
|
|
|
if process.returncode is not None:
|
|
if process.returncode is not None:
|
|
|
stderr = await process.stderr.read()
|
|
stderr = await process.stderr.read()
|
|
|
- logger.error("ffmpeg RTSP stream failed immediately: %s", stderr.decode()[:300])
|
|
|
|
|
|
|
+ # ffmpeg echoes the RTSP input URL, which carries the camera password.
|
|
|
|
|
+ logger.error("ffmpeg RTSP stream failed immediately: %s", redact_url_credentials(stderr.decode())[:300])
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
buffer = b""
|
|
buffer = b""
|