Implement range bans (#478)

* Implement range bans

People connecting from banned IP ranges are unable to upload
torrents anonymously, and need to manually have their accounts
activated.

This adds a new table "rangebans", and a command line utility,
"rangeban.py", which can be used to add, list and remove rangebans
from the command line.

As an example:

./rangeban.py ban 192.168.0.0/24

This would rangeban anything in this /24.

The temporary_tor column allows automated scripts to clean out and
re-add ever-changing sets of ranges to be banned without affecting
the other ranges.

This has only been tested for IPv4.

* Revise Rangebans

Add an id column, and change "temporary_tor" to "temp". Also
index masked_cidr and mask.

* rangebans: fix enabled and the binary op

kill me

* Add enabling/disabling bans to rangeban.py

* rangebans: fail earlier on garbage arguments

* rangebans: fix linter errors

* rangeban.py: don't shadow builtin keyword 'id'

* rangebans: change temporary ban logic, column

The 'temp' column is now a nullable time column. If the field is
null, the ban is understood to be permanent. If there is a time
in there, it's understood to be the creation time of the ban.

This allows scripts to e.g. delete all temporary bans older than
a certain amount of time.

Also, rename the '_cidr_string' column to 'cidr_string', because
reasons.

* rangeban.py: use ip_address to parse CIDR subnet

* rangebans: fixes to the mask calculation and query

Both were not bugs per-se, but just technically not needed/correct.

* De-meme apparently
This commit is contained in:
Nicolas F
2018-06-30 05:15:04 +02:00
committed by Arylide
parent f04e0fd2ae
commit a38e5d5b53
7 changed files with 237 additions and 13 deletions

View File

@@ -158,6 +158,12 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False):
upload_form.ratelimit.errors = ["You've gone over the upload ratelimit."]
raise TorrentExtraValidationException()
if not uploading_user:
if models.RangeBan.is_rangebanned(ip_address(flask.request.remote_addr).packed):
upload_form.rangebanned.errors = ["Your IP is banned from "
"uploading anonymously."]
raise TorrentExtraValidationException()
# Delete existing torrent which is marked as deleted
if torrent_data.db_id is not None:
old_torrent = models.Torrent.by_id(torrent_data.db_id)

View File

@@ -349,6 +349,7 @@ class UploadForm(FlaskForm):
])
ratelimit = HiddenField()
rangebanned = HiddenField()
def validate_torrent_file(form, field):
# Decode and ensure data is bencoded data

View File

@@ -775,6 +775,44 @@ class TrackerApiBase(DeclarativeHelperBase):
self.method = method
class RangeBan(db.Model):
__tablename__ = 'rangebans'
id = db.Column(db.Integer, primary_key=True)
_cidr_string = db.Column('cidr_string', db.String(length=18), nullable=False)
masked_cidr = db.Column(db.BigInteger, nullable=False,
index=True)
mask = db.Column(db.BigInteger, nullable=False, index=True)
enabled = db.Column(db.Boolean, nullable=False, default=True)
# If this rangeban may be automatically cleared once it becomes
# out of date, set this column to the creation time of the ban.
# None (or NULL in the db) is understood as the ban being permanent.
temp = db.Column(db.DateTime(timezone=False), nullable=True, default=None)
@property
def cidr_string(self):
return self._cidr_string
@cidr_string.setter
def cidr_string(self, s):
subnet, masked_bits = s.split('/')
subnet_b = ip_address(subnet).packed
self.mask = (1 << 32) - (1 << (32 - int(masked_bits)))
self.masked_cidr = int.from_bytes(subnet_b, 'big') & self.mask
self._cidr_string = s
@classmethod
def is_rangebanned(cls, ip):
if len(ip) > 4:
raise NotImplementedError("IPv6 is unsupported.")
elif len(ip) < 4:
raise ValueError("Not an IP address.")
ip_int = int.from_bytes(ip, 'big')
q = cls.query.filter(cls.mask.op('&')(ip_int) == cls.masked_cidr,
cls.enabled)
return q.count() > 0
# Actually declare our site-specific classes
# Torrent

View File

@@ -37,6 +37,18 @@
</div>
{% endif %}
{% if upload_form.rangebanned.errors %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger" role="alert">
{% for error in upload_form.rangebanned.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-12">
{{ render_upload(upload_form.torrent_file, accept=".torrent") }}

View File

@@ -89,19 +89,27 @@ def register():
user.last_login_ip = ip_address(flask.request.remote_addr).packed
db.session.add(user)
db.session.commit()
if app.config['USE_EMAIL_VERIFICATION']: # force verification, enable email
send_verification_email(user)
return flask.render_template('waiting.html')
else: # disable verification, set user as active and auto log in
user.status = models.UserStatusType.ACTIVE
db.session.add(user)
db.session.commit()
flask.g.user = user
flask.session['user_id'] = user.id
flask.session.permanent = True
flask.session.modified = True
return flask.redirect(redirect_url())
if models.RangeBan.is_rangebanned(user.last_login_ip):
flask.flash(flask.Markup('Your IP is blocked from creating new accounts. '
'Please <a href="{}">ask a moderator</a> to manually '
'activate your account <a href="{}">\'{}\'</a>.'
.format(flask.url_for('site.help') + '#irchelp',
flask.url_for('users.view_user',
user_name=user.username),
user.username)), 'warning')
else:
if app.config['USE_EMAIL_VERIFICATION']: # force verification, enable email
send_verification_email(user)
return flask.render_template('waiting.html')
else: # disable verification, set user as active and auto log in
user.status = models.UserStatusType.ACTIVE
db.session.add(user)
db.session.commit()
flask.g.user = user
flask.session['user_id'] = user.id
flask.session.permanent = True
flask.session.modified = True
return flask.redirect(redirect_url())
return flask.render_template('register.html', form=form)