mirror of
https://gitlab.com/SIGBUS/nyaa.git
synced 2025-01-26 07:05:13 +00:00
[Config change] Upload ratelimit for non-trusted uploaders (#384)
* Implement upload ratelimit for non-trusted uploaders Users may upload X torrents in Y minutes after which they will have to wait Z minutes between uploads. * Show torrent period count when ratelimited * Only ratelimit new accounts
This commit is contained in:
parent
37546354a7
commit
de1fd2f1bc
|
@ -90,6 +90,15 @@ TRACKER_API_AUTH = 'topsecret'
|
||||||
## Account ##
|
## Account ##
|
||||||
#############
|
#############
|
||||||
|
|
||||||
|
# Limit torrent upload rate
|
||||||
|
RATELIMIT_UPLOADS = True
|
||||||
|
RATELIMIT_ACCOUNT_AGE = 7 * 24 * 3600
|
||||||
|
# After uploading MAX_UPLOAD_BURST torrents within UPLOAD_BURST_DURATION,
|
||||||
|
# the following uploads must be at least UPLOAD_TIMEOUT seconds after the previous upload.
|
||||||
|
MAX_UPLOAD_BURST = 5
|
||||||
|
UPLOAD_BURST_DURATION = 45 * 60
|
||||||
|
UPLOAD_TIMEOUT = 15 * 60
|
||||||
|
|
||||||
# Torrents uploaded without an account must be at least this big in total (bytes)
|
# Torrents uploaded without an account must be at least this big in total (bytes)
|
||||||
# Set to 0 to disable
|
# Set to 0 to disable
|
||||||
MINIMUM_ANONYMOUS_TORRENT_SIZE = 1 * 1024 * 1024
|
MINIMUM_ANONYMOUS_TORRENT_SIZE = 1 * 1024 * 1024
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
@ -7,6 +8,7 @@ from urllib.request import urlopen
|
||||||
import flask
|
import flask
|
||||||
from werkzeug import secure_filename
|
from werkzeug import secure_filename
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
from orderedset import OrderedSet
|
from orderedset import OrderedSet
|
||||||
|
|
||||||
from nyaa import models, utils
|
from nyaa import models, utils
|
||||||
|
@ -16,7 +18,7 @@ app = flask.current_app
|
||||||
|
|
||||||
|
|
||||||
class TorrentExtraValidationException(Exception):
|
class TorrentExtraValidationException(Exception):
|
||||||
def __init__(self, errors):
|
def __init__(self, errors={}):
|
||||||
self.errors = errors
|
self.errors = errors
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,6 +107,40 @@ def validate_torrent_post_upload(torrent, upload_form=None):
|
||||||
raise TorrentExtraValidationException(errors)
|
raise TorrentExtraValidationException(errors)
|
||||||
|
|
||||||
|
|
||||||
|
def check_uploader_ratelimit(user):
|
||||||
|
''' Figures out if user (or IP address from flask.request) may
|
||||||
|
upload within upload ratelimit.
|
||||||
|
Returns a tuple of current datetime, count of torrents uploaded
|
||||||
|
within burst duration and timestamp for next allowed upload. '''
|
||||||
|
now = datetime.utcnow()
|
||||||
|
next_allowed_time = now
|
||||||
|
|
||||||
|
Torrent = models.Torrent
|
||||||
|
|
||||||
|
def filter_uploader(query):
|
||||||
|
if user:
|
||||||
|
return query.filter(Torrent.user == user)
|
||||||
|
else:
|
||||||
|
return query.filter(Torrent.uploader_ip == ip_address(flask.request.remote_addr).packed)
|
||||||
|
|
||||||
|
time_range_start = datetime.utcnow() - timedelta(seconds=app.config['UPLOAD_BURST_DURATION'])
|
||||||
|
# Count torrents uploaded by user/ip within given time period
|
||||||
|
torrent_count_query = db.session.query(sqlalchemy.func.count(Torrent.id))
|
||||||
|
torrent_count = filter_uploader(torrent_count_query).filter(
|
||||||
|
Torrent.created_time >= time_range_start).scalar()
|
||||||
|
|
||||||
|
# If user has reached burst limit...
|
||||||
|
if torrent_count >= app.config['MAX_UPLOAD_BURST']:
|
||||||
|
# Check how long ago their latest torrent was (we know at least one will exist)
|
||||||
|
last_torrent = filter_uploader(Torrent.query).order_by(Torrent.created_time.desc()).first()
|
||||||
|
after_timeout = last_torrent.created_time + timedelta(seconds=app.config['UPLOAD_TIMEOUT'])
|
||||||
|
|
||||||
|
if now < after_timeout:
|
||||||
|
next_allowed_time = after_timeout
|
||||||
|
|
||||||
|
return now, torrent_count, next_allowed_time
|
||||||
|
|
||||||
|
|
||||||
def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False):
|
def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False):
|
||||||
''' Stores a torrent to the database.
|
''' Stores a torrent to the database.
|
||||||
May throw TorrentExtraValidationException if the form/torrent fails
|
May throw TorrentExtraValidationException if the form/torrent fails
|
||||||
|
@ -112,6 +148,18 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False):
|
||||||
relevant fields on the given form. '''
|
relevant fields on the given form. '''
|
||||||
torrent_data = upload_form.torrent_file.parsed_data
|
torrent_data = upload_form.torrent_file.parsed_data
|
||||||
|
|
||||||
|
# Anonymous uploaders and non-trusted uploaders
|
||||||
|
no_or_new_account = (not uploading_user
|
||||||
|
or (uploading_user.age < app.config['RATELIMIT_ACCOUNT_AGE']
|
||||||
|
and not uploading_user.is_trusted))
|
||||||
|
|
||||||
|
if app.config['RATELIMIT_UPLOADS'] and no_or_new_account:
|
||||||
|
now, torrent_count, next_time = check_uploader_ratelimit(uploading_user)
|
||||||
|
if next_time > now:
|
||||||
|
# This will flag the dialog in upload.html red and tell API users what's wrong
|
||||||
|
upload_form.ratelimit.errors = ["You've gone over the upload ratelimit."]
|
||||||
|
raise TorrentExtraValidationException()
|
||||||
|
|
||||||
# Delete exisiting torrent which is marked as deleted
|
# Delete exisiting torrent which is marked as deleted
|
||||||
if torrent_data.db_id is not None:
|
if torrent_data.db_id is not None:
|
||||||
models.Torrent.query.filter_by(id=torrent_data.db_id).delete()
|
models.Torrent.query.filter_by(id=torrent_data.db_id).delete()
|
||||||
|
|
|
@ -323,6 +323,8 @@ class UploadForm(FlaskForm):
|
||||||
Length(max=10 * 1024, message='Description must be at most %(max)d characters long.')
|
Length(max=10 * 1024, message='Description must be at most %(max)d characters long.')
|
||||||
])
|
])
|
||||||
|
|
||||||
|
ratelimit = HiddenField()
|
||||||
|
|
||||||
def validate_torrent_file(form, field):
|
def validate_torrent_file(form, field):
|
||||||
# Decode and ensure data is bencoded data
|
# Decode and ensure data is bencoded data
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -91,6 +91,7 @@ function _format_time_difference(seconds) {
|
||||||
if (seconds < 0) {
|
if (seconds < 0) {
|
||||||
suffix = "";
|
suffix = "";
|
||||||
prefix = "After ";
|
prefix = "After ";
|
||||||
|
seconds = -seconds;
|
||||||
} else if (seconds == 0) {
|
} else if (seconds == 0) {
|
||||||
return "Just now"
|
return "Just now"
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,6 +94,13 @@ def get_utc_timestamp(datetime_str):
|
||||||
return int((datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S') - UTC_EPOCH).total_seconds())
|
return int((datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S') - UTC_EPOCH).total_seconds())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.app_template_filter('utc_timestamp')
|
||||||
|
def get_utc_timestamp_seconds(datetime_instance):
|
||||||
|
""" Returns a UTC POSIX timestamp, as seconds """
|
||||||
|
UTC_EPOCH = datetime.utcfromtimestamp(0)
|
||||||
|
return int((datetime_instance - UTC_EPOCH).total_seconds())
|
||||||
|
|
||||||
|
|
||||||
@bp.app_template_filter('display_time')
|
@bp.app_template_filter('display_time')
|
||||||
def get_display_time(datetime_str):
|
def get_display_time(datetime_str):
|
||||||
return datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S').strftime('%Y-%m-%d %H:%M')
|
return datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S').strftime('%Y-%m-%d %H:%M')
|
||||||
|
|
|
@ -20,6 +20,23 @@
|
||||||
{% if config.ENFORCE_MAIN_ANNOUNCE_URL %}<p><strong>Important:</strong> Please include <kbd>{{ config.MAIN_ANNOUNCE_URL }}</kbd> in your trackers.</p>{% endif %}
|
{% if config.ENFORCE_MAIN_ANNOUNCE_URL %}<p><strong>Important:</strong> Please include <kbd>{{ config.MAIN_ANNOUNCE_URL }}</kbd> in your trackers.</p>{% endif %}
|
||||||
<p><strong>Important:</strong> Make sure you have read <strong><a href="{{ url_for('site.rules') }}">the rules</a></strong> before uploading!</p>
|
<p><strong>Important:</strong> Make sure you have read <strong><a href="{{ url_for('site.rules') }}">the rules</a></strong> before uploading!</p>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
{% if show_ratelimit %}
|
||||||
|
{% set ratelimit_class = 'danger' if upload_form.ratelimit.errors else 'warning' %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-{{ ratelimit_class }}" role="alert">
|
||||||
|
<div>You've reached your maximum upload rate ({{ config.MAX_UPLOAD_BURST }} per {{ config.UPLOAD_BURST_DURATION // 60}} minutes, you have <b>{{ ratelimit_count }}</b>) and will now have to wait {{ config.UPLOAD_TIMEOUT // 60 }} minutes between uploads.</div>
|
||||||
|
{% if next_upload_time %}
|
||||||
|
<div>You may upload again at <b data-timestamp="{{ next_upload_time|utc_timestamp }}">{{ next_upload_time.strftime('%Y-%m-%d %H:%M UTC') }}</b>.</div>
|
||||||
|
{% else %}
|
||||||
|
<div>You may upload again now.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{{ render_upload(upload_form.torrent_file, accept=".torrent") }}
|
{{ render_upload(upload_form.torrent_file, accept=".torrent") }}
|
||||||
|
|
|
@ -37,7 +37,6 @@ def cached_function(f):
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
def decorator(*args, **kwargs):
|
def decorator(*args, **kwargs):
|
||||||
if f._cached_value is sentinel:
|
if f._cached_value is sentinel:
|
||||||
print('Evaluating', f, args, kwargs)
|
|
||||||
f._cached_value = f(*args, **kwargs)
|
f._cached_value = f(*args, **kwargs)
|
||||||
return f._cached_value
|
return f._cached_value
|
||||||
return decorator
|
return decorator
|
||||||
|
|
|
@ -387,6 +387,21 @@ def upload():
|
||||||
upload_form = forms.UploadForm(CombinedMultiDict((flask.request.files, flask.request.form)))
|
upload_form = forms.UploadForm(CombinedMultiDict((flask.request.files, flask.request.form)))
|
||||||
upload_form.category.choices = _create_upload_category_choices()
|
upload_form.category.choices = _create_upload_category_choices()
|
||||||
|
|
||||||
|
show_ratelimit = False
|
||||||
|
next_upload_time = None
|
||||||
|
ratelimit_count = 0
|
||||||
|
|
||||||
|
# Anonymous uploaders and non-trusted uploaders
|
||||||
|
|
||||||
|
no_or_new_account = (not flask.g.user
|
||||||
|
or (flask.g.user.age < app.config['RATELIMIT_ACCOUNT_AGE']
|
||||||
|
and not flask.g.user.is_trusted))
|
||||||
|
|
||||||
|
if app.config['RATELIMIT_UPLOADS'] and no_or_new_account:
|
||||||
|
now, ratelimit_count, next_upload_time = backend.check_uploader_ratelimit(flask.g.user)
|
||||||
|
show_ratelimit = ratelimit_count >= app.config['MAX_UPLOAD_BURST']
|
||||||
|
next_upload_time = next_upload_time if next_upload_time > now else None
|
||||||
|
|
||||||
if flask.request.method == 'POST' and upload_form.validate():
|
if flask.request.method == 'POST' and upload_form.validate():
|
||||||
try:
|
try:
|
||||||
torrent = backend.handle_torrent_upload(upload_form, flask.g.user)
|
torrent = backend.handle_torrent_upload(upload_form, flask.g.user)
|
||||||
|
@ -397,7 +412,11 @@ def upload():
|
||||||
|
|
||||||
# If we get here with a POST, it means the form data was invalid: return a non-okay status
|
# If we get here with a POST, it means the form data was invalid: return a non-okay status
|
||||||
status_code = 400 if flask.request.method == 'POST' else 200
|
status_code = 400 if flask.request.method == 'POST' else 200
|
||||||
return flask.render_template('upload.html', upload_form=upload_form), status_code
|
return flask.render_template('upload.html',
|
||||||
|
upload_form=upload_form,
|
||||||
|
show_ratelimit=show_ratelimit,
|
||||||
|
ratelimit_count=ratelimit_count,
|
||||||
|
next_upload_time=next_upload_time), status_code
|
||||||
|
|
||||||
|
|
||||||
@cached_function
|
@cached_function
|
||||||
|
|
Loading…
Reference in a new issue