Skip to content

Commit

Permalink
Merge pull request #162 from hydephp/release/v0.7.62
Browse files Browse the repository at this point in the history
HydeCLI v0.7.62
  • Loading branch information
caendesilva authored Apr 23, 2024
2 parents d035919 + 4f863a5 commit ea81306
Show file tree
Hide file tree
Showing 7 changed files with 376 additions and 42 deletions.
2 changes: 1 addition & 1 deletion app/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

class Application extends \Hyde\Foundation\Application
{
final public const APP_VERSION = '0.7.61';
final public const APP_VERSION = '0.7.62';

public function getCachedPackagesPath(): string
{
Expand Down
194 changes: 156 additions & 38 deletions app/Commands/SelfUpdateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use function implode;
use function tempnam;
use function dirname;
use function defined;
use function passthru;
use function in_array;
use function array_map;
Expand All @@ -40,10 +41,12 @@
use function array_combine;
use function clearstatcache;
use function escapeshellarg;
use function openssl_verify;
use function sys_get_temp_dir;
use function extension_loaded;
use function file_get_contents;
use function get_included_files;
use function openssl_pkey_get_public;

/**
* @experimental This command is highly experimental and may contain bugs.
Expand Down Expand Up @@ -71,15 +74,22 @@ class SelfUpdateCommand extends Command

protected const COMPOSER_COMMAND = 'composer global require hyde/cli';

/** @var array<string, string|array<string>> The latest release information from the GitHub API */
protected array $release;
/**
* The latest release information from the GitHub API.
*
* @var object{tag: string, assets: array<string, array{name: string, browser_download_url: string}>}
*/
protected object $release;

/**
* @var string The path to the application executable
* @example Generally /user/bin/hyde, /usr/local/bin/hyde, /home/<User>/.config/composer/vendor/bin/hyde, or C:\Users\<User>\AppData\Roaming\Composer\vendor\bin\hyde
*/
protected string $applicationPath;

/** @internal Mocking hook for unit tests */
protected ?string $releaseResponse = null;

public function handle(): int
{
try {
Expand All @@ -101,10 +111,13 @@ public function handle(): int

$this->debug('Update strategy: '.($strategy === self::STRATEGY_COMPOSER ? 'Composer' : 'Direct download'));

$this->debug('Getting the latest release information from GitHub...');
$this->release = $this->getLatestReleaseInformationFromGitHub();

$currentVersion = $this->parseVersion(Application::APP_VERSION);
$this->debug('Current version: v'.implode('.', $currentVersion));

$latestVersion = $this->parseVersion($this->getLatestReleaseVersion());
$latestVersion = $this->parseVersion($this->release->tag);
$this->debug('Latest version: v'.implode('.', $latestVersion));

$this->printNewlineIfVerbose();
Expand Down Expand Up @@ -172,20 +185,12 @@ public function handle(): int
}
}

protected function getLatestReleaseVersion(): string
protected function getLatestReleaseInformationFromGitHub(): object
{
$this->getLatestReleaseInformation();
/** @see tests/Fixtures/general/github-release-api-response.json */
$data = json_decode($this->releaseResponse ?? $this->makeGitHubApiResponse(), true);

return $this->release['tag_name'];
}

protected function getLatestReleaseInformation(): void
{
$data = json_decode($this->makeGitHubApiResponse(), true);

$this->validateReleaseData($data);

$this->release = $data;
return $this->makeGitHubReleaseObject($data);
}

protected function makeGitHubApiResponse(): string
Expand All @@ -201,22 +206,6 @@ protected function getUserAgent(): string
return sprintf('HydePHP CLI updater v%s (github.com/hydephp/cli)', Application::APP_VERSION);
}

protected function validateReleaseData(array $data): void
{
$this->assertReleaseEntryIsValid(isset($data['tag_name']));
$this->assertReleaseEntryIsValid(isset($data['assets']));
$this->assertReleaseEntryIsValid(isset($data['assets'][0]));
$this->assertReleaseEntryIsValid(isset($data['assets'][0]['browser_download_url']));
$this->assertReleaseEntryIsValid(isset($data['assets'][0]['name']) && $data['assets'][0]['name'] === 'hyde');
}

protected function assertReleaseEntryIsValid(bool $condition): void
{
if (! $condition) {
throw new RuntimeException('Invalid release data received from the GitHub API.');
}
}

/** @return array{major: int, minor: int, patch: int} */
protected function parseVersion(string $semver): array
{
Expand Down Expand Up @@ -256,9 +245,9 @@ protected function printVersionStateInformation(int $state): void
};

if ($state === self::STATE_BEHIND) {
$this->line(sprintf('<info>%s</info> (<comment>%s</comment> <fg=gray>-></> <comment>%s</comment>)', $message, 'v'.Application::APP_VERSION, $this->release['tag_name']));
$this->line(sprintf('<info>%s</info> (<comment>%s</comment> <fg=gray>-></> <comment>%s</comment>)', $message, 'v'.Application::APP_VERSION, $this->release->tag));
} else {
$this->line(sprintf('<info>%s</info> (<comment>%s</comment>)', $message, $this->release['tag_name']));
$this->line(sprintf('<info>%s</info> (<comment>%s</comment>)', $message, $this->release->tag));
}
}

