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.
- active_boxes/__init__.py +12 -0
- active_boxes/__version__.py +11 -0
- active_boxes/activitypub.py +984 -0
- active_boxes/backend.py +128 -0
- active_boxes/collection.py +70 -0
- active_boxes/content_helper.py +69 -0
- active_boxes/errors.py +92 -0
- active_boxes/httpsig.py +145 -0
- active_boxes/key.py +63 -0
- active_boxes/linked_data_sig.py +83 -0
- active_boxes/urlutils.py +66 -0
- active_boxes/webfinger.py +92 -0
- active_boxes-0.0.1.dev2.dist-info/LICENSE +22 -0
- active_boxes-0.0.1.dev2.dist-info/METADATA +45 -0
- active_boxes-0.0.1.dev2.dist-info/RECORD +16 -0
- active_boxes-0.0.1.dev2.dist-info/WHEEL +4 -0
active_boxes/urlutils.py
ADDED
|
@@ -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,,
|