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 = [
CHECK_TIMEOUT = 10
_check_lock = threading.Lock()
_list_lock = threading.Lock()
def _check_one(name, proxy, url, ipver="-4"):
......@@ -1041,9 +1042,10 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.send_json(OPENAPI_SPEC)
elif path == "/api/list":
data = {}
for mode, fpath in LIST_FILES.items():
data[mode] = read_list(fpath)
with _list_lock:
data = {}
for mode, fpath in LIST_FILES.items():
data[mode] = read_list(fpath)
self.send_json(data)
elif path == "/api/status":
......@@ -1118,30 +1120,32 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.send_error_json(400, "Invalid domain or IP")
return
# Check if already in the same list
entries = read_list(LIST_FILES[mode])
if domain in entries:
self.send_error_json(409, "Already in %s list" % mode)
return
with _list_lock:
# Check if already in the same list
entries = read_list(LIST_FILES[mode])
if domain in entries:
self.send_error_json(409, "Already in %s list" % mode)
return
if total_entries() >= MAX_ENTRIES:
self.send_error_json(400, "Entry limit reached (%d)" % MAX_ENTRIES)
return
if total_entries() >= MAX_ENTRIES:
self.send_error_json(400, "Entry limit reached (%d)" % MAX_ENTRIES)
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:
self.log_message("MOVE %s: %s -> %s", domain, moved_from, mode)
else:
......@@ -1159,13 +1163,15 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.send_error_json(400, "Invalid domain or IP")
return
entries = read_list(LIST_FILES[mode])
if domain not in entries:
self.send_error_json(404, "Not found in %s list" % mode)
return
with _list_lock:
entries = read_list(LIST_FILES[mode])
if domain not in entries:
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.send_json({"ok": True, "domain": domain, "mode": mode})
......@@ -1181,20 +1187,22 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.send_error_json(400, "Invalid domain or IP")
return
src_entries = read_list(LIST_FILES[src])
if domain not in src_entries:
self.send_error_json(404, "Not found in %s list" % src)
return
with _list_lock:
src_entries = read_list(LIST_FILES[src])
if domain not in src_entries:
self.send_error_json(404, "Not found in %s list" % src)
return
dst_entries = read_list(LIST_FILES[dst])
if domain in dst_entries:
self.send_error_json(409, "Already in %s list" % dst)
return
dst_entries = read_list(LIST_FILES[dst])
if domain in dst_entries:
self.send_error_json(409, "Already in %s list" % dst)
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.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