Expand Down Expand Up @@ -298,16 +287,32 @@ protected function updateDirectly(): void

$this->debug('Downloading the latest version...');

$tempPath = tempnam(sys_get_temp_dir(), 'hyde');

// Download the latest release from GitHub
$downloadUrl = $this->release['assets'][0]['browser_download_url'];
$downloadedFile = tempnam(sys_get_temp_dir(), 'hyde');
$this->downloadFile($downloadUrl, $downloadedFile);
$phar = $tempPath.'.phar';
$this->downloadFile($this->release->asset('hyde')['browser_download_url'], $phar);
$signature = $tempPath.'.sig';
$this->downloadFile($this->release->asset('signature.bin')['browser_download_url'], $signature);

if (! extension_loaded('openssl')) {
$this->warn('Skipping signature verification as the OpenSSL extension is not available.');
} else {
$this->debug('Verifying the signature...');
$isValid = $this->verifySignature($phar, $signature);

if ($isValid) {
$this->debug('Signature is valid!');
} else {
throw new RuntimeException('The signature is invalid! The downloaded file may be corrupted or tampered with.');
}
}

// Make the downloaded file executable
chmod($downloadedFile, 0755);
chmod($phar, 0755);

// Replace the current application with the downloaded one
$this->replaceApplication($downloadedFile);
$this->replaceApplication($phar);
}

protected function downloadFile(string $url, string $destination): void
Expand All @@ -325,6 +330,34 @@ protected function downloadFile(string $url, string $destination): void
fclose($file);
}

/**
* Verify the signature of the downloaded file against the public embedded public key.
*
* @param string $phar The path to the downloaded PHAR file
* @param string $signature The path to the downloaded signature file
*
* @return bool Whether the signature is valid, true if it is, false otherwise
*
* @throws RuntimeException If the public key could not be loaded or the needed algorithm is missing.
*/
protected function verifySignature(string $phar, string $signature): bool
{
$publicKey = openssl_pkey_get_public(self::publicKey());

if ($publicKey === false) {
throw new RuntimeException('Failed to load the public key.');
}

if (! defined('OPENSSL_ALGO_SHA512')) {
throw new RuntimeException('The OpenSSL extension is missing the SHA-512 algorithm.');
}

$data = file_get_contents($phar);
$signature = file_get_contents($signature);

return openssl_verify($data, $signature, $publicKey, OPENSSL_ALGO_SHA512) === 1;
}

