mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Support special characters in passwords, redacted logs & debug config (#4057)
* Consts for regex * Add regex for camera username and password * Redact user:pass from ffmpeg logs * Redact ffmpeg commands * Move common function to util * Add tests * Formatting * Remove unused imports * Fix test * Add port to test * Support special characters in passwords * Add tests for special character handling * Remove docs about not supporting special characters
This commit is contained in:
parent
11624d4759
commit
1bc9efd529
@ -45,12 +45,6 @@ More details on available detectors can be found [here](/configuration/detectors
|
|||||||
|
|
||||||
Now let's add the first camera:
|
Now let's add the first camera:
|
||||||
|
|
||||||
:::caution
|
|
||||||
|
|
||||||
Note that passwords that contain special characters often cause issues with ffmpeg connecting to the camera. If receiving `end-of-file` or `unauthorized` errors with a verified correct password, try changing the password to something simple to rule out the possibility that the password is the issue.
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
mqtt:
|
mqtt:
|
||||||
host: <ip of your mqtt server>
|
host: <ip of your mqtt server>
|
||||||
|
@ -12,8 +12,18 @@ import yaml
|
|||||||
from pydantic import BaseModel, Extra, Field, validator
|
from pydantic import BaseModel, Extra, Field, validator
|
||||||
from pydantic.fields import PrivateAttr
|
from pydantic.fields import PrivateAttr
|
||||||
|
|
||||||
from frigate.const import BASE_DIR, CACHE_DIR, YAML_EXT
|
from frigate.const import (
|
||||||
from frigate.util import create_mask, deep_merge, load_labels
|
BASE_DIR,
|
||||||
|
CACHE_DIR,
|
||||||
|
REGEX_CAMERA_NAME,
|
||||||
|
YAML_EXT,
|
||||||
|
)
|
||||||
|
from frigate.util import (
|
||||||
|
create_mask,
|
||||||
|
deep_merge,
|
||||||
|
escape_special_characters,
|
||||||
|
load_labels,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -540,7 +550,7 @@ class CameraUiConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CameraConfig(FrigateBaseModel):
|
class CameraConfig(FrigateBaseModel):
|
||||||
name: Optional[str] = Field(title="Camera name.", regex="^[a-zA-Z0-9_-]+$")
|
name: Optional[str] = Field(title="Camera name.", regex=REGEX_CAMERA_NAME)
|
||||||
enabled: bool = Field(default=True, title="Enable camera.")
|
enabled: bool = Field(default=True, title="Enable camera.")
|
||||||
ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
|
ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
|
||||||
best_image_timeout: int = Field(
|
best_image_timeout: int = Field(
|
||||||
@ -695,7 +705,7 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
+ global_args
|
+ global_args
|
||||||
+ hwaccel_args
|
+ hwaccel_args
|
||||||
+ input_args
|
+ input_args
|
||||||
+ ["-i", ffmpeg_input.path]
|
+ ["-i", escape_special_characters(ffmpeg_input.path)]
|
||||||
+ ffmpeg_output_args
|
+ ffmpeg_output_args
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,3 +5,8 @@ CACHE_DIR = "/tmp/cache"
|
|||||||
YAML_EXT = (".yaml", ".yml")
|
YAML_EXT = (".yaml", ".yml")
|
||||||
PLUS_ENV_VAR = "PLUS_API_KEY"
|
PLUS_ENV_VAR = "PLUS_API_KEY"
|
||||||
PLUS_API_HOST = "https://api.frigate.video"
|
PLUS_API_HOST = "https://api.frigate.video"
|
||||||
|
|
||||||
|
# Regex Consts
|
||||||
|
|
||||||
|
REGEX_CAMERA_NAME = "^[a-zA-Z0-9_-]+$"
|
||||||
|
REGEX_CAMERA_USER_PASS = "[a-zA-Z0-9_-]+:[a-zA-Z0-9!*'();:@&=+$,?%#_-]+@"
|
||||||
|
@ -30,6 +30,7 @@ from frigate.const import CLIPS_DIR
|
|||||||
from frigate.models import Event, Recordings
|
from frigate.models import Event, Recordings
|
||||||
from frigate.object_processing import TrackedObject, TrackedObjectProcessor
|
from frigate.object_processing import TrackedObject, TrackedObjectProcessor
|
||||||
from frigate.stats import stats_snapshot
|
from frigate.stats import stats_snapshot
|
||||||
|
from frigate.util import clean_camera_user_pass
|
||||||
from frigate.version import VERSION
|
from frigate.version import VERSION
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -581,7 +582,7 @@ def config():
|
|||||||
camera_dict = config["cameras"][camera_name]
|
camera_dict = config["cameras"][camera_name]
|
||||||
camera_dict["ffmpeg_cmds"] = copy.deepcopy(camera.ffmpeg_cmds)
|
camera_dict["ffmpeg_cmds"] = copy.deepcopy(camera.ffmpeg_cmds)
|
||||||
for cmd in camera_dict["ffmpeg_cmds"]:
|
for cmd in camera_dict["ffmpeg_cmds"]:
|
||||||
cmd["cmd"] = " ".join(cmd["cmd"])
|
cmd["cmd"] = clean_camera_user_pass(" ".join(cmd["cmd"]))
|
||||||
|
|
||||||
config["plus"] = {"enabled": current_app.plus_api.is_active()}
|
config["plus"] = {"enabled": current_app.plus_api.is_active()}
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
import queue
|
import queue
|
||||||
from multiprocessing.queues import Queue
|
from multiprocessing.queues import Queue
|
||||||
from logging import handlers
|
from logging import handlers
|
||||||
@ -10,6 +9,8 @@ from setproctitle import setproctitle
|
|||||||
from typing import Deque
|
from typing import Deque
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
|
from frigate.util import clean_camera_user_pass
|
||||||
|
|
||||||
|
|
||||||
def listener_configurer() -> None:
|
def listener_configurer() -> None:
|
||||||
root = logging.getLogger()
|
root = logging.getLogger()
|
||||||
@ -55,6 +56,11 @@ class LogPipe(threading.Thread):
|
|||||||
self.pipeReader = os.fdopen(self.fdRead)
|
self.pipeReader = os.fdopen(self.fdRead)
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
|
def cleanup_log(self, log: str) -> str:
|
||||||
|
"""Cleanup the log line to remove sensitive info and string tokens."""
|
||||||
|
log = clean_camera_user_pass(log).strip("\n")
|
||||||
|
return log
|
||||||
|
|
||||||
def fileno(self) -> int:
|
def fileno(self) -> int:
|
||||||
"""Return the write file descriptor of the pipe"""
|
"""Return the write file descriptor of the pipe"""
|
||||||
return self.fdWrite
|
return self.fdWrite
|
||||||
@ -62,7 +68,7 @@ class LogPipe(threading.Thread):
|
|||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
"""Run the thread, logging everything."""
|
"""Run the thread, logging everything."""
|
||||||
for line in iter(self.pipeReader.readline, ""):
|
for line in iter(self.pipeReader.readline, ""):
|
||||||
self.deque.append(line.strip("\n"))
|
self.deque.append(self.cleanup_log(line))
|
||||||
|
|
||||||
self.pipeReader.close()
|
self.pipeReader.close()
|
||||||
|
|
||||||
|
33
frigate/test/test_camera_pw.py
Normal file
33
frigate/test/test_camera_pw.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"""Test camera user and password cleanup."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from frigate.util import clean_camera_user_pass, escape_special_characters
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserPassCleanup(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.rtsp_with_pass = "rtsp://user:password@192.168.0.2:554/live"
|
||||||
|
self.rtsp_with_special_pass = "rtsp://user:password#$@@192.168.0.2:554/live"
|
||||||
|
self.rtsp_no_pass = "rtsp://192.168.0.3:554/live"
|
||||||
|
|
||||||
|
def test_cleanup(self):
|
||||||
|
"""Test that user / pass are cleaned up."""
|
||||||
|
clean = clean_camera_user_pass(self.rtsp_with_pass)
|
||||||
|
assert clean != self.rtsp_with_pass
|
||||||
|
assert "user:password" not in clean
|
||||||
|
|
||||||
|
def test_no_cleanup(self):
|
||||||
|
"""Test that nothing changes when no user / pass are defined."""
|
||||||
|
clean = clean_camera_user_pass(self.rtsp_no_pass)
|
||||||
|
assert clean == self.rtsp_no_pass
|
||||||
|
|
||||||
|
def test_special_char_password(self):
|
||||||
|
"""Test that special characters in pw are escaped, but not others."""
|
||||||
|
escaped = escape_special_characters(self.rtsp_with_special_pass)
|
||||||
|
assert escaped == "rtsp://user:password%23%24%40@192.168.0.2:554/live"
|
||||||
|
|
||||||
|
def test_no_special_char_password(self):
|
||||||
|
"""Test that no change is made to path with no special characters."""
|
||||||
|
escaped = escape_special_characters(self.rtsp_with_pass)
|
||||||
|
assert escaped == self.rtsp_with_pass
|
@ -1,25 +1,22 @@
|
|||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import math
|
import re
|
||||||
import signal
|
import signal
|
||||||
import subprocess as sp
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import traceback
|
import traceback
|
||||||
|
import urllib.parse
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from multiprocessing import shared_memory
|
from multiprocessing import shared_memory
|
||||||
from typing import AnyStr
|
from typing import AnyStr
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import os
|
import os
|
||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
|
from frigate.const import REGEX_CAMERA_USER_PASS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -625,6 +622,23 @@ def load_labels(path, encoding="utf-8"):
|
|||||||
return {index: line.strip() for index, line in enumerate(lines)}
|
return {index: line.strip() for index, line in enumerate(lines)}
|
||||||
|
|
||||||
|
|
||||||
|
def clean_camera_user_pass(line: str) -> str:
|
||||||
|
"""Removes user and password from line."""
|
||||||
|
# todo also remove http password like reolink
|
||||||
|
return re.sub(REGEX_CAMERA_USER_PASS, "*:*@", line)
|
||||||
|
|
||||||
|
|
||||||
|
def escape_special_characters(path: str) -> str:
|
||||||
|
"""Cleans reserved characters to encodings for ffmpeg."""
|
||||||
|
try:
|
||||||
|
found = re.search(REGEX_CAMERA_USER_PASS, path).group(0)[:-1]
|
||||||
|
pw = found[(found.index(":") + 1) :]
|
||||||
|
return path.replace(pw, urllib.parse.quote_plus(pw))
|
||||||
|
except AttributeError:
|
||||||
|
# path does not have user:pass
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
class FrameManager(ABC):
|
class FrameManager(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def create(self, name, size) -> AnyStr:
|
def create(self, name, size) -> AnyStr:
|
||||||
|
Loading…
Reference in New Issue
Block a user