Skip to content

Commit 205c758

Browse files
authored
Replace Tornado with asyncio (#45)
1 parent 0048e61 commit 205c758

9 files changed

Lines changed: 77 additions & 186 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ classifiers = [
3030
"Topic :: System :: Networking",
3131
]
3232
dependencies = [
33-
"netaddr>=1.3.0",
34-
"tornado>=6.4.1",
3533
"beautifulsoup4>=4.12.3",
3634
"requests>=2.32.3",
3735
]
@@ -119,11 +117,12 @@ ignore = [
119117
"PLW2901",
120118
"S310",
121119
"S317",
122-
"T201", # Allow print
120+
"T201", # Allow print
123121
"TID252",
124122
"UP006",
125123
"UP035",
126124
"EM101",
125+
"N817",
127126
]
128127

129128
[tool.ruff.lint.isort]

requirements-dev.lock

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ coverage==7.6.1 \
188188
--hash=sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234 \
189189
--hash=sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc
190190
# via pytest-cov
191-
exceptiongroup==1.2.2 ; python_version < '3.11' \
191+
exceptiongroup==1.2.2 ; python_full_version < '3.11' \
192192
--hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b \
193193
--hash=sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc
194194
# via pytest
@@ -232,10 +232,6 @@ mypy-extensions==1.0.0 \
232232
--hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
233233
--hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
234234
# via mypy
235-
netaddr==1.3.0 \
236-
--hash=sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a \
237-
--hash=sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe
238-
# via uwhoisd
239235
packaging==24.1 \
240236
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
241237
--hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124
@@ -259,25 +255,12 @@ soupsieve==2.6 \
259255
--hash=sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb \
260256
--hash=sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9
261257
# via beautifulsoup4
262-
tomli==2.0.1 ; python_full_version <= '3.11.0a6' or python_version < '3.11' \
258+
tomli==2.0.1 ; python_full_version <= '3.11' \
263259
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
264260
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
265261
# via coverage
266262
# via mypy
267263
# via pytest
268-
tornado==6.4.1 \
269-
--hash=sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8 \
270-
--hash=sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f \
271-
--hash=sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4 \
272-
--hash=sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3 \
273-
--hash=sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14 \
274-
--hash=sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842 \
275-
--hash=sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9 \
276-
--hash=sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698 \
277-
--hash=sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7 \
278-
--hash=sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d \
279-
--hash=sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4
280-
# via uwhoisd
281264
types-beautifulsoup4==4.12.0.20240511 \
282265
--hash=sha256:004f6096fdd83b19cdbf6cb10e4eae57b10205eccc365d0a69d77da836012e28 \
283266
--hash=sha256:7ceda66a93ba28d759d5046d7fec9f4cad2f563a77b3a789efc90bcadafeefd1

requirements.lock

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,6 @@ idna==3.7 \
114114
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
115115
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
116116
# via requests
117-
netaddr==1.3.0 \
118-
--hash=sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a \
119-
--hash=sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe
120-
# via uwhoisd
121117
requests==2.32.3 \
122118
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
123119
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
@@ -126,19 +122,6 @@ soupsieve==2.6 \
126122
--hash=sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb \
127123
--hash=sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9
128124
# via beautifulsoup4
129-
tornado==6.4.1 \
130-
--hash=sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8 \
131-
--hash=sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f \
132-
--hash=sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4 \
133-
--hash=sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3 \
134-
--hash=sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14 \
135-
--hash=sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842 \
136-
--hash=sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9 \
137-
--hash=sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698 \
138-
--hash=sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7 \
139-
--hash=sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d \
140-
--hash=sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4
141-
# via uwhoisd
142125
urllib3==2.2.2 \
143126
--hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
144127
--hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168

src/uwhoisd/__init__.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
A 'universal' WHOIS proxy server.
33
"""
44

5+
import asyncio
56
import configparser
67
import logging
78
import logging.config
@@ -10,7 +11,7 @@
1011
import socket
1112
import sys
1213

13-
from . import caching, net, utils
14+
from . import caching, client, server, utils
1415

1516
USAGE = "Usage: %s <config>"
1617

@@ -90,7 +91,7 @@ def get_prefix(self, zone):
9091
"""
9192
return self.prefixes[zone] if zone in self.prefixes else ""
9293

93-
def whois(self, query):
94+
async def whois(self, query: str) -> str:
9495
"""
9596
Query the appropriate WHOIS server.
9697
"""
@@ -103,9 +104,8 @@ def whois(self, query):
103104

104105
# Query the registry's WHOIS server.
105106
server, port = self.get_whois_server(zone)
106-
with net.WhoisClient(server, port) as client:
107-
logger.info("Querying %s about %s", server, query)
108-
response = client.whois(self.get_prefix(zone) + query)
107+
logger.info("Querying %s about %s", server, query)
108+
response = await client.query_whois(server, port, self.get_prefix(zone) + query)
109109

110110
# Thin registry? Query the registrar's WHOIS server.
111111
if zone in self.recursion_patterns:
@@ -116,9 +116,8 @@ def whois(self, query):
116116
elif self.page_feed:
117117
# A form feed character so it's possible to find the split.
118118
response += "\f"
119-
with net.WhoisClient(server, port) as client:
120-
logger.info("Recursive query to %s about %s", server, query)
121-
response += client.whois(query)
119+
logger.info("Recursive query to %s about %s", server, query)
120+
response += await client.query_whois(server, port, query)
122121

123122
return response
124123

@@ -150,9 +149,12 @@ def main():
150149
logger.exception("Could not parse config file")
151150
return 1
152151
else:
153-
net.start_service(iface, port, whois)
152+
asyncio.run(server.start_service(iface, port, whois))
154153
return 0
155154

156155

157156
if __name__ == "__main__":
158-
sys.exit(main())
157+
try:
158+
sys.exit(main())
159+
except KeyboardInterrupt:
160+
pass

src/uwhoisd/caching.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ def wrap_whois(cache, whois_func):
4040
if cache is None:
4141
return whois_func
4242

43-
def wrapped(query):
43+
async def wrapped(query):
4444
response = cache.get(query)
4545
if response is None:
46-
response = whois_func(query)
46+
response = await whois_func(query)
4747
cache.set(query, response)
4848
else:
4949
logger.info("Cache hit for '%s'", query)
@@ -52,21 +52,19 @@ def wrapped(query):
5252
return wrapped
5353

5454

55-
# pylint: disable-msg=R0924
5655
class LFU:
57-
"""
58-
A simple LFU cache.
56+
"""A simple LFU cache.
57+
58+
The eviction queue contains 2-tuples consisting of the time the item was
59+
put into the cache and the cache key. The cache maps cache keys onto
60+
2-tuples consisting of a counter giving the number of times this item
61+
occurs on the eviction queue and the value.
62+
5963
"""
6064

61-
# This is implemented as an LFU cache. The eviction queue contains
62-
# 2-tuples consisting of the time the item was put into the cache and the
63-
# cache key. The cache maps cache keys onto 2-tuples consisting of a
64-
# counter giving the number of times this item occurs on the eviction queue
65-
# and the value.
66-
#
67-
# I may end up reimplementing this as an LRU cache if it turns out that's
68-
# more apt, but I haven't went that route as an LRU cache is somewhat more
69-
# awkward and involved to implement correctly.
65+
# I may end up reimplementing an LRU cache if it turns out that's more apt,
66+
# but I haven't went that route as an LRU cache is somewhat more awkward
67+
# and involved to implement correctly.
7068

7169
__slots__ = ("cache", "max_age", "max_size", "queue")
7270

src/uwhoisd/client.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Client."""
2+
3+
import asyncio
4+
5+
6+
async def query_whois(host: str, port: int, query: str) -> str:
7+
reader, writer = await asyncio.open_connection(host, port)
8+
9+
writer.write(f"{query}\r\n".encode())
10+
await writer.drain()
11+
12+
response = await asyncio.wait_for(reader.read(), timeout=5)
13+
14+
writer.close()
15+
await writer.wait_closed()
16+
17+
return str(response, "utf-8", "ignore")

src/uwhoisd/net.py

Lines changed: 0 additions & 121 deletions
This file was deleted.

src/uwhoisd/scraper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import sys
99
import typing as t
1010
from urllib.parse import urljoin
11-
import xml.etree.ElementTree as etree # noqa: N813
11+
import xml.etree.ElementTree as ET
1212

1313
from bs4 import BeautifulSoup
1414
import requests
@@ -28,7 +28,7 @@ def fetch_ipv4_assignments(url: str):
2828
Fetch WHOIS server list for the IPv4 /8 assignments from IANA.
2929
"""
3030
res = requests.get(url, stream=False, timeout=10)
31-
root = etree.fromstring(res.text) # noqa: S314
31+
root = ET.fromstring(res.text) # noqa: S314
3232
for record in root.findall("assignments:record", NSS):
3333
status = record.findtext("assignments:status", default="", namespaces=NSS)
3434
if status not in ("ALLOCATED", "LEGACY"):

src/uwhoisd/server.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import asyncio
2+
3+
from . import utils
4+
5+
6+
async def start_service(iface: str, port: int, whois):
7+
async def handle_request(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
8+
try:
9+
query = await asyncio.wait_for(reader.readuntil(b"\r\n"), timeout=5)
10+
except asyncio.TimeoutError:
11+
writer.write(b"; Query timeout: closing\r\n")
12+
await writer.drain()
13+
writer.close()
14+
return
15+
16+
cleaned = query.decode().strip().lower()
17+
if not utils.is_well_formed_fqdn(cleaned):
18+
result = f"; Bad query: '{cleaned}'\r\n"
19+
else:
20+
try:
21+
result = await whois(cleaned)
22+
except asyncio.TimeoutError:
23+
result = "; Timeout from upstream server\r\n"
24+
writer.write(result.encode())
25+
await writer.drain()
26+
writer.close()
27+
28+
svr = await asyncio.start_server(handle_request, host=iface, port=port)
29+
async with svr:
30+
await svr.serve_forever()

0 commit comments

Comments
 (0)