Merge lp:~jml/pkgme-service/lzma-support into lp:pkgme-service

Proposed by Jonathan Lange
Status: Merged
Approved by: James Westby
Approved revision: 133
Merged at revision: 121
Proposed branch: lp:~jml/pkgme-service/lzma-support
Merge into: lp:pkgme-service
Prerequisite: lp:~jml/pkgme-service/move-utils-from-tasks
Diff against target: 177 lines (+93/-21)
2 files modified
src/djpkgme/osutils.py (+33/-21)
src/djpkgme/tests/test_osutils.py (+60/-0)
To merge this branch: bzr merge lp:~jml/pkgme-service/lzma-support
Reviewer Review Type Date Requested Status
James Westby (community) Approve
Review via email: mp+124930@code.launchpad.net

Commit message

Support .tar.lzma files.

Description of the change

Adds a thing to try to use the 'tar' command to extract what we're given. This is only used as a fallback after the existing tarfile and zipfile extractors, since I'm assuming that spawning subprocesses is more expensive.

This gives us lzma support, as tar automatically detects compression to use and as lzma support is available on any precise machine, due to apt depending on xz-utils, which provides it.

There are direct tests, as well as a test for prepare_file.

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

Hi,

In execing tar there are a few things we have to be careful of to prevent
attacks.

--force-local will mean that if someone uploads a tarball with a colon in the name
it won't try and connect to a remote machine to find the file.

We want --no-same-owner and --no-same-permissions, but they are default for
non-root users, so I don't know if we want to be explicit or not.

In addition, I think this will fix some issues we see in scoreboard with
e.g. unable to process tarfiles, or trying to extract to absolute paths.

Rather than fixing those, we could just replace the existing tarfile extractor
with this one. You are right that it is more expensive though, so maybe we
don't want to.

Thanks,

James

review: Needs Fixing
lp:~jml/pkgme-service/lzma-support updated
133. By Jonathan Lange

Only use 'tar' command, rather than Python's tarfile to try to extract
tarballs.

Add some options to prevent certain security exploits.

Revision history for this message
Jonathan Lange (jml) wrote :

We haven't demonstrated that the greater expense matters. We have demonstrated that the errors do. Have deleted the old _try_extract_tarfile. We now rely only on try_extract_with_tar_command.

Have added the extra options for defense-in-depth.

