feat: test webfinger response for compulsory parameters and the endpoint for CORS

This commit is contained in:
Aravinth Manivannan 2023-09-06 19:42:37 +05:30
commit ea0fa9246c
Signed by: realaravinth
GPG Key ID: F8F50389936984FF
3 changed files with 346 additions and 0 deletions

3
.env_sample Normal file
View File

@ -0,0 +1,3 @@
export FTEST_AUTH="foobar"
export FTEST_TARGET_HOST="http://localhost:3000"
export FTEST_USER="john@example.org"

162
.gitignore vendored Normal file
View File

@ -0,0 +1,162 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.env
webfinger.log

181
run.py Executable file
View File

@ -0,0 +1,181 @@
#!/bin/env /usr/bin/python
import logging
import os
import requests
from urllib.parse import urlparse, urlunparse
def configure_logger():
logger = logging.getLogger("webfinger")
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler("webfinger.log")
fh.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
fh.setFormatter(formatter)
ch.setFormatter(formatter)
logger.addHandler(fh)
logger.addHandler(ch)
return logger
def get_env(name) -> str:
env = FTEST_AUTH = os.environ.get(name)
logger.info(f"Environment: {name}: {env}")
return env
logger = configure_logger()
FTEST_AUTH = get_env("FTEST_AUTH")
TARGET_HOST = get_env("FTEST_TARGET_HOST")
TEST_USER = get_env("FTEST_USER") # actor ex: john@example.org
TEST_HOST = urlparse(TARGET_HOST).netloc
def query_webfinger():
parsed_target_host = urlparse(TARGET_HOST)
webfinger = urlunparse(
(
parsed_target_host.scheme,
parsed_target_host.netloc,
"/.well-known/webfinger",
"",
f"resource=acct:{TEST_USER}",
"",
)
)
logger.info(f"Query WebFinger: {webfinger}")
res = requests.get(webfinger, headers={"Origin": "http://example.org"})
logger.debug(
f"WebFinger response:\n\nSTATUS: {res.status_code}\n\nHEADERS:\n {res.headers}\n\nRESPONSE PAYLOAD:\n{res.json()}"
)
assert res.status_code == 200
logger.info("[SUCCESS] WebFinger query response is HTTP 200")
return res
def test_main_params(resp):
assert "subject" in resp, "Parameter 'subject' is not present in WebFinger response"
assert "links" in resp, "Parameter 'links' is not present in WebFinger response"
logger.info(
"[SUCCESS] WebFinger response has 'subject', 'aliases' and 'links' parameters'"
)
def test_links(resp):
self_link = None
profile_link = None
for link in resp["links"]:
if link["rel"] == "self":
self_link = link
elif link["rel"] == "http://webfinger.net/rel/profile-page":
profile_link = link
logger.debug(
"'rel==http://webfinger.net/rel/profile-page' is present in 'links' WebFinger response parameter"
)
assert (
self_link is not None
), "'rel==self' is not present in 'links' WebFinger response parameter"
assert (
self_link["rel"] == "self"
), f'[rel==self] expected rel:self, got rel: {self_link["rel"]}'
assert (
self_link["type"] == f"application/activity+json"
), f"[rel==self] expected application/activity+json; got {self_link['type']}"
assert "href" in self_link, "[rel==self] href not present in link item"
logger.info("[SUCESS] rel==self passed schema validation")
if profile_link:
assert (
profile_link["rel"] == "http://webfinger.net/rel/profile-page"
), f"expected http://webfinger.net/rel/profile-page got {profile_link['rel']}"
assert (
profile_link["type"] == "text/html"
), f"expected text/html got {profile_link['type']}"
assert (
"href" in profile_link
), "[rel==profile link] href not present in link item"
logger.info("[SUCESS] rel==profile-page passed schema validation")
logger.info("[SUCESS] 'links' object passed validation")
def test_subject(resp):
subject = f"acct:{TEST_USER}"
assert (
resp["subject"] == subject
), f"Subject parameter doesn't match. Expected {subject} got {resp['subject']}"
def test_access_control_allow_origin(resp):
# request.headers is case insensitive
assert resp.headers[
"access-control-allow-origin"
] == "*", "Access-Control-Allow-Origin header should be '*' to allow any domain to access the resource with CORS. Please see https://www.rfc-editor.org/rfc/rfc7033.html#section-5"
logger.info("[SUCESS] WebFinger endpoint is configured correctly for CORS")
if __name__ == "__main__":
max_score = 5
score = 0
resp = query_webfinger()
json = resp.json()
score += 1
success = []
failures = { }
try:
test_main_params(json)
score += 1
success.append("test_main_params")
except Exception as e:
logger.error(e)
failures["test_main_params"] = e
try:
test_links(json)
score += 1
success.append("test_links")
except Exception as e:
logger.error(e)
failures["test_links"] = e
try:
test_subject(json)
score += 1
success.append("test_subject")
except Exception as e:
logger.error(e)
failures["test_subject"] = e
try:
test_access_control_allow_origin(resp)
score += 1
success.append("test_access_control_allow_origin")
except Exception as e:
logger.error(e)
failures["test_access_control_allow_origin"] = e
print("\n\n===============")
if score == max_score:
print("All tests passed")
elif score > 0:
print(f"Partial success. {score} out of {max_score} tests passed")
print("Summary:\n")
if success:
print(f"Successful tests:\n")
for s in success:
print(f"[OK] {s}")
if failures:
print(f"\n\nFailed tests:\n")
for _, (test, error) in enumerate(failures.items()):
print(f"[FAIL] {test} failed with error:\n{error}\n-----\n")