summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml6
-rw-r--r--README.md62
-rw-r--r--docker-compose.yml1
-rw-r--r--main.py232
-rw-r--r--requirements.txt4
-rw-r--r--test/test.py (renamed from tests.py)33
-rw-r--r--test/test_api.py155
-rw-r--r--utils.py22
8 files changed, 430 insertions, 85 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a667620..9c2b39e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -23,6 +23,8 @@ tests:debian:
23 stage: test 23 stage: test
24 script: 24 script:
25 - apt-get -qqy update 25 - apt-get -qqy update
26 - apt-get -qqy install --no-install-recommends mat2 python3-flask python3-coverage 26 - apt-get -qqy install --no-install-recommends mat2 python3-flask python3-coverage python3-pip python3-setuptools
27 - python3-coverage run --branch --include main.py -m unittest discover 27 - pip3 install wheel
28 - pip3 install -r requirements.txt
29 - python3-coverage run --branch --include main.py -m unittest discover -s test
28 - python3-coverage report -m 30 - python3-coverage report -m
diff --git a/README.md b/README.md
index 0ee91ac..98b82bd 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,11 @@ Nginx is the recommended web engine, but you can also use Apache if you prefer,
52by copying [this file](https://0xacab.org/jvoisin/mat2-web/tree/master/config/apache2.config) 52by copying [this file](https://0xacab.org/jvoisin/mat2-web/tree/master/config/apache2.config)
53to your `/etc/apache2/sites-enabled/mat2-web` file. 53to your `/etc/apache2/sites-enabled/mat2-web` file.
54 54
55Then configure the environment variable: `MAT2_ALLOW_ORIGIN_WHITELIST=https://myhost1.org https://myhost2.org`
56Note that you can add multiple hosts from which you want to accept API requests. These need to be separated by
57a space.
58**IMPORTANT:** The default value if the variable is not set is: `Access-Control-Allow-Origin: *`
59
55Finally, restart uWSGI and your web server: 60Finally, restart uWSGI and your web server:
56 61
57``` 62```
@@ -85,6 +90,63 @@ the docker dev environment. Mat2-web is now accessible on your host machine at `
85Every code change triggers a restart of the app. 90Every code change triggers a restart of the app.
86If you want to add/remove dependencies you have to rebuild the container. 91If you want to add/remove dependencies you have to rebuild the container.
87 92
93# RESTful API
94
95## Upload Endpoint
96
97**Endpoint:** `/api/upload`
98
99**HTTP Verbs:** POST
100
101**Body:**
102```json
103{
104 "file_name": "my-filename.jpg",
105 "file": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
106}
107```
108
109The `file_name` parameter takes the file name.
110The `file` parameter is the base64 encoded file which will be cleaned.
111
112**Example Response:**
113```json
114{
115 "output_filename": "fancy.cleaned.jpg",
116 "key": "81a541f9ebc0233d419d25ed39908b16f82be26a783f32d56c381559e84e6161",
117 "meta": {
118 "BitDepth": 8,
119 "ColorType": "RGB with Alpha",
120 "Compression": "Deflate/Inflate",
121 "Filter": "Adaptive",
122 "Interlace": "Noninterlaced"
123 },
124 "meta_after": {},
125 "download_link": "http://localhost:5000/download/81a541f9ebc0233d419d25ed39908b16f82be26a783f32d56c381559e84e6161/fancy.cleaned.jpg"
126}
127```
128
129## Supported Extensions Endpoint
130
131**Endpoint:** `/api/extension`
132
133**HTTP Verbs:** GET
134
135**Example Response (shortened):**
136```json
137[
138 ".asc",
139 ".avi",
140 ".bat",
141 ".bmp",
142 ".brf",
143 ".c",
144 ".css",
145 ".docx",
146 ".epub"
147]
148```
149
88# Custom templates 150# Custom templates
89 151
90You can override the default templates from `templates/` by putting replacements 152You can override the default templates from `templates/` by putting replacements
diff --git a/docker-compose.yml b/docker-compose.yml
index c5f8b32..fda006e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,6 +5,7 @@ services:
5 environment: 5 environment:
6 - FLASK_APP=main.py 6 - FLASK_APP=main.py
7 - FLASK_ENV=development 7 - FLASK_ENV=development
8 - MAT2_ALLOW_ORIGIN_WHITELIST=*
8 ports: 9 ports:
9 - "5000:5000" 10 - "5000:5000"
10 volumes: 11 volumes:
diff --git a/main.py b/main.py
index ab2ba41..24a15ba 100644
--- a/main.py
+++ b/main.py
@@ -1,107 +1,191 @@
1import os 1import os
2import hashlib
3import hmac 2import hmac
4import mimetypes as mtype 3import mimetypes as mtype
4import jinja2
5import base64
6import io
7import binascii
8import utils
5 9
6from libmat2 import parser_factory 10from libmat2 import parser_factory
11from flask import Flask, flash, request, redirect, url_for, render_template, send_from_directory, after_this_request
12from flask_restful import Resource, Api, reqparse, abort
13from werkzeug.utils import secure_filename
14from werkzeug.datastructures import FileStorage
15from flask_cors import CORS
16from urllib.parse import urljoin
7 17
8from flask import Flask, flash, request, redirect, url_for, render_template
9from flask import send_from_directory, after_this_request
10import jinja2
11 18
12from werkzeug.utils import secure_filename 19def create_app(test_config=None):
20 app = Flask(__name__)
21 app.config['SECRET_KEY'] = os.urandom(32)
22 app.config['UPLOAD_FOLDER'] = './uploads/'
23 app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB
24 app.config['CUSTOM_TEMPLATES_DIR'] = 'custom_templates'
13 25
26 app.jinja_loader = jinja2.ChoiceLoader([ # type: ignore
27 jinja2.FileSystemLoader(app.config['CUSTOM_TEMPLATES_DIR']),
28 app.jinja_loader,
29 ])
14 30
15app = Flask(__name__) 31 api = Api(app)
16app.config['SECRET_KEY'] = os.urandom(32) 32 CORS(app, resources={r"/api/*": {"origins": utils.get_allow_origin_header_value()}})
17app.config['UPLOAD_FOLDER'] = './uploads/'
18app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB
19app.config['CUSTOM_TEMPLATES_DIR'] = 'custom_templates'
20 33
21app.jinja_loader = jinja2.ChoiceLoader([ # type: ignore 34 @app.route('/download/<string:key>/<string:filename>')
22 jinja2.FileSystemLoader(app.config['CUSTOM_TEMPLATES_DIR']), 35 def download_file(key:str, filename:str):
23 app.jinja_loader, 36 if filename != secure_filename(filename):
24 ]) 37 return redirect(url_for('upload_file'))
38
39 complete_path, filepath = get_file_paths(filename)
40
41 if not os.path.exists(complete_path):
42 return redirect(url_for('upload_file'))
43 if hmac.compare_digest(utils.hash_file(complete_path), key) is False:
44 return redirect(url_for('upload_file'))
25 45
26def __hash_file(filepath: str) -> str: 46 @after_this_request
27 sha256 = hashlib.sha256() 47 def remove_file(response):
28 with open(filepath, 'rb') as f: 48 os.remove(complete_path)
29 while True: 49 return response
30 data = f.read(65536) # read the file by chunk of 64k 50 return send_from_directory(app.config['UPLOAD_FOLDER'], filepath)
31 if not data:
32 break
33 sha256.update(data)
34 return sha256.hexdigest()
35 51
52 @app.route('/', methods=['GET', 'POST'])
53 def upload_file():
54 utils.check_upload_folder(app.config['UPLOAD_FOLDER'])
55 mimetypes = get_supported_extensions()
36 56
37@app.route('/download/<string:key>/<string:filename>') 57 if request.method == 'POST':
38def download_file(key:str, filename:str): 58 if 'file' not in request.files: # check if the post request has the file part
39 if filename != secure_filename(filename): 59 flash('No file part')
40 return redirect(url_for('upload_file')) 60 return redirect(request.url)
41 61
42 filepath = secure_filename(filename) 62 uploaded_file = request.files['file']
63 if not uploaded_file.filename:
64 flash('No selected file')
65 return redirect(request.url)
43 66
44 complete_path = os.path.join(app.config['UPLOAD_FOLDER'], filepath) 67 filename, filepath = save_file(uploaded_file)
45 if not os.path.exists(complete_path): 68 parser, mime = get_file_parser(filepath)
46 return redirect(url_for('upload_file'))
47 if hmac.compare_digest(__hash_file(complete_path), key) is False:
48 print('hash: %s, key: %s' % (__hash_file(complete_path), key))
49 return redirect(url_for('upload_file'))
50 69
51 @after_this_request 70 if parser is None:
52 def remove_file(response): 71 flash('The type %s is not supported' % mime)
53 os.remove(complete_path) 72 return redirect(url_for('upload_file'))
54 return response
55 return send_from_directory(app.config['UPLOAD_FOLDER'], filepath)
56 73
57@app.route('/', methods=['GET', 'POST']) 74 meta = parser.get_meta()
58def upload_file():
59 if not os.path.exists(app.config['UPLOAD_FOLDER']):
60 os.mkdir(app.config['UPLOAD_FOLDER'])
61 75
62 mimetypes = set() 76 if parser.remove_all() is not True:
63 for parser in parser_factory._get_parsers(): 77 flash('Unable to clean %s' % mime)
64 for m in parser.mimetypes: 78 return redirect(url_for('upload_file'))
65 mimetypes |= set(mtype.guess_all_extensions(m, strict=False))
66 # since `guess_extension` might return `None`, we need to filter it out
67 mimetypes = sorted(filter(None, mimetypes))
68 79
69 if request.method == 'POST': 80 key, meta_after, output_filename = cleanup(parser, filepath)
70 if 'file' not in request.files: # check if the post request has the file part 81
71 flash('No file part') 82 return render_template(
72 return redirect(request.url) 83 'download.html', mimetypes=mimetypes, meta=meta, filename=output_filename, meta_after=meta_after, key=key
73 uploaded_file = request.files['file'] 84 )
74 if not uploaded_file.filename: 85
75 flash('No selected file') 86 max_file_size = int(app.config['MAX_CONTENT_LENGTH'] / 1024 / 1024)
76 return redirect(request.url) 87 return render_template('index.html', max_file_size=max_file_size, mimetypes=mimetypes)
77 filename = secure_filename(uploaded_file.filename) 88
89 def get_supported_extensions():
90 extensions = set()
91 for parser in parser_factory._get_parsers():
92 for m in parser.mimetypes:
93 extensions |= set(mtype.guess_all_extensions(m, strict=False))
94 # since `guess_extension` might return `None`, we need to filter it out
95 return sorted(filter(None, extensions))
96
97 def save_file(file):
98 filename = secure_filename(file.filename)
78 filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) 99 filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
79 uploaded_file.save(os.path.join(filepath)) 100 file.save(os.path.join(filepath))
101 return filename, filepath
80 102
103 def get_file_parser(filepath: str):
81 parser, mime = parser_factory.get_parser(filepath) 104 parser, mime = parser_factory.get_parser(filepath)
82 if parser is None: 105 return parser, mime
83 flash('The type %s is not supported' % mime)
84 return redirect(url_for('upload_file'))
85
86 meta = parser.get_meta()
87 106
88 if parser.remove_all() is not True: 107 def cleanup(parser, filepath):
89 flash('Unable to clean %s' % mime)
90 return redirect(url_for('upload_file'))
91 output_filename = os.path.basename(parser.output_filename) 108 output_filename = os.path.basename(parser.output_filename)
92
93 # Get metadata after cleanup
94 parser, _ = parser_factory.get_parser(parser.output_filename) 109 parser, _ = parser_factory.get_parser(parser.output_filename)
95 meta_after = parser.get_meta() 110 meta_after = parser.get_meta()
96 os.remove(filepath) 111 os.remove(filepath)
97 112
98 key = __hash_file(os.path.join(app.config['UPLOAD_FOLDER'], output_filename)) 113 key = utils.hash_file(os.path.join(app.config['UPLOAD_FOLDER'], output_filename))
114 return key, meta_after, output_filename
115
116 def get_file_paths(filename):
117 filepath = secure_filename(filename)
118
119 complete_path = os.path.join(app.config['UPLOAD_FOLDER'], filepath)
120 return complete_path, filepath
121
122 class APIUpload(Resource):
123
124 def post(self):
125 utils.check_upload_folder(app.config['UPLOAD_FOLDER'])
126 req_parser = reqparse.RequestParser()
127 req_parser.add_argument('file_name', type=str, required=True, help='Post parameter is not specified: file_name')
128 req_parser.add_argument('file', type=str, required=True, help='Post parameter is not specified: file')
129
130 args = req_parser.parse_args()
131 try:
132 file_data = base64.b64decode(args['file'])
133 except binascii.Error as err:
134 abort(400, message='Failed decoding file: ' + str(err))
135
136 file = FileStorage(stream=io.BytesIO(file_data), filename=args['file_name'])
137 filename, filepath = save_file(file)
138 parser, mime = get_file_parser(filepath)
139
140 if parser is None:
141 abort(415, message='The type %s is not supported' % mime)
142
143 meta = parser.get_meta()
144 if not parser.remove_all():
145 abort(500, message='Unable to clean %s' % mime)
146
147 key, meta_after, output_filename = cleanup(parser, filepath)
148 return {
149 'output_filename': output_filename,
150 'key': key,
151 'meta': meta,
152 'meta_after': meta_after,
153 'download_link': urljoin(request.host_url, '%s/%s/%s/%s' % ('api', 'download', key, output_filename))
154 }
155
156 class APIDownload(Resource):
157 def get(self, key: str, filename: str):
158
159 if filename != secure_filename(filename):
160 abort(400, message='Insecure filename')
161
162 complete_path, filepath = get_file_paths(filename)
163
164 if not os.path.exists(complete_path):
165 abort(404, message='File not found')
166 return redirect(url_for('upload_file'))
167
168 if hmac.compare_digest(utils.hash_file(complete_path), key) is False:
169 abort(400, message='The file hash does not match')
170 return redirect(url_for('upload_file'))
171
172 @after_this_request
173 def remove_file(response):
174 os.remove(complete_path)
175 return response
176
177 return send_from_directory(app.config['UPLOAD_FOLDER'], filepath)
178
179 class APIMSupportedExtensions(Resource):
180 def get(self):
181 return get_supported_extensions()
99 182
100 return render_template('download.html', mimetypes=mimetypes, meta=meta, filename=output_filename, meta_after=meta_after, key=key) 183 api.add_resource(APIUpload, '/api/upload')
184 api.add_resource(APIDownload, '/api/download/<string:key>/<string:filename>')
185 api.add_resource(APIMSupportedExtensions, '/api/extension')
101 186
102 max_file_size = int(app.config['MAX_CONTENT_LENGTH'] / 1024 / 1024) 187 return app
103 return render_template('index.html', max_file_size=max_file_size, mimetypes=mimetypes)
104 188
105 189
106if __name__ == '__main__': # pragma: no cover 190if __name__ == '__main__': # pragma: no cover
107 app.run() 191 create_app().run()
diff --git a/requirements.txt b/requirements.txt
index 7cab5aa..8796aaa 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,4 +2,6 @@ mutagen==1.42.0
2ffmpeg==1.4 2ffmpeg==1.4
3bubblewrap==1.2.0 3bubblewrap==1.2.0
4mat2==0.9.0 4mat2==0.9.0
5flask==1.0.3 \ No newline at end of file 5flask==1.0.3
6Flask-RESTful==0.3.7
7Flask-Cors==3.0.8 \ No newline at end of file
diff --git a/tests.py b/test/test.py
index 4c85a74..34245d9 100644
--- a/tests.py
+++ b/test/test.py
@@ -2,18 +2,24 @@ import unittest
2import tempfile 2import tempfile
3import shutil 3import shutil
4import io 4import io
5import os
5 6
6import main 7import main
7 8
8 9
9class FlaskrTestCase(unittest.TestCase): 10class Mat2WebTestCase(unittest.TestCase):
10 def setUp(self): 11 def setUp(self):
11 main.app.testing = True 12 os.environ.setdefault('MAT2_ALLOW_ORIGIN_WHITELIST', 'origin1.gnu origin2.gnu')
12 main.app.config['UPLOAD_FOLDER'] = tempfile.mkdtemp() 13 app = main.create_app()
13 self.app = main.app.test_client() 14 self.upload_folder = tempfile.mkdtemp()
15 app.config.update(
16 TESTING=True,
17 UPLOAD_FOLDER=self.upload_folder
18 )
19 self.app = app.test_client()
14 20
15 def tearDown(self): 21 def tearDown(self):
16 shutil.rmtree(main.app.config['UPLOAD_FOLDER']) 22 shutil.rmtree(self.upload_folder)
17 23
18 def test_get_root(self): 24 def test_get_root(self):
19 rv = self.app.get('/') 25 rv = self.app.get('/')
@@ -36,7 +42,6 @@ class FlaskrTestCase(unittest.TestCase):
36 rv = self.app.get('/download/1337/non_existant') 42 rv = self.app.get('/download/1337/non_existant')
37 self.assertEqual(rv.status_code, 302) 43 self.assertEqual(rv.status_code, 302)
38 44
39
40 def test_get_upload_without_file(self): 45 def test_get_upload_without_file(self):
41 rv = self.app.post('/') 46 rv = self.app.post('/')
42 self.assertEqual(rv.status_code, 302) 47 self.assertEqual(rv.status_code, 302)
@@ -60,12 +65,11 @@ class FlaskrTestCase(unittest.TestCase):
60 def test_get_upload_no_file_name(self): 65 def test_get_upload_no_file_name(self):
61 rv = self.app.post('/', 66 rv = self.app.post('/',
62 data=dict( 67 data=dict(
63 file=(io.BytesIO(b"aaa"), ''), 68 file=(io.BytesIO(b"aaa")),
64 ), follow_redirects=True) 69 ), follow_redirects=True)
65 self.assertIn(b'No file part', rv.data) 70 self.assertIn(b'No file part', rv.data)
66 self.assertEqual(rv.status_code, 200) 71 self.assertEqual(rv.status_code, 200)
67 72
68
69 def test_get_upload_harmless_file(self): 73 def test_get_upload_harmless_file(self):
70 rv = self.app.post('/', 74 rv = self.app.post('/',
71 data=dict( 75 data=dict(
@@ -73,6 +77,7 @@ class FlaskrTestCase(unittest.TestCase):
73 ), follow_redirects=True) 77 ), follow_redirects=True)
74 self.assertIn(b'/download/4c2e9e6da31a64c70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt', rv.data) 78 self.assertIn(b'/download/4c2e9e6da31a64c70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt', rv.data)
75 self.assertEqual(rv.status_code, 200) 79 self.assertEqual(rv.status_code, 200)
80 self.assertNotIn('Access-Control-Allow-Origin', rv.headers)
76 81
77 rv = self.app.get('/download/4c2e9e6da31a64c70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt') 82 rv = self.app.get('/download/4c2e9e6da31a64c70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt')
78 self.assertEqual(rv.status_code, 200) 83 self.assertEqual(rv.status_code, 200)
@@ -80,6 +85,18 @@ class FlaskrTestCase(unittest.TestCase):
80 rv = self.app.get('/download/4c2e9e6da31a64c70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt') 85 rv = self.app.get('/download/4c2e9e6da31a64c70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt')
81 self.assertEqual(rv.status_code, 302) 86 self.assertEqual(rv.status_code, 302)
82 87
88 def test_upload_wrong_hash(self):
89 rv = self.app.post('/',
90 data=dict(
91 file=(io.BytesIO(b"Some text"), 'test.txt'),
92 ), follow_redirects=True)
93 self.assertIn(b'/download/4c2e9e6da31a64c70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt',
94 rv.data)
95 self.assertEqual(rv.status_code, 200)
96
97 rv = self.app.get('/download/70623619c449a040968cdbea85945bf384fa30ed2d5d24fa3/test.cleaned.txt')
98 self.assertEqual(rv.status_code, 302)
99
83 100
84if __name__ == '__main__': 101if __name__ == '__main__':
85 unittest.main() 102 unittest.main()
diff --git a/test/test_api.py b/test/test_api.py
new file mode 100644
index 0000000..d913cc4
--- /dev/null
+++ b/test/test_api.py
@@ -0,0 +1,155 @@
1import unittest
2import tempfile
3import shutil
4import json
5import os
6
7import main
8
9
10class Mat2APITestCase(unittest.TestCase):
11 def setUp(self):
12 os.environ.setdefault('MAT2_ALLOW_ORIGIN_WHITELIST', 'origin1.gnu origin2.gnu')
13 app = main.create_app()
14 self.upload_folder = tempfile.mkdtemp()
15 app.config.update(
16 TESTING=True,
17 UPLOAD_FOLDER=self.upload_folder
18 )
19 self.app = app.test_client()
20
21 def tearDown(self):
22 shutil.rmtree(self.upload_folder)
23 if os.environ.get('MAT2_ALLOW_ORIGIN_WHITELIST'):
24 del os.environ['MAT2_ALLOW_ORIGIN_WHITELIST']
25
26 def test_api_upload_valid(self):
27 request = self.app.post('/api/upload',
28 data='{"file_name": "test_name.jpg", '
29 '"file": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAf'
30 'FcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="}',
31 headers={'content-type': 'application/json'}
32 )
33 self.assertEqual(request.headers['Content-Type'], 'application/json')
34 self.assertEqual(request.headers['Access-Control-Allow-Origin'], 'origin1.gnu')
35 self.assertEqual(request.status_code, 200)
36
37 data = json.loads(request.data.decode('utf-8'))
38 expected = {
39 'output_filename': 'test_name.cleaned.jpg',
40 'key': '81a541f9ebc0233d419d25ed39908b16f82be26a783f32d56c381559e84e6161',
41 'meta': {
42 'BitDepth': 8,
43 'ColorType': 'RGB with Alpha',
44 'Compression': 'Deflate/Inflate',
45 'Filter': 'Adaptive',
46 'Interlace': 'Noninterlaced'
47 },
48 'meta_after': {},
49 'download_link': 'http://localhost/api/download/'
50 '81a541f9ebc0233d419d25ed39908b16f82be26a783f32d56c381559e84e6161/test_name.cleaned.jpg'
51 }
52 self.assertEqual(data, expected)
53
54 def test_api_upload_missing_params(self):
55 request = self.app.post('/api/upload',
56 data='{"file_name": "test_name.jpg"}',
57 headers={'content-type': 'application/json'}
58 )
59 self.assertEqual(request.headers['Content-Type'], 'application/json')
60
61 self.assertEqual(request.status_code, 400)
62 error = json.loads(request.data.decode('utf-8'))['message']
63 self.assertEqual(error['file'], 'Post parameter is not specified: file')
64
65 request = self.app.post('/api/upload',
66 data='{"file_name": "test_name.jpg", "file": "invalid base46 string"}',
67 headers={'content-type': 'application/json'}
68 )
69 self.assertEqual(request.headers['Content-Type'], 'application/json')
70
71 self.assertEqual(request.status_code, 400)
72 error = json.loads(request.data.decode('utf-8'))['message']
73 self.assertEqual(error, 'Failed decoding file: Incorrect padding')
74
75 def test_api_not_supported(self):
76 request = self.app.post('/api/upload',
77 data='{"file_name": "test_name.pdf", '
78 '"file": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAf'
79 'FcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="}',
80 headers={'content-type': 'application/json'}
81 )
82 self.assertEqual(request.headers['Content-Type'], 'application/json')
83 self.assertEqual(request.status_code, 415)
84
85 error = json.loads(request.data.decode('utf-8'))['message']
86 self.assertEqual(error, 'The type application/pdf is not supported')
87
88 def test_api_supported_extensions(self):
89 rv = self.app.get('/api/extension')
90 self.assertEqual(rv.status_code, 200)
91 self.assertEqual(rv.headers['Content-Type'], 'application/json')
92 self.assertEqual(rv.headers['Access-Control-Allow-Origin'], 'origin1.gnu')
93
94 extensions = json.loads(rv.data.decode('utf-8'))
95 self.assertIn('.pot', extensions)
96 self.assertIn('.asc', extensions)
97 self.assertIn('.png', extensions)
98 self.assertIn('.zip', extensions)
99
100 def test_api_cors_not_set(self):
101 del os.environ['MAT2_ALLOW_ORIGIN_WHITELIST']
102 app = main.create_app()
103 app.config.update(
104 TESTING=True
105 )
106 app = app.test_client()
107
108 rv = app.get('/api/extension')
109 self.assertEqual(rv.headers['Access-Control-Allow-Origin'], '*')
110
111 def test_api_cors(self):
112 rv = self.app.get('/api/extension')
113 self.assertEqual(rv.headers['Access-Control-Allow-Origin'], 'origin1.gnu')
114
115 rv = self.app.get('/api/extension', headers={'Origin': 'origin2.gnu'})
116 self.assertEqual(rv.headers['Access-Control-Allow-Origin'], 'origin2.gnu')
117
118 rv = self.app.get('/api/extension', headers={'Origin': 'origin1.gnu'})
119 self.assertEqual(rv.headers['Access-Control-Allow-Origin'], 'origin1.gnu')
120
121 def test_api_download(self):
122 request = self.app.post('/api/upload',
123 data='{"file_name": "test_name.jpg", '
124 '"file": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAf'
125 'FcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="}',
126 headers={'content-type': 'application/json'}
127 )
128 self.assertEqual(request.status_code, 200)
129 data = json.loads(request.data.decode('utf-8'))
130
131 request = self.app.get('http://localhost/api/download/'
132 '81a541f9ebc0233d419d25ed39908b16f82be26a783f32d56c381559e84e6161/test name.cleaned.jpg')
133 self.assertEqual(request.status_code, 400)
134 error = json.loads(request.data.decode('utf-8'))['message']
135 self.assertEqual(error, 'Insecure filename')
136
137 request = self.app.get('http://localhost/api/download/'
138 '81a541f9ebc0233d419d25ed39908b16f82be26a783f32d56c381559e84e6161/'
139 'wrong_file_name.jpg')
140 self.assertEqual(request.status_code, 404)
141 error = json.loads(request.data.decode('utf-8'))['message']
142 self.assertEqual(error, 'File not found')
143
144 request = self.app.get('http://localhost/api/download/81a541f9e/test_name.cleaned.jpg')
145 self.assertEqual(request.status_code, 400)
146
147 error = json.loads(request.data.decode('utf-8'))['message']
148 self.assertEqual(error, 'The file hash does not match')
149
150 request = self.app.get(data['download_link'])
151 self.assertEqual(request.status_code, 200)
152
153
154if __name__ == '__main__':
155 unittest.main()
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..fb2fb08
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,22 @@
1import os
2import hashlib
3
4
5def get_allow_origin_header_value():
6 return os.environ.get('MAT2_ALLOW_ORIGIN_WHITELIST', '*').split(" ")
7
8
9def hash_file(filepath: str) -> str:
10 sha256 = hashlib.sha256()
11 with open(filepath, 'rb') as f:
12 while True:
13 data = f.read(65536) # read the file by chunk of 64k
14 if not data:
15 break
16 sha256.update(data)
17 return sha256.hexdigest()
18
19
20def check_upload_folder(upload_folder):
21 if not os.path.exists(upload_folder):
22 os.mkdir(upload_folder) \ No newline at end of file