Revision history for this message
James Westby (james-w) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/djpkgme/osutils.py'
--- src/djpkgme/osutils.py 2012-09-18 12:19:18 +0000
+++ src/djpkgme/osutils.py 2012-09-18 14:46:18 +0000
@@ -12,25 +12,10 @@
12import tarfile12import tarfile
13import zipfile13import zipfile
1414
1515from pkgme.run_script import (
16def _try_extract_tarfile(path, target_dir):16 run_subprocess,
17 """Try extracting `path` to `target_dir` as a tarfile.17 ScriptFailed,
1818 )
19 :return: True if successful, False if the file at `path` isn't
20 a `tarfile`.
21 """
22 # XXX: untested.
23 try:
24 t = tarfile.open(path)
25 except tarfile.ReadError:
26 return False
27 else:
28 try:
29 t.extractall(target_dir)
30 finally:
31 t.close()
32 os.unlink(path)
33 return True
3419
3520
36def _try_extract_zipfile(path, target_dir):21def _try_extract_zipfile(path, target_dir):
@@ -53,10 +38,37 @@
53 return True38 return True
5439
5540
41def try_extract_with_tar_command(tarball, target_dir):
42 # The other try_extract dudes assume target_dir doesn't exist, create it,
43 # and have it not exist when they fail. We implement the same interface.
44 if not os.path.exists(target_dir):
45 os.makedirs(target_dir)
46 cmd = ['tar',
47 '--force-local',
48 '--no-same-owner',
49 '--no-same-permissions',
50 '-xf', tarball,
51 '-C', target_dir]
52 try:
53 run_subprocess(cmd)
54 except ScriptFailed, e:
55 os.rmdir(target_dir)
56 if e.returncode == 2:
57 return False
58 raise
59 return True
60
61
56# The name of the directory that we create inside of the temporary build62# The name of the directory that we create inside of the temporary build
57# directory that pkgme does all of its work on.63# directory that pkgme does all of its work on.
58DEFAULT_WORKING_DIRECTORY_NAME = 'working'64DEFAULT_WORKING_DIRECTORY_NAME = 'working'
5965
66_EXTRACTORS = (
67 _try_extract_zipfile,
68 try_extract_with_tar_command,
69 )
70
71
60def prepare_file(path, output_dir,72def prepare_file(path, output_dir,
61 working_dir_name=DEFAULT_WORKING_DIRECTORY_NAME):73 working_dir_name=DEFAULT_WORKING_DIRECTORY_NAME):
62 """Get 'path' ready for automated packaging inside 'output_dir'.74 """Get 'path' ready for automated packaging inside 'output_dir'.
@@ -77,8 +89,8 @@
77 :return: A path to a newly-created directory.89 :return: A path to a newly-created directory.
78 """90 """
79 working_path = os.path.join(output_dir, working_dir_name)91 working_path = os.path.join(output_dir, working_dir_name)
80 for method in [_try_extract_tarfile, _try_extract_zipfile]:92 for extract in _EXTRACTORS:
81 if method(path, working_path):93 if extract(path, working_path):
82 break94 break
83 else:95 else:
84 os.makedirs(working_path)96 os.makedirs(working_path)
8597
=== modified file 'src/djpkgme/tests/test_osutils.py'
--- src/djpkgme/tests/test_osutils.py 2012-09-18 12:19:18 +0000
+++ src/djpkgme/tests/test_osutils.py 2012-09-18 14:46:18 +0000
@@ -1,5 +1,6 @@
1import errno1import errno
2import os2import os
3import subprocess
3import tarfile4import tarfile
4import zipfile5import zipfile
56
@@ -9,17 +10,60 @@
9 DirExists,10 DirExists,
10 FileContains,11 FileContains,
11 HasPermissions,12 HasPermissions,
13 Not,
12 TarballContains,14 TarballContains,
13 )15 )
16from treeshape import (
17 from_rough_spec,
18 HasFileTree,
19 FileTree,
20 MatchesFileTree,
21 )
1422
15from ..osutils import (23from ..osutils import (
16 DEFAULT_WORKING_DIRECTORY_NAME,24 DEFAULT_WORKING_DIRECTORY_NAME,
17 make_tarball,25 make_tarball,
18 prepare_file,26 prepare_file,
27 try_extract_with_tar_command,
19 )28 )
20from .factory import PkgmeObjectFactory29from .factory import PkgmeObjectFactory
2130
2231
32class TestExtractWithTarCommand(TestCase):
33
34 def test_extracts_tarball(self):
35 factory = self.useFixture(PkgmeObjectFactory())
36 path = factory.make_tarball(self.useFixture(TempDir()).path)
37 tarball = tarfile.open(path)
38 expected_top_level = [tarball.getnames()[0] + '/']
39 tempdir = self.useFixture(TempDir()).path
40 success = try_extract_with_tar_command(path, tempdir)
41 self.assertEqual(True, success)
42 self.assertThat(
43 tempdir, HasFileTree(from_rough_spec(expected_top_level)))
44
45 def test_non_existent_target_dir(self):
46 factory = self.useFixture(PkgmeObjectFactory())
47 path = factory.make_tarball(self.useFixture(TempDir()).path)
48 tarball = tarfile.open(path)
49 expected_top_level = [tarball.getnames()[0] + '/']
50 tempdir = self.useFixture(FileTree({}))
51 success = try_extract_with_tar_command(
52 path, tempdir.join('nonexistent'))
53 self.assertEqual(True, success)
54 self.assertThat(
55 tempdir.join('nonexistent'),
56 HasFileTree(from_rough_spec(expected_top_level)))
57
58 def test_does_not_extract_non_tarball(self):
59 filename = 'afile'
60 tree = self.useFixture(FileTree({filename: {}}))
61 tempdir = self.useFixture(TempDir()).path
62 success = try_extract_with_tar_command(tree.join(filename), tempdir)
63 self.assertEqual(False, success)
64 self.assertThat(tempdir, Not(DirExists()))
65
66
23class TestPrepareFile(TestCase):67class TestPrepareFile(TestCase):
2468
25 def test_extracts_tarball(self):69 def test_extracts_tarball(self):
@@ -53,6 +97,22 @@
53 self.assertThat(new_path, DirExists())97 self.assertThat(new_path, DirExists())
54 self.assertThat(os.path.join(new_path, top_level_dir), DirExists())98 self.assertThat(os.path.join(new_path, top_level_dir), DirExists())
5599
100 def test_extracts_lzma(self):
101 tempdir = self.useFixture(TempDir()).path
102 lzma_path = os.path.join(tempdir, 'application.tar.lzma')
103 tree = {'topdir/afile': {}, 'topdir/somedir/morefiles': {}}
104 orig_data = self.useFixture(FileTree(tree)).path
105 popen = subprocess.Popen(
106 ['tar', '--lzma', '-cf', lzma_path, '-C', orig_data, 'topdir'],
107 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
108 out, err = popen.communicate()
109 self.assertEqual((0, '', ''), (popen.returncode, out, err))
110 new_path = prepare_file(lzma_path, tempdir)
111 self.assertEqual(
112 os.path.join(tempdir, DEFAULT_WORKING_DIRECTORY_NAME),
113 new_path)
114 self.assertThat(new_path, MatchesFileTree(tree))
115
56 def test_copies_non_tarball(self):116 def test_copies_non_tarball(self):
57 # If the file downloaded is not a tarball then it is copied117 # If the file downloaded is not a tarball then it is copied
58 source_dir = self.useFixture(TempDir()).path118 source_dir = self.useFixture(TempDir()).path

Subscribers

People subscribed via source and target branches