summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--main.py2
-rw-r--r--matweb/frontend.py14
-rw-r--r--matweb/rest_api.py19
-rw-r--r--matweb/utils.py35
-rw-r--r--requirements.txt2
-rw-r--r--templates/download.html2
-rw-r--r--templates/index.html2
-rw-r--r--test/test.py89
-rw-r--r--test/test_api.py74
9 files changed, 148 insertions, 91 deletions
diff --git a/main.py b/main.py
index c21c1a1..db186d1 100644
--- a/main.py
+++ b/main.py
@@ -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>')
22def download_file(key: str, filename: str): 22def 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
15def hash_file(filepath: str) -> str: 15def 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
26def check_upload_folder(upload_folder): 32def 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
31def return_file_created_response(output_filename, mime, key, meta, meta_after, download_link): 37def 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
73def get_file_paths(filename, upload_folder): 88def 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
80def is_valid_api_download_file(filename, key, upload_folder): 95def 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
6Flask-RESTful==0.3.7 6Flask-RESTful==0.3.7
7Flask-Cors==3.0.8 7Flask-Cors==3.0.8
8Cerberus==1.3.1 8Cerberus==1.3.1
9Flask-Testing==0.8.0
10blinker==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
6import os 6import os
7 7
8from unittest.mock import patch 8from unittest.mock import patch
9from flask_testing import TestCase
9 10
10import main 11import main
11 12
12 13
13class Mat2WebTestCase(unittest.TestCase): 14class 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 }