Skip to content

Commit 31179e0

Browse files
committed
Introduction of a way for end-user to bring special rules.
This patch (partially) fixes #409. Indeed, before this patch, end-user couldn't bring their own rules. Therefore, through this patch, we expose part of our internal through a "user-friendly" configuration key. There a room for improvement, but this first version should handle most of the cases.
1 parent 296a613 commit 31179e0

File tree

9 files changed

+610
-3
lines changed

9 files changed

+610
-3
lines changed

Diff for: .coveragerc

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ omit =
1313
PyFunceble/dataset/mariadb_base.py
1414
PyFunceble/dataset/*/mariadb.py
1515
PyFunceble/dataset/*/mysql.py
16-
PyFunceble/checker/availability/extra_rules.py
16+
PyFunceble/checker/availability/extras/*
1717
PyFunceble/__init__.py
1818
PyFunceble/logger.py
1919

Diff for: PyFunceble/checker/availability/base.py

+2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
from PyFunceble.checker.availability.extras.base import ExtraRuleHandlerBase
6565
from PyFunceble.checker.availability.extras.dns import DNSRulesHandler
6666
from PyFunceble.checker.availability.extras.etoxic import EToxicHandler
67+
from PyFunceble.checker.availability.extras.external import ExternalRulesHandler
6768
from PyFunceble.checker.availability.extras.rules import ExtraRulesHandler
6869
from PyFunceble.checker.availability.extras.subject_switch import (
6970
SubjectSwitchRulesHandler,
@@ -174,6 +175,7 @@ def __init__(
174175
DNSRulesHandler(),
175176
EToxicHandler(),
176177
ExtraRulesHandler(),
178+
ExternalRulesHandler(rulesets=PyFunceble.storage.SPECIAL_RULES),
177179
]
178180
self.db_session = db_session
179181

Diff for: PyFunceble/checker/availability/extras/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ def do_on_header_match(
328328
def handle_regex_match_mode(_req: requests.Response):
329329
matches2search_result = {}
330330

331-
for header, loc_matches in matches:
331+
for header, loc_matches in matches.items():
332332
matches2search_result[header] = False
333333

334334
if header not in _req.headers:

Diff for: PyFunceble/checker/availability/extras/external.py

+341
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
"""
2+
The tool to check the availability or syntax of domain, IP or URL.
3+
4+
::
5+
6+
7+
██████╗ ██╗ ██╗███████╗██╗ ██╗███╗ ██╗ ██████╗███████╗██████╗ ██╗ ███████╗
8+
██╔══██╗╚██╗ ██╔╝██╔════╝██║ ██║████╗ ██║██╔════╝██╔════╝██╔══██╗██║ ██╔════╝
9+
██████╔╝ ╚████╔╝ █████╗ ██║ ██║██╔██╗ ██║██║ █████╗ ██████╔╝██║ █████╗
10+
██╔═══╝ ╚██╔╝ ██╔══╝ ██║ ██║██║╚██╗██║██║ ██╔══╝ ██╔══██╗██║ ██╔══╝
11+
██║ ██║ ██║ ╚██████╔╝██║ ╚████║╚██████╗███████╗██████╔╝███████╗███████╗
12+
╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝╚══════╝╚═════╝ ╚══════╝╚══════╝
13+
14+
Provides the extra rules handler based on some DNS records.
15+
16+
Author:
17+
Nissar Chababy, @funilrys, contactTATAfunilrysTODTODcom
18+
19+
Special thanks:
20+
https://pyfunceble.github.io/#/special-thanks
21+
22+
Contributors:
23+
https://pyfunceble.github.io/#/contributors
24+
25+
Project link:
26+
https://github.com/funilrys/PyFunceble
27+
28+
Project documentation:
29+
https://docs.pyfunceble.com
30+
31+
Project homepage:
32+
https://pyfunceble.github.io/
33+
34+
License:
35+
::
36+
37+
38+
Copyright 2017, 2018, 2019, 2020, 2022, 2023, 2024 Nissar Chababy
39+
40+
Licensed under the Apache License, Version 2.0 (the "License");
41+
you may not use this file except in compliance with the License.
42+
You may obtain a copy of the License at
43+
44+
https://www.apache.org/licenses/LICENSE-2.0
45+
46+
Unless required by applicable law or agreed to in writing, software
47+
distributed under the License is distributed on an "AS IS" BASIS,
48+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
49+
See the License for the specific language governing permissions and
50+
limitations under the License.
51+
"""
52+
53+
# pylint: disable=line-too-long
54+
55+
from typing import Optional
56+
57+
from PyFunceble.checker.availability.extras.base import ExtraRuleHandlerBase
58+
from PyFunceble.checker.availability.status import AvailabilityCheckerStatus
59+
60+
61+
class ExternalRulesHandler(ExtraRuleHandlerBase):
62+
"""
63+
Provides the external rules handler that is used to handle the external
64+
provided rules.
65+
66+
Through this handler, end-user can provide their own rules to handle
67+
the availability status of a subject.
68+
69+
:param status:
70+
The previously gathered status.
71+
:type status:
72+
:class:`~PyFunceble.checker.availability.status.AvailabilityCheckerStatus`
73+
"""
74+
75+
rulesets: list = []
76+
"""
77+
The rulesets to process.
78+
79+
If you want to switch from the status code, you should provide a dict
80+
with the following structure:
81+
82+
{
83+
"subject_pattern": ".*", // The pattern the subject should match.
84+
"validation_type": "status_code", // Type of validation (status_code, headers, body, etc.)
85+
"state_transition": "up", // "up" -> ACTIVE, "down" -> INACTIVE
86+
"required_status_code": [404], // Status code to match.
87+
}
88+
89+
If you want to switch from the headers, you should provide a dict
90+
91+
{
92+
"subject_pattern": ".*", // The pattern the subject should match.
93+
"validation_type": "headers", // Type of validation (status_code, headers, body, etc.)
94+
"state_transition": "up", // "up" -> ACTIVE, "down" -> INACTIVE
95+
"required_headers_patterns": { // Required, the headers to match.
96+
"header_name": ["possible", "values"]
97+
},
98+
}
99+
100+
If you want to switch from the body, you should provide a dict
101+
102+
{
103+
"subject_pattern": ".*", // The pattern the subject should match.
104+
"validation_type": "body", // Type of validation (status_code, headers, body, etc.)
105+
"state_transition": "up", // "up" -> ACTIVE, "down" -> INACTIVE
106+
"required_body_patterns": ["regex1", "regex2"] // Required, the body patterns to match.
107+
}
108+
109+
If you want to switch from a combination of headers and body, you should provide a dict
110+
111+
{
112+
"subject_pattern": ".*", // The pattern the subject should match.
113+
"validation_type": "headers+body", // Type of validation (status_code, headers, body, etc.)
114+
"state_transition": "up", // "up" -> ACTIVE, "down" -> INACTIVE
115+
"required_headers_patterns": { // Required, the headers to match.
116+
"header_name": ["possible", "values"]
117+
},
118+
"required_body_patterns": ["regex1", "regex2"] // Required, the body patterns to match.
119+
}
120+
121+
If you want to switch from a combination of all, you should provide a dict
122+
123+
{
124+
"subject_pattern": ".*", // The pattern the subject should match.
125+
"validation_type": "all", // Type of validation (status_code, headers, body, etc.)
126+
"state_transition": "up", // "up" -> ACTIVE, "down" -> INACTIVE
127+
"required_status_code": [404], // Optional, Status code to match.
128+
"required_headers_patterns": { // Optional, the headers to match.
129+
"header_name": ["possible", "values"]
130+
},
131+
"required_body_patterns": ["regex1", "regex2"] // Optional, the body patterns to match.
132+
}
133+
134+
"""
135+
136+
def __init__(
137+
self,
138+
status: Optional[AvailabilityCheckerStatus] = None,
139+
*,
140+
rulesets: list = None
141+
) -> None:
142+
if rulesets is not None:
143+
self.rulesets = rulesets
144+
145+
super().__init__(status)
146+
147+
def switch_from_status_code_rule(self, rule: dict) -> "ExternalRulesHandler":
148+
"""
149+
Switch from the status code rule.
150+
151+
:param rule:
152+
The rule to switch from.
153+
:type rule: dict
154+
"""
155+
156+
required_keys = ["validation_type", "required_status_code"]
157+
158+
if any(x not in rule for x in required_keys):
159+
return self
160+
161+
if rule["validation_type"] != "status_code":
162+
return self
163+
164+
if all(
165+
self.status.http_status_code != int(x) for x in rule["required_status_code"]
166+
):
167+
return self
168+
169+
if rule["state_transition"] == "up":
170+
return self.switch_to_up()
171+
172+
if rule["state_transition"] == "down":
173+
return self.switch_to_down()
174+
175+
return self
176+
177+
def switch_from_headers_rule(self, rule: dict) -> "ExternalRulesHandler":
178+
"""
179+
Switch from the headers rule.
180+
181+
:param rule:
182+
The rule to switch from.
183+
:type rule: dict
184+
"""
185+
186+
required_keys = ["validation_type", "required_headers_patterns"]
187+
188+
if any(x not in rule for x in required_keys):
189+
return self
190+
191+
if rule["validation_type"] != "headers":
192+
return self
193+
194+
if rule["state_transition"] == "up":
195+
switch_method = self.switch_to_up
196+
197+
if rule["state_transition"] == "down":
198+
switch_method = self.switch_to_down
199+
200+
if "required_headers_patterns" in rule and rule["required_headers_patterns"]:
201+
# pylint: disable=possibly-used-before-assignment
202+
self.do_on_header_match(
203+
self.req_url,
204+
rule["required_headers_patterns"],
205+
method=switch_method,
206+
strict=False,
207+
allow_redirects=False,
208+
)
209+
210+
return self
211+
212+
def switch_from_body_rule(self, rule: dict) -> "ExternalRulesHandler":
213+
"""
214+
Switch from the body rule.
215+
216+
:param rule:
217+
The rule to switch from.
218+
:type rule: dict
219+
"""
220+
221+
required_keys = ["validation_type", "required_body_patterns"]
222+
223+
if any(x not in rule for x in required_keys):
224+
return self
225+
226+
if rule["validation_type"] != "body":
227+
return self
228+
229+
if rule["state_transition"] == "up":
230+
switch_method = self.switch_to_up
231+
232+
if rule["state_transition"] == "down":
233+
switch_method = self.switch_to_down
234+
235+
if "required_body_patterns" in rule and rule["required_body_patterns"]:
236+
# pylint: disable=possibly-used-before-assignment
237+
self.do_on_body_match(
238+
self.req_url,
239+
rule["required_body_patterns"],
240+
method=switch_method,
241+
strict=False,
242+
allow_redirects=False,
243+
)
244+
245+
return self
246+
247+
def switch_from_all_rule(self, rule: dict) -> "ExternalRulesHandler":
248+
"""
249+
Switch from the all rule.
250+
251+
:param rule:
252+
The rule to switch from.
253+
:type rule: dict
254+
"""
255+
256+
required_keys = [
257+
"validation_type",
258+
]
259+
260+
if any(x not in rule for x in required_keys):
261+
return self
262+
263+
if rule["validation_type"] != "all":
264+
return self
265+
266+
if rule["state_transition"] == "up":
267+
switch_method = self.switch_to_up
268+
269+
if rule["state_transition"] == "down":
270+
switch_method = self.switch_to_down
271+
272+
if (
273+
"required_status_code" in rule
274+
and rule["required_status_code"]
275+
and any(
276+
self.status.http_status_code == int(x)
277+
for x in rule["required_status_code"]
278+
)
279+
):
280+
# pylint: disable=possibly-used-before-assignment
281+
switch_method()
282+
283+
if "required_headers_patterns" in rule and rule["required_headers_patterns"]:
284+
self.do_on_header_match(
285+
self.req_url,
286+
rule["required_headers_patterns"],
287+
method=switch_method,
288+
strict=False,
289+
allow_redirects=False,
290+
)
291+
292+
if "required_body_patterns" in rule and rule["required_body_patterns"]:
293+
self.do_on_body_match(
294+
self.req_url,
295+
rule["required_body_patterns"],
296+
method=switch_method,
297+
strict=False,
298+
allow_redirects=False,
299+
)
300+
301+
return self
302+
303+
@ExtraRuleHandlerBase.ensure_status_is_given
304+
@ExtraRuleHandlerBase.setup_status_before
305+
@ExtraRuleHandlerBase.setup_status_after
306+
def start(self) -> "ExternalRulesHandler":
307+
"""
308+
Process the check and handling of the external rules for the given subject.
309+
"""
310+
311+
required_keys = ["subject_pattern", "validation_type", "state_transition"]
312+
313+
for rule in self.rulesets:
314+
if any(x not in rule for x in required_keys):
315+
continue
316+
317+
if not self.regex_helper.set_regex(rule["subject_pattern"]).match(
318+
self.status.netloc, return_match=False
319+
):
320+
continue
321+
322+
if rule["state_transition"] not in ["up", "down"]:
323+
continue
324+
325+
if self.status.status_after_extra_rules:
326+
# We already switched the status.
327+
break
328+
329+
if rule["validation_type"] == "status_code":
330+
self.switch_from_status_code_rule(rule)
331+
elif rule["validation_type"] == "headers":
332+
self.switch_from_headers_rule(rule)
333+
elif rule["validation_type"] == "body":
334+
self.switch_from_body_rule(rule)
335+
elif rule["validation_type"] == "headers+body":
336+
self.switch_from_headers_rule(rule)
337+
self.switch_from_body_rule(rule)
338+
elif rule["validation_type"] == "all":
339+
self.switch_from_all_rule(rule)
340+
341+
return self

Diff for: PyFunceble/config/loader.py

+4
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,9 @@ def start(self) -> "ConfigLoader":
651651
if "proxy" in config and config["proxy"]:
652652
PyFunceble.storage.PROXY = Box(config["proxy"])
653653

654+
if "special_rules" in config and config["special_rules"]:
655+
PyFunceble.storage.SPECIAL_RULES = config["special_rules"]
656+
654657
# Early load user agents to allow usage of defined user agents.
655658
UserAgentDataset().get_latest()
656659

@@ -676,6 +679,7 @@ def destroy(self, keep_custom: bool = False) -> "ConfigLoader":
676679
PyFunceble.storage.PLATFORM = Box({})
677680
PyFunceble.storage.LINKS = Box({})
678681
PyFunceble.storage.PROXY = Box({})
682+
PyFunceble.storage.SPECIAL_RULES = Box({})
679683
except (AttributeError, TypeError): # pragma: no cover ## Safety.
680684
pass
681685

0 commit comments

Comments
 (0)