diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc31532c..42742132 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ When it matures enough, PyFedi will aim to work in a way consistent with the [Co Please discuss your ideas in an issue at https://codeberg.org/rimu/pyfedi/issues before starting any large pieces of work to ensure alignment with the roadmap, architecture and processes. -The general style and philosphy behind the way things have been constructed is well described by +The general style and philosophy behind the way things have been constructed is well described by [The Grug Brained Developer](https://grugbrain.dev/). If that page resonates with you then you'll probably enjoy your time here! The codebase needs to be simple enough that new developers of all skill levels can easily understand what's going on and onboard quickly without a lot of upfront diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 76af6e8a..2a855e7b 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -5,7 +5,7 @@ from typing import Union, Tuple from flask import current_app from sqlalchemy import text from app import db, cache -from app.models import User, Post, Community, BannedInstances, File, PostReply +from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances import time import base64 import requests @@ -14,7 +14,7 @@ from cryptography.hazmat.primitives.asymmetric import padding from app.constants import * from urllib.parse import urlparse -from app.utils import get_request, allowlist_html, html_to_markdown +from app.utils import get_request, allowlist_html, html_to_markdown, get_setting def public_key(): @@ -184,6 +184,15 @@ def instance_blocked(host: str) -> bool: return instance is not None +@cache.memoize(150) +def instance_allowed(host: str) -> bool: + host = host.lower() + if 'https://' in host or 'http://' in host: + host = urlparse(host).hostname + instance = AllowedInstances.query.filter_by(domain=host.strip()).first() + return instance is not None + + def find_actor_or_create(actor: str) -> Union[User, Community, None]: user = None # actor parameter must be formatted as https://server/u/actor or https://server/c/actor @@ -197,8 +206,12 @@ def find_actor_or_create(actor: str) -> Union[User, Community, None]: return None elif actor.startswith('https://'): server, address = extract_domain_and_actor(actor) - if instance_blocked(server): - return None + if get_setting('use_allowlist', False): + if not instance_allowed(server): + return None + else: + if instance_blocked(server): + return None user = User.query.filter_by( ap_profile_id=actor).first() # finds users formatted like https://kbin.social/u/tables if user.banned: diff --git a/app/models.py b/app/models.py index a04401ff..d336ea22 100644 --- a/app/models.py +++ b/app/models.py @@ -451,6 +451,12 @@ class BannedInstances(db.Model): created_at = db.Column(db.DateTime, default=datetime.utcnow) +class AllowedInstances(db.Model): + id = db.Column(db.Integer, primary_key=True) + domain = db.Column(db.String(256), index=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + class Instance(db.Model): id = db.Column(db.Integer, primary_key=True) domain = db.Column(db.String(256), index=True) diff --git a/env.sample b/env.sample new file mode 100644 index 00000000..86606b96 --- /dev/null +++ b/env.sample @@ -0,0 +1,10 @@ +SECRET_KEY= +SERVER_NAME='127.0.0.1:5000' +DATABASE_URL=postgresql+psycopg2://pyfedi:pyfedi@127.0.0.1/pyfedi +MAIL_SERVER='' +RECAPTCHA3_PUBLIC_KEY='' +RECAPTCHA3_PRIVATE_KEY='' +MODE='development' +FULL_AP_CONTEXT=True +CACHE_TYPE='FileSystemCache' +CACHE_DIR='/dev/shm/pyfedi' diff --git a/migrations/versions/6a9bec0c492e_allowed_instances.py b/migrations/versions/6a9bec0c492e_allowed_instances.py new file mode 100644 index 00000000..2b0846c3 --- /dev/null +++ b/migrations/versions/6a9bec0c492e_allowed_instances.py @@ -0,0 +1,39 @@ +"""allowed instances + +Revision ID: 6a9bec0c492e +Revises: c88bbba381b5 +Create Date: 2023-11-03 20:23:43.536572 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6a9bec0c492e' +down_revision = 'c88bbba381b5' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('allowed_instances', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('domain', sa.String(length=256), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('allowed_instances', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_allowed_instances_domain'), ['domain'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('allowed_instances', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_allowed_instances_domain')) + + op.drop_table('allowed_instances') + # ### end Alembic commands ###