Commit c2ccca0e authored by Vitaly Lipatov's avatar Vitaly Lipatov

route-web-api: add thread lock for concurrent list operations

Protect read-modify-write cycles in handle_add/remove/move and consistent reads in /api/list with _list_lock to prevent data loss when multiple users modify lists simultaneously. Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent b87a3775
...@@ -54,6 +54,7 @@ CHECK_GATEWAYS = [ ...@@ -54,6 +54,7 @@ CHECK_GATEWAYS = [
CHECK_TIMEOUT = 10 CHECK_TIMEOUT = 10
_check_lock = threading.Lock() _check_lock = threading.Lock()
_list_lock = threading.Lock()
def _check_one(name, proxy, url, ipver="-4"): def _check_one(name, proxy, url, ipver="-4"):
...@@ -1041,9 +1042,10 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1041,9 +1042,10 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.send_json(OPENAPI_SPEC) self.send_json(OPENAPI_SPEC)
elif path == "/api/list": elif path == "/api/list":
data = {} with _list_lock:
for mode, fpath in LIST_FILES.items(): data = {}
data[mode] = read_list(fpath) for mode, fpath in LIST_FILES.items():
data[mode] = read_list(fpath)
self.send_json(data) self.send_json(data)
elif path == "/api/status": elif path == "/api/status":
...@@ -1118,30 +1120,32 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1118,30 +1120,32 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.send_error_json(400, "Invalid domain or IP") self.send_error_json(400, "Invalid domain or IP")
return return
# Check if already in the same list with _list_lock:
entries = read_list(LIST_FILES[mode]) # Check if already in the same list
if domain in entries: entries = read_list(LIST_FILES[mode])
self.send_error_json(409, "Already in %s list" % mode) if domain in entries:
return self.send_error_json(409, "Already in %s list" % mode)
return
if total_entries() >= MAX_ENTRIES: if total_entries() >= MAX_ENTRIES:
self.send_error_json(400, "Entry limit reached (%d)" % MAX_ENTRIES) self.send_error_json(400, "Entry limit reached (%d)" % MAX_ENTRIES)
return return
# Remove from the other list if present
moved_from = None
for m, fpath in LIST_FILES.items():
if m == mode:
continue
other_entries = read_list(fpath)
if domain in other_entries:
other_entries.remove(domain)
write_list(fpath, other_entries)
moved_from = m
break
entries.append(domain)
write_list(LIST_FILES[mode], entries)
# Remove from the other list if present
moved_from = None
for m, fpath in LIST_FILES.items():
if m == mode:
continue
other_entries = read_list(fpath)
if domain in other_entries:
other_entries.remove(domain)
write_list(fpath, other_entries)
moved_from = m
break
entries.append(domain)
write_list(LIST_FILES[mode], entries)
if moved_from: if moved_from:
self.log_message("MOVE %s: %s -> %s", domain, moved_from, mode) self.log_message("MOVE %s: %s -> %s", domain, moved_from, mode)
else: else:
...@@ -1159,13 +1163,15 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1159,13 +1163,15 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.send_error_json(400, "Invalid domain or IP") self.send_error_json(400, "Invalid domain or IP")
return return
entries = read_list(LIST_FILES[mode]) with _list_lock:
if domain not in entries: entries = read_list(LIST_FILES[mode])
self.send_error_json(404, "Not found in %s list" % mode) if domain not in entries:
return self.send_error_json(404, "Not found in %s list" % mode)
return
entries.remove(domain)
write_list(LIST_FILES[mode], entries)
entries.remove(domain)
write_list(LIST_FILES[mode], entries)
self.log_message("REMOVE %s from %s", domain, mode) self.log_message("REMOVE %s from %s", domain, mode)
self.send_json({"ok": True, "domain": domain, "mode": mode}) self.send_json({"ok": True, "domain": domain, "mode": mode})
...@@ -1181,20 +1187,22 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1181,20 +1187,22 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.send_error_json(400, "Invalid domain or IP") self.send_error_json(400, "Invalid domain or IP")
return return
src_entries = read_list(LIST_FILES[src]) with _list_lock:
if domain not in src_entries: src_entries = read_list(LIST_FILES[src])
self.send_error_json(404, "Not found in %s list" % src) if domain not in src_entries:
return self.send_error_json(404, "Not found in %s list" % src)
return
dst_entries = read_list(LIST_FILES[dst]) dst_entries = read_list(LIST_FILES[dst])
if domain in dst_entries: if domain in dst_entries:
self.send_error_json(409, "Already in %s list" % dst) self.send_error_json(409, "Already in %s list" % dst)
return return
src_entries.remove(domain)
dst_entries.append(domain)
write_list(LIST_FILES[src], src_entries)
write_list(LIST_FILES[dst], dst_entries)
src_entries.remove(domain)
dst_entries.append(domain)
write_list(LIST_FILES[src], src_entries)
write_list(LIST_FILES[dst], dst_entries)
self.log_message("MOVE %s: %s -> %s", domain, src, dst) self.log_message("MOVE %s: %s -> %s", domain, src, dst)
self.send_json({"ok": True, "domain": domain, "from": src, "to": dst}) self.send_json({"ok": True, "domain": domain, "from": src, "to": dst})
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment