From 302e5a50ce357ca0bcc91ab0061db7f08e8b32cc Mon Sep 17 00:00:00 2001 From: Christopher Vagnetoft Date: Sun, 11 Dec 2016 22:36:27 +0100 Subject: [PATCH] hotfix: Added aliases, implemented new runners --- bin/bootstrap.php | 19 ++++ examples/bash-facts.fix | 10 ++ hotfix.conf | 4 + installer/make-installer.sh | 5 + installer/src/install.sh | 7 ++ src/Command/ApplyCommand.php | 112 +++++++++++---------- src/Command/FactsCommand.php | 6 +- src/Exception/ConfigurationException.php | 7 ++ src/Hotfix/AliasManager.php | 41 ++++++++ src/Hotfix/Header.php | 83 ++++++++++++++++ src/Hotfix/Hotfix.php | 119 ++++++----------------- src/Hotfix/HotfixLoader.php | 75 ++++++++++++++ src/Hotfix/Loader.php | 86 ---------------- src/Hotfix/Signature.php | 98 +++++++++++++++++++ src/Runner/BashRunner.php | 28 ++++++ src/Runner/PhpRunner.php | 28 ++++++ src/Runner/RunnerFactory.php | 17 ++++ src/Runner/RunnerInterface.php | 4 +- src/Service/IxService.php | 27 +++++ src/Service/PublisherInterface.php | 7 ++ src/Service/ReaderInterface.php | 7 ++ src/Service/ServiceInterface.php | 7 ++ src/Service/ServiceManager.php | 25 +++++ 23 files changed, 597 insertions(+), 225 deletions(-) create mode 100644 examples/bash-facts.fix create mode 100644 hotfix.conf create mode 100755 installer/make-installer.sh create mode 100644 src/Exception/ConfigurationException.php create mode 100644 src/Hotfix/AliasManager.php create mode 100644 src/Hotfix/Header.php create mode 100644 src/Hotfix/HotfixLoader.php delete mode 100644 src/Hotfix/Loader.php create mode 100644 src/Hotfix/Signature.php create mode 100644 src/Service/IxService.php create mode 100644 src/Service/PublisherInterface.php create mode 100644 src/Service/ReaderInterface.php create mode 100644 src/Service/ServiceInterface.php create mode 100644 src/Service/ServiceManager.php diff --git a/bin/bootstrap.php b/bin/bootstrap.php index 8179951..bf90aa0 100644 --- a/bin/bootstrap.php +++ b/bin/bootstrap.php @@ -7,7 +7,26 @@ require_once __DIR__."/systemtest.php"; if (file_exists(__DIR__."/banner.php")) require_once __DIR__."/banner.php"; +use NoccyLabs\Hotfix\Hotfix\AliasManager; +use NoccyLabs\Hotfix\Service\ServiceManager; +use NoccyLabs\Hotfix\Service; use NoccyLabs\Hotfix\HotfixApplication; +// Register services +ServiceManager::registerService(new Service\IxService()); + +// Register aliases +$paths = [ + getenv("HOME")."/.hotfix.conf", + "/etc/hotfix.conf" +]; +foreach ($paths as $path) { + if (file_exists($path)) { + AliasManager::registerFromConfig($path); + break; + } +} + +// Start the application $app = new HotfixApplication(); $app->run(); diff --git a/examples/bash-facts.fix b/examples/bash-facts.fix new file mode 100644 index 0000000..b730d84 --- /dev/null +++ b/examples/bash-facts.fix @@ -0,0 +1,10 @@ +hotfix: This hotfix show how to use facts in bash +info: > + This lets you retrieve some additional system information during + runtime. Use 'hotfix facts' to see all the known facts. +author: Noccy +lang: bash +--- + +info "Architecture: $(fact system.arch)" +info "Distribution: $(fact lsb.id)" diff --git a/hotfix.conf b/hotfix.conf new file mode 100644 index 0000000..acd1c42 --- /dev/null +++ b/hotfix.conf @@ -0,0 +1,4 @@ +[alias] +gist=https://gist.githubusercontent.com/{fix}/raw +pastebin=https:/pastebin.com/raw/{fix} +noccy=http://files.noccy.com/hotfix/{fix}.fix.signed diff --git a/installer/make-installer.sh b/installer/make-installer.sh new file mode 100755 index 0000000..91203de --- /dev/null +++ b/installer/make-installer.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cp ../hotfix src/hotfix +cp ../hotfix.conf src/hotfix.conf +cp ../LICENSE src/LICENSE +makeself --copy src hotfix-installer.run hotfix ./install.sh diff --git a/installer/src/install.sh b/installer/src/install.sh index 3a11dc2..589f566 100755 --- a/installer/src/install.sh +++ b/installer/src/install.sh @@ -66,6 +66,13 @@ function do_install { fi chmod +x $1/hotfix + if [ -f $HOME/.hotfix.conf ]; then + debug "~/.hotfix.conf already exists, will not overwrite" + else + debug "Copying hotfix.conf into ~/.hotfix.conf" + cp hotfix.conf $HOME/.hotfix.conf + fi + debug "Verifying that hotfix is callable..." source ~/.profile &>/dev/null source ~/.bashrc &>/dev/null diff --git a/src/Command/ApplyCommand.php b/src/Command/ApplyCommand.php index 2d52e69..713ac2e 100644 --- a/src/Command/ApplyCommand.php +++ b/src/Command/ApplyCommand.php @@ -12,8 +12,10 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use NoccyLabs\Hotfix\Hotfix\Loader; +use NoccyLabs\Hotfix\Hotfix\Hotfix; use NoccyLabs\Hotfix\System\Facts; - +use NoccyLabs\Hotfix\Runner\RunnerFactory; +use NoccyLabs\Hotfix\Hotfix\HotfixLoader; class ApplyCommand extends Command { @@ -26,7 +28,7 @@ class ApplyCommand extends Command $this->addOption("insecure", "I", InputOption::VALUE_NONE, "Disable signature checks. Don't use unless you know what you are doing!"); $this->addOption("preview", "p", InputOption::VALUE_NONE, "Only preview the hotfix script, don't apply anything"); - $this->addArgument("hotfix", InputArgument::OPTIONAL, "The identifier, path or filename of the hotfix"); + $this->addArgument("hotfix", InputArgument::OPTIONAL, "The identifier or filename of the hotfix"); } protected function execute(InputInterface $input, OutputInterface $output) @@ -34,69 +36,45 @@ class ApplyCommand extends Command $this->output = $output; $fix = $input->getArgument("hotfix"); - $insecure = $input->getOption("insecure"); - $loader = new Loader(); if (!$fix) { - - $loaders = $loader->getLoaders(); - $output->writeln("Supported loaders:\n"); - foreach ($loaders as $loader) { - $output->writeln(" * ".$loader->getInfo()); - } + $output->writeln("No hotfix specified."); return; } + //$loader = new Loader(); $output->writeln("Reading hotfix {$fix}..."); - try { - $hotfix = $loader->load($fix, $insecure); + $hotfix = HotfixLoader::load($fix); + //$hotfix = $loader->load($fix, $insecure); } catch (\Exception $e) { $output->writeln("Error: ".$e->getMessage().""); return; } - if (!$hotfix) { - $output->writeln("Could not load hotfix"); + + $output->writeln(""); + + $header = $hotfix->getHeader(); + $output->writeln(" Hotfix: ".$header->getName().""); + $output->writeln(" Author: ".$header->getAuthor().""); + $info = explode("\n",wordwrap(trim($header->getInfo()), 60)); + $info = "".join("\n ", $info).""; + $output->writeln(" Info: ".$info); + $output->writeln(""); + + if (!$this->checkSignature($hotfix, $output)) { + if (!$input->getOption('insecure')) { + $output->writeln("Hotfix can not be authenticated. Aborting!"); + return; + } + } + if (!$this->checkRequirements($hotfix, $output)) { + $output->writeln("Error: This hotfix it not compatible with the current distribution"); return; } $output->writeln(""); - if (($signer = $hotfix->getSignedBy())) { - $keyid = $hotfix->getKeyId(); - $output->writeln("Hotfix has good signature from {$signer} (keyid {$keyid})"); - } else { - $output->writeln("Warning: Hotfix is not signed or signature not valid!"); - } - - $requires = $hotfix->getRequirements(); - if (count($requires)>0) { - $engine = new ExpressionLanguage(); - $facts = Facts::getSystemFacts()->getFacts(); - while (true) { - $expr = array_shift($requires); - $ret = $engine->evaluate($expr, $facts); - if ($ret) { - //$output->writeln("Hotfix is compatible with the current distribution"); - break; - } - if (count($requires)==0) { - $output->writeln("Error: This hotfix it not compatible with the current distribution"); - return false; - } - } - //} else { - //$output->writeln("Hotfix indicates universal compatibility."); - } - - $output->writeln(""); - $output->writeln(" Hotfix: ".$hotfix->getName().""); - $output->writeln(" Author: ".$hotfix->getAuthor().""); - $info = explode("\n",wordwrap(trim($hotfix->getInfo()), 60)); - $info = "".join("\n ", $info).""; - $output->writeln(" Info: ".$info); - $output->writeln(""); - if ($input->getOption("preview")) { $output->writeln("This is the script that will be executed:"); $body = $hotfix->getBody(); @@ -111,6 +89,7 @@ class ApplyCommand extends Command if (!$helper->ask($input, $output, $question)) { return; } + $output->writeln("\nApplying hotfix...\n"); try { @@ -121,4 +100,39 @@ class ApplyCommand extends Command $output->writeln("\nHotfix applied successfully"); } + private function checkSignature(Hotfix $hotfix, OutputInterface $output) + { + $signature = $hotfix->getSignature(); + if ($signature->isValid()) { + $keyid = $signature->getKeyId(); + $signer = $signature->getSigner(); + $output->writeln("Hotfix has good signature from {$signer} (keyid {$keyid})"); + } else { + $error = $signature->getError(); + $output->writeln("Warning: {$error}"); + } + } + + private function checkRequirements(Hotfix $hotfix, OutputInterface $output) + { + $requires = $hotfix->getHeader()->getRequirements(); + if (count($requires)>0) { + $engine = new ExpressionLanguage(); + $facts = Facts::getSystemFacts()->getFacts(); + while (true) { + $expr = array_shift($requires); + $ret = $engine->evaluate($expr, $facts); + if ($ret) { + //$output->writeln("Hotfix is compatible with the current distribution"); + break; + } + if (count($requires)==0) { + return false; + } + } + } + return true; + + } + } diff --git a/src/Command/FactsCommand.php b/src/Command/FactsCommand.php index e5047c7..0d0755b 100644 --- a/src/Command/FactsCommand.php +++ b/src/Command/FactsCommand.php @@ -26,8 +26,10 @@ class FactsCommand extends Command protected function execute(InputInterface $input, OutputInterface $output) { $facts = Facts::getSystemFacts()->getFlat(); - - print_r($facts); + + foreach ($facts as $fact=>$value) { + $output->writeln(" {$fact}: {$value}"); + } } } diff --git a/src/Exception/ConfigurationException.php b/src/Exception/ConfigurationException.php new file mode 100644 index 0000000..5bc9440 --- /dev/null +++ b/src/Exception/ConfigurationException.php @@ -0,0 +1,7 @@ +$url) { + self::registerAlias($prefix, $url); + } + } + + public static function registerAlias($prefix, $url) + { + if (strpos($url,'{fix}')===false) { + throw new ConfigurationException("The alias URL for {$prefix} does not contain the magic '{fix}' placeholder"); + } + self::$aliases[$prefix] = $url; + } + + public static function getRegisteredAlises() + { + return self::$aliases; + } + + public static function getAlias($prefix) + { + if (!array_key_exists($prefix,self::$aliases)) return null; + return self::$aliases[$prefix]; + } + +} \ No newline at end of file diff --git a/src/Hotfix/Header.php b/src/Hotfix/Header.php new file mode 100644 index 0000000..091e456 --- /dev/null +++ b/src/Hotfix/Header.php @@ -0,0 +1,83 @@ +name = $header['hotfix']; + $this->author = $header['author']; + $this->info = $header['info']; + $this->language = $header['lang']; + + if (array_key_exists('for', $header)) { + $this->requirements = $header['for']; + } + } + + public function getName() + { + return $this->name; + } + + public function getAuthor() + { + return $this->author; + } + + public function getInfo() + { + return $this->info; + } + + public function getLanguage() + { + return $this->language; + } + + public function getRequirements() + { + return $this->requirements; + } +} diff --git a/src/Hotfix/Hotfix.php b/src/Hotfix/Hotfix.php index 1e9497f..789e4a2 100644 --- a/src/Hotfix/Hotfix.php +++ b/src/Hotfix/Hotfix.php @@ -3,23 +3,45 @@ namespace NoccyLabs\Hotfix\Hotfix; use Symfony\Component\Yaml\Yaml; +use NoccyLabs\Hotfix\Runner\RunnerFactory; class Hotfix { - protected $signer; - - protected $keyId; - protected $header; protected $body; - public function __construct($hotfix, $signer) + protected $signature; + + protected $origin; + + public function __construct($body, Header $header, Signature $signature, $origin) { - $this->load($hotfix); - $this->signer = $signer[0]; - $this->keyId = $signer[1]; + $this->header = $header; + $this->body = $body; + $this->signature = $signature; + $this->origin = $origin; + } + + public function getHeader() + { + return $this->header; + } + + public function getHash() + { + return sha1($this->body); + } + + public function getSignature() + { + return $this->signature; + } + + public function getBody() + { + return $this->body; } protected function load($hotfix) @@ -34,88 +56,11 @@ class Hotfix $this->body = $body; } - public function getBody() - { - return $this->body; - } public function apply() { - if (!array_key_exists('lang', $this->header)) { - $lang = 'bash'; - } else { - $lang = strtolower($this->header['lang']); - } - - $script = null; - $head = null; - $foot = null; - switch ($lang) { - case 'bash': - $exec = "/bin/bash"; - $head = file_get_contents(__DIR__."/../stubs/bash.stub"); - break; - case 'php': - $exec = "/usr/bin/env php"; - $head = "header['hotfix']); - - file_put_contents($tmpfile, $head."\n".$this->body."\n".$foot); - passthru($exec." ".$tmpfile); - unlink($tmpfile); - } - - public function getRequirements() - { - if (!array_key_exists('for',$this->header)) { - return []; - } - return $this->header['for']; - } - - public function getSignedBy() - { - if (!$this->signer) { - return null; - } - return sprintf("%s <%s>", - $this->signer['uids'][0]['name'], - $this->signer['uids'][0]['email'] - ); - } - - public function getKeyId() - { - return $this->keyId; - } - - public function getName() - { - if (!array_key_exists('hotfix', $this->header)) { - return "Untitled hotfix"; - } - return $this->header['hotfix']; - } - - public function getInfo() - { - if (!array_key_exists('info', $this->header)) { - return "No additional information"; - } - return $this->header['info']; - } - - public function getAuthor() - { - if (!array_key_exists('author', $this->header)) { - return "Unknown author"; - } - return $this->header['author']; + $runner = RunnerFactory::createRunner($this); + $runner->apply(); } } diff --git a/src/Hotfix/HotfixLoader.php b/src/Hotfix/HotfixLoader.php new file mode 100644 index 0000000..d959eb3 --- /dev/null +++ b/src/Hotfix/HotfixLoader.php @@ -0,0 +1,75 @@ +read($id); + return self::loadHotfix($source,$service->getName()); + } + + private static function loadHotfix($source,$origin) + { + + // Check for a signature header + if (false === strpos($source, self::PGP_HEADER_TOKEN)) { + $body = $source; + $signature = new Signature($body,null); + } else { + list ($body, $sig) = explode(self::PGP_HEADER_TOKEN, $source); + $sig = self::PGP_HEADER_TOKEN.$sig; + $signature = new Signature($body,$sig); + } + + // Extract header next + list ($header,$body) = explode("\n---\n", $body, 2); + $header = new Header($header); + + $hotfix = new Hotfix($body, $header, $signature, $origin); + return $hotfix; + } + +} \ No newline at end of file diff --git a/src/Hotfix/Loader.php b/src/Hotfix/Loader.php deleted file mode 100644 index 794b0b9..0000000 --- a/src/Hotfix/Loader.php +++ /dev/null @@ -1,86 +0,0 @@ -addLoader(new Loader\FileLoader()); - $this->addLoader(new Loader\HttpLoader()); - $this->addLoader(new Loader\GistLoader()); - $this->addLoader(new Loader\PastebinLoader()); - } - - public function addLoader(Loader\LoaderInterface $loader) - { - $this->loaders[] = $loader; - } - - public function getLoaders() - { - return $this->loaders; - } - - public function load($fix, $insecure=false) - { - foreach ($this->loaders as $loader) { - $hotfix = $loader->load($fix); - if ($hotfix === false) { - continue; - } - $sigHeader = '-----BEGIN PGP SIGNATURE-----'; - if (false === strpos($hotfix, $sigHeader)) { - if (!$insecure) { - throw new \Exception("Hotfix is not signed"); - } - $body = $hotfix; - $signer = null; - } else { - list ($body, $signature) = explode($sigHeader, $hotfix); - $signature = $sigHeader.$signature; - if (!$insecure) { - $signer = $this->verifySignature($body, $signature); - } else { - $signer = null; - } - } - return new Hotfix($body, $signer); - } - fprintf(STDERR, "Error: Couldn't load '%s'\n", $fix); - } - - protected function verifySignature($body, $signature) - { - $gpg = gnupg_init(); - - $sigInfo = gnupg_verify($gpg, $body, $signature); - - if ($sigInfo === false) { - throw new \Exception("Hotfix signature is not valid!"); - } - - $fingerprint = $sigInfo[0]['fingerprint']; - $keyInfo = gnupg_keyinfo($gpg, $fingerprint); - - if (empty($keyInfo)) { - throw new \Exception("Unknown signer (key id {$sigInfo[0]['fingerprint']})"); - } - - $subKeys = $keyInfo[0]['subkeys']; - $keyId = null; - foreach ($subKeys as $subKey) { - if ($subKey['fingerprint'] == $fingerprint) { - $keyId = $subKey['keyid']; - break; - } - } - - return [ $keyInfo[0], $keyId ]; - } -} diff --git a/src/Hotfix/Signature.php b/src/Hotfix/Signature.php new file mode 100644 index 0000000..39e8259 --- /dev/null +++ b/src/Hotfix/Signature.php @@ -0,0 +1,98 @@ +body = $body; + $this->signature = $signature; + $this->verify(); + } + + public function verify() + { + if (!$this->signature) { + $this->error = "Hotfix is not signed!"; + return; + } + + $gpg = gnupg_init(); + + $sigInfo = gnupg_verify($gpg, $this->body, $this->signature); + + if ($sigInfo === false) { + $this->error = "Invalid signature"; + return; + } + + $fingerprint = $sigInfo[0]['fingerprint']; + $keyInfo = gnupg_keyinfo($gpg, $fingerprint); + + if (empty($keyInfo)) { + $this->error = "Unknown signer (key id {$sigInfo[0]['fingerprint']})"; + return; + } + + $subKeys = $keyInfo[0]['subkeys']; + $keyId = null; + foreach ($subKeys as $subKey) { + if ($subKey['fingerprint'] == $fingerprint) { + $keyId = $subKey['keyid']; + break; + } + } + + $signerInfo = sprintf("%s (%s)", $keyInfo[0]['uids'][0]['name'], $keyInfo[0]['uids'][0]['email']); + + $this->valid = true; + $this->signer = $signerInfo; + $this->keyId = $keyId; + } + + public function isValid() + { + return ($this->valid === true); + } + + public function getSigner() + { + return $this->signer; + } + + public function getKeyId() + { + return $this->keyId; + } + + public function getError() + { + return $this->error; + } + +} diff --git a/src/Runner/BashRunner.php b/src/Runner/BashRunner.php index 8a1022c..53d46cf 100644 --- a/src/Runner/BashRunner.php +++ b/src/Runner/BashRunner.php @@ -13,12 +13,40 @@ use NoccyLabs\Hotfix\Hotfix\Hotfix; class BashRunner implements RunnerInterface { + const BASH_STUB = "/../stubs/bash.stub"; + + protected $hotfix; + + protected $facts; + public function prepare(Hotfix $hotfix, Facts $facts) { + $this->hotfix = $hotfix; + $this->facts = $facts; } public function apply() { + $head = file_get_contents(__DIR__.self::BASH_STUB); + $body = $this->hotfix->getBody(); + $hash = $this->hotfix->getHash(); + + $facts = $this->facts->getFlat(); + $head .= 'function fact {'.PHP_EOL; + $head .= ' case "$1" in'.PHP_EOL; + foreach ($facts as $key=>$fact) { + $head .= ' "'.$key.'") echo "'.$fact.'";;'.PHP_EOL; + } + $head .= ' esac'.PHP_EOL; + $head .= '}'.PHP_EOL; + + // Create temporary filename based on the hash of the hotfix + $tmpfile = sys_get_temp_dir()."/hotfix_".$hash; + file_put_contents($tmpfile, $head."\n".$body."\n"); + + // Execute the hotfix and clean up + passthru("bash {$tmpfile}"); + unlink($tmpfile); } } \ No newline at end of file diff --git a/src/Runner/PhpRunner.php b/src/Runner/PhpRunner.php index ce2f5ad..e3d5593 100644 --- a/src/Runner/PhpRunner.php +++ b/src/Runner/PhpRunner.php @@ -13,12 +13,40 @@ use NoccyLabs\Hotfix\Hotfix\Hotfix; class PhpRunner implements RunnerInterface { + const PHP_STUB = "/../stubs/php.stub"; + + protected $hotfix; + + protected $facts; + public function prepare(Hotfix $hotfix, Facts $facts) { + $this->hotfix = $hotfix; + $this->facts = $facts; } public function apply() { + $stub = file_get_contents(__DIR__.self::PHP_STUB); + $body = $this->hotfix->getBody(); + $hash = $this->hotfix->getHash(); + + $stub.= 'function fact($key) { '; + $stub.= 'switch ($key) {'; + $facts = $this->facts->getFlat(); + foreach ($facts as $key=>$value) { + $stub.= 'case "'.$key.'": return "'.$value.'";'; + } + $stub.= '}}'.PHP_EOL; + + // Create temporary filename based on the hash of the hotfix filename + $tmpfile = sys_get_temp_dir()."/hotfix_".$hash; + file_put_contents($tmpfile, 'getHeader(); + switch ($meta->getLanguage()) { + case 'php': + $runner = new PhpRunner(); + break; + case 'bash': + $runner = new BashRunner(); + break; + case 'python': + $runner = new PythonRunner(); + break; + default: + throw new UnsupportedRunnerException("This version of hotfix doesn't support the runner ".$meta->getLanguage()); + } + $runner->prepare($hotfix, $facts); + + return $runner; } } \ No newline at end of file diff --git a/src/Runner/RunnerInterface.php b/src/Runner/RunnerInterface.php index 500aa35..900d2cc 100644 --- a/src/Runner/RunnerInterface.php +++ b/src/Runner/RunnerInterface.php @@ -9,6 +9,8 @@ use NoccyLabs\Hotfix\Hotfix\Hotfix; interface RunnerInterface { - public function applyHotfix(Hotfix $hotfix, Facts $facts); + public function prepare(Hotfix $hotfix, Facts $facts); + + public function apply(); } \ No newline at end of file diff --git a/src/Service/IxService.php b/src/Service/IxService.php new file mode 100644 index 0000000..ba738c3 --- /dev/null +++ b/src/Service/IxService.php @@ -0,0 +1,27 @@ +getId()] = $service; + } + + public static function getRegisteredServices() + { + return self::$services; + } + + public static function getService($id) + { + if (!array_key_exists($id,self::$services)) return null; + return self::$services[$id]; + } + +} \ No newline at end of file