active-boxes 0.0.1.dev2__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.
@@ -0,0 +1,66 @@
1
+ import ipaddress
2
+ import logging
3
+ import socket
4
+ from typing import Dict
5
+ from urllib.parse import urlparse
6
+
7
+ from .errors import Error
8
+ from .errors import ServerError
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ _CACHE: Dict[str, bool] = {}
14
+
15
+
16
+ class InvalidURLError(ServerError):
17
+ pass
18
+
19
+
20
+ class URLLookupFailedError(Error):
21
+ pass
22
+
23
+
24
+ def is_url_valid(url: str, debug: bool = False) -> bool:
25
+ parsed = urlparse(url)
26
+ if parsed.scheme not in ["http", "https"]:
27
+ return False
28
+
29
+ # XXX in debug mode, we want to allow requests to localhost to test the federation with local instances
30
+ if debug: # pragma: no cover
31
+ return True
32
+
33
+ if parsed.hostname in ["localhost"]:
34
+ return False
35
+
36
+ if _CACHE.get(parsed.hostname, False):
37
+ return True
38
+
39
+ try:
40
+ ip_address = ipaddress.ip_address(parsed.hostname)
41
+ except ValueError:
42
+ try:
43
+ ip_address = socket.getaddrinfo(parsed.hostname, parsed.port or 80)[0][4][0]
44
+ logger.debug(f"dns lookup: {parsed.hostname} -> {ip_address}")
45
+ except socket.gaierror:
46
+ logger.exception(f"failed to lookup url {url}")
47
+ _CACHE[parsed.hostname] = False
48
+ raise URLLookupFailedError(f"failed to lookup url {url}")
49
+
50
+ logger.debug(f"{ip_address}")
51
+
52
+ if ipaddress.ip_address(ip_address).is_private:
53
+ logger.info(f"rejecting private URL {url}")
54
+ _CACHE[parsed.hostname] = False
55
+ return False
56
+
57
+ _CACHE[parsed.hostname] = True
58
+ return True
59
+
60
+
61
+ def check_url(url: str, debug: bool = False) -> None:
62
+ logger.debug(f"check_url {url} debug={debug}")
63
+ if not is_url_valid(url, debug=debug):
64
+ raise InvalidURLError(f'"{url}" is invalid')
65
+
66
+ return None
@@ -0,0 +1,92 @@
1
+ import json
2
+ import logging
3
+ from typing import Any
4
+ from typing import Dict
5
+ from typing import Optional
6
+ from urllib.parse import urlparse
7
+
8
+ import requests
9
+
10
+ from .activitypub import get_backend
11
+ from .urlutils import check_url
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def webfinger(
17
+ resource: str, debug: bool = False
18
+ ) -> Optional[Dict[str, Any]]: # noqa: C901
19
+ """Mastodon-like WebFinger resolution to retrieve the activity stream Actor URL.
20
+ """
21
+ logger.info(f"performing webfinger resolution for {resource}")
22
+ protos = ["https", "http"]
23
+ if resource.startswith("http://"):
24
+ protos.reverse()
25
+ host = urlparse(resource).netloc
26
+ elif resource.startswith("https://"):
27
+ host = urlparse(resource).netloc
28
+ else:
29
+ if resource.startswith("acct:"):
30
+ resource = resource[5:]
31
+ if resource.startswith("@"):
32
+ resource = resource[1:]
33
+ _, host = resource.split("@", 1)
34
+ resource = "acct:" + resource
35
+
36
+ # Security check on the url (like not calling localhost)
37
+ check_url(f"https://{host}", debug=debug)
38
+ is_404 = False
39
+
40
+ for i, proto in enumerate(protos):
41
+ try:
42
+ url = f"{proto}://{host}/.well-known/webfinger"
43
+ # FIXME(tsileo): BACKEND.fetch_json so we can set a UserAgent
44
+ resp = get_backend().fetch_json(url, params={"resource": resource})
45
+ break
46
+ except requests.ConnectionError:
47
+ logger.exception("req failed")
48
+ # If we tried https first and the domain is "http only"
49
+ if i == 0:
50
+ continue
51
+ break
52
+ except requests.HTTPError as http_error:
53
+ logger.exception("HTTP error")
54
+ if http_error.response.status_code in [403, 404]:
55
+ is_404 = True
56
+ continue
57
+ raise
58
+ if is_404:
59
+ return None
60
+ resp.raise_for_status()
61
+ try:
62
+ return resp.json()
63
+ except json.JSONDecodeError:
64
+ return None
65
+
66
+
67
+ def get_remote_follow_template(resource: str, debug: bool = False) -> Optional[str]:
68
+ data = webfinger(resource, debug=debug)
69
+ if data is None:
70
+ return None
71
+ for link in data["links"]:
72
+ if link.get("rel") == "http://ostatus.org/schema/1.0/subscribe":
73
+ return link.get("template")
74
+ return None
75
+
76
+
77
+ def get_actor_url(resource: str, debug: bool = False) -> Optional[str]:
78
+ """Mastodon-like WebFinger resolution to retrieve the activity stream Actor URL.
79
+
80
+ Returns:
81
+ the Actor URL or None if the resolution failed.
82
+ """
83
+ data = webfinger(resource, debug=debug)
84
+ if data is None:
85
+ return None
86
+ for link in data["links"]:
87
+ if (
88
+ link.get("rel") == "self"
89
+ and link.get("type") == "application/activity+json"
90
+ ):
91
+ return link.get("href")
92
+ return None
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018, Thomas Sileo
4
+ Copyright (c) 2025, Chaiwat Suttipongsakul
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR PERFORMANCE OF THE
22
+ SOFTWARE.
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.3
2
+ Name: active-boxes
3
+ Version: 0.0.1.dev2
4
+ Summary: Tiny ActivityPub framework written in Python, both database and server agnostic.
5
+ License: MIT
6
+ Author: Chaiwat Suttipongsakul
7
+ Author-email: cwt@bashell.com
8
+ Requires-Python: >=3.6.0
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.6
13
+ Classifier: Programming Language :: Python :: 3.7
14
+ Classifier: Programming Language :: Python :: Implementation :: CPython
15
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
16
+ Provides-Extra: dev
17
+ Requires-Dist: black ; extra == "dev"
18
+ Requires-Dist: codecov ; extra == "dev"
19
+ Requires-Dist: flake8 ; extra == "dev"
20
+ Requires-Dist: httpretty ; extra == "dev"
21
+ Requires-Dist: mypy ; extra == "dev"
22
+ Requires-Dist: pytest ; extra == "dev"
23
+ Requires-Dist: pytest-cov ; extra == "dev"
24
+ Project-URL: Homepage, https://github.com/cwt/active-boxes
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Active Boxes (Modernized Little Boxes)
28
+
29
+ This project is a fork of [Little Boxes](https://github.com/tsileo/little-boxes) that is currently being modernized and relicensed from ISC to MIT.
30
+
31
+ ⚠️ **Work in Progress** ⚠️
32
+
33
+ This project is in the process of being modernized and updated to current Python packaging standards.
34
+ The original README can be found in [ORIGINAL-README.md](ORIGINAL-README.md).
35
+
36
+ ## Modernization Progress
37
+
38
+ - [x] Migrated from `setup.py` to `pyproject.toml`
39
+ - [x] Moved development dependencies to `pyproject.toml`
40
+ - [x] Switched to Poetry for dependency management and building
41
+ - [ ] More modernization steps to come...
42
+
43
+ ## Original Project
44
+
45
+ For information about the original project, please refer to [ORIGINAL-README.md](ORIGINAL-README.md).
@@ -0,0 +1,16 @@
1
+ active_boxes/__init__.py,sha256=ZCY4whXMFRux4hRNuo-krzKv7SgXFUGxBtDx_40EO50,291
2
+ active_boxes/__version__.py,sha256=k-pUXV5otZaKDeBgIdBQmw_3zGHYt0YIJTUVOqYO-8I,300
3
+ active_boxes/activitypub.py,sha256=J-VMqPKG_eOPeXd_P8zFiIX2kygPgG5ZAz9AEfy5T_o,30668
4
+ active_boxes/backend.py,sha256=2SkfectK_TNDPXFdVqkjoQvOXVOblih3nYncV_CXATI,4005
5
+ active_boxes/collection.py,sha256=OoQkjlYVFGhq3K1hB3uzh4V1p5P3xgtNuXw0ME-5e_A,2371
6
+ active_boxes/content_helper.py,sha256=cEtcWpCSzhmGVpEQQCo9Vv0_NzI8ocR1AyOlm20qrak,2210
7
+ active_boxes/errors.py,sha256=FwQHtYRbAXvKhR5PNmzi93Gk0OPf69WkopkHq9OUW9k,2504
8
+ active_boxes/httpsig.py,sha256=5weDW2RHUY8wZEIsujQjGq5GIrgH5cWdPrFPwlfjFlw,4395
9
+ active_boxes/key.py,sha256=2ALhblEd0uTzfyNa2cidyTwNJo2QEk-E2spdgHhknoE,2024
10
+ active_boxes/linked_data_sig.py,sha256=0vMtqdmFwqtORo0fNhRRnl3qOO5zzq9jKAfnXAzH3Lc,2418
11
+ active_boxes/urlutils.py,sha256=das0iGifazXxXJYrZe26Eig_UrIp1HmB9XyF-l4G7nY,1717
12
+ active_boxes/webfinger.py,sha256=xyKSMCnisq2sscj2_ol7sRrEK1OMUFG6-fubNqKAHr0,2852
13
+ active_boxes-0.0.1.dev2.dist-info/LICENSE,sha256=8z10AN_4FDMHZgAlfpD955ATwtbuDpnuX2MSMtZS-98,1109
14
+ active_boxes-0.0.1.dev2.dist-info/METADATA,sha256=Qzf_nlAbumZp46l1zwE5yU7pgRJmQ-07lk7bFH8QSC4,1805
15
+ active_boxes-0.0.1.dev2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
16
+ active_boxes-0.0.1.dev2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any