diff options
Diffstat (limited to 'matweb')
| -rw-r--r-- | matweb/frontend.py | 14 | ||||
| -rw-r--r-- | matweb/rest_api.py | 19 | ||||
| -rw-r--r-- | matweb/utils.py | 35 |
3 files changed, 47 insertions, 21 deletions
diff --git a/matweb/frontend.py b/matweb/frontend.py index 93432b4..2e25467 100644 --- a/matweb/frontend.py +++ b/matweb/frontend.py | |||
| @@ -18,8 +18,8 @@ def info(): | |||
| 18 | ) | 18 | ) |
| 19 | 19 | ||
| 20 | 20 | ||
| 21 | @routes.route('/download/<string:key>/<string:filename>') | 21 | @routes.route('/download/<string:key>/<string:secret>/<string:filename>') |
| 22 | def download_file(key: str, filename: str): | 22 | def download_file(key: str, secret: str, filename: str): |
| 23 | if filename != secure_filename(filename): | 23 | if filename != secure_filename(filename): |
| 24 | return redirect(url_for('routes.upload_file')) | 24 | return redirect(url_for('routes.upload_file')) |
| 25 | 25 | ||
| @@ -28,7 +28,7 @@ def download_file(key: str, filename: str): | |||
| 28 | 28 | ||
| 29 | if not os.path.exists(complete_path): | 29 | if not os.path.exists(complete_path): |
| 30 | return redirect(url_for('routes.upload_file')) | 30 | return redirect(url_for('routes.upload_file')) |
| 31 | if hmac.compare_digest(utils.hash_file(complete_path), key) is False: | 31 | if hmac.compare_digest(utils.hash_file(complete_path, secret), key) is False: |
| 32 | return redirect(url_for('routes.upload_file')) | 32 | return redirect(url_for('routes.upload_file')) |
| 33 | 33 | ||
| 34 | @after_this_request | 34 | @after_this_request |
| @@ -67,10 +67,14 @@ def upload_file(): | |||
| 67 | flash('Unable to clean %s' % mime) | 67 | flash('Unable to clean %s' % mime) |
| 68 | return redirect(url_for('routes.upload_file')) | 68 | return redirect(url_for('routes.upload_file')) |
| 69 | 69 | ||
| 70 | key, meta_after, output_filename = utils.cleanup(parser, filepath, current_app.config['UPLOAD_FOLDER']) | 70 | key, secret, meta_after, output_filename = utils.cleanup(parser, filepath, current_app.config['UPLOAD_FOLDER']) |
| 71 | 71 | ||
| 72 | return render_template( | 72 | return render_template( |
| 73 | 'download.html', mimetypes=mime_types, meta=meta, filename=output_filename, meta_after=meta_after, key=key | 73 | 'download.html', |
| 74 | mimetypes=mime_types, | ||
| 75 | meta=meta, | ||
| 76 | download_uri=url_for('routes.download_file', key=key, secret=secret, filename=output_filename), | ||
| 77 | meta_after=meta_after, | ||
| 74 | ) | 78 | ) |
| 75 | 79 | ||
| 76 | max_file_size = int(current_app.config['MAX_CONTENT_LENGTH'] / 1024 / 1024) | 80 | max_file_size = int(current_app.config['MAX_CONTENT_LENGTH'] / 1024 / 1024) |
diff --git a/matweb/rest_api.py b/matweb/rest_api.py index 60d834f..4098050 100644 --- a/matweb/rest_api.py +++ b/matweb/rest_api.py | |||
| @@ -42,14 +42,15 @@ class APIUpload(Resource): | |||
| 42 | if not parser.remove_all(): | 42 | if not parser.remove_all(): |
| 43 | abort(500, message='Unable to clean %s' % mime) | 43 | abort(500, message='Unable to clean %s' % mime) |
| 44 | 44 | ||
| 45 | key, meta_after, output_filename = utils.cleanup(parser, filepath, self.upload_folder) | 45 | key, secret, meta_after, output_filename = utils.cleanup(parser, filepath, self.upload_folder) |
| 46 | return utils.return_file_created_response( | 46 | return utils.return_file_created_response( |
| 47 | output_filename, | 47 | output_filename, |
| 48 | mime, | 48 | mime, |
| 49 | key, | 49 | key, |
| 50 | secret, | ||
| 50 | meta, | 51 | meta, |
| 51 | meta_after, | 52 | meta_after, |
| 52 | urljoin(request.host_url, '%s/%s/%s/%s' % ('api', 'download', key, output_filename)) | 53 | urljoin(request.host_url, '%s/%s/%s/%s/%s' % ('api', 'download', key, secret, output_filename)) |
| 53 | ) | 54 | ) |
| 54 | 55 | ||
| 55 | 56 | ||
| @@ -58,8 +59,8 @@ class APIDownload(Resource): | |||
| 58 | def __init__(self, **kwargs): | 59 | def __init__(self, **kwargs): |
| 59 | self.upload_folder = kwargs['upload_folder'] | 60 | self.upload_folder = kwargs['upload_folder'] |
| 60 | 61 | ||
| 61 | def get(self, key: str, filename: str): | 62 | def get(self, key: str, secret: str, filename: str): |
| 62 | complete_path, filepath = utils.is_valid_api_download_file(filename, key, self.upload_folder) | 63 | complete_path, filepath = utils.is_valid_api_download_file(filename, key, secret, self.upload_folder) |
| 63 | # Make sure the file is NOT deleted on HEAD requests | 64 | # Make sure the file is NOT deleted on HEAD requests |
| 64 | if request.method == 'GET': | 65 | if request.method == 'GET': |
| 65 | file_removal_scheduler.run_file_removal_job(self.upload_folder) | 66 | file_removal_scheduler.run_file_removal_job(self.upload_folder) |
| @@ -87,6 +88,7 @@ class APIBulkDownloadCreator(Resource): | |||
| 87 | 'type': 'dict', | 88 | 'type': 'dict', |
| 88 | 'schema': { | 89 | 'schema': { |
| 89 | 'key': {'type': 'string', 'required': True}, | 90 | 'key': {'type': 'string', 'required': True}, |
| 91 | 'secret': {'type': 'string', 'required': True}, | ||
| 90 | 'file_name': {'type': 'string', 'required': True} | 92 | 'file_name': {'type': 'string', 'required': True} |
| 91 | } | 93 | } |
| 92 | } | 94 | } |
| @@ -108,6 +110,7 @@ class APIBulkDownloadCreator(Resource): | |||
| 108 | complete_path, file_path = utils.is_valid_api_download_file( | 110 | complete_path, file_path = utils.is_valid_api_download_file( |
| 109 | file_candidate['file_name'], | 111 | file_candidate['file_name'], |
| 110 | file_candidate['key'], | 112 | file_candidate['key'], |
| 113 | file_candidate['secret'], | ||
| 111 | self.upload_folder | 114 | self.upload_folder |
| 112 | ) | 115 | ) |
| 113 | try: | 116 | try: |
| @@ -124,13 +127,17 @@ class APIBulkDownloadCreator(Resource): | |||
| 124 | parser, mime = utils.get_file_parser(zip_path) | 127 | parser, mime = utils.get_file_parser(zip_path) |
| 125 | if not parser.remove_all(): | 128 | if not parser.remove_all(): |
| 126 | abort(500, message='Unable to clean %s' % mime) | 129 | abort(500, message='Unable to clean %s' % mime) |
| 127 | key, meta_after, output_filename = utils.cleanup(parser, zip_path, self.upload_folder) | 130 | key, secret, meta_after, output_filename = utils.cleanup(parser, zip_path, self.upload_folder) |
| 128 | return { | 131 | return { |
| 129 | 'output_filename': output_filename, | 132 | 'output_filename': output_filename, |
| 130 | 'mime': mime, | 133 | 'mime': mime, |
| 131 | 'key': key, | 134 | 'key': key, |
| 135 | 'secret': secret, | ||
| 132 | 'meta_after': meta_after, | 136 | 'meta_after': meta_after, |
| 133 | 'download_link': urljoin(request.host_url, '%s/%s/%s/%s' % ('api', 'download', key, output_filename)) | 137 | 'download_link': urljoin( |
| 138 | request.host_url, | ||
| 139 | '%s/%s/%s/%s/%s' % ('api', 'download', key, secret, output_filename) | ||
| 140 | ) | ||
| 134 | }, 201 | 141 | }, 201 |
| 135 | 142 | ||
| 136 | 143 | ||
diff --git a/matweb/utils.py b/matweb/utils.py index 8dfff45..ec9b99c 100644 --- a/matweb/utils.py +++ b/matweb/utils.py | |||
| @@ -12,15 +12,21 @@ def get_allow_origin_header_value(): | |||
| 12 | return os.environ.get('MAT2_ALLOW_ORIGIN_WHITELIST', '*').split(" ") | 12 | return os.environ.get('MAT2_ALLOW_ORIGIN_WHITELIST', '*').split(" ") |
| 13 | 13 | ||
| 14 | 14 | ||
| 15 | def hash_file(filepath: str) -> str: | 15 | def hash_file(filepath: str, secret: str) -> str: |
| 16 | sha256 = hashlib.sha256() | 16 | """ |
| 17 | The goal of the hmac is to ONLY make the hashes unpredictable | ||
| 18 | :param filepath: Path of the file | ||
| 19 | :param secret: a server side generated secret | ||
| 20 | :return: digest, secret | ||
| 21 | """ | ||
| 22 | mac = hmac.new(secret.encode(), None, hashlib.sha256) | ||
| 17 | with open(filepath, 'rb') as f: | 23 | with open(filepath, 'rb') as f: |
| 18 | while True: | 24 | while True: |
| 19 | data = f.read(65536) # read the file by chunk of 64k | 25 | data = f.read(65536) # read the file by chunk of 64k |
| 20 | if not data: | 26 | if not data: |
| 21 | break | 27 | break |
| 22 | sha256.update(data) | 28 | mac.update(data) |
| 23 | return sha256.hexdigest() | 29 | return mac.hexdigest() |
| 24 | 30 | ||
| 25 | 31 | ||
| 26 | def check_upload_folder(upload_folder): | 32 | def check_upload_folder(upload_folder): |
| @@ -28,11 +34,20 @@ def check_upload_folder(upload_folder): | |||
| 28 | os.mkdir(upload_folder) | 34 | os.mkdir(upload_folder) |
| 29 | 35 | ||
| 30 | 36 | ||
| 31 | def return_file_created_response(output_filename, mime, key, meta, meta_after, download_link): | 37 | def return_file_created_response( |
| 38 | output_filename: str, | ||
| 39 | mime: str, | ||
| 40 | key: str, | ||
| 41 | secret: str, | ||
| 42 | meta: list, | ||
| 43 | meta_after: list, | ||
| 44 | download_link: str | ||
| 45 | ) -> dict: | ||
| 32 | return { | 46 | return { |
| 33 | 'output_filename': output_filename, | 47 | 'output_filename': output_filename, |
| 34 | 'mime': mime, | 48 | 'mime': mime, |
| 35 | 'key': key, | 49 | 'key': key, |
| 50 | 'secret': secret, | ||
| 36 | 'meta': meta, | 51 | 'meta': meta, |
| 37 | 'meta_after': meta_after, | 52 | 'meta_after': meta_after, |
| 38 | 'download_link': download_link | 53 | 'download_link': download_link |
| @@ -65,9 +80,9 @@ def cleanup(parser, filepath, upload_folder): | |||
| 65 | parser, _ = parser_factory.get_parser(parser.output_filename) | 80 | parser, _ = parser_factory.get_parser(parser.output_filename) |
| 66 | meta_after = parser.get_meta() | 81 | meta_after = parser.get_meta() |
| 67 | os.remove(filepath) | 82 | os.remove(filepath) |
| 68 | 83 | secret = os.urandom(32).hex() | |
| 69 | key = hash_file(os.path.join(upload_folder, output_filename)) | 84 | key = hash_file(os.path.join(upload_folder, output_filename), secret) |
| 70 | return key, meta_after, output_filename | 85 | return key, secret, meta_after, output_filename |
| 71 | 86 | ||
| 72 | 87 | ||
| 73 | def get_file_paths(filename, upload_folder): | 88 | def get_file_paths(filename, upload_folder): |
| @@ -77,7 +92,7 @@ def get_file_paths(filename, upload_folder): | |||
| 77 | return complete_path, filepath | 92 | return complete_path, filepath |
| 78 | 93 | ||
| 79 | 94 | ||
| 80 | def is_valid_api_download_file(filename, key, upload_folder): | 95 | def is_valid_api_download_file(filename: str, key: str, secret: str, upload_folder: str) -> [str, str]: |
| 81 | if filename != secure_filename(filename): | 96 | if filename != secure_filename(filename): |
| 82 | abort(400, message='Insecure filename') | 97 | abort(400, message='Insecure filename') |
| 83 | 98 | ||
| @@ -86,6 +101,6 @@ def is_valid_api_download_file(filename, key, upload_folder): | |||
| 86 | if not os.path.exists(complete_path): | 101 | if not os.path.exists(complete_path): |
| 87 | abort(404, message='File not found') | 102 | abort(404, message='File not found') |
| 88 | 103 | ||
| 89 | if hmac.compare_digest(hash_file(complete_path), key) is False: | 104 | if hmac.compare_digest(hash_file(complete_path, secret), key) is False: |
| 90 | abort(400, message='The file hash does not match') | 105 | abort(400, message='The file hash does not match') |
| 91 | return complete_path, filepath | 106 | return complete_path, filepath |
