Repository: nextcloud/appstore

Issue:
Upload of app with invalid archive fails with 500
The UI doesn't reflect if there happens a 500 during upload of an invalid archive:

```
Traceback (most recent call last):
  File "/usr/lib/python3.5/tarfile.py", line 1629, in gzopen
    t = cls.taropen(name, mode, fileobj, **kwargs)
  File "/usr/lib/python3.5/tarfile.py", line 1605, in taropen
    return cls(name, mode, fileobj, **kwargs)
  File "/usr/lib/python3.5/tarfile.py", line 1470, in __init__
    self.firstmember = self.next()
  File "/usr/lib/python3.5/tarfile.py", line 2279, in next
    tarinfo = self.tarinfo.fromtarfile(self)
  File "/usr/lib/python3.5/tarfile.py", line 1082, in fromtarfile
    buf = tarfile.fileobj.read(BLOCKSIZE)
  File "/usr/lib/python3.5/gzip.py", line 274, in read
    return self._buffer.read(size)
  File "/usr/lib/python3.5/_compression.py", line 68, in readinto
    data = self.read(len(byte_view))
  File "/usr/lib/python3.5/gzip.py", line 461, in read
    if not self._read_gzip_header():
  File "/usr/lib/python3.5/gzip.py", line 409, in _read_gzip_header
    raise OSError('Not a gzipped file (%r)' % magic)
OSError: Not a gzipped file (b'us')

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/appstore/venv/lib/python3.5/site-packages/django/core/handlers/exception.py", line 35, in inner
    response = get_response(request)
  File "/opt/appstore/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 128, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/opt/appstore/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 126, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/opt/appstore/venv/lib/python3.5/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/opt/appstore/venv/lib/python3.5/site-packages/django/views/generic/base.py", line 69, in view
    return self.dispatch(request, *args, **kwargs)
  File "/opt/appstore/venv/lib/python3.5/site-packages/rest_framework/views.py", line 483, in dispatch
    response = self.handle_exception(exc)
  File "/opt/appstore/venv/lib/python3.5/site-packages/rest_framework/views.py", line 443, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/opt/appstore/venv/lib/python3.5/site-packages/rest_framework/views.py", line 480, in dispatch
    response = handler(request, *args, **kwargs)
  File "/opt/appstore/nextcloudappstore/api/v1/views.py", line 181, in post
    info, data = provider.get_release_info(url, is_nightly)
  File "/opt/appstore/nextcloudappstore/api/v1/release/provider.py", line 34, in get_release_info
    meta = self.extractor.extract_app_metadata(download.filename)
  File "/opt/appstore/nextcloudappstore/api/v1/release/parser.py", line 74, in extract_app_metadata
    with tarfile.open(archive_path, 'r:gz') as tar:  # type: ignore
  File "/usr/lib/python3.5/tarfile.py", line 1575, in open
    return func(name, filemode, fileobj, **kwargs)
  File "/usr/lib/python3.5/tarfile.py", line 1633, in gzopen
    raise ReadError("not a gzip file")
tarfile.ReadError: not a gzip file
Internal Server Error: /api/v1/apps/releases
```

cc @BernhardPosselt @adsworth 

Found by @violoncelloCH in https://github.com/nextcloud/user_external/pull/26#issuecomment-451886622


Discussion:
And by @skjnldsv today
Ah, should be a 400

Resolving patch:
diff --git a/nextcloudappstore/api/v1/release/parser.py b/nextcloudappstore/api/v1/release/parser.py
--- a/nextcloudappstore/api/v1/release/parser.py
+++ b/nextcloudappstore/api/v1/release/parser.py
@@ -64,6 +64,7 @@ def extract_app_metadata(self, archive_path: str) -> AppMetaData:
         :raises InvalidAppPackageStructureException: if the first level folder
         does not equal the app_id or no info.xml file could be found in the
         appinfo folder
+        :raises: UnsupportedAppArchiveException if it's not an archive
         :return: the info.xml, database.xml, the app id and the changelog as
         string
         """
@@ -71,9 +72,14 @@ def extract_app_metadata(self, archive_path: str) -> AppMetaData:
             msg = '%s is not a valid tar.gz archive ' % archive_path
             raise UnsupportedAppArchiveException(msg)
 
-        with tarfile.open(archive_path, 'r:gz') as tar:  # type: ignore
-            result = self._parse_archive(tar)
-        return result
+        try:
+            with tarfile.open(archive_path, 'r:gz') as tar:  # type: ignore
+                result = self._parse_archive(tar)
+            return result
+        # for some reason there are still various errors possible although we
+        # checked for issues with tarfile.is_tarfile
+        except tarfile.ReadError as e:
+            raise UnsupportedAppArchiveException(str(e))
 
     def _parse_archive(self, tar: Any) -> AppMetaData:
         app_id = find_app_id(tar, self.app_folder_regex)


Test patch:
diff --git a/nextcloudappstore/api/v1/tests/data/archives/notgzipped.tar.gz b/nextcloudappstore/api/v1/tests/data/archives/notgzipped.tar.gz
new file mode 100644
Binary files /dev/null and b/nextcloudappstore/api/v1/tests/data/archives/notgzipped.tar.gz differ
diff --git a/nextcloudappstore/api/v1/tests/test_parser.py b/nextcloudappstore/api/v1/tests/test_parser.py
--- a/nextcloudappstore/api/v1/tests/test_parser.py
+++ b/nextcloudappstore/api/v1/tests/test_parser.py
@@ -108,6 +108,12 @@ def test_parse_invalid_elements(self):
                                self.config.pre_info_xslt,
                                self.config.info_xslt)
 
+    def test_parse_invalid_archive(self):
+        path = self.get_path('data/archives/notgzipped.tar.gz')
+        extractor = GunZipAppMetadataExtractor(self.config)
+        with (self.assertRaises(UnsupportedAppArchiveException)):
+            extractor.extract_app_metadata(path)
+
     def test_parse_minimal_nextcloud(self):
         xml = self._get_contents('data/infoxmls/nextcloud.xml')
         result = parse_app_metadata(xml, self.config.info_schema,