diff --git a/bauh/gems/arch/aur.py b/bauh/gems/arch/aur.py index 884ab71a..d2afd8a5 100644 --- a/bauh/gems/arch/aur.py +++ b/bauh/gems/arch/aur.py @@ -1,11 +1,16 @@ import re from typing import Set, List +import requests + from bauh.api.http import HttpClient URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&' +URL_SRC_INFO = 'https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h=' URL_SEARCH = 'https://aur.archlinux.org/rpc/?v=5&type=search&arg=' +RE_SRCINFO_KEYS = re.compile(r'(\w+)\s+=\s+(.+)\n') + def map_pkgbuild(pkgbuild: str) -> dict: return {attr: val.replace('"', '').replace("'", '').replace('(', '').replace(')', '') for attr, val in re.findall(r'\n(\w+)=(.+)', pkgbuild)} @@ -24,5 +29,24 @@ class AURClient: res = self.http_client.get_json(URL_INFO + self._map_names_as_queries(names)) return res['results'] if res and res.get('results') else [] + def get_src_info(self, name: str) -> dict: + res = self.http_client.get(URL_SRC_INFO + name) + + if res and res.text: + info = {} + for field in RE_SRCINFO_KEYS.findall(res.text): + if field[0] not in info: + info[field[0]] = field[1] + else: + if not isinstance(info[field[0]], list): + info[field[0]] = [info[field[0]]] + + info[field[0]].append(field[1]) + + if info.get('validpgpkeys') and isinstance(info['validpgpkeys'], str): + info['validpgpkeys'] = [info['validpgpkeys']] + + return info + def _map_names_as_queries(self, names) -> str: return '&'.join(['arg[{}]={}'.format(i, n) for i, n in enumerate(names)]) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 09610e21..6d6c9a14 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -14,7 +14,7 @@ from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePacka from bauh.api.abstract.view import MessageType from bauh.commons.html import bold from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, new_root_subprocess -from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, pkgbuild, message, confirmation, disk, git, suggestions +from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, pkgbuild, message, confirmation, disk, git, suggestions, gpg from bauh.gems.arch.aur import AURClient from bauh.gems.arch.mapper import ArchDataMapper from bauh.gems.arch.model import ArchPackage @@ -254,11 +254,14 @@ class ArchManager(SoftwareManager): '10_url': pkg.url_download, } - res_srcinfo = self.context.http_client.get(URL_SRC_INFO + pkg.name) + srcinfo = self.aur_client.get_src_info(pkg.name) - if res_srcinfo and res_srcinfo.text: - info['11_dependson'] = '\n'.join(pkgbuild.read_depends_on(res_srcinfo.text)) - info['12_optdepends'] = '\n'.join(pkgbuild.read_optdeps(res_srcinfo.text)) + if srcinfo: + if srcinfo.get('depends'): + info['11_dependson'] = srcinfo['depends'] + + if srcinfo.get('optdepends'): + info['12_optdepends'] = srcinfo['optdepends'] if pkg.pkgbuild: info['13_pkg_build'] = pkg.pkgbuild @@ -344,7 +347,7 @@ class ArchManager(SoftwareManager): def _make_pkg(self, pkgname: str, root_password: str, handler: ProcessHandler, build_dir: str, project_dir: str, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool: self._update_progress(handler.watcher, 50, change_progress) - if not self._install_missings_deps(pkgname, root_password, handler, project_dir): + if not self._install_missings_deps_and_keys(pkgname, root_password, handler, project_dir): return False # building main package @@ -373,30 +376,44 @@ class ArchManager(SoftwareManager): return False - def _install_missings_deps(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str) -> bool: + def _install_missings_deps_and_keys(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str) -> bool: handler.watcher.change_substatus(self.i18n['arch.checking.deps'].format(bold(pkgname))) - missing_deps = makepkg.check_missing_deps(pkgdir, handler.watcher) + check_res = makepkg.check_missing_deps(pkgdir, handler.watcher) - if missing_deps: - depnames = {RE_SPLIT_VERSION.split(dep)[0] for dep in missing_deps} - dep_mirrors = self._map_mirrors(depnames) + if check_res: - for dep in depnames: # cheking if a dependency could not be found in any mirror - if dep not in dep_mirrors: - message.show_dep_not_found(dep, self.i18n, handler.watcher) + if check_res.get('gpg_key'): + if handler.watcher.request_confirmation(title=self.i18n['arch.aur.install.unknown_key.title'], + body=self.i18n['arch.install.aur.unknown_key.body'].format(bold(pkgname), bold(check_res['gpg_key']))): + handler.watcher.change_substatus(self.i18n['arch.aur.install.unknown_key.status'].format(bold(check_res['gpg_key']))) + if not handler.handle(gpg.receive_key(check_res['gpg_key'])): + handler.watcher.show_message(title=self.i18n['error'], + body=self.i18n['arch.aur.install.unknown_key.receive_error'].format(bold(check_res['gpg_key']))) + return False + else: + handler.watcher.print(self.i18n['action.cancelled']) return False - handler.watcher.change_substatus(self.i18n['arch.missing_deps_found'].format(bold(pkgname))) + if check_res.get('missing_deps'): + depnames = {RE_SPLIT_VERSION.split(dep)[0] for dep in check_res['missing_deps']} + dep_mirrors = self._map_mirrors(depnames) - if not confirmation.request_install_missing_deps(pkgname, dep_mirrors, handler.watcher, self.i18n): - handler.watcher.print(self.i18n['action.cancelled']) - return False + for dep in depnames: # cheking if a dependency could not be found in any mirror + if dep not in dep_mirrors: + message.show_dep_not_found(dep, self.i18n, handler.watcher) + return False - dep_not_installed = self._install_deps(depnames, dep_mirrors, root_password, handler, change_progress=False) + handler.watcher.change_substatus(self.i18n['arch.missing_deps_found'].format(bold(pkgname))) - if dep_not_installed: - message.show_dep_not_installed(handler.watcher, pkgname, dep_not_installed, self.i18n) - return False + if not confirmation.request_install_missing_deps(pkgname, dep_mirrors, handler.watcher, self.i18n): + handler.watcher.print(self.i18n['action.cancelled']) + return False + + dep_not_installed = self._install_deps(depnames, dep_mirrors, root_password, handler, change_progress=False) + + if dep_not_installed: + message.show_dep_not_installed(handler.watcher, pkgname, dep_not_installed, self.i18n) + return False return True @@ -478,6 +495,41 @@ class ArchManager(SoftwareManager): if change_progress: watcher.change_progress(val) + def _import_pgp_keys(self, pkgname: str, root_password: str, handler: ProcessHandler): + srcinfo = self.aur_client.get_src_info(pkgname) + + if srcinfo.get('validpgpkeys'): + handler.watcher.print(self.i18n['arch.aur.install.verifying_pgp']) + keys_to_download = [key for key in srcinfo['validpgpkeys'] if not pacman.verify_pgp_key(key)] + + if keys_to_download: + keys_str = ''.join( + ['
- {}'.format(k) for k in keys_to_download]) + msg_body = '{}:
{}