protected function replaceApplication(string $downloadedFile): void
{
$this->debug("Moving file $downloadedFile to $this->applicationPath");
Expand Down Expand Up @@ -441,4 +474,89 @@ protected function printNewlineIfVerbose(): void
{
$this->debug('');
}

protected function makeGitHubReleaseObject(array $data): object
{
return new class ($data) {
protected readonly array $data;

/** @var string The tag name of the release */
public readonly string $tag;

/** @var array<string, array{name: string, browser_download_url: string}> Release assets keyed by their name */
public readonly array $assets;

public function __construct(array $data)
{
$this->validate($data);

$this->data = $data;

$this->tag = $data['tag_name'];
$this->assets = array_combine(array_map(fn (array $asset): string => $asset['name'], $data['assets']), $data['assets']);
}

public function __get(string $name): mixed
{
return $this->data[$name] ?? null;
}

public function asset(string $name): array
{
return $this->assets[$name];
}

protected function validate(array $data): void
{
$this->assertReleaseEntryIsValid(isset($data['tag_name']));
$this->assertReleaseEntryIsValid(isset($data['assets']));

foreach ($data['assets'] as $asset) {
$this->assertReleaseEntryIsValid(isset($asset['name']));
$this->assertReleaseEntryIsValid(isset($asset['browser_download_url']));
}
}

protected function assertReleaseEntryIsValid(bool $condition): void
{
if (! $condition) {
throw new RuntimeException('Invalid release data received from the GitHub API.');
}
}
};
}

/**
* The public key used to verify the signature of the downloaded file.
*
* @uid HydePHP CLI Bravo RSA Key
* @id EE5FC423177F61B096D768E3B3D3CA94C5435426
*
* @created 2024‑04‑22
* @expires <never>
*
* @link https://trustservices.hydephp.com/certificates/EE5FC423177F61B096D768E3B3D3CA94C5435426.pem
* @link https://github.com/hydephp/certificates/tree/master/EE5FC423177F61B096D768E3B3D3CA94C5435426
*
* @return string The public key in ASCII-armored format
*/
protected static final function publicKey(): string
{
return <<<'TXT'
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAs3cVirZlZhS/zl2svR09
6gcoQg1QNbyHQzomRWwiO3Zk0TphFzRJ/wATFQ+BjytgQzjOEi6YKSVZLgu0CKHd
JG27wpFyuLO0OkykCnHOQ/O81K9YI0WpgAd/pA60BpOh+5LUx0lsjRqPzV/O2Rk4
YekJk7bdLMgwoAM6fTpg1gM1/5ytFd0Gc1461s4cmQCH51pX2NPdldGYNOjgSZKk
qJMYpvDNfLNqzhc2gXHqenswAwWGspWgC32lcm0TVknC5+wt1SDGei5IyP/hv/L5
Hr2C9QvzH3nDuK3qea8Hpk5IbcRoiUm+HIBQ/wRzCa3UOkNGmipNlVicOHxaSwpn
M2x94TIjR2f3adUA9hmjHicPPPmDCc8wUfLmfktF2+4C6NL4BwdRuC2bdp/Dfsys
pW2Rjq4KDU06IzfPq1B6PNs6vwwCbQ4AT/X3hhFl1e25ygRaneB1NRLBCj+/X9j3
lhlxVDo6y83E9QkqebiBJpJ0aGFPfi8vpAt+IgRr2C7rAFiCrjDUIRQaNZfC19W1
UxkBzXPe+HXIOc9CVSWtVgf2fPkyn0WkZmSrN5M0UA12snMoLPDzPJ+K50TwO5Y1
60NQorbPFpjIy5WSAn+a+F5SwZ+3umk1eL+17SjqEmQ/jHYWTX1Hn+LJY+CVUqCz
Xys3FeRJy25FQ/J/npGcxRcCAwEAAQ==
-----END PUBLIC KEY-----
TXT;
}
}
Binary file modified builds/hyde
Binary file not shown.
Binary file modified builds/hyde.sig
Binary file not shown.
Binary file modified builds/signature.bin
Binary file not shown.
Loading

0 comments on commit ea81306

Please sign in to comment.