diff --git a/Makefile b/Makefile index 6f6de787..ac59a6a3 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,13 @@ updater.phar: box updater.php lib/*.php buildVersionFile.php rm lib/Version.php clean: - rm updater.phar + rm updater.phar index.php + +index.php: + # First put openining php tag and license + awk '/^<\?php$$/,/\*\//' index.web.php > index.php + # Then concat all files while filtering php tag and license + cat lib/UpdateException.php lib/LogException.php lib/RecursiveDirectoryIteratorWithoutData.php lib/Updater.php index.web.php| grep -v "^namespace" | awk '/^<\?php$$/,/\*\//{next} 1' >> index.php test/vendor: cd tests && composer install diff --git a/index.php b/index.php index a56219cf..68e0b788 100644 --- a/index.php +++ b/index.php @@ -21,6 +21,7 @@ * */ + class UpdateException extends \Exception { protected $data; @@ -33,9 +34,11 @@ public function getData() { } } + class LogException extends \Exception { } + class RecursiveDirectoryIteratorWithoutData extends \RecursiveFilterIterator { public function accept(): bool { /** @var \DirectoryIterator $this */ @@ -49,96 +52,6 @@ public function accept(): bool { } } -class Auth { - /** @var Updater */ - private $updater; - /** @var string */ - private $password; - - /** - * @param Updater $updater - * @param string $password - */ - public function __construct(Updater $updater, - $password) { - $this->updater = $updater; - $this->password = $password; - } - /** - * Compares two strings. - * - * This method implements a constant-time algorithm to compare strings. - * Regardless of the used implementation, it will leak length information. - * - * @param string $knownString The string of known length to compare against - * @param string $userInput The string that the user can control - * - * @return bool true if the two strings are the same, false otherwise - * @license MIT - * @source https://github.com/symfony/security-core/blob/56721d5f5f63da7e08d05aa7668a5a9ef2367e1e/Util/StringUtils.php - */ - private static function equals($knownString, $userInput) { - // Avoid making unnecessary duplications of secret data - if (!is_string($knownString)) { - $knownString = (string) $knownString; - } - if (!is_string($userInput)) { - $userInput = (string) $userInput; - } - if (function_exists('hash_equals')) { - return hash_equals($knownString, $userInput); - } - $knownLen = self::safeStrlen($knownString); - $userLen = self::safeStrlen($userInput); - if ($userLen !== $knownLen) { - return false; - } - $result = 0; - for ($i = 0; $i < $knownLen; ++$i) { - $result |= (ord($knownString[$i]) ^ ord($userInput[$i])); - } - // They are only identical strings if $result is exactly 0... - return 0 === $result; - } - /** - * Returns the number of bytes in a string. - * - * @param string $string The string whose length we wish to obtain - * - * @return int - * @license MIT - * @source https://github.com/symfony/security-core/blob/56721d5f5f63da7e08d05aa7668a5a9ef2367e1e/Util/StringUtils.php - */ - private static function safeStrlen($string) { - // Premature optimization - // Since this cannot be changed at runtime, we can cache it - static $funcExists = null; - if (null === $funcExists) { - $funcExists = function_exists('mb_strlen'); - } - if ($funcExists) { - return mb_strlen($string, '8bit'); - } - return strlen($string); - } - - /** - * Whether the current user is authenticated - * - * @return bool - */ - public function isAuthenticated() { - $storedHash = $this->updater->getConfigOption('updater.secret'); - - // As a sanity check the stored hash or the sent password can never be empty - if ($storedHash === '' || $storedHash === null || $this->password === null) { - return false; - } - - // As we still support PHP 5.4 we have to use some magic involving "crypt" - return $this->equals($storedHash, crypt($this->password, $storedHash)); - } -} class Updater { /** @var string */ @@ -1291,6 +1204,97 @@ public function logVersion() { } } +class Auth { + /** @var Updater */ + private $updater; + /** @var string */ + private $password; + + /** + * @param Updater $updater + * @param string $password + */ + public function __construct(Updater $updater, + $password) { + $this->updater = $updater; + $this->password = $password; + } + /** + * Compares two strings. + * + * This method implements a constant-time algorithm to compare strings. + * Regardless of the used implementation, it will leak length information. + * + * @param string $knownString The string of known length to compare against + * @param string $userInput The string that the user can control + * + * @return bool true if the two strings are the same, false otherwise + * @license MIT + * @source https://github.com/symfony/security-core/blob/56721d5f5f63da7e08d05aa7668a5a9ef2367e1e/Util/StringUtils.php + */ + private static function equals($knownString, $userInput) { + // Avoid making unnecessary duplications of secret data + if (!is_string($knownString)) { + $knownString = (string) $knownString; + } + if (!is_string($userInput)) { + $userInput = (string) $userInput; + } + if (function_exists('hash_equals')) { + return hash_equals($knownString, $userInput); + } + $knownLen = self::safeStrlen($knownString); + $userLen = self::safeStrlen($userInput); + if ($userLen !== $knownLen) { + return false; + } + $result = 0; + for ($i = 0; $i < $knownLen; ++$i) { + $result |= (ord($knownString[$i]) ^ ord($userInput[$i])); + } + // They are only identical strings if $result is exactly 0... + return 0 === $result; + } + /** + * Returns the number of bytes in a string. + * + * @param string $string The string whose length we wish to obtain + * + * @return int + * @license MIT + * @source https://github.com/symfony/security-core/blob/56721d5f5f63da7e08d05aa7668a5a9ef2367e1e/Util/StringUtils.php + */ + private static function safeStrlen($string) { + // Premature optimization + // Since this cannot be changed at runtime, we can cache it + static $funcExists = null; + if (null === $funcExists) { + $funcExists = function_exists('mb_strlen'); + } + if ($funcExists) { + return mb_strlen($string, '8bit'); + } + return strlen($string); + } + + /** + * Whether the current user is authenticated + * + * @return bool + */ + public function isAuthenticated() { + $storedHash = $this->updater->getConfigOption('updater.secret'); + + // As a sanity check the stored hash or the sent password can never be empty + if ($storedHash === '' || $storedHash === null || $this->password === null) { + return false; + } + + // As we still support PHP 5.4 we have to use some magic involving "crypt" + return $this->equals($storedHash, crypt($this->password, $storedHash)); + } +} + ini_set('display_errors', '0'); ini_set('log_errors', '1'); diff --git a/index.web.php b/index.web.php new file mode 100644 index 00000000..397fb48b --- /dev/null +++ b/index.web.php @@ -0,0 +1,1064 @@ + + * @copyright Copyright (c) 2016 Morris Jobke + * @copyright Copyright (c) 2018 Jonas Sulzer + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +class Auth { + /** @var Updater */ + private $updater; + /** @var string */ + private $password; + + /** + * @param Updater $updater + * @param string $password + */ + public function __construct(Updater $updater, + $password) { + $this->updater = $updater; + $this->password = $password; + } + /** + * Compares two strings. + * + * This method implements a constant-time algorithm to compare strings. + * Regardless of the used implementation, it will leak length information. + * + * @param string $knownString The string of known length to compare against + * @param string $userInput The string that the user can control + * + * @return bool true if the two strings are the same, false otherwise + * @license MIT + * @source https://github.com/symfony/security-core/blob/56721d5f5f63da7e08d05aa7668a5a9ef2367e1e/Util/StringUtils.php + */ + private static function equals($knownString, $userInput) { + // Avoid making unnecessary duplications of secret data + if (!is_string($knownString)) { + $knownString = (string) $knownString; + } + if (!is_string($userInput)) { + $userInput = (string) $userInput; + } + if (function_exists('hash_equals')) { + return hash_equals($knownString, $userInput); + } + $knownLen = self::safeStrlen($knownString); + $userLen = self::safeStrlen($userInput); + if ($userLen !== $knownLen) { + return false; + } + $result = 0; + for ($i = 0; $i < $knownLen; ++$i) { + $result |= (ord($knownString[$i]) ^ ord($userInput[$i])); + } + // They are only identical strings if $result is exactly 0... + return 0 === $result; + } + /** + * Returns the number of bytes in a string. + * + * @param string $string The string whose length we wish to obtain + * + * @return int + * @license MIT + * @source https://github.com/symfony/security-core/blob/56721d5f5f63da7e08d05aa7668a5a9ef2367e1e/Util/StringUtils.php + */ + private static function safeStrlen($string) { + // Premature optimization + // Since this cannot be changed at runtime, we can cache it + static $funcExists = null; + if (null === $funcExists) { + $funcExists = function_exists('mb_strlen'); + } + if ($funcExists) { + return mb_strlen($string, '8bit'); + } + return strlen($string); + } + + /** + * Whether the current user is authenticated + * + * @return bool + */ + public function isAuthenticated() { + $storedHash = $this->updater->getConfigOption('updater.secret'); + + // As a sanity check the stored hash or the sent password can never be empty + if ($storedHash === '' || $storedHash === null || $this->password === null) { + return false; + } + + // As we still support PHP 5.4 we have to use some magic involving "crypt" + return $this->equals($storedHash, crypt($this->password, $storedHash)); + } +} + +ini_set('display_errors', '0'); +ini_set('log_errors', '1'); + +// Check if the config.php is at the expected place +try { + $updater = new Updater(__DIR__); + if ($updater->isDisabled()) { + http_response_code(403); + die('Updater is disabled, please use the command line'); + } +} catch (\Exception $e) { + // logging here is not possible because we don't know the data directory + http_response_code(500); + die($e->getMessage()); +} + +// Check if the updater.log can be written to +try { + $updater->log('[info] request to updater'); +} catch (\Exception $e) { + if (isset($_POST['step'])) { + // mark step as failed + http_response_code(500); + echo(json_encode(['proceed' => false, 'response' => $e->getMessage()])); + die(); + } + // show logging error to user + die($e->getMessage()); +} + +// Check for authentication +$password = isset($_SERVER['HTTP_X_UPDATER_AUTH']) ? $_SERVER['HTTP_X_UPDATER_AUTH'] : (isset($_POST['updater-secret-input']) ? $_POST['updater-secret-input'] : ''); +$auth = new Auth($updater, $password); + +// Check if already a step is in process +$currentStep = $updater->currentStep(); +$stepNumber = 0; +if ($currentStep !== []) { + $stepState = $currentStep['state']; + $stepNumber = $currentStep['step']; + $updater->log('[info] Step ' . $stepNumber . ' is in state "' . $stepState . '".'); + + if ($stepState === 'start') { + die( + sprintf( + 'Step %s is currently in process. Please reload this page later.', + $stepNumber + ) + ); + } +} + +if (isset($_POST['step'])) { + $updater->log('[info] POST request for step "' . $_POST['step'] . '"'); + set_time_limit(0); + try { + if (!$auth->isAuthenticated()) { + throw new \Exception('Not authenticated'); + } + + $step = (int)$_POST['step']; + if ($step > 12 || $step < 1) { + throw new \Exception('Invalid step'); + } + + $updater->startStep($step); + switch ($step) { + case 1: + $updater->checkForExpectedFilesAndFolders(); + break; + case 2: + $updater->checkWritePermissions(); + break; + case 3: + $updater->createBackup(); + break; + case 4: + $updater->downloadUpdate(); + break; + case 5: + $updater->verifyIntegrity(); + break; + case 6: + $updater->extractDownload(); + break; + case 7: + $updater->setMaintenanceMode(true); + break; + case 8: + $updater->replaceEntryPoints(); + break; + case 9: + $updater->deleteOldFiles(); + break; + case 10: + $updater->moveNewVersionInPlace(); + break; + case 11: + $updater->setMaintenanceMode(false); + break; + case 12: + $updater->finalize(); + break; + } + $updater->endStep($step); + echo(json_encode(['proceed' => true])); + } catch (UpdateException $e) { + $message = $e->getData(); + + try { + $updater->log('[error] POST request failed with UpdateException'); + $updater->logException($e); + } catch (LogException $logE) { + $message .= ' (and writing to log failed also with: ' . $logE->getMessage() . ')'; + } + + if (isset($step)) { + $updater->rollbackChanges($step); + } + http_response_code(500); + echo(json_encode(['proceed' => false, 'response' => $message])); + } catch (\Exception $e) { + $message = $e->getMessage(); + + try { + $updater->log('[error] POST request failed with other exception'); + $updater->logException($e); + } catch (LogException $logE) { + $message .= ' (and writing to log failed also with: ' . $logE->getMessage() . ')'; + } + + if (isset($step)) { + $updater->rollbackChanges($step); + } + http_response_code(500); + echo(json_encode(['proceed' => false, 'response' => $message])); + } + + die(); +} + +$updater->log('[info] show HTML page'); +$updater->logVersion(); +$updaterUrl = explode('?', $_SERVER['REQUEST_URI'], 2)[0]; +if (strpos($updaterUrl, 'index.php') === false) { + $updaterUrl = rtrim($updaterUrl, '/') . '/index.php'; +} +?> + + + + + + + + + + +
+
+ +
+ isAuthenticated()): ?> +
    +
  • +

    Initializing

    +
    Current version is getCurrentVersion()); ?>.
    + checkForUpdate()); ?>
    + + updateAvailable() || $stepNumber > 0) { + $buttonText = 'Start update'; + if ($stepNumber > 0) { + $buttonText = 'Continue update'; + } ?> + + + +
    +
  • +
  • +

    Check for expected files

    + +
  • +
  • +

    Check for write permissions

    + +
  • +
  • +

    Create backup

    + +
  • +
  • +

    Downloading

    + +
  • +
  • +

    Verifying integrity

    + +
  • +
  • +

    Extracting

    + +
  • +
  • +

    Enable maintenance mode

    + +
  • +
  • +

    Replace entry points

    + +
  • +
  • +

    Delete old files

    + +
  • +
  • +

    Move new files in place

    + +
  • +
  • +

    Continue with web based updater

    + +
  • +
  • +

    Done

    + +
  • +
+ +
+

Authentication

+

To login you need to provide the unhashed value of "updater.secret" in your config file.

+

If you don't know that value, you can access this updater directly via the Nextcloud admin screen or generate + your own secret:

+ php -r '$password = trim(shell_exec("openssl rand -base64 48"));if(strlen($password) === 64) {$hash = password_hash($password, PASSWORD_DEFAULT) . "\n"; echo "Insert as \"updater.secret\": ".$hash; echo "The plaintext value is: ".$password."\n";}else{echo "Could not execute OpenSSL.\n";};' +
+
+ + +
+
+ isAuthenticated()): ?> +

Invalid password

+ +
+ +
+
+
+ + +isAuthenticated()): ?> + + + + +