unzip-http 0.7__tar.gz → 0.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {unzip_http-0.7/unzip_http.egg-info → unzip_http-0.9}/PKG-INFO +1 -1
- {unzip_http-0.7 → unzip_http-0.9}/pyproject.toml +1 -1
- unzip_http-0.9/tests/test_unzip_http.py +37 -0
- {unzip_http-0.7 → unzip_http-0.9/unzip_http.egg-info}/PKG-INFO +1 -1
- {unzip_http-0.7 → unzip_http-0.9}/unzip_http.egg-info/SOURCES.txt +1 -0
- {unzip_http-0.7 → unzip_http-0.9}/unzip_http.py +25 -5
- {unzip_http-0.7 → unzip_http-0.9}/LICENSE-mit.txt +0 -0
- {unzip_http-0.7 → unzip_http-0.9}/MANIFEST.in +0 -0
- {unzip_http-0.7 → unzip_http-0.9}/README.md +0 -0
- {unzip_http-0.7 → unzip_http-0.9}/setup.cfg +0 -0
- {unzip_http-0.7 → unzip_http-0.9}/unzip_http.egg-info/dependency_links.txt +0 -0
- {unzip_http-0.7 → unzip_http-0.9}/unzip_http.egg-info/entry_points.txt +0 -0
- {unzip_http-0.7 → unzip_http-0.9}/unzip_http.egg-info/requires.txt +0 -0
- {unzip_http-0.7 → unzip_http-0.9}/unzip_http.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import pathlib
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import unzip_http
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fake_rzf(contents):
|
|
11
|
+
rzf = unzip_http.RemoteZipFile.__new__(unzip_http.RemoteZipFile)
|
|
12
|
+
rzf.open = lambda fn: io.BytesIO(contents)
|
|
13
|
+
return rzf
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.parametrize('member', ['../escaped.txt', '/tmp/absolute.txt', 'sub/../../escaped.txt'])
|
|
17
|
+
def test_extract_refuses_escaping_member(tmp_path, member):
|
|
18
|
+
target = tmp_path/'target'
|
|
19
|
+
target.mkdir()
|
|
20
|
+
with pytest.raises(Exception, match='unsafe path'):
|
|
21
|
+
fake_rzf(b'x').extract(member, path=target)
|
|
22
|
+
assert list(tmp_path.rglob('*')) == [target]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_extract_writes_inside_target(tmp_path):
|
|
26
|
+
fake_rzf(b'hello').extract('sub/ok.txt', path=tmp_path)
|
|
27
|
+
assert (tmp_path/'sub'/'ok.txt').read_bytes() == b'hello'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_cli_full_filepaths_refuses_escaping_member(tmp_path, monkeypatch):
|
|
31
|
+
monkeypatch.chdir(tmp_path)
|
|
32
|
+
monkeypatch.setattr(unzip_http, 'extract_one', lambda outfile, rzf, f, ofname: outfile.write(b'x'))
|
|
33
|
+
f = SimpleNamespace(filename='../escaped.txt', compress_size=1)
|
|
34
|
+
args = SimpleNamespace(files=['*'], stdout=False, full_filepaths=True)
|
|
35
|
+
with pytest.raises(Exception, match='unsafe path'):
|
|
36
|
+
unzip_http.download_file(f, None, args)
|
|
37
|
+
assert not (tmp_path.parent/'escaped.txt').exists()
|
|
@@ -56,7 +56,7 @@ import pathlib
|
|
|
56
56
|
import urllib.parse
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
__version__ = '0.
|
|
59
|
+
__version__ = '0.9'
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def error(s):
|
|
@@ -117,6 +117,14 @@ class RemoteZipInfo:
|
|
|
117
117
|
i += fieldsz
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
def safe_extract_path(base, member):
|
|
121
|
+
base = pathlib.Path(base).resolve()
|
|
122
|
+
target = (base / member).resolve()
|
|
123
|
+
if base != target and base not in target.parents:
|
|
124
|
+
raise Exception(f'refusing unsafe path in zip member: {member!r}')
|
|
125
|
+
return target
|
|
126
|
+
|
|
127
|
+
|
|
120
128
|
class RemoteZipFile:
|
|
121
129
|
fmt_eocd = '<IHHHHIIH' # end of central directory
|
|
122
130
|
fmt_eocd64 = '<IQHHIIQQQQ' # end of central directory ZIP64
|
|
@@ -129,6 +137,11 @@ class RemoteZipFile:
|
|
|
129
137
|
import urllib3
|
|
130
138
|
self.url = url
|
|
131
139
|
self.http = urllib3.PoolManager()
|
|
140
|
+
self.default_headers = {
|
|
141
|
+
'User-Agent': f'unzip-http/{__version__}',
|
|
142
|
+
'Accept': '*/*',
|
|
143
|
+
'Connection': 'keep-alive',
|
|
144
|
+
}
|
|
132
145
|
self.zip_size = 0
|
|
133
146
|
|
|
134
147
|
def __enter__(self):
|
|
@@ -150,7 +163,7 @@ class RemoteZipFile:
|
|
|
150
163
|
return list(r.filename for r in self.infoiter())
|
|
151
164
|
|
|
152
165
|
def infoiter(self):
|
|
153
|
-
resp = self.
|
|
166
|
+
resp = self._request('HEAD')
|
|
154
167
|
if not (200 <= resp.status <= 299):
|
|
155
168
|
error(f'cannot open URL: HTTP status {resp.status}')
|
|
156
169
|
|
|
@@ -223,10 +236,10 @@ class RemoteZipFile:
|
|
|
223
236
|
|
|
224
237
|
path = path or pathlib.Path('.')
|
|
225
238
|
|
|
226
|
-
outpath = path
|
|
239
|
+
outpath = safe_extract_path(path, member)
|
|
227
240
|
os.makedirs(outpath.parent, exist_ok=True)
|
|
228
241
|
with self.open(member) as fpin:
|
|
229
|
-
with open(
|
|
242
|
+
with open(outpath, mode='wb') as fpout:
|
|
230
243
|
while True:
|
|
231
244
|
r = fpin.read(65536)
|
|
232
245
|
if not r:
|
|
@@ -237,8 +250,14 @@ class RemoteZipFile:
|
|
|
237
250
|
for fn in members or self.namelist():
|
|
238
251
|
self.extract(fn, path, pwd=pwd)
|
|
239
252
|
|
|
253
|
+
def _request(self, method, headers=None, preload_content=True):
|
|
254
|
+
h = dict(self.default_headers)
|
|
255
|
+
if headers:
|
|
256
|
+
h.update(headers)
|
|
257
|
+
return self.http.request(method, self.url, headers=h, preload_content=preload_content)
|
|
258
|
+
|
|
240
259
|
def get_range(self, start, n):
|
|
241
|
-
return self.
|
|
260
|
+
return self._request('GET', headers={'Range': f'bytes={start}-{start+n-1}'}, preload_content=False)
|
|
242
261
|
|
|
243
262
|
def matching_files(self, *globs):
|
|
244
263
|
for f in self.files.values():
|
|
@@ -354,6 +373,7 @@ def download_file(f, rzf, args):
|
|
|
354
373
|
else:
|
|
355
374
|
path = pathlib.Path(f.filename)
|
|
356
375
|
if args.full_filepaths:
|
|
376
|
+
path = safe_extract_path('.', path)
|
|
357
377
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
358
378
|
else:
|
|
359
379
|
path = path.name
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|