| # Copyright 2012-2013 Gentoo Foundation |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| import errno |
| import re |
| import subprocess |
| |
| from portage import os |
| from portage import _unicode_encode, _encodings |
| from portage.const import MANIFEST2_IDENTIFIERS |
| from portage.util import (atomic_ofstream, grablines, |
| shlex_split, varexpand, writemsg) |
| from portage.util._async.PipeLogger import PipeLogger |
| from portage.util._async.PopenProcess import PopenProcess |
| from _emerge.CompositeTask import CompositeTask |
| from _emerge.PipeReader import PipeReader |
| from .ManifestProcess import ManifestProcess |
| |
| class ManifestTask(CompositeTask): |
| |
| __slots__ = ("cp", "distdir", "fetchlist_dict", "gpg_cmd", |
| "gpg_vars", "repo_config", "force_sign_key", "_manifest_path") |
| |
| _PGP_HEADER = b"BEGIN PGP SIGNED MESSAGE" |
| _manifest_line_re = re.compile(r'^(%s) ' % "|".join(MANIFEST2_IDENTIFIERS)) |
| _gpg_key_id_re = re.compile(r'^[0-9A-F]*$') |
| _gpg_key_id_lengths = (8, 16, 24, 32, 40) |
| |
| def _start(self): |
| self._manifest_path = os.path.join(self.repo_config.location, |
| self.cp, "Manifest") |
| manifest_proc = ManifestProcess(cp=self.cp, distdir=self.distdir, |
| fetchlist_dict=self.fetchlist_dict, repo_config=self.repo_config, |
| scheduler=self.scheduler) |
| self._start_task(manifest_proc, self._manifest_proc_exit) |
| |
| def _manifest_proc_exit(self, manifest_proc): |
| self._assert_current(manifest_proc) |
| if manifest_proc.returncode not in (os.EX_OK, manifest_proc.MODIFIED): |
| self.returncode = manifest_proc.returncode |
| self._current_task = None |
| self.wait() |
| return |
| |
| modified = manifest_proc.returncode == manifest_proc.MODIFIED |
| sign = self.gpg_cmd is not None |
| |
| if not modified and sign: |
| sign = self._need_signature() |
| if not sign and self.force_sign_key is not None \ |
| and os.path.exists(self._manifest_path): |
| self._check_sig_key() |
| return |
| |
| if not sign or not os.path.exists(self._manifest_path): |
| self.returncode = os.EX_OK |
| self._current_task = None |
| self.wait() |
| return |
| |
| self._start_gpg_proc() |
| |
| def _check_sig_key(self): |
| null_fd = os.open('/dev/null', os.O_RDONLY) |
| popen_proc = PopenProcess(proc=subprocess.Popen( |
| ["gpg", "--verify", self._manifest_path], |
| stdin=null_fd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT), |
| pipe_reader=PipeReader()) |
| os.close(null_fd) |
| popen_proc.pipe_reader.input_files = { |
| "producer" : popen_proc.proc.stdout} |
| self._start_task(popen_proc, self._check_sig_key_exit) |
| |
| @staticmethod |
| def _parse_gpg_key(output): |
| """ |
| Returns the first token which appears to represent a gpg key |
| id, or None if there is no such token. |
| """ |
| regex = ManifestTask._gpg_key_id_re |
| lengths = ManifestTask._gpg_key_id_lengths |
| for token in output.split(): |
| m = regex.match(token) |
| if m is not None and len(m.group(0)) in lengths: |
| return m.group(0) |
| return None |
| |
| @staticmethod |
| def _normalize_gpg_key(key_str): |
| """ |
| Strips leading "0x" and trailing "!", and converts to uppercase |
| (intended to be the same format as that in gpg --verify output). |
| """ |
| key_str = key_str.upper() |
| if key_str.startswith("0X"): |
| key_str = key_str[2:] |
| key_str = key_str.rstrip("!") |
| return key_str |
| |
| def _check_sig_key_exit(self, proc): |
| self._assert_current(proc) |
| |
| parsed_key = self._parse_gpg_key( |
| proc.pipe_reader.getvalue().decode('utf_8', 'replace')) |
| if parsed_key is not None and \ |
| self._normalize_gpg_key(parsed_key) == \ |
| self._normalize_gpg_key(self.force_sign_key): |
| self.returncode = os.EX_OK |
| self._current_task = None |
| self.wait() |
| return |
| |
| if self._was_cancelled(): |
| self.wait() |
| return |
| |
| self._strip_sig(self._manifest_path) |
| self._start_gpg_proc() |
| |
| @staticmethod |
| def _strip_sig(manifest_path): |
| """ |
| Strip an existing signature from a Manifest file. |
| """ |
| line_re = ManifestTask._manifest_line_re |
| lines = grablines(manifest_path) |
| f = None |
| try: |
| f = atomic_ofstream(manifest_path) |
| for line in lines: |
| if line_re.match(line) is not None: |
| f.write(line) |
| f.close() |
| f = None |
| finally: |
| if f is not None: |
| f.abort() |
| |
| def _start_gpg_proc(self): |
| gpg_vars = self.gpg_vars |
| if gpg_vars is None: |
| gpg_vars = {} |
| else: |
| gpg_vars = gpg_vars.copy() |
| gpg_vars["FILE"] = self._manifest_path |
| gpg_cmd = varexpand(self.gpg_cmd, mydict=gpg_vars) |
| gpg_cmd = shlex_split(gpg_cmd) |
| gpg_proc = PopenProcess(proc=subprocess.Popen(gpg_cmd, |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT)) |
| # PipeLogger echos output and efficiently monitors for process |
| # exit by listening for the stdout EOF event. |
| gpg_proc.pipe_reader = PipeLogger(background=self.background, |
| input_fd=gpg_proc.proc.stdout, scheduler=self.scheduler) |
| self._start_task(gpg_proc, self._gpg_proc_exit) |
| |
| def _gpg_proc_exit(self, gpg_proc): |
| if self._default_exit(gpg_proc) != os.EX_OK: |
| self.wait() |
| return |
| |
| rename_args = (self._manifest_path + ".asc", self._manifest_path) |
| try: |
| os.rename(*rename_args) |
| except OSError as e: |
| writemsg("!!! rename('%s', '%s'): %s\n" % rename_args + (e,), |
| noiselevel=-1) |
| try: |
| os.unlink(self._manifest_path + ".asc") |
| except OSError: |
| pass |
| self.returncode = 1 |
| else: |
| self.returncode = os.EX_OK |
| |
| self._current_task = None |
| self.wait() |
| |
| def _need_signature(self): |
| try: |
| with open(_unicode_encode(self._manifest_path, |
| encoding=_encodings['fs'], errors='strict'), 'rb') as f: |
| return self._PGP_HEADER not in f.readline() |
| except IOError as e: |
| if e.errno in (errno.ENOENT, errno.ESTALE): |
| return False |
| raise |