diff --git a/relay/application.py b/relay/application.py index 5bdc04f..22115f6 100644 --- a/relay/application.py +++ b/relay/application.py @@ -12,7 +12,7 @@ from aiohttp.web import HTTPException, StaticResource from aiohttp_swagger import setup_swagger from aputils.signer import Signer from base64 import b64encode -from blib import HttpError +from blib import File, HttpError, port_check from bsql import Database from collections.abc import Awaitable, Callable from datetime import datetime, timedelta @@ -27,7 +27,7 @@ from .config import Config from .database import Connection, get_database from .database.schema import Instance from .http_client import HttpClient -from .misc import JSON_PATHS, TOKEN_PATHS, Message, Response, check_open_port, get_resource +from .misc import JSON_PATHS, TOKEN_PATHS, Message, Response from .template import Template from .views import VIEWS from .views.api import handle_api_path @@ -90,7 +90,7 @@ class Application(web.Application): setup_swagger( self, ui_version = 3, - swagger_from_file = get_resource('data/swagger.yaml') + swagger_from_file = File.from_resource('relay', 'data/swagger.yaml') ) @@ -154,10 +154,12 @@ class Application(web.Application): def register_static_routes(self) -> None: if self['dev']: - static = StaticResource('/static', get_resource('frontend/static')) + static = StaticResource('/static', File.from_resource('relay', 'frontend/static')) else: - static = CachedStaticResource('/static', get_resource('frontend/static')) + static = CachedStaticResource( + '/static', Path(File.from_resource('relay', 'frontend/static')) + ) self.router.register_resource(static) @@ -170,7 +172,7 @@ class Application(web.Application): host = self.config.listen port = self.config.port - if not check_open_port(host, port): + if port_check(port, '127.0.0.1' if host == '0.0.0.0' else host): logging.error(f'A server is already running on {host}:{port}') return diff --git a/relay/cache.py b/relay/cache.py index 3960c36..4a6b7dd 100644 --- a/relay/cache.py +++ b/relay/cache.py @@ -4,7 +4,7 @@ import json import os from abc import ABC, abstractmethod -from blib import Date +from blib import Date, convert_to_boolean from bsql import Database, Row from collections.abc import Callable, Iterator from dataclasses import asdict, dataclass @@ -13,7 +13,7 @@ from redis import Redis from typing import TYPE_CHECKING, Any, TypedDict from .database import Connection, get_database -from .misc import Message, boolean +from .misc import Message if TYPE_CHECKING: from .application import Application @@ -26,7 +26,7 @@ BACKENDS: dict[str, type[Cache]] = {} CONVERTERS: dict[str, tuple[SerializerCallback, DeserializerCallback]] = { 'str': (str, str), 'int': (str, int), - 'bool': (str, boolean), + 'bool': (str, convert_to_boolean), 'json': (json.dumps, json.loads), 'message': (lambda x: x.to_json(), Message.parse) } diff --git a/relay/compat.py b/relay/compat.py index 54b6573..1f81296 100644 --- a/relay/compat.py +++ b/relay/compat.py @@ -2,13 +2,12 @@ import json import os import yaml +from blib import convert_to_boolean from functools import cached_property from pathlib import Path from typing import Any from urllib.parse import urlparse -from .misc import boolean - class RelayConfig(dict[str, Any]): def __init__(self, path: str): @@ -31,7 +30,7 @@ class RelayConfig(dict[str, Any]): elif key == 'whitelist_enabled': if not isinstance(value, bool): - value = boolean(value) + value = convert_to_boolean(value) super().__setitem__(key, value) diff --git a/relay/database/__init__.py b/relay/database/__init__.py index 03198ab..db5120b 100644 --- a/relay/database/__init__.py +++ b/relay/database/__init__.py @@ -1,6 +1,6 @@ import sqlite3 -from blib import Date +from blib import Date, File from bsql import Database from .config import THEMES, ConfigData @@ -9,7 +9,6 @@ from .schema import TABLES, VERSIONS, migrate_0 from .. import logger as logging from ..config import Config -from ..misc import get_resource sqlite3.register_adapter(Date, Date.timestamp) @@ -37,7 +36,7 @@ def get_database(config: Config, migrate: bool = True) -> Database[Connection]: **options ) - db.load_prepared_statements(get_resource('data/statements.sql')) + db.load_prepared_statements(File.from_resource('relay', 'data/statements.sql')) db.connect() if not migrate: diff --git a/relay/database/config.py b/relay/database/config.py index 3f3c7e0..206e757 100644 --- a/relay/database/config.py +++ b/relay/database/config.py @@ -2,13 +2,13 @@ from __future__ import annotations # removing the above line turns annotations into types instead of str objects which messes with # `Field.type` +from blib import convert_to_boolean from bsql import Row from collections.abc import Callable, Sequence from dataclasses import Field, asdict, dataclass, fields from typing import TYPE_CHECKING, Any from .. import logger as logging -from ..misc import boolean if TYPE_CHECKING: from typing import Self @@ -66,7 +66,7 @@ THEMES = { CONFIG_CONVERT: dict[str, tuple[Callable[[Any], str], Callable[[str], Any]]] = { 'str': (str, str), 'int': (str, int), - 'bool': (str, boolean), + 'bool': (str, convert_to_boolean), 'logging.LogLevel': (lambda x: x.name, logging.LogLevel.parse) } diff --git a/relay/database/connection.py b/relay/database/connection.py index 14ff60a..d217c32 100644 --- a/relay/database/connection.py +++ b/relay/database/connection.py @@ -3,7 +3,7 @@ from __future__ import annotations import secrets from argon2 import PasswordHasher -from blib import Date +from blib import Date, convert_to_boolean from bsql import Connection as SqlConnection, Row, Update from collections.abc import Iterator from datetime import datetime, timezone @@ -17,7 +17,7 @@ from .config import ( ) from .. import logger as logging -from ..misc import Message, boolean, get_app +from ..misc import Message, get_app if TYPE_CHECKING: from ..application import Application @@ -111,7 +111,7 @@ class Connection(SqlConnection): self.app['workers'].set_log_level(value) elif key in {'approval-required', 'whitelist-enabled'}: - value = boolean(value) + value = convert_to_boolean(value) elif key == 'theme': if value not in THEMES: diff --git a/relay/misc.py b/relay/misc.py index aa44956..3fa0b7d 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -4,13 +4,10 @@ import aputils import json import os import platform -import socket from aiohttp.web import Response as AiohttpResponse from collections.abc import Sequence from datetime import datetime -from importlib.resources import files as pkgfiles -from pathlib import Path from typing import TYPE_CHECKING, Any, TypedDict, TypeVar from uuid import uuid4 @@ -40,11 +37,6 @@ MIMETYPES = { 'webmanifest': 'application/manifest+json' } -NODEINFO_NS = { - '20': 'http://nodeinfo.diaspora.software/ns/schema/2.0', - '21': 'http://nodeinfo.diaspora.software/ns/schema/2.1' -} - ACTOR_FORMATS = { 'mastodon': 'https://{domain}/actor', 'akkoma': 'https://{domain}/relay', @@ -84,43 +76,6 @@ TOKEN_PATHS: tuple[str, ...] = ( ) -def boolean(value: Any) -> bool: - if isinstance(value, str): - if value.lower() in {'on', 'y', 'yes', 'true', 'enable', 'enabled', '1'}: - return True - - if value.lower() in {'off', 'n', 'no', 'false', 'disable', 'disabled', '0'}: - return False - - raise TypeError(f'Cannot parse string "{value}" as a boolean') - - if isinstance(value, int): - if value == 1: - return True - - if value == 0: - return False - - raise ValueError('Integer value must be 1 or 0') - - if value is None: - return False - - return bool(value) - - -def check_open_port(host: str, port: int) -> bool: - if host == '0.0.0.0': - host = '127.0.0.1' - - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - try: - return s.connect_ex((host, port)) != 0 - - except socket.error: - return False - - def get_app() -> Application: from .application import Application @@ -130,10 +85,6 @@ def get_app() -> Application: return Application.DEFAULT -def get_resource(path: str) -> Path: - return Path(str(pkgfiles('relay'))).joinpath(path) - - class JsonEncoder(json.JSONEncoder): def default(self, o: Any) -> str: if isinstance(o, datetime): @@ -250,18 +201,6 @@ class Response(AiohttpResponse): return cls(**kwargs) - @classmethod - def new_error(cls: type[Self], - status: int, - body: str | bytes | dict[str, Any], - ctype: str = 'text') -> Self: - - if ctype == 'json': - body = {'error': body} - - return cls.new(body=body, status=status, ctype=ctype) - - @classmethod def new_redir(cls: type[Self], path: str, status: int = 307) -> Self: body = f'Redirect to {path}' diff --git a/relay/template.py b/relay/template.py index 3ee2855..153f957 100644 --- a/relay/template.py +++ b/relay/template.py @@ -3,6 +3,7 @@ from __future__ import annotations import textwrap from aiohttp.web import Request +from blib import File from collections.abc import Callable from hamlish_jinja import HamlishExtension from jinja2 import Environment, FileSystemLoader @@ -13,7 +14,6 @@ from markdown import Markdown from typing import TYPE_CHECKING, Any from . import __version__ -from .misc import get_resource if TYPE_CHECKING: from .application import Application @@ -33,7 +33,7 @@ class Template(Environment): MarkdownExtension ], loader = FileSystemLoader([ - get_resource('frontend'), + File.from_resource('relay', 'frontend'), app.config.path.parent.joinpath('template') ]) ) diff --git a/relay/views/activitypub.py b/relay/views/activitypub.py index 4551c88..50b4bb6 100644 --- a/relay/views/activitypub.py +++ b/relay/views/activitypub.py @@ -48,7 +48,7 @@ class ActorView(View): # reject if actor is banned if conn.get_domain_ban(self.actor.domain): logging.verbose('Ignored request from banned actor: %s', self.actor.id) - return Response.new_error(403, 'access denied', 'json') + raise HttpError(403, 'access denied') # reject if activity type isn't 'Follow' and the actor isn't following if self.message.type != 'Follow' and not self.instance: @@ -57,7 +57,7 @@ class ActorView(View): self.actor.id ) - return Response.new_error(401, 'access denied', 'json') + raise HttpError(401, 'access denied') logging.debug('>> payload %s', self.message.to_json(4)) @@ -78,7 +78,7 @@ class ActorView(View): except Exception: traceback.print_exc() - logging.verbose('Failed to parse inbox message') + logging.verbose('Failed to parse message from actor: %s', self.signature.keyid) raise HttpError(400, 'failed to parse message') if message is None: @@ -94,13 +94,14 @@ class ActorView(View): try: self.actor = await self.client.get(self.signature.keyid, True, Message) - except HttpError: + except HttpError as e: # ld signatures aren't handled atm, so just ignore it if self.message.type == 'Delete': logging.verbose('Instance sent a delete which cannot be handled') raise HttpError(202, '') logging.verbose('Failed to fetch actor: %s', self.signature.keyid) + logging.debug('HTTP Status %i: %s', e.status, e.message) raise HttpError(400, 'failed to fetch actor') except Exception: @@ -162,10 +163,10 @@ class WebfingerView(View): subject = request.query['resource'] except KeyError: - return Response.new_error(400, 'missing "resource" query key', 'json') + raise HttpError(400, 'missing "resource" query key') if subject != f'acct:relay@{self.config.domain}': - return Response.new_error(404, 'user not found', 'json') + raise HttpError(404, 'user not found') data = aputils.Webfinger.new( handle = 'relay', diff --git a/relay/views/api.py b/relay/views/api.py index e7cb5fb..b92802c 100644 --- a/relay/views/api.py +++ b/relay/views/api.py @@ -10,7 +10,7 @@ from .base import View, register_route from .. import __version__ from ..database import ConfigData, schema -from ..misc import Message, Response, boolean +from ..misc import Message, Response DEFAULT_REDIRECT: str = 'urn:ietf:wg:oauth:2.0:oob' @@ -97,7 +97,7 @@ class OauthAuthorize(View): with self.database.session(True) as conn: if (app := conn.get_app(data['client_id'], data['client_secret'])) is None: - return Response.new_error(404, 'Could not find app', 'json') + raise HttpError(404, 'Could not find app') if convert_to_boolean(data['response']): if app.token is not None: @@ -393,7 +393,10 @@ class RequestView(View): try: with self.database.session(True) as conn: - instance = conn.put_request_response(data['domain'], boolean(data['accept'])) + instance = conn.put_request_response( + data['domain'], + convert_to_boolean(data['accept']) + ) except KeyError: raise HttpError(404, 'Request not found') from None @@ -402,7 +405,7 @@ class RequestView(View): host = self.config.domain, actor = instance.actor, followid = instance.followid, - accept = boolean(data['accept']) + accept = convert_to_boolean(data['accept']) ) self.app.push_message(instance.inbox, message, instance)