{}'.format(self.i18n['arch.aur.install.pgp.body'].format(bold(pkgname)), + keys_str, self.i18n['ask.continue']) + + if handler.watcher.request_confirmation(title=self.i18n['arch.aur.install.pgp.title'], body=msg_body): + for key in keys_to_download: + handler.watcher.change_substatus(self.i18n['arch.aur.install.pgp.substatus'].format(bold(key))) + if not handler.handle(pacman.receive_key(key, root_password)): + handler.watcher.show_message(title=self.i18n['error'], + body=self.i18n['arch.aur.install.pgp.receive_fail'].format( + bold(key)), + type_=MessageType.ERROR) + return False + + if not handler.handle(pacman.sign_key(key, root_password)): + handler.watcher.show_message(title=self.i18n['error'], + body=self.i18n['arch.aur.install.pgp.sign_fail'].format( + bold(key)), + type_=MessageType.ERROR) + return False + + handler.watcher.change_substatus(self.i18n['arch.aur.install.pgp.success']) + else: + handler.watcher.print(self.i18n['action.cancelled']) + return False + def _install_from_aur(self, pkgname: str, root_password: str, handler: ProcessHandler, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool: app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time())) diff --git a/bauh/gems/arch/gpg.py b/bauh/gems/arch/gpg.py new file mode 100644 index 00000000..7b9e3378 --- /dev/null +++ b/bauh/gems/arch/gpg.py @@ -0,0 +1,5 @@ +from bauh.commons.system import SystemProcess, new_subprocess + + +def receive_key(key: str) -> SystemProcess: + return SystemProcess(new_subprocess(['gpg', '--recv-key', key]), check_error_output=False) diff --git a/bauh/gems/arch/makepkg.py b/bauh/gems/arch/makepkg.py index 96218c5b..843aac78 100644 --- a/bauh/gems/arch/makepkg.py +++ b/bauh/gems/arch/makepkg.py @@ -1,15 +1,15 @@ import re -from typing import List from bauh.api.abstract.handler import ProcessWatcher from bauh.commons.system import new_subprocess RE_DEPS_PATTERN = re.compile(r'\n?\s+->\s(.+)\n') +RE_UNKNOWN_GPG_KEY = re.compile(r'\(unknown public key (\w+)\)') -def check_missing_deps(pkgdir: str, watcher: ProcessWatcher) -> List[str]: +def check_missing_deps(pkgdir: str, watcher: ProcessWatcher) -> dict: depcheck = new_subprocess(['makepkg', '-L', '--check'], cwd=pkgdir) - + res = {} for o in depcheck.stdout: if o: line = o.decode().strip() @@ -33,4 +33,10 @@ def check_missing_deps(pkgdir: str, watcher: ProcessWatcher) -> List[str]: error_str = ''.join(error_lines) if 'Missing dependencies' in error_str: - return RE_DEPS_PATTERN.findall(error_str) + res['missing_deps'] = RE_DEPS_PATTERN.findall(error_str) + + gpg_keys = RE_UNKNOWN_GPG_KEY.findall(error_str) + if gpg_keys: + res['gpg_key'] = gpg_keys[0] + + return res diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 697f6df6..97ef6408 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -1,7 +1,9 @@ import re +import subprocess from typing import List, Set -from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess +from bauh.api.abstract.handler import ProcessWatcher +from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess, ProcessHandler RE_DEPS = re.compile(r'[\w\-_]+:[\s\w_\-\.]+\s+\[\w+\]') @@ -51,7 +53,8 @@ def get_info_dict(pkg_name: str) -> dict: info_dict = {} for info_data in info_list: attr = info_data[0].lower().strip() - info_dict[attr] = info_data[1] if '\n' not in info_data[1] else ' '.join([l.strip() for l in info_data[1].split('\n')]) + info_dict[attr] = info_data[1] if '\n' not in info_data[1] else ' '.join( + [l.strip() for l in info_data[1].split('\n')]) if info_dict[attr] == 'None': info_dict[attr] = None @@ -76,7 +79,8 @@ def list_and_map_installed() -> dict: # returns a dict with with package names allinfo = new_subprocess(['pacman', '-Qi'], stdin=installed).stdout # retrieving all installed packages info pkgs, current_pkg = {'mirrors': {}, 'not_signed': {}}, {} - for out in new_subprocess(['grep', '-E', '(Name|Description|Version|Validated By)'], stdin=allinfo).stdout: # filtering only the Name and Validated By fields: + for out in new_subprocess(['grep', '-E', '(Name|Description|Version|Validated By)'], + stdin=allinfo).stdout: # filtering only the Name and Validated By fields: if out: line = out.decode() @@ -90,7 +94,8 @@ def list_and_map_installed() -> dict: # returns a dict with with package names elif line.startswith('Validated'): if line.split(':')[1].strip().lower() == 'none': - pkgs['not_signed'][current_pkg['name']] = {'version': current_pkg['version'], 'description': current_pkg['description']} + pkgs['not_signed'][current_pkg['name']] = {'version': current_pkg['version'], + 'description': current_pkg['description']} current_pkg = {} @@ -98,7 +103,6 @@ def list_and_map_installed() -> dict: # returns a dict with with package names def install_as_process(pkgpath: str, root_password: str, aur: bool, pkgdir: str = '.') -> SystemProcess: - if aur: cmd = ['pacman', '-U', pkgpath, '--noconfirm'] # pkgpath = install file path else: @@ -143,3 +147,23 @@ def list_bin_paths(pkgnames: Set[str]) -> List[str]: bin_paths.append(line) return bin_paths + + +def verify_pgp_key(key: str) -> bool: + list_key = new_subprocess(['pacman-key', '-l']).stdout + + for out in new_subprocess(['grep', " " + key], stdin=list_key).stdout: + if out: + line = out.decode().strip() + if line and key in line: + return True + + return False + + +def receive_key(key: str, root_password: str) -> SystemProcess: + return SystemProcess(new_root_subprocess(['pacman-key', '-r', key], root_password=root_password), check_error_output=False) + + +def sign_key(key: str, root_password: str) -> SystemProcess: + return SystemProcess(new_root_subprocess(['pacman-key', '--lsign-key', key], root_password=root_password), check_error_output=False) diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index a925b205..c3af4bfa 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -47,4 +47,15 @@ arch.install.optdeps.request.body={} was succesfully installed ! There are some arch.install.optdep.error=Could not install the optional package {} arch.optdeps.checking=Checking {} optional dependencies arch.warning.disabled={} seems not to be installed. It will not be possible to manage Arch / AUR packages. -arch.warning.git={} seems not to be installed. It will not be possible to downgrade AUR packages. \ No newline at end of file +arch.warning.git={} seems not to be installed. It will not be possible to downgrade AUR packages. +arch.aur.install.verifying_pgp=Verifying PGP keys +arch.aur.install.pgp.title=PGP keys required +arch.aur.install.pgp.body=To install {} is necessary to receive the following PGP keys +arch.aur.install.pgp.substatus=Receiving PGP key {} +arch.aur.install.pgp.success=PGP keys received and signed +arch.aur.install.pgp.sign_fail=Could not sign PGP key {} +arch.aur.install.pgp.receive_fail=Could not receive PGP key {} +arch.aur.install.unknown_key.title=Public key required +arch.install.aur.unknown_key.body=To continue {} installation is necessary to trust the following public key {} +arch.aur.install.unknown_key.status=Receiving public key {} +arch.aur.install.unknown_key.receive_error=Could not receive public key {} \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index b4e81370..5c72e2b2 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -79,4 +79,15 @@ arch.install.optdeps.request.body=¡{} se instaló correctamente! También hay a arch.install.optdep.error=No se pudo instalar el paquete opcional {} arch.optdeps.checking=Verificando las dependencias opcionales de {} arch.warning.disabled={} parece no estar instalado. No será posible administrar paquetes Arch / AUR. -arch.warning.git={} parece no estar instalado. No será posible revertir las versiones de paquetes Arch / AUR. \ No newline at end of file +arch.warning.git={} parece no estar instalado. No será posible revertir las versiones de paquetes Arch / AUR. +arch.aur.install.verifying_pgp=Verificando claves PGP +arch.aur.install.pgp.title=Claves PGP necesarias +arch.aur.install.pgp.body=Para instalar {} es necesario recibir las siguientes claves PGP +arch.aur.install.pgp.substatus=Recibiendo clave PGP {} +arch.aur.install.pgp.success=Claves PGP recibidas y firmadas +arch.aur.install.pgp.sign_fail=No fue posible firmar la clave PGP {} +arch.aur.install.pgp.sign_fail=No fue posible recibir la clave PGP {} +arch.aur.install.unknown_key.title=Clave pública necesaria +arch.aur.install.unknown_key.status=Recibiendo la clave pública {} +arch.aur.install.unknown_key.receive_error=No fue posible recibir la clave pública {} +arch.install.aur.unknown_key.body=Para continuar la instalación de {} es necesario confiar en la siguiente clave pública {} \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 803fc61c..03760cc0 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -79,4 +79,15 @@ arch.install.optdeps.request.body={} foi instalado com sucesso ! Existem alguns arch.install.optdep.error=Não foi possível instalar o pacote opcional {} arch.optdeps.checking=Verificando as dependências opcionais de {} arch.warning.disabled={} parece não estar instalado. Não será possível gerenciar pacotes Arch / AUR. -arch.warning.git={} parece não estar instalado. Não será possível reverter versões de pacotes Arch / AUR. \ No newline at end of file +arch.warning.git={} parece não estar instalado. Não será possível reverter versões de pacotes Arch / AUR. +arch.aur.install.verifying_pgp=Verificando chaves PGP +arch.aur.install.pgp.title=Chaves PGP necessárias +arch.aur.install.pgp.body=Para instalar {} é necessário receber as seguintes chaves PGP +arch.aur.install.pgp.substatus=Recebendo chave PGP {} +arch.aur.install.pgp.success=Chaves PGP recebidas e assinadas +arch.aur.install.pgp.sign_fail=Não foi possível assinar a chave PGP {} +arch.aur.install.pgp.receive_fail=Não foi possível receber a chave PGP {} +arch.aur.install.unknown_key.title=Chave pública necessária +arch.install.aur.unknown_key.body=Para continuar a instalação de {} é ncessário confiar na seguinte chave pública {} +arch.aur.install.unknown_key.status=Recebendo a chave pública {} +arch.aur.install.unknown_key.receive_error=Não fui possível receber a chave pública {} \ No newline at end of file