Add (optional) validation for minimum anonymous torrent size (#342)

MINIMUM_ANONYMOUS_TORRENT_SIZE can be used to require a minimum total
size of torrents uploaded by anonymous users (ie. without accounts).

Sets up a "framework" for post-WTForm torrent validation as well;
this can easily be extended into filename blacklists and such.
This commit is contained in:
Anna-Maria Meriniemi 2017-08-20 03:48:08 +03:00 committed by Arylide
parent 48d4217f02
commit 39fcfc0058
4 changed files with 67 additions and 20 deletions

View File

@ -46,6 +46,9 @@ ENFORCE_MAIN_ANNOUNCE_URL = False
MAIN_ANNOUNCE_URL = 'http://127.0.0.1:6881/announce' MAIN_ANNOUNCE_URL = 'http://127.0.0.1:6881/announce'
TRACKER_API_URL = 'http://127.0.0.1:6881/api' TRACKER_API_URL = 'http://127.0.0.1:6881/api'
TRACKER_API_AUTH = 'topsecret' TRACKER_API_AUTH = 'topsecret'
# Torrents uploaded without an account must be at least this big in total (bytes)
# Set to 0 to disable
MINIMUM_ANONYMOUS_TORRENT_SIZE = 1 * 1024 * 1024
BACKUP_TORRENT_FOLDER = 'torrents' BACKUP_TORRENT_FOLDER = 'torrents'

View File

@ -99,22 +99,25 @@ def v2_api_upload():
upload_form.category.choices = _create_upload_category_choices() upload_form.category.choices = _create_upload_category_choices()
if upload_form.validate(): if upload_form.validate():
torrent = backend.handle_torrent_upload(upload_form, flask.g.user) try:
torrent = backend.handle_torrent_upload(upload_form, flask.g.user)
# Create a response dict with relevant data # Create a response dict with relevant data
torrent_metadata = { torrent_metadata = {
'url': flask.url_for('torrents.view', torrent_id=torrent.id, _external=True), 'url': flask.url_for('torrents.view', torrent_id=torrent.id, _external=True),
'id': torrent.id, 'id': torrent.id,
'name': torrent.display_name, 'name': torrent.display_name,
'hash': torrent.info_hash.hex(), 'hash': torrent.info_hash.hex(),
'magnet': torrent.magnet_uri 'magnet': torrent.magnet_uri
} }
return flask.jsonify(torrent_metadata) return flask.jsonify(torrent_metadata)
else: except backend.TorrentExtraValidationException:
# Map errors back from form fields into the api keys pass
mapped_errors = {UPLOAD_API_FORM_KEYMAP.get(k, k): v for k, v in upload_form.errors.items()}
return flask.jsonify({'errors': mapped_errors}), 400 # Map errors back from form fields into the api keys
mapped_errors = {UPLOAD_API_FORM_KEYMAP.get(k, k): v for k, v in upload_form.errors.items()}
return flask.jsonify({'errors': mapped_errors}), 400
# #################################### TEMPORARY #################################### # #################################### TEMPORARY ####################################

View File

@ -15,6 +15,11 @@ from nyaa.extensions import db
app = flask.current_app app = flask.current_app
class TorrentExtraValidationException(Exception):
def __init__(self, errors):
self.errors = errors
@utils.cached_function @utils.cached_function
def get_category_id_map(): def get_category_id_map():
''' Reads database for categories and turns them into a dict with ''' Reads database for categories and turns them into a dict with
@ -44,7 +49,37 @@ def _replace_utf8_values(dict_or_list):
return did_change return did_change
def validate_torrent_post_upload(torrent, upload_form=None):
''' Validates a Torrent instance before it's saved to the database.
Enforcing user-and-such-based validations is more flexible here vs WTForm context '''
errors = {
'torrent_file': []
}
# Encorce minimum size for userless uploads
minimum_anonymous_torrent_size = app.config['MINIMUM_ANONYMOUS_TORRENT_SIZE']
if torrent.user is None and torrent.filesize < minimum_anonymous_torrent_size:
errors['torrent_file'].append('Torrent too small for an anonymous uploader')
print(errors)
# Remove keys with empty lists
errors = {k: v for k, v in errors.items() if v}
if errors:
if upload_form:
# Add error messages to the form fields
for field_name, field_errors in errors.items():
getattr(upload_form, field_name).errors.extend(field_errors)
# Clear out the wtforms dict to force a regeneration
upload_form._errors = None
raise TorrentExtraValidationException(errors)
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.
May throw TorrentExtraValidationException if the form/torrent fails
post-WTForm validation! Exception messages will also be added to their
relevant fields on the given form. '''
torrent_data = upload_form.torrent_file.parsed_data torrent_data = upload_form.torrent_file.parsed_data
# Delete exisiting torrent which is marked as deleted # Delete exisiting torrent which is marked as deleted
@ -197,6 +232,9 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False):
tracker_id=tracker.id, order=order) tracker_id=tracker.id, order=order)
db.session.add(torrent_tracker) db.session.add(torrent_tracker)
# Before final commit, validate the torrent again
validate_torrent_post_upload(torrent, upload_form)
db.session.commit() db.session.commit()
# Store the actual torrent file as well # Store the actual torrent file as well

View File

@ -307,13 +307,16 @@ def upload():
upload_form.category.choices = _create_upload_category_choices() upload_form.category.choices = _create_upload_category_choices()
if flask.request.method == 'POST' and upload_form.validate(): if flask.request.method == 'POST' and upload_form.validate():
torrent = backend.handle_torrent_upload(upload_form, flask.g.user) try:
torrent = backend.handle_torrent_upload(upload_form, flask.g.user)
return flask.redirect(flask.url_for('torrents.view', torrent_id=torrent.id)) return flask.redirect(flask.url_for('torrents.view', torrent_id=torrent.id))
else: except backend.TorrentExtraValidationException:
# If we get here with a POST, it means the form data was invalid: return a non-okay status pass
status_code = 400 if flask.request.method == 'POST' else 200
return flask.render_template('upload.html', upload_form=upload_form), status_code # 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
return flask.render_template('upload.html', upload_form=upload_form), status_code
@cached_function @cached_function