mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-20 05:32:49 +03:00
* reject push endpoints that crash the vapid audience parse The validator accepted hostnames like "." so https://./x passed, and check_registrations then raised ValueError slicing the endpoint, killing the dispatcher's IPC thread. The validator now rejects empty hostname labels and lives in webpush.py, the audience comes from urlparse, and _build_web_pushers skips stored rows whose endpoint fails validation. * keep the ipc thread alive when a handler raises An exception from any dispatcher handler escaped InterProcessCommunicator.read, which only caught ZMQError. The REP thread died owing a reply, so every InterProcessRequestor blocked forever. read now logs the error and replies with an empty response, and Dispatcher.publish and publish_local catch per communicator so one failing communicator can't take down the rest. * stop push sends from reaching internal addresses pywebpush posts through requests with redirects enabled, so an endpoint could answer 307 to http://127.0.0.1:5000/api/restart and the sender would follow it into the internal admin port on every alert. Pushers now use a session that never follows redirects, and each send first resolves the endpoint host and skips it unless every address is global. * keep the previous region grid when the refresh request fails The IPC reader now replies [] when a handler raises, and process_frames assigned that reply to region_grid unconditionally. The next region lookup then indexed into an empty list and killed the camera's detect process. An empty reply now keeps the current grid until the next daily refresh. * drop the send-time public address check Refusing non-global resolved addresses broke Tailscale (100.64.0.0/10), split-horizon DNS and HTTPS_PROXY-only setups, and added little once redirects are no longer followed and TLS verification stays on. * only skip stored push rows that break the vapid audience Running the full registration validator over stored rows dropped self-hosted endpoints registered before 0.18 with a non-443 port or a bare / path. _build_web_pushers now skips only non-dict rows, rows with no endpoint, unparsable endpoints and hosts with empty labels, and the warning names the host so the row can be found. Registration still uses the full validator.
77 lines
2.5 KiB
Python
77 lines
2.5 KiB
Python
"""Tests that IPC handler errors cannot stall inter-process communication."""
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import zmq
|
|
|
|
from frigate.comms.dispatcher import Dispatcher
|
|
from frigate.comms.inter_process import InterProcessCommunicator
|
|
|
|
|
|
class TestInterProcessHandlerErrors(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.address = f"ipc://{os.path.join(self.tmp.name, 'comms')}"
|
|
|
|
with patch("frigate.comms.inter_process.SOCKET_REP_REQ", self.address):
|
|
self.communicator = InterProcessCommunicator()
|
|
|
|
self.context = zmq.Context()
|
|
self.requestor = self.context.socket(zmq.REQ)
|
|
self.requestor.setsockopt(zmq.RCVTIMEO, 5000)
|
|
self.requestor.connect(self.address)
|
|
|
|
def tearDown(self):
|
|
self.requestor.close(linger=0)
|
|
self.context.destroy(linger=0)
|
|
self.communicator.stop()
|
|
self.tmp.cleanup()
|
|
|
|
def test_handler_exception_still_replies_and_keeps_reading(self):
|
|
def dispatcher(topic, value):
|
|
if topic == "boom":
|
|
raise ValueError("handler failed")
|
|
return value
|
|
|
|
self.communicator.subscribe(dispatcher)
|
|
self.communicator.start()
|
|
|
|
with self.assertLogs("frigate.comms.inter_process", level="ERROR"):
|
|
self.requestor.send_json(("boom", None))
|
|
self.assertEqual(self.requestor.recv_json(), [])
|
|
|
|
self.requestor.send_json(("echo", "ok"))
|
|
self.assertEqual(self.requestor.recv_json(), "ok")
|
|
self.assertTrue(self.communicator.reader_thread.is_alive())
|
|
|
|
|
|
class TestDispatcherPublishErrors(unittest.TestCase):
|
|
def test_failing_communicator_does_not_block_the_others(self):
|
|
failing = MagicMock()
|
|
failing.publish.side_effect = ValueError("publish failed")
|
|
healthy = MagicMock()
|
|
|
|
with (
|
|
patch("frigate.comms.dispatcher.CameraActivityManager"),
|
|
patch("frigate.comms.dispatcher.AudioActivityManager"),
|
|
):
|
|
dispatcher = Dispatcher(
|
|
MagicMock(), MagicMock(), MagicMock(), {}, [failing, healthy]
|
|
)
|
|
|
|
healthy.publish.reset_mock()
|
|
|
|
with self.assertLogs("frigate.comms.dispatcher", level="ERROR"):
|
|
dispatcher.publish("reviews", "{}")
|
|
dispatcher.publish_local("notices", "[]")
|
|
|
|
healthy.publish.assert_any_call("reviews", "{}", False)
|
|
healthy.publish.assert_any_call("notices", "[]", False)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|