diff options
| author | jfriedli | 2020-04-26 09:50:14 -0700 |
|---|---|---|
| committer | jfriedli | 2020-04-26 09:50:14 -0700 |
| commit | c301e472bd7fd79d675c5df089db0b16fd1e2cfe (patch) | |
| tree | c3332e0f974edc09881b5534c35becc5b9fffa3b | |
| parent | e1bac8b6a7fd857f38b7bcb678398c82baaa8fd5 (diff) | |
Resolve "Use a HMAC instead of a hash"
| -rw-r--r-- | main.py | 2 | ||||
| -rw-r--r-- | matweb/frontend.py | 14 | ||||
| -rw-r--r-- | matweb/rest_api.py | 19 | ||||
| -rw-r--r-- | matweb/utils.py | 35 | ||||
| -rw-r--r-- | requirements.txt | 2 | ||||
| -rw-r--r-- | templates/download.html | 2 | ||||
| -rw-r--r-- | templates/index.html | 2 | ||||
| -rw-r--r-- | test/test.py | 89 | ||||
| -rw-r--r-- | test/test_api.py | 74 |
9 files changed, 148 insertions, 91 deletions
| @@ -36,7 +36,7 @@ def create_app(test_config=None): | |||
| 36 | ) | 36 | ) |
| 37 | api.add_resource( | 37 | api.add_resource( |
| 38 | rest_api.APIDownload, | 38 | rest_api.APIDownload, |
| 39 | '/api/download/<string:key>/<string:filename>', | 39 | '/api/download/<string:key>/<string:secret>/<string:filename>', |
| 40 | resource_class_kwargs={'upload_folder': app.config['UPLOAD_FOLDER']} | 40 | resource_class_kwargs={'upload_folder': app.config['UPLOAD_FOLDER']} |
| 41 | ) | 41 | ) |
| 42 | api.add_resource( | 42 | api.add_resource( |
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 |
diff --git a/requirements.txt b/requirements.txt index 61f9711..4afa377 100644 --- a/requirements.txt +++ b/requirements.txt | |||
| @@ -6,3 +6,5 @@ flask==1.0.3 | |||
| 6 | Flask-RESTful==0.3.7 | 6 | Flask-RESTful==0.3.7 |
| 7 | Flask-Cors==3.0.8 | 7 | Flask-Cors==3.0.8 |
| 8 | Cerberus==1.3.1 | 8 | Cerberus==1.3.1 |
| 9 | Flask-Testing==0.8.0 | ||
| 10 | blinker==1.4 \ No newline at end of file | ||
diff --git a/templates/download.html b/templates/download.html index 736c9f5..8100623 100644 --- a/templates/download.html +++ b/templates/download.html | |||
| @@ -10,7 +10,7 @@ | |||
| 10 | {% endif %} | 10 | {% endif %} |
| 11 | <div class="uk-flex uk-flex-center"> | 11 | <div class="uk-flex uk-flex-center"> |
| 12 | <div> | 12 | <div> |
| 13 | <a class="uk-flex-1" href='{{ url_for('routes.download_file', key=key, filename=filename) }}'> | 13 | <a class="uk-flex-1" href='{{ download_uri }}'> |
| 14 | <button class="uk-button uk-button-primary"> | 14 | <button class="uk-button uk-button-primary"> |
| 15 | ⇩ download cleaned file | 15 | ⇩ download cleaned file |
| 16 | </button> | 16 | </button> |
diff --git a/templates/index.html b/templates/index.html index ed583d2..f92ef69 100644 --- a/templates/index.html +++ b/templates/index.html | |||
| @@ -3,7 +3,7 @@ | |||
| 3 | <div class="shadowed-box u-text-center u-center-block"> | 3 | <div class="shadowed-box u-text-center u-center-block"> |
| 4 | <h2 class="uk-text-center">Remove metadata</h2> | 4 | <h2 class="uk-text-center">Remove metadata</h2> |
| 5 | <p class="uk-text-center"> | 5 | <p class="uk-text-center"> |
| 6 | The file you see is just the tip of the iceberg. Remove the hidden meta | 6 | The file you see is just the tip of the iceberg. Remove the hidden metadata. |
| 7 | </p> | 7 | </p> |
| 8 | <div class="uk-flex uk-flex-center"> | 8 | <div class="uk-flex uk-flex-center"> |
| 9 | <div> | 9 | <div> |
diff --git a/test/test.py b/test/test.py index 02216ac..2d09662 100644 --- a/test/test.py +++ b/test/test.py | |||
| @@ -6,12 +6,13 @@ import io | |||
| 6 | import os | 6 | import os |
| 7 | 7 | ||
| 8 | from unittest.mock import patch | 8 | from unittest.mock import patch |
| 9 | from flask_testing import TestCase | ||
| 9 | 10 | ||
| 10 | import main | 11 | import main |
| 11 | 12 | ||
| 12 | 13 | ||
| 13 | class Mat2WebTestCase(unittest.TestCase): | 14 | class Mat2WebTestCase(TestCase): |
| 14 | def setUp(self): | 15 | def create_app(self): |
| 15 | os.environ.setdefault('MAT2_ALLOW_ORIGIN_WHITELIST', 'origin1.gnu origin2.gnu') | 16 | os.environ.setdefault('MAT2_ALLOW_ORIGIN_WHITELIST', 'origin1.gnu origin2.gnu') |
| 16 | self.upload_folder = tempfile.mkdtemp() | 17 | self.upload_folder = tempfile.mkdtemp() |
| 17 | app = main.create_app( | 18 | app = main.create_app( |
| @@ -20,45 +21,45 @@ class Mat2WebTestCase(unittest.TestCase): | |||
| 20 | 'UPLOAD_FOLDER': self.upload_folder | 21 | 'UPLOAD_FOLDER': self.upload_folder |
| 21 | } | 22 | } |
| 22 | ) | 23 | ) |
| 23 | self.app = app.test_client() | 24 | return app |
| 24 | 25 | ||
| 25 | def tearDown(self): | 26 | def tearDown(self): |
| 26 | shutil.rmtree(self.upload_folder) | 27 | shutil.rmtree(self.upload_folder) |
| 27 | 28 | ||
| 28 | def test_get_root(self): | 29 | def test_get_root(self): |
| 29 | rv = self.app.get('/') | 30 | rv = self.client.get('/') |
| 30 | self.assertIn(b'mat2-web', rv.data) | 31 | self.assertIn(b'mat2-web', rv.data) |
| 31 | 32 | ||
| 32 | def test_check_mimetypes(self): | 33 | def test_check_mimetypes(self): |
| 33 | rv = self.app.get('/') | 34 | rv = self.client.get('/') |
| 34 | self.assertIn(b'.torrent', rv.data) | 35 | self.assertIn(b'.torrent', rv.data) |
| 35 | self.assertIn(b'.ods', rv.data) | 36 | self.assertIn(b'.ods', rv.data) |
| 36 | 37 | ||
| 37 | def test_get_download_dangerous_file(self): | 38 | def test_get_download_dangerous_file(self): |
| 38 | rv = self.app.get('/download/1337/\..\filename') | 39 | rv = self.client.get('/download/1337/aabb/\..\filename') |
| 39 | self.assertEqual(rv.status_code, 302) | 40 | self.assertEqual(rv.status_code, 302) |
| 40 | 41 | ||
| 41 | def test_get_download_without_key_file(self): | 42 | def test_get_download_without_key_file(self): |
| 42 | rv = self.app.get('/download/non_existant') | 43 | rv = self.client.get('/download/non_existant') |
| 43 | self.assertEqual(rv.status_code, 404) | 44 | self.assertEqual(rv.status_code, 404) |
| 44 | 45 | ||
| 45 | def test_get_download_nonexistant_file(self): | 46 | def test_get_download_nonexistant_file(self): |
| 46 | rv = self.app.get('/download/1337/non_existant') | 47 | rv = self.client.get('/download/1337/aabb/non_existant') |
| 47 | self.assertEqual(rv.status_code, 302) | 48 | self.assertEqual(rv.status_code, 302) |
| 48 | 49 | ||
| 49 | def test_get_upload_without_file(self): | 50 | def test_get_upload_without_file(self): |
| 50 | rv = self.app.post('/') | 51 | rv = self.client.post('/') |
| 51 | self.assertEqual(rv.status_code, 302) | 52 | self.assertEqual(rv.status_code, 302) |
| 52 | 53 | ||
| 53 | def test_get_upload_empty_file(self): | 54 | def test_get_upload_empty_file(self): |
| 54 | rv = self.app.post('/', | 55 | rv = self.client.post('/', |
| 55 | data=dict( | 56 | data=dict( |
| 56 | file=(io.BytesIO(b""), 'test.pdf'), | 57 | file=(io.BytesIO(b""), 'test.pdf'), |
| 57 | ), follow_redirects=False) | 58 | ), follow_redirects=False) |
| 58 | self.assertEqual(rv.status_code, 302) | 59 | self.assertEqual(rv.status_code, 302) |
| 59 | 60 | ||
| 60 | def test_get_upload_empty_file_redir(self): | 61 | def test_get_upload_empty_file_redir(self): |
| 61 | rv = self.app.post('/', | 62 | rv = self.client.post('/', |
| 62 | data=dict( | 63 | data=dict( |
| 63 | file=(io.BytesIO(b""), 'test.pdf'), | 64 | file=(io.BytesIO(b""), 'test.pdf'), |
| 64 | ), follow_redirects=True) | 65 | ), follow_redirects=True) |
| @@ -67,7 +68,7 @@ class Mat2WebTestCase(unittest.TestCase): | |||
| 67 | self.assertEqual(rv.status_code, 200) | 68 | self.assertEqual(rv.status_code, 200) |
| 68 | 69 | ||
| 69 | def test_get_upload_no_selected_file(self): | 70 | def test_get_upload_no_selected_file(self): |
| 70 | rv = self.app.post('/', | 71 | rv = self.client.post('/', |
| 71 | data=dict( | 72 | data=dict( |
| 72 | file=(io.BytesIO(b""), ''), | 73 | file=(io.BytesIO(b""), ''), |
| 73 | ), follow_redirects=True) | 74 | ), follow_redirects=True) |
| @@ -86,7 +87,7 @@ class Mat2WebTestCase(unittest.TestCase): | |||
| 86 | 'AAAAAAAAAAApIFnAAAAdGVzdC5qc29uVVQNAAfomo9d6JqPXeiaj111eAsAAQTpAwAABOkDAAB' | 87 | 'AAAAAAAAAAApIFnAAAAdGVzdC5qc29uVVQNAAfomo9d6JqPXeiaj111eAsAAQTpAwAABOkDAAB' |
| 87 | 'QSwUGAAAAAAIAAgC8AAAAwAAAAAAA' | 88 | 'QSwUGAAAAAAIAAgC8AAAAwAAAAAAA' |
| 88 | ) | 89 | ) |
| 89 | rv = self.app.post('/', | 90 | rv = self.client.post('/', |
| 90 | data=dict( | 91 | data=dict( |
| 91 | file=(io.BytesIO(zip_file_bytes), 'test.zip'), | 92 | file=(io.BytesIO(zip_file_bytes), 'test.zip'), |
| 92 | ), follow_redirects=True) | 93 | ), follow_redirects=True) |
| @@ -94,7 +95,7 @@ class Mat2WebTestCase(unittest.TestCase): | |||
| 94 | self.assertEqual(rv.status_code, 200) | 95 | self.assertEqual(rv.status_code, 200) |
| 95 | 96 | ||
| 96 | def test_get_upload_no_file_name(self): | 97 | def test_get_upload_no_file_name(self): |
| 97 | rv = self.app.post('/', | 98 | rv = self.client.post('/', |
| 98 | data=dict( | 99 | data=dict( |
| 99 | file=(io.BytesIO(b"aaa")), | 100 | file=(io.BytesIO(b"aaa")), |
| 100 | ), follow_redirects=True) | 101 | ), follow_redirects=True) |
| @@ -102,30 +103,51 @@ class Mat2WebTestCase(unittest.TestCase): | |||
| 102 | self.assertEqual(rv.status_code, 200) | 103 | self.assertEqual(rv.status_code, 200) |
| 103 | 104 | ||
| 104 | def test_get_upload_harmless_file(self): | 105 | def test_get_upload_harmless_file(self): |
| 105 | rv = self.app.post('/', | 106 | rv = self.client.post( |
| 106 | data=dict( | 107 | '/', |
| 107 | file=(io.BytesIO(b"Some text"), 'test.txt'), | 108 | data=dict( |
| 108 | ), follow_redirects=True) | 109 | file=(io.BytesIO(b"Some text"), 'test.txt'), |
| 109 | self.assertIn(b'/download/4c2e9e6da31a64c70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt', rv.data) | 110 | ), |
| 111 | follow_redirects=True | ||
| 112 | ) | ||
| 113 | download_uri = self.get_context_variable('download_uri') | ||
| 114 | self.assertIn('/test.cleaned.txt', download_uri) | ||
| 110 | self.assertEqual(rv.status_code, 200) | 115 | self.assertEqual(rv.status_code, 200) |
| 111 | self.assertNotIn('Access-Control-Allow-Origin', rv.headers) | 116 | self.assertNotIn('Access-Control-Allow-Origin', rv.headers) |
| 112 | 117 | ||
| 113 | rv = self.app.get('/download/4c2e9e6da31a64c70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt') | 118 | rv = self.client.get(download_uri) |
| 114 | self.assertEqual(rv.status_code, 200) | 119 | self.assertEqual(rv.status_code, 200) |
| 115 | 120 | ||
| 116 | rv = self.app.get('/download/4c2e9e6da31a64c70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt') | 121 | rv = self.client.get(download_uri) |
| 117 | self.assertEqual(rv.status_code, 302) | 122 | self.assertEqual(rv.status_code, 302) |
| 118 | 123 | ||
| 119 | def test_upload_wrong_hash(self): | 124 | def test_upload_wrong_hash_or_secret(self): |
| 120 | rv = self.app.post('/', | 125 | rv = self.client.post( |
| 121 | data=dict( | 126 | '/', |
| 122 | file=(io.BytesIO(b"Some text"), 'test.txt'), | 127 | data=dict( |
| 123 | ), follow_redirects=True) | 128 | file=(io.BytesIO(b"Some text"), 'test.txt'), |
| 124 | self.assertIn(b'/download/4c2e9e6da31a64c70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt', | 129 | ), |
| 125 | rv.data) | 130 | follow_redirects=True |
| 131 | ) | ||
| 132 | |||
| 133 | download_uri = self.get_context_variable('download_uri') | ||
| 134 | |||
| 135 | self.assertIn('/test.cleaned.txt', download_uri) | ||
| 136 | self.assertIn('/download', download_uri) | ||
| 126 | self.assertEqual(rv.status_code, 200) | 137 | self.assertEqual(rv.status_code, 200) |
| 127 | 138 | ||
| 128 | rv = self.app.get('/download/70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt') | 139 | uri_parts = download_uri.split("/") |
| 140 | self.assertEqual(len(uri_parts[2]), len(uri_parts[3])) | ||
| 141 | self.assertEqual(64, len(uri_parts[2])) | ||
| 142 | |||
| 143 | key_uri_parts = uri_parts | ||
| 144 | key_uri_parts[2] = '70623619c' | ||
| 145 | rv = self.client.get("/".join(key_uri_parts)) | ||
| 146 | self.assertEqual(rv.status_code, 302) | ||
| 147 | |||
| 148 | key_uri_parts = uri_parts | ||
| 149 | key_uri_parts[3] = '70623619c' | ||
| 150 | rv = self.client.get("/".join(key_uri_parts)) | ||
| 129 | self.assertEqual(rv.status_code, 302) | 151 | self.assertEqual(rv.status_code, 302) |
| 130 | 152 | ||
| 131 | @patch('matweb.file_removal_scheduler.random.randint') | 153 | @patch('matweb.file_removal_scheduler.random.randint') |
| @@ -140,19 +162,18 @@ class Mat2WebTestCase(unittest.TestCase): | |||
| 140 | ) | 162 | ) |
| 141 | app = app.test_client() | 163 | app = app.test_client() |
| 142 | 164 | ||
| 143 | request = self.app.post('/', | 165 | request = self.client.post('/', |
| 144 | data=dict( | 166 | data=dict( |
| 145 | file=(io.BytesIO(b"Some text"), 'test.txt'), | 167 | file=(io.BytesIO(b"Some text"), 'test.txt'), |
| 146 | ), follow_redirects=True) | 168 | ), follow_redirects=True) |
| 147 | self.assertEqual(request.status_code, 200) | 169 | self.assertEqual(request.status_code, 200) |
| 148 | request = app.get( | 170 | |
| 149 | b'/download/4c2e9e6da31a64c70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt' | 171 | request = app.get(self.get_context_variable('download_uri')) |
| 150 | ) | ||
| 151 | self.assertEqual(302, request.status_code) | 172 | self.assertEqual(302, request.status_code) |
| 152 | os.environ['MAT2_MAX_FILE_AGE_FOR_REMOVAL'] = '9999' | 173 | os.environ['MAT2_MAX_FILE_AGE_FOR_REMOVAL'] = '9999' |
| 153 | 174 | ||
| 154 | def test_info_page(self): | 175 | def test_info_page(self): |
| 155 | rv = self.app.get('/info') | 176 | rv = self.client.get('/info') |
| 156 | self.assertIn(b'What are metadata?', rv.data) | 177 | self.assertIn(b'What are metadata?', rv.data) |
| 157 | self.assertIn(b'.asc', rv.data) | 178 | self.assertIn(b'.asc', rv.data) |
| 158 | self.assertIn(b'.mp2', rv.data) | 179 | self.assertIn(b'.mp2', rv.data) |
diff --git a/test/test_api.py b/test/test_api.py index 36aae9d..4925d9e 100644 --- a/test/test_api.py +++ b/test/test_api.py | |||
| @@ -30,33 +30,26 @@ class Mat2APITestCase(unittest.TestCase): | |||
| 30 | del os.environ['MAT2_ALLOW_ORIGIN_WHITELIST'] | 30 | del os.environ['MAT2_ALLOW_ORIGIN_WHITELIST'] |
| 31 | 31 | ||
| 32 | def test_api_upload_valid(self): | 32 | def test_api_upload_valid(self): |
| 33 | request = self.app.post('/api/upload', | 33 | request = self.app.post( |
| 34 | data='{"file_name": "test_name.jpg", ' | 34 | '/api/upload', |
| 35 | '"file": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAf' | 35 | data='{"file_name": "test_name.jpg", ' |
| 36 | 'FcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="}', | 36 | '"file": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAf' |
| 37 | headers={'content-type': 'application/json'} | 37 | 'FcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="}', |
| 38 | ) | 38 | headers={'content-type': 'application/json'} |
| 39 | ) | ||
| 39 | self.assertEqual(request.headers['Content-Type'], 'application/json') | 40 | self.assertEqual(request.headers['Content-Type'], 'application/json') |
| 40 | self.assertEqual(request.headers['Access-Control-Allow-Origin'], 'origin1.gnu') | 41 | self.assertEqual(request.headers['Access-Control-Allow-Origin'], 'origin1.gnu') |
| 41 | self.assertEqual(request.status_code, 200) | 42 | self.assertEqual(request.status_code, 200) |
| 42 | 43 | ||
| 43 | data = request.get_json() | 44 | data = request.get_json() |
| 44 | expected = { | 45 | self.assertEqual(data['output_filename'], 'test_name.cleaned.jpg') |
| 45 | 'output_filename': 'test_name.cleaned.jpg', | 46 | self.assertEqual(data['output_filename'], 'test_name.cleaned.jpg') |
| 46 | 'mime': 'image/jpeg', | 47 | self.assertEqual(data['mime'], 'image/jpeg') |
| 47 | 'key': '81a541f9ebc0233d419d25ed39908b16f82be26a783f32d56c381559e84e6161', | 48 | self.assertEqual(len(data['secret']), 64) |
| 48 | 'meta': { | 49 | self.assertEqual(len(data['key']), 64) |
| 49 | 'BitDepth': 8, | 50 | self.assertNotEqual(data['key'], data['secret']) |
| 50 | 'ColorType': 'RGB with Alpha', | 51 | self.assertTrue('http://localhost/api/download/' in data['download_link']) |
| 51 | 'Compression': 'Deflate/Inflate', | 52 | self.assertTrue('test_name.cleaned.jpg' in data['download_link']) |
| 52 | 'Filter': 'Adaptive', | ||
| 53 | 'Interlace': 'Noninterlaced' | ||
| 54 | }, | ||
| 55 | 'meta_after': {}, | ||
| 56 | 'download_link': 'http://localhost/api/download/' | ||
| 57 | '81a541f9ebc0233d419d25ed39908b16f82be26a783f32d56c381559e84e6161/test_name.cleaned.jpg' | ||
| 58 | } | ||
| 59 | self.assertEqual(data, expected) | ||
| 60 | 53 | ||
| 61 | def test_api_upload_missing_params(self): | 54 | def test_api_upload_missing_params(self): |
| 62 | request = self.app.post('/api/upload', | 55 | request = self.app.post('/api/upload', |
| @@ -141,7 +134,6 @@ class Mat2APITestCase(unittest.TestCase): | |||
| 141 | error = request.get_json()['message'] | 134 | error = request.get_json()['message'] |
| 142 | self.assertEqual(error, 'Unable to clean application/zip') | 135 | self.assertEqual(error, 'Unable to clean application/zip') |
| 143 | 136 | ||
| 144 | |||
| 145 | def test_api_download(self): | 137 | def test_api_download(self): |
| 146 | request = self.app.post('/api/upload', | 138 | request = self.app.post('/api/upload', |
| 147 | data='{"file_name": "test_name.jpg", ' | 139 | data='{"file_name": "test_name.jpg", ' |
| @@ -152,25 +144,36 @@ class Mat2APITestCase(unittest.TestCase): | |||
| 152 | self.assertEqual(request.status_code, 200) | 144 | self.assertEqual(request.status_code, 200) |
| 153 | data = request.get_json() | 145 | data = request.get_json() |
| 154 | 146 | ||
| 155 | request = self.app.get('http://localhost/api/download/' | 147 | request = self.app.get('http://localhost/api/download/161/' |
| 156 | '81a541f9ebc0233d419d25ed39908b16f82be26a783f32d56c381559e84e6161/test name.cleaned.jpg') | 148 | '81a541f9ebc0233d419d25ed39908b16f82be26a783f32d56c381559e84e6161/test name.cleaned.jpg') |
| 157 | self.assertEqual(request.status_code, 400) | 149 | self.assertEqual(request.status_code, 400) |
| 158 | error = request.get_json()['message'] | 150 | error = request.get_json()['message'] |
| 159 | self.assertEqual(error, 'Insecure filename') | 151 | self.assertEqual(error, 'Insecure filename') |
| 160 | 152 | ||
| 161 | request = self.app.get('http://localhost/api/download/' | 153 | request = self.app.get(data['download_link'].replace('test_name', 'wrong_test')) |
| 162 | '81a541f9ebc0233d419d25ed39908b16f82be26a783f32d56c381559e84e6161/' | ||
| 163 | 'wrong_file_name.jpg') | ||
| 164 | self.assertEqual(request.status_code, 404) | 154 | self.assertEqual(request.status_code, 404) |
| 165 | error = request.get_json()['message'] | 155 | error = request.get_json()['message'] |
| 166 | self.assertEqual(error, 'File not found') | 156 | self.assertEqual(error, 'File not found') |
| 167 | 157 | ||
| 168 | request = self.app.get('http://localhost/api/download/81a541f9e/test_name.cleaned.jpg') | 158 | uri_parts = data['download_link'].split("/") |
| 159 | self.assertEqual(len(uri_parts[5]), len(uri_parts[6])) | ||
| 160 | self.assertEqual(64, len(uri_parts[5])) | ||
| 161 | |||
| 162 | key_uri_parts = uri_parts | ||
| 163 | key_uri_parts[5] = '70623619c' | ||
| 164 | request = self.app.get("/".join(key_uri_parts)) | ||
| 169 | self.assertEqual(request.status_code, 400) | 165 | self.assertEqual(request.status_code, 400) |
| 170 | 166 | ||
| 171 | error = request.get_json()['message'] | 167 | error = request.get_json()['message'] |
| 172 | self.assertEqual(error, 'The file hash does not match') | 168 | self.assertEqual(error, 'The file hash does not match') |
| 173 | 169 | ||
| 170 | key_uri_parts = uri_parts | ||
| 171 | key_uri_parts[6] = '70623619c' | ||
| 172 | request = self.app.get("/".join(key_uri_parts)) | ||
| 173 | self.assertEqual(request.status_code, 400) | ||
| 174 | error = request.get_json()['message'] | ||
| 175 | self.assertEqual(error, 'The file hash does not match') | ||
| 176 | |||
| 174 | request = self.app.head(data['download_link']) | 177 | request = self.app.head(data['download_link']) |
| 175 | self.assertEqual(request.status_code, 200) | 178 | self.assertEqual(request.status_code, 200) |
| 176 | self.assertEqual(request.headers['Content-Length'], '633') | 179 | self.assertEqual(request.headers['Content-Length'], '633') |
| @@ -205,11 +208,13 @@ class Mat2APITestCase(unittest.TestCase): | |||
| 205 | u'download_list': [ | 208 | u'download_list': [ |
| 206 | { | 209 | { |
| 207 | u'file_name': upload_one['output_filename'], | 210 | u'file_name': upload_one['output_filename'], |
| 208 | u'key': upload_one['key'] | 211 | u'key': upload_one['key'], |
| 212 | u'secret': upload_one['secret'] | ||
| 209 | }, | 213 | }, |
| 210 | { | 214 | { |
| 211 | u'file_name': upload_two['output_filename'], | 215 | u'file_name': upload_two['output_filename'], |
| 212 | u'key': upload_two['key'] | 216 | u'key': upload_two['key'], |
| 217 | u'secret': upload_two['secret'] | ||
| 213 | } | 218 | } |
| 214 | ] | 219 | ] |
| 215 | } | 220 | } |
| @@ -261,7 +266,8 @@ class Mat2APITestCase(unittest.TestCase): | |||
| 261 | u'download_list': [ | 266 | u'download_list': [ |
| 262 | { | 267 | { |
| 263 | u'file_name': 'invalid_file_name', | 268 | u'file_name': 'invalid_file_name', |
| 264 | u'key': 'invalid_key' | 269 | u'key': 'invalid_key', |
| 270 | u'secret': 'invalid_secret' | ||
| 265 | } | 271 | } |
| 266 | ] | 272 | ] |
| 267 | } | 273 | } |
| @@ -348,11 +354,13 @@ class Mat2APITestCase(unittest.TestCase): | |||
| 348 | u'download_list': [ | 354 | u'download_list': [ |
| 349 | { | 355 | { |
| 350 | u'file_name': 'invalid_file_name1', | 356 | u'file_name': 'invalid_file_name1', |
| 351 | u'key': 'invalid_key1' | 357 | u'key': 'invalid_key1', |
| 358 | u'secret': 'invalid_secret1' | ||
| 352 | }, | 359 | }, |
| 353 | { | 360 | { |
| 354 | u'file_name': 'invalid_file_name2', | 361 | u'file_name': 'invalid_file_name2', |
| 355 | u'key': 'invalid_key2' | 362 | u'key': 'invalid_key2', |
| 363 | u'secret': 'invalid_secret2' | ||
| 356 | } | 364 | } |
| 357 | ] | 365 | ] |
| 358 | } | 366 | } |
