mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
IP address plus cookie-based ban system
This commit is contained in:
parent
ed13e2d03d
commit
afb253f6d0
6 changed files with 113 additions and 17 deletions
|
@ -1,5 +1,5 @@
|
|||
from datetime import date, datetime, timedelta
|
||||
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app
|
||||
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, g
|
||||
from werkzeug.urls import url_parse
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
from flask_babel import _
|
||||
|
@ -7,10 +7,10 @@ from app import db
|
|||
from app.auth import bp
|
||||
from app.auth.forms import LoginForm, RegistrationForm, ResetPasswordRequestForm, ResetPasswordForm
|
||||
from app.auth.util import random_token
|
||||
from app.models import User, utcnow
|
||||
from app.models import User, utcnow, IpBan
|
||||
from app.auth.email import send_password_reset_email, send_welcome_email, send_verification_email
|
||||
from app.activitypub.signature import RsaKeys
|
||||
from app.utils import render_template
|
||||
from app.utils import render_template, ip_address, user_ip_banned, user_cookie_banned
|
||||
|
||||
|
||||
@bp.route('/login', methods=['GET', 'POST'])
|
||||
|
@ -29,9 +29,6 @@ def login():
|
|||
if user.deleted:
|
||||
flash(_('No account exists with that user name.'), 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
if user.banned:
|
||||
flash(_('You have been banned.'), 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
if not user.check_password(form.password.data):
|
||||
if user.password_hash is None:
|
||||
if "@gmail.com" in user.email:
|
||||
|
@ -43,9 +40,24 @@ def login():
|
|||
return redirect(url_for('auth.login'))
|
||||
flash(_('Invalid password'))
|
||||
return redirect(url_for('auth.login'))
|
||||
if user.banned or user_ip_banned() or user_cookie_banned():
|
||||
flash(_('You have been banned.'), 'error')
|
||||
|
||||
response = make_response(redirect(url_for('auth.login')))
|
||||
# Detect if a banned user tried to log in from a new IP address
|
||||
if user.banned and not user_ip_banned():
|
||||
# If so, ban their new IP address as well
|
||||
new_ip_ban = IpBan(ip_address=ip_address(), notes=user.user_name + ' used new IP address')
|
||||
db.session.add(new_ip_ban)
|
||||
db.session.commit()
|
||||
|
||||
# Set a cookie so we have another way to track banned people
|
||||
response.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
|
||||
return response
|
||||
login_user(user, remember=True)
|
||||
current_user.last_seen = utcnow()
|
||||
current_user.verification_token = ''
|
||||
current_user.ip_address = ip_address()
|
||||
db.session.commit()
|
||||
next_page = request.args.get('next')
|
||||
if not next_page or url_parse(next_page).netloc != '':
|
||||
|
@ -76,7 +88,8 @@ def register():
|
|||
verification_token = random_token(16)
|
||||
form.user_name.data = form.user_name.data.strip()
|
||||
user = User(user_name=form.user_name.data, email=form.real_email.data,
|
||||
verification_token=verification_token, instance=1)
|
||||
verification_token=verification_token, instance=1, ipaddress=ip_address(),
|
||||
banned=user_ip_banned() or user_cookie_banned())
|
||||
user.set_password(form.password.data)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
@ -89,13 +102,11 @@ def register():
|
|||
|
||||
flash(_('Great, you are now a registered user!'))
|
||||
|
||||
# set a cookie so the login button is emphasised on the public site, for future visits
|
||||
resp = make_response(redirect(url_for('main.index')))
|
||||
resp.set_cookie('logged_in_before', value='1', expires=datetime.now() + timedelta(weeks=300),
|
||||
domain='.chorebuster.net')
|
||||
if user_ip_banned():
|
||||
resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
|
||||
return resp
|
||||
return render_template('auth/register.html', title=_('Register'),
|
||||
form=form)
|
||||
return render_template('auth/register.html', title=_('Register'), form=form, site=g.site)
|
||||
|
||||
|
||||
@bp.route('/reset_password_request', methods=['GET', 'POST'])
|
||||
|
|
|
@ -260,6 +260,7 @@ class User(UserMixin, db.Model):
|
|||
bot = db.Column(db.Boolean, default=False)
|
||||
ignore_bots = db.Column(db.Boolean, default=False)
|
||||
unread_notifications = db.Column(db.Integer, default=0)
|
||||
ip_address = db.Column(db.String(50))
|
||||
instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), index=True)
|
||||
|
||||
avatar = db.relationship('File', lazy='joined', foreign_keys=[avatar_id], single_parent=True, cascade="all, delete-orphan")
|
||||
|
@ -870,6 +871,13 @@ class Report(db.Model):
|
|||
updated = db.Column(db.DateTime, default=utcnow)
|
||||
|
||||
|
||||
class IpBan(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
ip_address = db.Column(db.String(50), index=True)
|
||||
notes = db.Column(db.String(150))
|
||||
created_at = db.Column(db.DateTime, default=utcnow)
|
||||
|
||||
|
||||
class Site(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(256))
|
||||
|
|
|
@ -18,7 +18,7 @@ from app.models import Post, PostReply, \
|
|||
from app.post import bp
|
||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, ap_datetime, return_304, \
|
||||
request_etag_matches
|
||||
request_etag_matches, ip_address
|
||||
|
||||
|
||||
def show_post(post_id: int):
|
||||
|
@ -224,6 +224,7 @@ def post_vote(post_id: int, vote_direction):
|
|||
flash('Failed to send vote', 'warning')
|
||||
|
||||
current_user.last_seen = utcnow()
|
||||
current_user.ip_address = ip_address()
|
||||
db.session.commit()
|
||||
current_user.recalculate_attitude()
|
||||
db.session.commit()
|
||||
|
@ -296,6 +297,7 @@ def comment_vote(comment_id, vote_direction):
|
|||
flash('Failed to send vote', 'error')
|
||||
|
||||
current_user.last_seen = utcnow()
|
||||
current_user.ip_address = ip_address()
|
||||
db.session.commit()
|
||||
current_user.recalculate_attitude()
|
||||
db.session.commit()
|
||||
|
@ -333,6 +335,7 @@ def add_reply(post_id: int, comment_id: int):
|
|||
form = NewReplyForm()
|
||||
if form.validate_on_submit():
|
||||
current_user.last_seen = utcnow()
|
||||
current_user.ip_address = ip_address()
|
||||
reply = PostReply(user_id=current_user.id, post_id=post.id, parent_id=in_reply_to.id, depth=in_reply_to.depth + 1,
|
||||
community_id=post.community.id, body=form.body.data,
|
||||
body_html=markdown_to_html(form.body.data), body_html_safe=True,
|
||||
|
|
|
@ -7,8 +7,12 @@
|
|||
<div class="col col-login mx-auto">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body p-6" id="registration_form">
|
||||
<div class="card-title text-center">{{ _('Create new account') }}</div>
|
||||
{{ render_form(form) }}
|
||||
{% if site.registration_mode != 'Closed' %}
|
||||
<div class="card-title text-center">{{ _('Create new account') }}</div>
|
||||
{{ render_form(form) }}
|
||||
{% else %}
|
||||
{{ _('Registration is closed. Only admins can create accounts.') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
28
app/utils.py
28
app/utils.py
|
@ -21,7 +21,7 @@ from wtforms.fields import SelectField, SelectMultipleField
|
|||
from wtforms.widgets import Select, html_params, ListWidget, CheckboxInput
|
||||
from app import db, cache
|
||||
|
||||
from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog
|
||||
from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan
|
||||
|
||||
|
||||
# Flask's render_template function, with support for themes added
|
||||
|
@ -344,4 +344,28 @@ def ap_datetime(date_time: datetime) -> str:
|
|||
|
||||
class MultiCheckboxField(SelectMultipleField):
|
||||
widget = ListWidget(prefix_label=False)
|
||||
option_widget = CheckboxInput()
|
||||
option_widget = CheckboxInput()
|
||||
|
||||
|
||||
def ip_address() -> str:
|
||||
ip = request.headers.get('X-Forwarded-For') or request.remote_addr
|
||||
if ',' in ip: # Remove all but first ip addresses
|
||||
ip = ip[:ip.index(',')].strip()
|
||||
return ip
|
||||
|
||||
|
||||
def user_ip_banned() -> bool:
|
||||
current_ip_address = ip_address()
|
||||
if current_ip_address:
|
||||
return current_ip_address in banned_ip_addresses()
|
||||
|
||||
|
||||
def user_cookie_banned() -> bool:
|
||||
cookie = request.cookies.get('sesion', None)
|
||||
return cookie is not None
|
||||
|
||||
|
||||
@cache.cached(timeout=300)
|
||||
def banned_ip_addresses() -> List[str]:
|
||||
ips = IpBan.query.all()
|
||||
return [ip.ip_address for ip in ips]
|
||||
|
|
46
migrations/versions/3f17b9ab55e4_ip_ban.py
Normal file
46
migrations/versions/3f17b9ab55e4_ip_ban.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
"""ip ban
|
||||
|
||||
Revision ID: 3f17b9ab55e4
|
||||
Revises: b58c4301d1ad
|
||||
Create Date: 2023-12-30 17:23:32.363913
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3f17b9ab55e4'
|
||||
down_revision = 'b58c4301d1ad'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('ip_ban',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('ip_address', sa.String(length=50), nullable=True),
|
||||
sa.Column('notes', sa.String(length=150), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('ip_ban', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_ip_ban_ip_address'), ['ip_address'], unique=False)
|
||||
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('ip_address', sa.String(length=50), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.drop_column('ip_address')
|
||||
|
||||
with op.batch_alter_table('ip_ban', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_ip_ban_ip_address'))
|
||||
|
||||
op.drop_table('ip_ban')
|
||||
# ### end Alembic commands ###
|
Loading…
Reference in a new issue