mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 11:07:31 +00:00
Attribute log entries to the client IP
Entries gain an "ip" field, rendered between the time and the source as asked. The buffer is shared by every viewer, so without attribution a log of keypresses from two people is unreadable. Resolving the address matters more than it looks: behind the nginx reverse proxy REMOTE_ADDR is always 127.0.0.1, so the first hop of X-Forwarded-For is what identifies the real client. Only the first entry is trusted -- the rest of the chain is set by the caller and a test covers that. Power actions are logged at the route rather than in the supervisor, which has no request context, so "who powered it off" is recorded. Entries with no client behind them keep ip=None and render as "-": firmware serial and QEMU stderr are not caused by a request. The sharing and history the user asked for already worked and needed no change -- verified rather than assumed. The front end starts at logCursor=0, so a page opened now receives the full buffer, including lines produced before it connected and lines from other people. Confirmed live through the proxy: a new reader saw entries attributed to 172.21.91.140, fd3c:3f9b:6424:2::5 and "-".
This commit is contained in:
1 parent
dbe7607720
commit
f2c5c6b31b
3 files changed
+107
-7
No files matched your search
@@ -607,5 +607,73 @@ class TestIdleKeepalive(unittest.TestCase):
|
||||
self.assertGreater(webui.IDLE_FRAME_INTERVAL_S, 1.0 / webui.TARGET_FPS)
|
||||
|
||||
|
||||
class TestClientIpInLogs(unittest.TestCase):
|
||||
"""Entries carry the client IP, so a shared log says who did what."""
|
||||
|
||||
def test_key_entry_records_the_client_ip(self):
|
||||
client, sup, http = make_supervised()
|
||||
log = http.application.config["LOG"]
|
||||
http.post("/api/key", json={"key": "MENU", "hold_ms": 60},
|
||||
environ_overrides={"REMOTE_ADDR": "172.21.91.137"})
|
||||
entries = [e for e in log.entries() if e["source"] == "key"]
|
||||
self.assertTrue(entries)
|
||||
self.assertEqual(entries[-1]["ip"], "172.21.91.137")
|
||||
|
||||
def test_power_entry_records_the_client_ip(self):
|
||||
client, sup, http = make_supervised()
|
||||
log = http.application.config["LOG"]
|
||||
http.post("/api/power/reset",
|
||||
environ_overrides={"REMOTE_ADDR": "172.21.91.137"})
|
||||
entries = [e for e in log.entries() if e["source"] == "power"]
|
||||
self.assertTrue(entries)
|
||||
self.assertEqual(entries[-1]["ip"], "172.21.91.137")
|
||||
|
||||
def test_ipv6_is_recorded(self):
|
||||
client, sup, http = make_supervised()
|
||||
log = http.application.config["LOG"]
|
||||
http.post("/api/key", json={"key": "UP", "hold_ms": 60},
|
||||
environ_overrides={"REMOTE_ADDR": "fd3c:3f9b:6424:2::99"})
|
||||
entries = [e for e in log.entries() if e["source"] == "key"]
|
||||
self.assertEqual(entries[-1]["ip"], "fd3c:3f9b:6424:2::99")
|
||||
|
||||
def test_x_forwarded_for_is_preferred_behind_a_proxy(self):
|
||||
"""nginx reverse-proxies this, so REMOTE_ADDR is always 127.0.0.1."""
|
||||
client, sup, http = make_supervised()
|
||||
log = http.application.config["LOG"]
|
||||
http.post("/api/key", json={"key": "UP", "hold_ms": 60},
|
||||
environ_overrides={"REMOTE_ADDR": "127.0.0.1",
|
||||
"HTTP_X_FORWARDED_FOR": "172.21.91.137"})
|
||||
entries = [e for e in log.entries() if e["source"] == "key"]
|
||||
self.assertEqual(entries[-1]["ip"], "172.21.91.137")
|
||||
|
||||
def test_only_the_first_hop_of_x_forwarded_for_is_used(self):
|
||||
"""The rest of the chain is attacker-controlled and must be ignored."""
|
||||
client, sup, http = make_supervised()
|
||||
log = http.application.config["LOG"]
|
||||
http.post("/api/key", json={"key": "UP", "hold_ms": 60},
|
||||
environ_overrides={
|
||||
"REMOTE_ADDR": "127.0.0.1",
|
||||
"HTTP_X_FORWARDED_FOR": "172.21.91.137, 10.0.0.1"})
|
||||
entries = [e for e in log.entries() if e["source"] == "key"]
|
||||
self.assertEqual(entries[-1]["ip"], "172.21.91.137")
|
||||
|
||||
def test_entries_without_a_request_have_no_ip(self):
|
||||
"""Firmware serial and qemu output come from no client at all."""
|
||||
from uvk5_logs import LogBuffer
|
||||
log = LogBuffer()
|
||||
log.add("serial", "boot banner")
|
||||
self.assertIsNone(log.entries()[-1]["ip"])
|
||||
|
||||
def test_page_renders_the_ip_between_time_and_source(self):
|
||||
_, http = make_app()
|
||||
body = http.get("/").get_data(as_text=True)
|
||||
# e.time + ip + source, in that order
|
||||
line = [l for l in body.splitlines() if "e.source" in l and "e.time" in l]
|
||||
self.assertTrue(line, "log line template not found")
|
||||
tmpl = line[0]
|
||||
self.assertLess(tmpl.index("e.time"), tmpl.index("ip"))
|
||||
self.assertLess(tmpl.index("ip"), tmpl.index("e.source"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user