Valentin David pushed to branch master at BuildStream / buildstream
Commits:
-
2e78e0d1
by Valentin David at 2018-11-29T13:18:17Z
-
a6144100
by Valentin David at 2018-11-29T13:18:17Z
-
f6c184f5
by Valentin David at 2018-11-29T16:15:56Z
9 changed files:
- .gitlab-ci.yml
- buildstream/plugins/sources/_downloadablefilesource.py
- dev-requirements.txt
- tests/sources/remote.py
- tests/sources/tar.py
- tests/sources/zip.py
- + tests/testutils/file_server.py
- + tests/testutils/ftp_server.py
- + tests/testutils/http_server.py
Changes:
1 |
-image: buildstream/testsuite-debian:9-master-119-552f5fc6
|
|
1 |
+image: buildstream/testsuite-debian:9-master-123-7ce6581b
|
|
2 | 2 |
|
3 | 3 |
cache:
|
4 | 4 |
key: "$CI_JOB_NAME-"
|
... | ... | @@ -140,7 +140,7 @@ tests-unix: |
140 | 140 |
|
141 | 141 |
tests-fedora-missing-deps:
|
142 | 142 |
# Ensure that tests behave nicely while missing bwrap and ostree
|
143 |
- image: buildstream/testsuite-fedora:28-master-119-552f5fc6
|
|
143 |
+ image: buildstream/testsuite-fedora:28-master-123-7ce6581b
|
|
144 | 144 |
<<: *tests
|
145 | 145 |
|
146 | 146 |
script:
|
... | ... | @@ -5,16 +5,77 @@ import urllib.request |
5 | 5 |
import urllib.error
|
6 | 6 |
import contextlib
|
7 | 7 |
import shutil
|
8 |
+import netrc
|
|
8 | 9 |
|
9 | 10 |
from buildstream import Source, SourceError, Consistency
|
10 | 11 |
from buildstream import utils
|
11 | 12 |
|
12 | 13 |
|
14 |
+class _NetrcFTPOpener(urllib.request.FTPHandler):
|
|
15 |
+ |
|
16 |
+ def __init__(self, netrc_config):
|
|
17 |
+ self.netrc = netrc_config
|
|
18 |
+ |
|
19 |
+ def _split(self, netloc):
|
|
20 |
+ userpass, hostport = urllib.parse.splituser(netloc)
|
|
21 |
+ host, port = urllib.parse.splitport(hostport)
|
|
22 |
+ if userpass:
|
|
23 |
+ user, passwd = urllib.parse.splitpasswd(userpass)
|
|
24 |
+ else:
|
|
25 |
+ user = None
|
|
26 |
+ passwd = None
|
|
27 |
+ return host, port, user, passwd
|
|
28 |
+ |
|
29 |
+ def _unsplit(self, host, port, user, passwd):
|
|
30 |
+ if port:
|
|
31 |
+ host = '{}:{}'.format(host, port)
|
|
32 |
+ if user:
|
|
33 |
+ if passwd:
|
|
34 |
+ user = '{}:{}'.format(user, passwd)
|
|
35 |
+ host = '{}@{}'.format(user, host)
|
|
36 |
+ |
|
37 |
+ return host
|
|
38 |
+ |
|
39 |
+ def ftp_open(self, req):
|
|
40 |
+ host, port, user, passwd = self._split(req.host)
|
|
41 |
+ |
|
42 |
+ if user is None and self.netrc:
|
|
43 |
+ entry = self.netrc.authenticators(host)
|
|
44 |
+ if entry:
|
|
45 |
+ user, _, passwd = entry
|
|
46 |
+ |
|
47 |
+ req.host = self._unsplit(host, port, user, passwd)
|
|
48 |
+ |
|
49 |
+ return super().ftp_open(req)
|
|
50 |
+ |
|
51 |
+ |
|
52 |
+class _NetrcPasswordManager:
|
|
53 |
+ |
|
54 |
+ def __init__(self, netrc_config):
|
|
55 |
+ self.netrc = netrc_config
|
|
56 |
+ |
|
57 |
+ def add_password(self, realm, uri, user, passwd):
|
|
58 |
+ pass
|
|
59 |
+ |
|
60 |
+ def find_user_password(self, realm, authuri):
|
|
61 |
+ if not self.netrc:
|
|
62 |
+ return None, None
|
|
63 |
+ parts = urllib.parse.urlsplit(authuri)
|
|
64 |
+ entry = self.netrc.authenticators(parts.hostname)
|
|
65 |
+ if not entry:
|
|
66 |
+ return None, None
|
|
67 |
+ else:
|
|
68 |
+ login, _, password = entry
|
|
69 |
+ return login, password
|
|
70 |
+ |
|
71 |
+ |
|
13 | 72 |
class DownloadableFileSource(Source):
|
14 | 73 |
# pylint: disable=attribute-defined-outside-init
|
15 | 74 |
|
16 | 75 |
COMMON_CONFIG_KEYS = Source.COMMON_CONFIG_KEYS + ['url', 'ref', 'etag']
|
17 | 76 |
|
77 |
+ __urlopener = None
|
|
78 |
+ |
|
18 | 79 |
def configure(self, node):
|
19 | 80 |
self.original_url = self.node_get_member(node, str, 'url')
|
20 | 81 |
self.ref = self.node_get_member(node, str, 'ref', None)
|
... | ... | @@ -118,7 +179,8 @@ class DownloadableFileSource(Source): |
118 | 179 |
if etag and self.get_consistency() == Consistency.CACHED:
|
119 | 180 |
request.add_header('If-None-Match', etag)
|
120 | 181 |
|
121 |
- with contextlib.closing(urllib.request.urlopen(request)) as response:
|
|
182 |
+ opener = self.__get_urlopener()
|
|
183 |
+ with contextlib.closing(opener.open(request)) as response:
|
|
122 | 184 |
info = response.info()
|
123 | 185 |
|
124 | 186 |
etag = info['ETag'] if 'ETag' in info else None
|
... | ... | @@ -164,3 +226,19 @@ class DownloadableFileSource(Source): |
164 | 226 |
|
165 | 227 |
def _get_mirror_file(self, sha=None):
|
166 | 228 |
return os.path.join(self._get_mirror_dir(), sha or self.ref)
|
229 |
+ |
|
230 |
+ def __get_urlopener(self):
|
|
231 |
+ if not DownloadableFileSource.__urlopener:
|
|
232 |
+ try:
|
|
233 |
+ netrc_config = netrc.netrc()
|
|
234 |
+ except FileNotFoundError:
|
|
235 |
+ DownloadableFileSource.__urlopener = urllib.request.build_opener()
|
|
236 |
+ except netrc.NetrcParseError as e:
|
|
237 |
+ self.warn('{}: While reading .netrc: {}'.format(self, e))
|
|
238 |
+ return urllib.request.build_opener()
|
|
239 |
+ else:
|
|
240 |
+ netrc_pw_mgr = _NetrcPasswordManager(netrc_config)
|
|
241 |
+ http_auth = urllib.request.HTTPBasicAuthHandler(netrc_pw_mgr)
|
|
242 |
+ ftp_handler = _NetrcFTPOpener(netrc_config)
|
|
243 |
+ DownloadableFileSource.__urlopener = urllib.request.build_opener(http_auth, ftp_handler)
|
|
244 |
+ return DownloadableFileSource.__urlopener
|
... | ... | @@ -9,3 +9,4 @@ pytest-pep8 |
9 | 9 |
pytest-pylint
|
10 | 10 |
pytest-xdist
|
11 | 11 |
pytest-timeout
|
12 |
+pyftpdlib
|
... | ... | @@ -5,6 +5,7 @@ import pytest |
5 | 5 |
from buildstream._exceptions import ErrorDomain
|
6 | 6 |
from buildstream import _yaml
|
7 | 7 |
from tests.testutils import cli
|
8 |
+from tests.testutils.file_server import create_file_server
|
|
8 | 9 |
|
9 | 10 |
DATA_DIR = os.path.join(
|
10 | 11 |
os.path.dirname(os.path.realpath(__file__)),
|
... | ... | @@ -22,6 +23,16 @@ def generate_project(project_dir, tmpdir): |
22 | 23 |
}, project_file)
|
23 | 24 |
|
24 | 25 |
|
26 |
+def generate_project_file_server(server, project_dir):
|
|
27 |
+ project_file = os.path.join(project_dir, "project.conf")
|
|
28 |
+ _yaml.dump({
|
|
29 |
+ 'name': 'foo',
|
|
30 |
+ 'aliases': {
|
|
31 |
+ 'tmpdir': server.base_url()
|
|
32 |
+ }
|
|
33 |
+ }, project_file)
|
|
34 |
+ |
|
35 |
+ |
|
25 | 36 |
# Test that without ref, consistency is set appropriately.
|
26 | 37 |
@pytest.mark.datafiles(os.path.join(DATA_DIR, 'no-ref'))
|
27 | 38 |
def test_no_ref(cli, tmpdir, datafiles):
|
... | ... | @@ -164,3 +175,35 @@ def test_executable(cli, tmpdir, datafiles): |
164 | 175 |
assert (mode & stat.S_IEXEC)
|
165 | 176 |
# Assert executable by anyone
|
166 | 177 |
assert(mode & (stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH))
|
178 |
+ |
|
179 |
+ |
|
180 |
+@pytest.mark.parametrize('server_type', ('FTP', 'HTTP'))
|
|
181 |
+@pytest.mark.datafiles(os.path.join(DATA_DIR, 'single-file'))
|
|
182 |
+def test_use_netrc(cli, datafiles, server_type, tmpdir):
|
|
183 |
+ fake_home = os.path.join(str(tmpdir), 'fake_home')
|
|
184 |
+ os.makedirs(fake_home, exist_ok=True)
|
|
185 |
+ project = str(datafiles)
|
|
186 |
+ checkoutdir = os.path.join(str(tmpdir), 'checkout')
|
|
187 |
+ |
|
188 |
+ os.environ['HOME'] = fake_home
|
|
189 |
+ with open(os.path.join(fake_home, '.netrc'), 'wb') as f:
|
|
190 |
+ os.fchmod(f.fileno(), 0o700)
|
|
191 |
+ f.write(b'machine 127.0.0.1\n')
|
|
192 |
+ f.write(b'login testuser\n')
|
|
193 |
+ f.write(b'password 12345\n')
|
|
194 |
+ |
|
195 |
+ with create_file_server(server_type) as server:
|
|
196 |
+ server.add_user('testuser', '12345', project)
|
|
197 |
+ generate_project_file_server(server, project)
|
|
198 |
+ |
|
199 |
+ server.start()
|
|
200 |
+ |
|
201 |
+ result = cli.run(project=project, args=['fetch', 'target.bst'])
|
|
202 |
+ result.assert_success()
|
|
203 |
+ result = cli.run(project=project, args=['build', 'target.bst'])
|
|
204 |
+ result.assert_success()
|
|
205 |
+ result = cli.run(project=project, args=['checkout', 'target.bst', checkoutdir])
|
|
206 |
+ result.assert_success()
|
|
207 |
+ |
|
208 |
+ checkout_file = os.path.join(checkoutdir, 'file')
|
|
209 |
+ assert(os.path.exists(checkout_file))
|
... | ... | @@ -3,11 +3,13 @@ import pytest |
3 | 3 |
import tarfile
|
4 | 4 |
import tempfile
|
5 | 5 |
import subprocess
|
6 |
+import urllib.parse
|
|
6 | 7 |
from shutil import copyfile, rmtree
|
7 | 8 |
|
8 | 9 |
from buildstream._exceptions import ErrorDomain
|
9 | 10 |
from buildstream import _yaml
|
10 | 11 |
from tests.testutils import cli
|
12 |
+from tests.testutils.file_server import create_file_server
|
|
11 | 13 |
from tests.testutils.site import HAVE_LZIP
|
12 | 14 |
from . import list_dir_contents
|
13 | 15 |
|
... | ... | @@ -49,6 +51,16 @@ def generate_project(project_dir, tmpdir): |
49 | 51 |
}, project_file)
|
50 | 52 |
|
51 | 53 |
|
54 |
+def generate_project_file_server(base_url, project_dir):
|
|
55 |
+ project_file = os.path.join(project_dir, "project.conf")
|
|
56 |
+ _yaml.dump({
|
|
57 |
+ 'name': 'foo',
|
|
58 |
+ 'aliases': {
|
|
59 |
+ 'tmpdir': base_url
|
|
60 |
+ }
|
|
61 |
+ }, project_file)
|
|
62 |
+ |
|
63 |
+ |
|
52 | 64 |
# Test that without ref, consistency is set appropriately.
|
53 | 65 |
@pytest.mark.datafiles(os.path.join(DATA_DIR, 'no-ref'))
|
54 | 66 |
def test_no_ref(cli, tmpdir, datafiles):
|
... | ... | @@ -302,3 +314,77 @@ def test_read_only_dir(cli, tmpdir, datafiles): |
302 | 314 |
else:
|
303 | 315 |
os.remove(path)
|
304 | 316 |
rmtree(str(tmpdir), onerror=make_dir_writable)
|
317 |
+ |
|
318 |
+ |
|
319 |
+@pytest.mark.parametrize('server_type', ('FTP', 'HTTP'))
|
|
320 |
+@pytest.mark.datafiles(os.path.join(DATA_DIR, 'fetch'))
|
|
321 |
+def test_use_netrc(cli, datafiles, server_type, tmpdir):
|
|
322 |
+ file_server_files = os.path.join(str(tmpdir), 'file_server')
|
|
323 |
+ fake_home = os.path.join(str(tmpdir), 'fake_home')
|
|
324 |
+ os.makedirs(file_server_files, exist_ok=True)
|
|
325 |
+ os.makedirs(fake_home, exist_ok=True)
|
|
326 |
+ project = str(datafiles)
|
|
327 |
+ checkoutdir = os.path.join(str(tmpdir), 'checkout')
|
|
328 |
+ |
|
329 |
+ os.environ['HOME'] = fake_home
|
|
330 |
+ with open(os.path.join(fake_home, '.netrc'), 'wb') as f:
|
|
331 |
+ os.fchmod(f.fileno(), 0o700)
|
|
332 |
+ f.write(b'machine 127.0.0.1\n')
|
|
333 |
+ f.write(b'login testuser\n')
|
|
334 |
+ f.write(b'password 12345\n')
|
|
335 |
+ |
|
336 |
+ with create_file_server(server_type) as server:
|
|
337 |
+ server.add_user('testuser', '12345', file_server_files)
|
|
338 |
+ generate_project_file_server(server.base_url(), project)
|
|
339 |
+ |
|
340 |
+ src_tar = os.path.join(file_server_files, 'a.tar.gz')
|
|
341 |
+ _assemble_tar(os.path.join(str(datafiles), 'content'), 'a', src_tar)
|
|
342 |
+ |
|
343 |
+ server.start()
|
|
344 |
+ |
|
345 |
+ result = cli.run(project=project, args=['track', 'target.bst'])
|
|
346 |
+ result.assert_success()
|
|
347 |
+ result = cli.run(project=project, args=['fetch', 'target.bst'])
|
|
348 |
+ result.assert_success()
|
|
349 |
+ result = cli.run(project=project, args=['build', 'target.bst'])
|
|
350 |
+ result.assert_success()
|
|
351 |
+ result = cli.run(project=project, args=['checkout', 'target.bst', checkoutdir])
|
|
352 |
+ result.assert_success()
|
|
353 |
+ |
|
354 |
+ original_dir = os.path.join(str(datafiles), 'content', 'a')
|
|
355 |
+ original_contents = list_dir_contents(original_dir)
|
|
356 |
+ checkout_contents = list_dir_contents(checkoutdir)
|
|
357 |
+ assert(checkout_contents == original_contents)
|
|
358 |
+ |
|
359 |
+ |
|
360 |
+@pytest.mark.parametrize('server_type', ('FTP', 'HTTP'))
|
|
361 |
+@pytest.mark.datafiles(os.path.join(DATA_DIR, 'fetch'))
|
|
362 |
+def test_netrc_already_specified_user(cli, datafiles, server_type, tmpdir):
|
|
363 |
+ file_server_files = os.path.join(str(tmpdir), 'file_server')
|
|
364 |
+ fake_home = os.path.join(str(tmpdir), 'fake_home')
|
|
365 |
+ os.makedirs(file_server_files, exist_ok=True)
|
|
366 |
+ os.makedirs(fake_home, exist_ok=True)
|
|
367 |
+ project = str(datafiles)
|
|
368 |
+ checkoutdir = os.path.join(str(tmpdir), 'checkout')
|
|
369 |
+ |
|
370 |
+ os.environ['HOME'] = fake_home
|
|
371 |
+ with open(os.path.join(fake_home, '.netrc'), 'wb') as f:
|
|
372 |
+ os.fchmod(f.fileno(), 0o700)
|
|
373 |
+ f.write(b'machine 127.0.0.1\n')
|
|
374 |
+ f.write(b'login testuser\n')
|
|
375 |
+ f.write(b'password 12345\n')
|
|
376 |
+ |
|
377 |
+ with create_file_server(server_type) as server:
|
|
378 |
+ server.add_user('otheruser', '12345', file_server_files)
|
|
379 |
+ parts = urllib.parse.urlsplit(server.base_url())
|
|
380 |
+ base_url = urllib.parse.urlunsplit([parts[0]] + ['otheruser@{}'.format(parts[1])] + list(parts[2:]))
|
|
381 |
+ generate_project_file_server(base_url, project)
|
|
382 |
+ |
|
383 |
+ src_tar = os.path.join(file_server_files, 'a.tar.gz')
|
|
384 |
+ _assemble_tar(os.path.join(str(datafiles), 'content'), 'a', src_tar)
|
|
385 |
+ |
|
386 |
+ server.start()
|
|
387 |
+ |
|
388 |
+ result = cli.run(project=project, args=['track', 'target.bst'])
|
|
389 |
+ result.assert_main_error(ErrorDomain.STREAM, None)
|
|
390 |
+ result.assert_task_error(ErrorDomain.SOURCE, None)
|
... | ... | @@ -5,6 +5,7 @@ import zipfile |
5 | 5 |
from buildstream._exceptions import ErrorDomain
|
6 | 6 |
from buildstream import _yaml
|
7 | 7 |
from tests.testutils import cli
|
8 |
+from tests.testutils.file_server import create_file_server
|
|
8 | 9 |
from . import list_dir_contents
|
9 | 10 |
|
10 | 11 |
DATA_DIR = os.path.join(
|
... | ... | @@ -35,6 +36,16 @@ def generate_project(project_dir, tmpdir): |
35 | 36 |
}, project_file)
|
36 | 37 |
|
37 | 38 |
|
39 |
+def generate_project_file_server(server, project_dir):
|
|
40 |
+ project_file = os.path.join(project_dir, "project.conf")
|
|
41 |
+ _yaml.dump({
|
|
42 |
+ 'name': 'foo',
|
|
43 |
+ 'aliases': {
|
|
44 |
+ 'tmpdir': server.base_url()
|
|
45 |
+ }
|
|
46 |
+ }, project_file)
|
|
47 |
+ |
|
48 |
+ |
|
38 | 49 |
# Test that without ref, consistency is set appropriately.
|
39 | 50 |
@pytest.mark.datafiles(os.path.join(DATA_DIR, 'no-ref'))
|
40 | 51 |
def test_no_ref(cli, tmpdir, datafiles):
|
... | ... | @@ -176,3 +187,44 @@ def test_stage_explicit_basedir(cli, tmpdir, datafiles): |
176 | 187 |
original_contents = list_dir_contents(original_dir)
|
177 | 188 |
checkout_contents = list_dir_contents(checkoutdir)
|
178 | 189 |
assert(checkout_contents == original_contents)
|
190 |
+ |
|
191 |
+ |
|
192 |
+@pytest.mark.parametrize('server_type', ('FTP', 'HTTP'))
|
|
193 |
+@pytest.mark.datafiles(os.path.join(DATA_DIR, 'fetch'))
|
|
194 |
+def test_use_netrc(cli, datafiles, server_type, tmpdir):
|
|
195 |
+ file_server_files = os.path.join(str(tmpdir), 'file_server')
|
|
196 |
+ fake_home = os.path.join(str(tmpdir), 'fake_home')
|
|
197 |
+ os.makedirs(file_server_files, exist_ok=True)
|
|
198 |
+ os.makedirs(fake_home, exist_ok=True)
|
|
199 |
+ project = str(datafiles)
|
|
200 |
+ checkoutdir = os.path.join(str(tmpdir), 'checkout')
|
|
201 |
+ |
|
202 |
+ os.environ['HOME'] = fake_home
|
|
203 |
+ with open(os.path.join(fake_home, '.netrc'), 'wb') as f:
|
|
204 |
+ os.fchmod(f.fileno(), 0o700)
|
|
205 |
+ f.write(b'machine 127.0.0.1\n')
|
|
206 |
+ f.write(b'login testuser\n')
|
|
207 |
+ f.write(b'password 12345\n')
|
|
208 |
+ |
|
209 |
+ with create_file_server(server_type) as server:
|
|
210 |
+ server.add_user('testuser', '12345', file_server_files)
|
|
211 |
+ generate_project_file_server(server, project)
|
|
212 |
+ |
|
213 |
+ src_zip = os.path.join(file_server_files, 'a.zip')
|
|
214 |
+ _assemble_zip(os.path.join(str(datafiles), 'content'), src_zip)
|
|
215 |
+ |
|
216 |
+ server.start()
|
|
217 |
+ |
|
218 |
+ result = cli.run(project=project, args=['track', 'target.bst'])
|
|
219 |
+ result.assert_success()
|
|
220 |
+ result = cli.run(project=project, args=['fetch', 'target.bst'])
|
|
221 |
+ result.assert_success()
|
|
222 |
+ result = cli.run(project=project, args=['build', 'target.bst'])
|
|
223 |
+ result.assert_success()
|
|
224 |
+ result = cli.run(project=project, args=['checkout', 'target.bst', checkoutdir])
|
|
225 |
+ result.assert_success()
|
|
226 |
+ |
|
227 |
+ original_dir = os.path.join(str(datafiles), 'content', 'a')
|
|
228 |
+ original_contents = list_dir_contents(original_dir)
|
|
229 |
+ checkout_contents = list_dir_contents(checkoutdir)
|
|
230 |
+ assert(checkout_contents == original_contents)
|
1 |
+from contextlib import contextmanager
|
|
2 |
+ |
|
3 |
+from .ftp_server import SimpleFtpServer
|
|
4 |
+from .http_server import SimpleHttpServer
|
|
5 |
+ |
|
6 |
+ |
|
7 |
+@contextmanager
|
|
8 |
+def create_file_server(file_server_type):
|
|
9 |
+ if file_server_type == 'FTP':
|
|
10 |
+ server = SimpleFtpServer()
|
|
11 |
+ elif file_server_type == 'HTTP':
|
|
12 |
+ server = SimpleHttpServer()
|
|
13 |
+ else:
|
|
14 |
+ assert False
|
|
15 |
+ |
|
16 |
+ try:
|
|
17 |
+ yield server
|
|
18 |
+ finally:
|
|
19 |
+ server.stop()
|
1 |
+import multiprocessing
|
|
2 |
+ |
|
3 |
+from pyftpdlib.authorizers import DummyAuthorizer
|
|
4 |
+from pyftpdlib.handlers import FTPHandler
|
|
5 |
+from pyftpdlib.servers import FTPServer
|
|
6 |
+ |
|
7 |
+ |
|
8 |
+class SimpleFtpServer(multiprocessing.Process):
|
|
9 |
+ def __init__(self):
|
|
10 |
+ super().__init__()
|
|
11 |
+ self.authorizer = DummyAuthorizer()
|
|
12 |
+ handler = FTPHandler
|
|
13 |
+ handler.authorizer = self.authorizer
|
|
14 |
+ self.server = FTPServer(('127.0.0.1', 0), handler)
|
|
15 |
+ |
|
16 |
+ def run(self):
|
|
17 |
+ self.server.serve_forever()
|
|
18 |
+ |
|
19 |
+ def stop(self):
|
|
20 |
+ self.server.close_all()
|
|
21 |
+ self.server.close()
|
|
22 |
+ self.terminate()
|
|
23 |
+ self.join()
|
|
24 |
+ |
|
25 |
+ def allow_anonymous(self, cwd):
|
|
26 |
+ self.authorizer.add_anonymous(cwd)
|
|
27 |
+ |
|
28 |
+ def add_user(self, user, password, cwd):
|
|
29 |
+ self.authorizer.add_user(user, password, cwd, perm='elradfmwMT')
|
|
30 |
+ |
|
31 |
+ def base_url(self):
|
|
32 |
+ return 'ftp://127.0.0.1:{}'.format(self.server.address[1])
|
1 |
+import multiprocessing
|
|
2 |
+import os
|
|
3 |
+import posixpath
|
|
4 |
+import html
|
|
5 |
+import threading
|
|
6 |
+import base64
|
|
7 |
+from http.server import SimpleHTTPRequestHandler, HTTPServer, HTTPStatus
|
|
8 |
+ |
|
9 |
+ |
|
10 |
+class Unauthorized(Exception):
|
|
11 |
+ pass
|
|
12 |
+ |
|
13 |
+ |
|
14 |
+class RequestHandler(SimpleHTTPRequestHandler):
|
|
15 |
+ |
|
16 |
+ def get_root_dir(self):
|
|
17 |
+ authorization = self.headers.get('authorization')
|
|
18 |
+ if not authorization:
|
|
19 |
+ if not self.server.anonymous_dir:
|
|
20 |
+ raise Unauthorized('unauthorized')
|
|
21 |
+ return self.server.anonymous_dir
|
|
22 |
+ else:
|
|
23 |
+ authorization = authorization.split()
|
|
24 |
+ if len(authorization) != 2 or authorization[0].lower() != 'basic':
|
|
25 |
+ raise Unauthorized('unauthorized')
|
|
26 |
+ try:
|
|
27 |
+ decoded = base64.decodebytes(authorization[1].encode('ascii'))
|
|
28 |
+ user, password = decoded.decode('ascii').split(':')
|
|
29 |
+ expected_password, directory = self.server.users[user]
|
|
30 |
+ if password == expected_password:
|
|
31 |
+ return directory
|
|
32 |
+ except:
|
|
33 |
+ raise Unauthorized('unauthorized')
|
|
34 |
+ return None
|
|
35 |
+ |
|
36 |
+ def unauthorized(self):
|
|
37 |
+ shortmsg, longmsg = self.responses[HTTPStatus.UNAUTHORIZED]
|
|
38 |
+ self.send_response(HTTPStatus.UNAUTHORIZED, shortmsg)
|
|
39 |
+ self.send_header('Connection', 'close')
|
|
40 |
+ |
|
41 |
+ content = (self.error_message_format % {
|
|
42 |
+ 'code': HTTPStatus.UNAUTHORIZED,
|
|
43 |
+ 'message': html.escape(longmsg, quote=False),
|
|
44 |
+ 'explain': html.escape(longmsg, quote=False)
|
|
45 |
+ })
|
|
46 |
+ body = content.encode('UTF-8', 'replace')
|
|
47 |
+ self.send_header('Content-Type', self.error_content_type)
|
|
48 |
+ self.send_header('Content-Length', str(len(body)))
|
|
49 |
+ self.send_header('WWW-Authenticate', 'Basic realm="{}"'.format(self.server.realm))
|
|
50 |
+ self.end_headers()
|
|
51 |
+ self.end_headers()
|
|
52 |
+ |
|
53 |
+ if self.command != 'HEAD' and body:
|
|
54 |
+ self.wfile.write(body)
|
|
55 |
+ |
|
56 |
+ def do_GET(self):
|
|
57 |
+ try:
|
|
58 |
+ super().do_GET()
|
|
59 |
+ except Unauthorized:
|
|
60 |
+ self.unauthorized()
|
|
61 |
+ |
|
62 |
+ def do_HEAD(self):
|
|
63 |
+ try:
|
|
64 |
+ super().do_HEAD()
|
|
65 |
+ except Unauthorized:
|
|
66 |
+ self.unauthorized()
|
|
67 |
+ |
|
68 |
+ def translate_path(self, path):
|
|
69 |
+ path = path.split('?', 1)[0]
|
|
70 |
+ path = path.split('#', 1)[0]
|
|
71 |
+ path = posixpath.normpath(path)
|
|
72 |
+ assert(posixpath.isabs(path))
|
|
73 |
+ path = posixpath.relpath(path, '/')
|
|
74 |
+ return os.path.join(self.get_root_dir(), path)
|
|
75 |
+ |
|
76 |
+ |
|
77 |
+class AuthHTTPServer(HTTPServer):
|
|
78 |
+ def __init__(self, *args, **kwargs):
|
|
79 |
+ self.users = {}
|
|
80 |
+ self.anonymous_dir = None
|
|
81 |
+ self.realm = 'Realm'
|
|
82 |
+ super().__init__(*args, **kwargs)
|
|
83 |
+ |
|
84 |
+ |
|
85 |
+class SimpleHttpServer(multiprocessing.Process):
|
|
86 |
+ def __init__(self):
|
|
87 |
+ self.__stop = multiprocessing.Queue()
|
|
88 |
+ super().__init__()
|
|
89 |
+ self.server = AuthHTTPServer(('127.0.0.1', 0), RequestHandler)
|
|
90 |
+ self.started = False
|
|
91 |
+ |
|
92 |
+ def start(self):
|
|
93 |
+ self.started = True
|
|
94 |
+ super().start()
|
|
95 |
+ |
|
96 |
+ def run(self):
|
|
97 |
+ t = threading.Thread(target=self.server.serve_forever)
|
|
98 |
+ t.start()
|
|
99 |
+ self.__stop.get()
|
|
100 |
+ self.server.shutdown()
|
|
101 |
+ t.join()
|
|
102 |
+ |
|
103 |
+ def stop(self):
|
|
104 |
+ if not self.started:
|
|
105 |
+ return
|
|
106 |
+ self.__stop.put(None)
|
|
107 |
+ self.terminate()
|
|
108 |
+ self.join()
|
|
109 |
+ |
|
110 |
+ def allow_anonymous(self, cwd):
|
|
111 |
+ self.server.anonymous_dir = cwd
|
|
112 |
+ |
|
113 |
+ def add_user(self, user, password, cwd):
|
|
114 |
+ self.server.users[user] = (password, cwd)
|
|
115 |
+ |
|
116 |
+ def base_url(self):
|
|
117 |
+ return 'http://127.0.0.1:{}'.format(self.server.server_port)
|