fmtr.tools 1.2.4__py3-none-any.whl → 1.2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fmtr.tools might be problematic. Click here for more details.
- fmtr/tools/__init__.py +1 -5
- fmtr/tools/dns_tools/__init__.py +6 -0
- fmtr/tools/dns_tools/client.py +80 -0
- fmtr/tools/dns_tools/dm.py +110 -0
- fmtr/tools/dns_tools/server.py +89 -0
- fmtr/tools/version +1 -1
- {fmtr_tools-1.2.4.dist-info → fmtr_tools-1.2.6.dist-info}/METADATA +44 -42
- {fmtr_tools-1.2.4.dist-info → fmtr_tools-1.2.6.dist-info}/RECORD +12 -9
- fmtr/tools/dns_tools.py +0 -221
- {fmtr_tools-1.2.4.dist-info → fmtr_tools-1.2.6.dist-info}/WHEEL +0 -0
- {fmtr_tools-1.2.4.dist-info → fmtr_tools-1.2.6.dist-info}/entry_points.txt +0 -0
- {fmtr_tools-1.2.4.dist-info → fmtr_tools-1.2.6.dist-info}/licenses/LICENSE +0 -0
- {fmtr_tools-1.2.4.dist-info → fmtr_tools-1.2.6.dist-info}/top_level.txt +0 -0
fmtr/tools/__init__.py
CHANGED
|
@@ -29,6 +29,7 @@ from fmtr.tools.constants import Constants
|
|
|
29
29
|
# Submodules
|
|
30
30
|
from fmtr.tools.path_tools import Path, PackagePaths, AppPaths
|
|
31
31
|
from fmtr.tools import ai_tools as ai
|
|
32
|
+
from fmtr.tools import dns_tools as dns
|
|
32
33
|
|
|
33
34
|
import fmtr.tools.setup_tools as setup
|
|
34
35
|
from fmtr.tools.setup_tools import Setup, SetupPaths, Dependencies, Tools
|
|
@@ -172,11 +173,6 @@ try:
|
|
|
172
173
|
except ImportError as exception:
|
|
173
174
|
patterns = MissingExtraMockModule('patterns', exception)
|
|
174
175
|
|
|
175
|
-
try:
|
|
176
|
-
from fmtr.tools import dns_tools as dns
|
|
177
|
-
except ImportError as exception:
|
|
178
|
-
dns = MissingExtraMockModule('dns', exception)
|
|
179
|
-
|
|
180
176
|
try:
|
|
181
177
|
from fmtr.tools import http_tools as http
|
|
182
178
|
from fmtr.tools.http_tools import Client
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import dns
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from dns import query
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from httpx_retries import Retry, RetryTransport
|
|
6
|
+
|
|
7
|
+
from fmtr.tools import http_tools as http
|
|
8
|
+
from fmtr.tools.dns_tools.dm import Exchange, Response, Request
|
|
9
|
+
from fmtr.tools.logging_tools import logger
|
|
10
|
+
|
|
11
|
+
RETRY_STRATEGY = Retry(
|
|
12
|
+
total=2, # initial + 1 retry
|
|
13
|
+
allowed_methods={"GET", "POST"},
|
|
14
|
+
status_forcelist={502, 503, 504},
|
|
15
|
+
retry_on_exceptions=None, # defaults to httpx.TransportError etc.
|
|
16
|
+
backoff_factor=0.25, # short backoff (e.g. 0.25s, 0.5s)
|
|
17
|
+
max_backoff_wait=0.75, # max total delay before giving up
|
|
18
|
+
backoff_jitter=0.1, # small jitter to avoid retry bursts
|
|
19
|
+
respect_retry_after_header=False, # DoH resolvers probably won't set this
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HTTPClientDoH(http.Client):
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
Base HTTP client for DoH-appropriate retry strategy.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
TRANSPORT = RetryTransport(retry=RETRY_STRATEGY)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ClientBasePlain:
|
|
33
|
+
def __init__(self, host, port=53):
|
|
34
|
+
self.host = host
|
|
35
|
+
self.port = port
|
|
36
|
+
|
|
37
|
+
def resolve(self, exchange: Exchange):
|
|
38
|
+
with logger.span(f'UDP {self.host}:{self.port}'):
|
|
39
|
+
response = query.udp(q=exchange.request.message, where=self.host, port=self.port)
|
|
40
|
+
exchange.response_upstream = Response.from_message(response)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ClientDoH:
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
Base DoH client.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
HEADERS = {"Content-Type": "application/dns-message"}
|
|
52
|
+
CLIENT = HTTPClientDoH()
|
|
53
|
+
BOOTSTRAP = ClientBasePlain('8.8.8.8')
|
|
54
|
+
|
|
55
|
+
host: str
|
|
56
|
+
url: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@cached_property
|
|
60
|
+
def ip(self):
|
|
61
|
+
message = dns.message.make_query(self.host, dns.rdatatype.A, flags=0)
|
|
62
|
+
request = Request.from_message(message)
|
|
63
|
+
exchange = Exchange(request=request, ip=None, port=None)
|
|
64
|
+
self.BOOTSTRAP.resolve(exchange)
|
|
65
|
+
ip = next(iter(exchange.response_upstream.answer.items.keys())).address
|
|
66
|
+
return ip
|
|
67
|
+
|
|
68
|
+
def resolve(self, exchange: Exchange):
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
Resolve via DoH
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
request = exchange.request
|
|
75
|
+
headers = self.HEADERS | dict(Host=self.host)
|
|
76
|
+
url = self.url.format(host=self.ip)
|
|
77
|
+
response_doh = self.CLIENT.post(url, headers=headers, content=request.wire)
|
|
78
|
+
response_doh.raise_for_status()
|
|
79
|
+
response = Response.from_http(response_doh)
|
|
80
|
+
exchange.response_upstream = response
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import dns
|
|
2
|
+
import httpx
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from dns.message import Message
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from typing import Self, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class BaseDNSData:
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
DNS response object.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
wire: bytes
|
|
17
|
+
|
|
18
|
+
@cached_property
|
|
19
|
+
def message(self) -> Message:
|
|
20
|
+
return dns.message.from_wire(self.wire)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_message(cls, message: Message) -> Self:
|
|
24
|
+
return cls(message.to_wire())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Response(BaseDNSData):
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
DNS response object.
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
http: Optional[httpx.Response] = None
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_http(cls, response: httpx.Response) -> Self:
|
|
39
|
+
self = cls(response.content, http=response)
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
@cached_property
|
|
43
|
+
def answer(self):
|
|
44
|
+
return self.message.answer[-1]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Request(BaseDNSData):
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
DNS request object.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
wire: bytes
|
|
55
|
+
|
|
56
|
+
@cached_property
|
|
57
|
+
def question(self):
|
|
58
|
+
return self.message.question[0]
|
|
59
|
+
|
|
60
|
+
@cached_property
|
|
61
|
+
def is_valid(self):
|
|
62
|
+
return len(self.message.question) != 0
|
|
63
|
+
|
|
64
|
+
@cached_property
|
|
65
|
+
def type(self):
|
|
66
|
+
return self.question.rdtype
|
|
67
|
+
|
|
68
|
+
@cached_property
|
|
69
|
+
def type_text(self):
|
|
70
|
+
return dns.rdatatype.to_text(self.type)
|
|
71
|
+
|
|
72
|
+
@cached_property
|
|
73
|
+
def name(self):
|
|
74
|
+
return self.question.name
|
|
75
|
+
|
|
76
|
+
@cached_property
|
|
77
|
+
def name_text(self):
|
|
78
|
+
return self.name.to_text()
|
|
79
|
+
|
|
80
|
+
@cached_property
|
|
81
|
+
def blackhole(self) -> Response:
|
|
82
|
+
blackhole = dns.message.make_response(self.message)
|
|
83
|
+
blackhole.flags |= dns.flags.RA
|
|
84
|
+
blackhole.set_rcode(dns.rcode.NXDOMAIN)
|
|
85
|
+
response = Response.from_message(blackhole)
|
|
86
|
+
return response
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class Exchange:
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
Entire DNS exchange for a DNS Proxy: request -> upstream response -> response
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
ip: str
|
|
97
|
+
port: int
|
|
98
|
+
|
|
99
|
+
request: Request
|
|
100
|
+
response: Optional[Response] = None
|
|
101
|
+
response_upstream: Optional[Response] = None
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_wire(cls, wire: bytes, ip: str, port: int) -> Self:
|
|
105
|
+
request = Request(wire)
|
|
106
|
+
return cls(request=request, ip=ip, port=port)
|
|
107
|
+
|
|
108
|
+
@cached_property
|
|
109
|
+
def client(self):
|
|
110
|
+
return f'{self.ip}:{self.port}'
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from fmtr.tools import logger
|
|
5
|
+
from fmtr.tools.dns_tools.client import ClientDoH
|
|
6
|
+
from fmtr.tools.dns_tools.dm import Exchange
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ServerBasePlain:
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
Base for starting a plain DNS server
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
host: str
|
|
18
|
+
port: int
|
|
19
|
+
|
|
20
|
+
def __post_init__(self):
|
|
21
|
+
|
|
22
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
23
|
+
|
|
24
|
+
def resolve(self, exchange: Exchange):
|
|
25
|
+
raise NotImplemented
|
|
26
|
+
|
|
27
|
+
def start(self):
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
Listen and resolve via overridden resolve method.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
34
|
+
sock.bind((self.host, self.port))
|
|
35
|
+
print(f"Listening on {self.host}:{self.port}")
|
|
36
|
+
while True:
|
|
37
|
+
data, (ip, port) = sock.recvfrom(512)
|
|
38
|
+
exchange = Exchange.from_wire(data, ip=ip, port=port)
|
|
39
|
+
self.resolve(exchange)
|
|
40
|
+
sock.sendto(exchange.response.wire, (ip, port))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ServerBaseDoHProxy(ServerBasePlain):
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
Base for a DNS Proxy server
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
client: ClientDoH
|
|
52
|
+
|
|
53
|
+
def process_question(self, exchange: Exchange):
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
def process_upstream(self, exchange: Exchange):
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
def resolve(self, exchange: Exchange):
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
Resolve a request, processing each stage, initial question, upstream response etc.
|
|
63
|
+
Subclasses can override the relevant processing methods to implement custom behaviour.
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
request = exchange.request
|
|
68
|
+
|
|
69
|
+
with logger.span(f'Handling request for {request.name_text} from {exchange.client}...'):
|
|
70
|
+
|
|
71
|
+
if not request.is_valid:
|
|
72
|
+
raise ValueError(f'Only one question per request is supported. Got {len(request.question)} questions.')
|
|
73
|
+
|
|
74
|
+
with logger.span(f'Processing question...'):
|
|
75
|
+
self.process_question(exchange)
|
|
76
|
+
if exchange.response:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
with logger.span(f'Making upstream request for {request.name_text}...'):
|
|
80
|
+
self.client.resolve(exchange)
|
|
81
|
+
|
|
82
|
+
with logger.span(f'Processing upstream response...'):
|
|
83
|
+
self.process_upstream(exchange)
|
|
84
|
+
|
|
85
|
+
if exchange.response:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
exchange.response = exchange.response_upstream
|
|
89
|
+
return
|
fmtr/tools/version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.2.
|
|
1
|
+
1.2.6
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fmtr.tools
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.6
|
|
4
4
|
Summary: Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML
|
|
5
5
|
Home-page: https://github.com/fmtr/tools
|
|
6
6
|
Author: Frontmatter
|
|
@@ -106,6 +106,8 @@ Requires-Dist: pydevd-pycharm; extra == "debug"
|
|
|
106
106
|
Provides-Extra: sets
|
|
107
107
|
Requires-Dist: pydantic-settings; extra == "sets"
|
|
108
108
|
Requires-Dist: pydantic; extra == "sets"
|
|
109
|
+
Requires-Dist: yamlscript; extra == "sets"
|
|
110
|
+
Requires-Dist: pyyaml; extra == "sets"
|
|
109
111
|
Provides-Extra: path-app
|
|
110
112
|
Requires-Dist: appdirs; extra == "path-app"
|
|
111
113
|
Provides-Extra: path-type
|
|
@@ -128,60 +130,60 @@ Requires-Dist: logfire[httpx]; extra == "http"
|
|
|
128
130
|
Provides-Extra: setup
|
|
129
131
|
Requires-Dist: setuptools; extra == "setup"
|
|
130
132
|
Provides-Extra: all
|
|
131
|
-
Requires-Dist: dask[bag]; extra == "all"
|
|
132
|
-
Requires-Dist: huggingface_hub; extra == "all"
|
|
133
|
-
Requires-Dist: tabulate; extra == "all"
|
|
134
|
-
Requires-Dist: sre_yield; extra == "all"
|
|
135
|
-
Requires-Dist: semver; extra == "all"
|
|
136
|
-
Requires-Dist: distributed; extra == "all"
|
|
137
|
-
Requires-Dist: docker; extra == "all"
|
|
138
|
-
Requires-Dist: filetype; extra == "all"
|
|
139
|
-
Requires-Dist: pydevd-pycharm; extra == "all"
|
|
140
133
|
Requires-Dist: uvicorn[standard]; extra == "all"
|
|
134
|
+
Requires-Dist: google-auth-httplib2; extra == "all"
|
|
135
|
+
Requires-Dist: html2text; extra == "all"
|
|
136
|
+
Requires-Dist: distributed; extra == "all"
|
|
137
|
+
Requires-Dist: tabulate; extra == "all"
|
|
138
|
+
Requires-Dist: pydantic; extra == "all"
|
|
139
|
+
Requires-Dist: openpyxl; extra == "all"
|
|
141
140
|
Requires-Dist: httpx; extra == "all"
|
|
142
|
-
Requires-Dist:
|
|
141
|
+
Requires-Dist: tokenizers; extra == "all"
|
|
142
|
+
Requires-Dist: fastapi; extra == "all"
|
|
143
143
|
Requires-Dist: logfire; extra == "all"
|
|
144
|
-
Requires-Dist: pydantic-settings; extra == "all"
|
|
145
|
-
Requires-Dist: logfire[httpx]; extra == "all"
|
|
146
|
-
Requires-Dist: tinynetrc; extra == "all"
|
|
147
|
-
Requires-Dist: Unidecode; extra == "all"
|
|
148
|
-
Requires-Dist: openai; extra == "all"
|
|
149
|
-
Requires-Dist: contexttimer; extra == "all"
|
|
150
|
-
Requires-Dist: appdirs; extra == "all"
|
|
151
|
-
Requires-Dist: sentence_transformers; extra == "all"
|
|
152
|
-
Requires-Dist: pandas; extra == "all"
|
|
153
144
|
Requires-Dist: ollama; extra == "all"
|
|
154
|
-
Requires-Dist:
|
|
155
|
-
Requires-Dist:
|
|
145
|
+
Requires-Dist: flet[all]; extra == "all"
|
|
146
|
+
Requires-Dist: flet-webview; extra == "all"
|
|
156
147
|
Requires-Dist: flet-video; extra == "all"
|
|
157
|
-
Requires-Dist: tokenizers; extra == "all"
|
|
158
|
-
Requires-Dist: pyyaml; extra == "all"
|
|
159
148
|
Requires-Dist: torchvision; extra == "all"
|
|
160
|
-
Requires-Dist:
|
|
161
|
-
Requires-Dist:
|
|
149
|
+
Requires-Dist: httpx_retries; extra == "all"
|
|
150
|
+
Requires-Dist: tinynetrc; extra == "all"
|
|
151
|
+
Requires-Dist: docker; extra == "all"
|
|
162
152
|
Requires-Dist: setuptools; extra == "all"
|
|
163
|
-
Requires-Dist:
|
|
164
|
-
Requires-Dist: torchaudio; extra == "all"
|
|
165
|
-
Requires-Dist: regex; extra == "all"
|
|
166
|
-
Requires-Dist: openpyxl; extra == "all"
|
|
153
|
+
Requires-Dist: pytest-cov; extra == "all"
|
|
167
154
|
Requires-Dist: logfire[fastapi]; extra == "all"
|
|
168
|
-
Requires-Dist:
|
|
155
|
+
Requires-Dist: transformers[sentencepiece]; extra == "all"
|
|
169
156
|
Requires-Dist: google-auth-oauthlib; extra == "all"
|
|
170
|
-
Requires-Dist:
|
|
171
|
-
Requires-Dist:
|
|
172
|
-
Requires-Dist:
|
|
173
|
-
Requires-Dist:
|
|
174
|
-
Requires-Dist:
|
|
175
|
-
Requires-Dist:
|
|
176
|
-
Requires-Dist:
|
|
157
|
+
Requires-Dist: sentence_transformers; extra == "all"
|
|
158
|
+
Requires-Dist: semver; extra == "all"
|
|
159
|
+
Requires-Dist: Unidecode; extra == "all"
|
|
160
|
+
Requires-Dist: faker; extra == "all"
|
|
161
|
+
Requires-Dist: sre_yield; extra == "all"
|
|
162
|
+
Requires-Dist: regex; extra == "all"
|
|
163
|
+
Requires-Dist: openai; extra == "all"
|
|
164
|
+
Requires-Dist: pymupdf; extra == "all"
|
|
165
|
+
Requires-Dist: yamlscript; extra == "all"
|
|
166
|
+
Requires-Dist: bokeh; extra == "all"
|
|
167
|
+
Requires-Dist: contexttimer; extra == "all"
|
|
168
|
+
Requires-Dist: json_repair; extra == "all"
|
|
177
169
|
Requires-Dist: peft; extra == "all"
|
|
170
|
+
Requires-Dist: google-api-python-client; extra == "all"
|
|
178
171
|
Requires-Dist: deepmerge; extra == "all"
|
|
179
|
-
Requires-Dist:
|
|
172
|
+
Requires-Dist: torchaudio; extra == "all"
|
|
173
|
+
Requires-Dist: google-auth; extra == "all"
|
|
174
|
+
Requires-Dist: pyyaml; extra == "all"
|
|
180
175
|
Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
|
|
181
|
-
Requires-Dist:
|
|
182
|
-
Requires-Dist:
|
|
176
|
+
Requires-Dist: appdirs; extra == "all"
|
|
177
|
+
Requires-Dist: huggingface_hub; extra == "all"
|
|
178
|
+
Requires-Dist: pandas; extra == "all"
|
|
179
|
+
Requires-Dist: diskcache; extra == "all"
|
|
180
|
+
Requires-Dist: logfire[httpx]; extra == "all"
|
|
181
|
+
Requires-Dist: filetype; extra == "all"
|
|
182
|
+
Requires-Dist: pydevd-pycharm; extra == "all"
|
|
183
|
+
Requires-Dist: pydantic-settings; extra == "all"
|
|
184
|
+
Requires-Dist: dask[bag]; extra == "all"
|
|
183
185
|
Requires-Dist: dnspython[doh]; extra == "all"
|
|
184
|
-
Requires-Dist:
|
|
186
|
+
Requires-Dist: pymupdf4llm; extra == "all"
|
|
185
187
|
Dynamic: author
|
|
186
188
|
Dynamic: author-email
|
|
187
189
|
Dynamic: description
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
fmtr/tools/__init__.py,sha256=
|
|
1
|
+
fmtr/tools/__init__.py,sha256=vCxKR0SymygpO-EtCGPmGZEQv5O1goM7Jr8P4m-eJsQ,5676
|
|
2
2
|
fmtr/tools/api_tools.py,sha256=w8Zrp_EwN5KlUghwLoTCXo4z1irg5tAsReCqDLjASfE,2133
|
|
3
3
|
fmtr/tools/async_tools.py,sha256=ewz757WcveQJd-G5SVr2JDOQVbdLGecCgl-tsBGVZz4,284
|
|
4
4
|
fmtr/tools/augmentation_tools.py,sha256=-6ESbO4CDlKqVOV1J1V6qBeoBMzbFIinkDHRHnCBej0,55
|
|
@@ -8,7 +8,6 @@ fmtr/tools/data_modelling_tools.py,sha256=0BFm-F_cYzVTxftWQwORkPd0FM2BTLVh9-s0-r
|
|
|
8
8
|
fmtr/tools/dataclass_tools.py,sha256=0Gt6KeLhtPgubo_2tYkIVqB8oQ91Qzag8OAGZDdjvMU,1209
|
|
9
9
|
fmtr/tools/datatype_tools.py,sha256=3P4AWIFGkJ-UqvXlj0Jc9IvkIIgTOE9jRrOk3NVbpH8,1508
|
|
10
10
|
fmtr/tools/debugging_tools.py,sha256=_xzqS0V5BpL8d06j-jVQjGgI7T020QsqVXKAKMz7Du8,2082
|
|
11
|
-
fmtr/tools/dns_tools.py,sha256=7OoELFd9lZ3ULdJ6h8QwHYFNUYS9atqYrflKEzzFW-g,5461
|
|
12
11
|
fmtr/tools/docker_tools.py,sha256=rdaZje2xhlmnfQqZnR7IHgRdWncTLjrJcViUTt5oEwk,617
|
|
13
12
|
fmtr/tools/environment_tools.py,sha256=ZO2e8pTzbQ8jNXnmKpaZF7_3qkVM6U5iqku5YSwOszE,1849
|
|
14
13
|
fmtr/tools/function_tools.py,sha256=_oW3-HZXMst2pcU-I-U7KTMmzo0g9MIQKmX-c2_NEoE,858
|
|
@@ -45,12 +44,16 @@ fmtr/tools/tabular_tools.py,sha256=tpIpZzYku1HcJrHZJL6BC39LmN3WUWVhFbK2N7nDVmE,1
|
|
|
45
44
|
fmtr/tools/tokenization_tools.py,sha256=me-IBzSLyNYejLybwjO9CNB6Mj2NYfKPaOVThXyaGNg,4268
|
|
46
45
|
fmtr/tools/tools.py,sha256=CAsApa1YwVdNE6H66Vjivs_mXYvOas3rh7fPELAnTpk,795
|
|
47
46
|
fmtr/tools/unicode_tools.py,sha256=yS_9wpu8ogNoiIL7s1G_8bETFFO_YQlo4LNPv1NLDeY,52
|
|
48
|
-
fmtr/tools/version,sha256=
|
|
47
|
+
fmtr/tools/version,sha256=PP3XZe61jmbow_ljgcG-wbwzB5i9ZhqLLvJeoEEgV60,5
|
|
49
48
|
fmtr/tools/version_tools.py,sha256=yNs_CGqWpqE4jbK9wsPIi14peJVXYbhIcMqHAFOw3yE,1480
|
|
50
49
|
fmtr/tools/yaml_tools.py,sha256=9kuYChqJelWQIjGlSnK4iDdOWWH06P0gp9jIcRrC3UI,1903
|
|
51
50
|
fmtr/tools/ai_tools/__init__.py,sha256=JZrLuOFNV1A3wvJgonxOgz_4WS-7MfCuowGWA5uYCjs,372
|
|
52
51
|
fmtr/tools/ai_tools/agentic_tools.py,sha256=acSEPFS-aguDXanWGs3fAAlRyJSYPZW7L-Kb2qDLm-I,4300
|
|
53
52
|
fmtr/tools/ai_tools/inference_tools.py,sha256=2UP2gXEyOJUjyyV6zmFIYmIxUsh1rXkRH0IbFvr2bRs,11908
|
|
53
|
+
fmtr/tools/dns_tools/__init__.py,sha256=PwHxnpiy6_isQfUmz_5V1hL0dcPaA6ItqvoGWW8MOfk,222
|
|
54
|
+
fmtr/tools/dns_tools/client.py,sha256=omHdk9bA_8u2-VMQhh0c9r9e-oG4mZ0MWA9lfbtSEIc,2371
|
|
55
|
+
fmtr/tools/dns_tools/dm.py,sha256=mvXacq6QJ86G0S0tkzJFhU7bOaSJytvsMNlxs5X9hfE,2236
|
|
56
|
+
fmtr/tools/dns_tools/server.py,sha256=hSZhK4EZD6Ox4uRI3ldbnyyZf6DYgMUcTfffbrZN5pk,2329
|
|
54
57
|
fmtr/tools/entrypoints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
58
|
fmtr/tools/entrypoints/cache_hfh.py,sha256=fQNs4J9twQuZH_Yj98-oOvEX7-LrSUP3kO8nzw2HrHs,60
|
|
56
59
|
fmtr/tools/entrypoints/ep_test.py,sha256=B8HfWISfSgw_xVX475CbJGh_QnpOe9MH65H8qGjTWbY,46
|
|
@@ -70,9 +73,9 @@ fmtr/tools/tests/test_environment.py,sha256=iHaiMQfECYZPkPKwfuIZV9uHuWe3aE-p_dN_
|
|
|
70
73
|
fmtr/tools/tests/test_json.py,sha256=IeSP4ziPvRcmS8kq7k9tHonC9rN5YYq9GSNT2ul6Msk,287
|
|
71
74
|
fmtr/tools/tests/test_path.py,sha256=AkZQa6_8BQ-VaCyL_J-iKmdf2ZaM-xFYR37Kun3k4_g,2188
|
|
72
75
|
fmtr/tools/tests/test_yaml.py,sha256=jc0TwwKu9eC0LvFGNMERdgBue591xwLxYXFbtsRwXVM,287
|
|
73
|
-
fmtr_tools-1.2.
|
|
74
|
-
fmtr_tools-1.2.
|
|
75
|
-
fmtr_tools-1.2.
|
|
76
|
-
fmtr_tools-1.2.
|
|
77
|
-
fmtr_tools-1.2.
|
|
78
|
-
fmtr_tools-1.2.
|
|
76
|
+
fmtr_tools-1.2.6.dist-info/licenses/LICENSE,sha256=FW9aa6vVN5IjRQWLT43hs4_koYSmpcbIovlKeAJ0_cI,10757
|
|
77
|
+
fmtr_tools-1.2.6.dist-info/METADATA,sha256=NE-9BBFqSBwTTLeaO5Fz2VpZO1KmolLzAoEmTl_Z5xA,16025
|
|
78
|
+
fmtr_tools-1.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
79
|
+
fmtr_tools-1.2.6.dist-info/entry_points.txt,sha256=fSQrDGNctdQXbUxpMWYVfVQ0mhZMDyaEDG3D3a0zOSc,278
|
|
80
|
+
fmtr_tools-1.2.6.dist-info/top_level.txt,sha256=LXem9xCgNOD72tE2gRKESdiQTL902mfFkwWb6-dlwEE,5
|
|
81
|
+
fmtr_tools-1.2.6.dist-info/RECORD,,
|
fmtr/tools/dns_tools.py
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import dns
|
|
2
|
-
import httpx
|
|
3
|
-
import socket
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from dns.message import Message
|
|
6
|
-
from functools import cached_property
|
|
7
|
-
from httpx_retries import Retry, RetryTransport
|
|
8
|
-
from typing import Optional, Self
|
|
9
|
-
|
|
10
|
-
from fmtr.tools import Client, logger
|
|
11
|
-
|
|
12
|
-
RETRY_STRATEGY = Retry(
|
|
13
|
-
total=2, # initial + 1 retry
|
|
14
|
-
allowed_methods={"GET", "POST"},
|
|
15
|
-
status_forcelist={502, 503, 504},
|
|
16
|
-
retry_on_exceptions=None, # defaults to httpx.TransportError etc.
|
|
17
|
-
backoff_factor=0.25, # short backoff (e.g. 0.25s, 0.5s)
|
|
18
|
-
max_backoff_wait=0.75, # max total delay before giving up
|
|
19
|
-
backoff_jitter=0.1, # small jitter to avoid retry bursts
|
|
20
|
-
respect_retry_after_header=False, # DoH resolvers probably won't set this
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class HTTPClientDoH(Client):
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
Base HTTP client for DoH-appropriate retry strategy.
|
|
28
|
-
|
|
29
|
-
"""
|
|
30
|
-
TRANSPORT = RetryTransport(retry=RETRY_STRATEGY)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@dataclass
|
|
34
|
-
class BaseDNSData:
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
DNS response object.
|
|
38
|
-
|
|
39
|
-
"""
|
|
40
|
-
wire: bytes
|
|
41
|
-
|
|
42
|
-
@cached_property
|
|
43
|
-
def message(self) -> Message:
|
|
44
|
-
return dns.message.from_wire(self.wire)
|
|
45
|
-
|
|
46
|
-
@classmethod
|
|
47
|
-
def from_message(cls, message: Message) -> Self:
|
|
48
|
-
return cls(message.to_wire())
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@dataclass
|
|
52
|
-
class Response(BaseDNSData):
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
DNS response object.
|
|
56
|
-
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
http: Optional[httpx.Response] = None
|
|
60
|
-
|
|
61
|
-
@classmethod
|
|
62
|
-
def from_http(cls, response: httpx.Response) -> Self:
|
|
63
|
-
self = cls(response.content, http=response)
|
|
64
|
-
return self
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@dataclass
|
|
68
|
-
class Request(BaseDNSData):
|
|
69
|
-
"""
|
|
70
|
-
|
|
71
|
-
DNS request object.
|
|
72
|
-
|
|
73
|
-
"""
|
|
74
|
-
wire: bytes
|
|
75
|
-
|
|
76
|
-
@cached_property
|
|
77
|
-
def question(self):
|
|
78
|
-
return self.message.question[0]
|
|
79
|
-
|
|
80
|
-
@cached_property
|
|
81
|
-
def is_valid(self):
|
|
82
|
-
return len(self.message.question) != 0
|
|
83
|
-
|
|
84
|
-
@cached_property
|
|
85
|
-
def type(self):
|
|
86
|
-
return self.question.rdtype
|
|
87
|
-
|
|
88
|
-
@cached_property
|
|
89
|
-
def type_text(self):
|
|
90
|
-
return dns.rdatatype.to_text(self.type)
|
|
91
|
-
|
|
92
|
-
@cached_property
|
|
93
|
-
def name(self):
|
|
94
|
-
return self.question.name
|
|
95
|
-
|
|
96
|
-
@cached_property
|
|
97
|
-
def name_text(self):
|
|
98
|
-
return self.name.to_text()
|
|
99
|
-
|
|
100
|
-
@cached_property
|
|
101
|
-
def blackhole(self) -> Response:
|
|
102
|
-
blackhole = dns.message.make_response(self.message)
|
|
103
|
-
blackhole.flags |= dns.flags.RA
|
|
104
|
-
blackhole.set_rcode(dns.rcode.NXDOMAIN)
|
|
105
|
-
response = Response.from_message(blackhole)
|
|
106
|
-
return response
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
@dataclass
|
|
110
|
-
class Exchange:
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
Entire DNS exchange for a DNS Proxy: request -> upstream response -> response
|
|
114
|
-
|
|
115
|
-
"""
|
|
116
|
-
ip: str
|
|
117
|
-
port: int
|
|
118
|
-
|
|
119
|
-
request: Request
|
|
120
|
-
response: Optional[Response] = None
|
|
121
|
-
response_upstream: Optional[Response] = None
|
|
122
|
-
|
|
123
|
-
@classmethod
|
|
124
|
-
def from_wire(cls, wire: bytes, ip: str, port: int) -> Self:
|
|
125
|
-
request = Request(wire)
|
|
126
|
-
return cls(request=request, ip=ip, port=port)
|
|
127
|
-
|
|
128
|
-
@cached_property
|
|
129
|
-
def client(self):
|
|
130
|
-
return f'{self.ip}:{self.port}'
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class BasePlain:
|
|
134
|
-
"""
|
|
135
|
-
|
|
136
|
-
Base for starting a plain DNS server
|
|
137
|
-
|
|
138
|
-
"""
|
|
139
|
-
|
|
140
|
-
def __init__(self, host, port):
|
|
141
|
-
self.host = host
|
|
142
|
-
self.port = port
|
|
143
|
-
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
144
|
-
|
|
145
|
-
def resolve(self, exchange: Exchange):
|
|
146
|
-
raise NotImplemented
|
|
147
|
-
|
|
148
|
-
def start(self):
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
Listen and resolve via overridden resolve method.
|
|
152
|
-
|
|
153
|
-
"""
|
|
154
|
-
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
155
|
-
sock.bind((self.host, self.port))
|
|
156
|
-
print(f"Listening on {self.host}:{self.port}")
|
|
157
|
-
while True:
|
|
158
|
-
data, (ip, port) = sock.recvfrom(512)
|
|
159
|
-
exchange = Exchange.from_wire(data, ip=ip, port=port)
|
|
160
|
-
self.resolve(exchange)
|
|
161
|
-
sock.sendto(exchange.response.wire, (ip, port))
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
class BaseDoHProxy(BasePlain):
|
|
165
|
-
"""
|
|
166
|
-
|
|
167
|
-
Base for a DNS Proxy server
|
|
168
|
-
|
|
169
|
-
"""
|
|
170
|
-
|
|
171
|
-
URL = None
|
|
172
|
-
HEADERS = {"Content-Type": "application/dns-message"}
|
|
173
|
-
client = HTTPClientDoH()
|
|
174
|
-
|
|
175
|
-
def process_question(self, exchange: Exchange):
|
|
176
|
-
return
|
|
177
|
-
|
|
178
|
-
def process_upstream(self, exchange: Exchange):
|
|
179
|
-
return
|
|
180
|
-
|
|
181
|
-
def from_upstream(self, exchange: Exchange) -> Exchange:
|
|
182
|
-
|
|
183
|
-
request = exchange.request
|
|
184
|
-
response_doh = self.client.post(self.URL, headers=self.HEADERS, content=request.wire)
|
|
185
|
-
response_doh.raise_for_status()
|
|
186
|
-
response = Response.from_http(response_doh)
|
|
187
|
-
exchange.response_upstream = response
|
|
188
|
-
|
|
189
|
-
return exchange
|
|
190
|
-
|
|
191
|
-
def resolve(self, exchange: Exchange):
|
|
192
|
-
"""
|
|
193
|
-
|
|
194
|
-
Resolve a request, processing each stage, initial question, upstream response etc.
|
|
195
|
-
Subclasses can override the relevant processing methods to implement custom behaviour.
|
|
196
|
-
|
|
197
|
-
"""
|
|
198
|
-
|
|
199
|
-
request = exchange.request
|
|
200
|
-
|
|
201
|
-
with logger.span(f'Handling request for {request.name_text} from {exchange.client}...'):
|
|
202
|
-
|
|
203
|
-
if not request.is_valid:
|
|
204
|
-
raise ValueError(f'Only one question per request is supported. Got {len(request.question)} questions.')
|
|
205
|
-
|
|
206
|
-
with logger.span(f'Processing question...'):
|
|
207
|
-
self.process_question(exchange)
|
|
208
|
-
if exchange.response:
|
|
209
|
-
return
|
|
210
|
-
|
|
211
|
-
with logger.span(f'Making upstream request for {request.name_text}...'):
|
|
212
|
-
self.from_upstream(exchange)
|
|
213
|
-
|
|
214
|
-
with logger.span(f'Processing upstream response...'):
|
|
215
|
-
self.process_upstream(exchange)
|
|
216
|
-
|
|
217
|
-
if exchange.response:
|
|
218
|
-
return
|
|
219
|
-
|
|
220
|
-
exchange.response = exchange.response_upstream
|
|
221
|
-
return
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|