/** * This interrupts requests if all else fails. * @param string[] ...$args * @return void * @throws CustomPageNotFoundException */ public function routeNotFound(...$args) { if (!\is1DArray($args)) { throw new CustomPageNotFoundException(\__('Invalid arguments')); } $dirs = $args; $file = \array_pop($dirs); // First: Do we have a custom page at this endpoint? try { if ($this->serveCustomPage($file, $dirs)) { return; } } catch (CustomPageNotFoundException $ex) { $this->log('Custom page not found', LogLevel::INFO, ['cabin' => $this->cabin, 'dirs' => $dirs, 'exception' => \Airship\throwableToArray($ex), 'file' => $file]); } // Second: Is there a redirect at this endpoint? try { $path = \implode('/', $args); if ($this->pages->serveRedirect($path)) { return; } } catch (RedirectException $ex) { $this->log('Redirect missed', LogLevel::INFO); } \http_response_code(404); // Finally: Return a 4o4 $this->lens('404'); return; }
/** * Process automatic updates: * * 1. Check if a new update is available. * 2. Download the upload file, store in a temporary file. * 3. Verify the signature (via Halite). * 4. Verify the update is recorded in Keyggdrasil. * 5. If all is well, run the update script. */ public function autoUpdate() { $state = State::instance(); try { /** * @var UpdateInfo[] */ $updateInfoArray = $this->updateCheck($state->universal['airship']['trusted-supplier'], $this->name, \AIRSHIP_VERSION, 'airship_version'); /** * Let's iterate through every possible available update, * until we find the desired version. */ foreach ($updateInfoArray as $updateInfo) { if (!$this->checkVersionSettings($updateInfo, \AIRSHIP_VERSION)) { // This is not the update we are looking for. $this->log('Skipping update', LogLevel::DEBUG, ['info' => $updateInfo->getResponse(), 'new_version' => $updateInfo->getVersion(), 'current_version' => \AIRSHIP_VERSION]); continue; } /** * @var UpdateFile */ $updateFile = $this->downloadUpdateFile($updateInfo, 'airship_download'); if ($this->bypassSecurityAndJustInstall) { // I'm sorry, Dave. I'm afraid I can't do that. $this->log('Core update verification cannot be bypassed', LogLevel::ERROR); self::$continuumLogger->store(LogLevel::ALERT, 'CMS Airship core update - security bypass ignored.', $this->getLogContext($updateInfo, $updateFile)); } /** * Don't proceed unless we've verified the signatures */ if ($this->verifyUpdateSignature($updateInfo, $updateFile)) { if ($this->checkKeyggdrasil($updateInfo, $updateFile)) { $this->install($updateInfo, $updateFile); } else { $this->log('Keyggdrasil check failed for Airship core update', LogLevel::ALERT); self::$continuumLogger->store(LogLevel::ALERT, 'CMS Airship core update failed -- checksum not registered in Keyggdrasil', $this->getLogContext($updateInfo, $updateFile)); } } else { $this->log('Invalid signature for this Airship core update', LogLevel::ALERT); self::$continuumLogger->store(LogLevel::ALERT, 'CMS Airship core update failed -- invalid signature', $this->getLogContext($updateInfo, $updateFile)); } } } catch (NoAPIResponse $ex) { // We should log this. $this->log('Automatic update failure: NO API Response.', LogLevel::ERROR, \Airship\throwableToArray($ex)); self::$continuumLogger->store(LogLevel::ALERT, 'CMS Airship core update failed -- no API Response', ['action' => 'UPDATE', 'name' => $this->name, 'supplier' => $this->supplier->getName(), 'type' => $this->type]); } }
/** * Upload files * * @param int $directoryId * @param string $cabin * @return array */ protected function uploadFiles($directoryId = null, string $cabin = '') : array { $results = []; $newFiles = $this->files->isolateFiles($_FILES['new_files']); if (empty($newFiles)) { return ['status' => 'ERROR', 'message' => 'No files were uploaded.']; } foreach ($newFiles as $file) { try { $results[] = $this->files->processUpload($directoryId, $cabin, $file, $this->attribution); } catch (UploadError $ex) { $this->log('File upload failed', LogLevel::ERROR, \Airship\throwableToArray($ex)); } } return ['status' => 'SUCCESS', 'message' => 'Upload successful']; }
echo "\n", \str_repeat('#', 80), "\n"; echo "PREVIOUS EXCEPTION (", $n, "): ", \get_class($e), "\n\n", $e->getMessage(), "\n\n", $e->getCode(), "\n\n", $e->getTraceAsString(); ++$n; if (!$e) { exit(255); } } elseif ($e instanceof \Error) { echo "\n", \str_repeat('#', 80), "\n"; echo "PREVIOUS ERROR (", $n, "): ", \get_class($e), "\n\n", $e->getMessage(), "\n\n", $e->getCode(), "\n\n", $e->getTraceAsString(); ++$n; if (!$e) { exit(255); } } else { break; } } exit(255); } // This is just for benchmarking purposes: echo '<!-- Load time: ' . \round(\microtime(true) - $start, 5) . ' s -->'; } else { try { $autoPilot->route(); } catch (\Throwable $e) { $state->logger->log(LogLevel::ERROR, $e->getMessage(), \Airship\throwableToArray($e)); \http_response_code(500); echo \file_get_contents(ROOT . '/error_pages/uncaught-exception.html'); exit(1); } }
/** * Get path information by a page ID * * @param int $pageId * @param string $cabin * @return array string[3] (cabin, directory, page) * @throws CustomPageNotFoundException */ public function getPathByPageId(int $pageId, string $cabin = '') : array { $page = $this->db->row('SELECT url, directory FROM airship_custom_page WHERE pageid = ?', $pageId); if (empty($page)) { throw new CustomPageNotFoundException(\trk('errors.pages.page_does_not_exist')); } if (empty($page['directory'])) { return []; } try { $path = \implode('/', $this->getPathFromDirectoryId((int) $page['directory'], $cabin)); return [$cabin, \trim($path, '/'), $page['url']]; } catch (CustomPageNotFoundException $ex) { $this->log('Custom directory not found', LogLevel::ALERT, ['pageid' => $pageId, 'url' => $page['url'], 'directory' => $page['directory'], 'cabin' => $cabin, 'exception' => \Airship\throwableToArray($ex)]); throw $ex; } }
/** * This serves the fallback route, if it's defined. * * The fallback route handles: * * - Custom pages (if any exist), or * - Redirects * * @return mixed */ protected function serveFallback() { // If we're still here, let's try the fallback handler if (isset($this->cabin['data']['route_fallback'])) { \preg_match('#^/?' . self::$patternPrefix . '/(.*)$#', $_SERVER['REQUEST_URI'], $args); try { return $this->serve($this->cabin['data']['route_fallback'], \explode('/', $args[1] ?? '')); } catch (FallbackLoop $e) { $state = State::instance(); $state->logger->error('Missing route definition', ['exception' => \Airship\throwableToArray($e)]); // We only catch this one } } // If we don't have a fallback handler defined, just give a 404 status and kill the script. \http_response_code(404); exit(255); }
/** * Get metadata about the package we're installing. * * @param string $minVersion * @return array * @throws NoAPIResponse */ public function getPackageData(string $minVersion = '') : array { $channelsConfigured = $this->supplier->getChannels(); if (empty($channelsConfigured)) { throw new NoAPIResponse(\trk('errors.hail.no_channel_configured', $this->supplier->getName())); } /** * HTTP POST arguments */ $args = ['type' => $this->type, 'supplier' => $this->supplier->getName(), 'package' => $this->package]; if (!empty($minVersion)) { $args['minimum'] = $minVersion; } /** * Let's try each of the channels this supplier * belongs to. This should in most cases only * run once. */ foreach ($channelsConfigured as $channel) { $chan = $this->getChannel($channel); $publicKey = $chan->getPublicKey(); // Iterate through all the available Channel URLs. // If the channel supports Tor, and we have tor-only // mode enabled, it will prioritize those URLs. foreach ($chan->getAllURLs() as $ch) { try { $result = $this->hail->postSignedJSON($ch . API::get('version'), $publicKey, $args); // Add the channel to this data... $result['channel'] = $ch; $result['minimum'] = (string) ($minVersion ?? '0.0.0'); return $result; } catch (TransferException $ex) { $this->log('This channel URL did not respond.', LogLevel::INFO, ['channel' => $channel, 'url' => $ch, 'exception' => \Airship\throwableToArray($ex)]); } catch (SignatureFailed $ex) { $this->log('Channel signature validation failed', LogLevel::WARNING, ['channel' => $channel, 'url' => $ch, 'exception' => \Airship\throwableToArray($ex)]); } // If we didn't return a result, we'll continue onto the next URL } } throw new NoAPIResponse(\trk('errors.hail.no_channel_responded')); }
<?php declare (strict_types=1); use Airship\Engine\{Gears, Hail, State}; /** * @global State $state * @global Hail $hail */ // Always check for changes to channel keys before initiating update require_once __DIR__ . '/channel.php'; /** * Initialize the automatic updater service * @var \Airship\Engine\Continuum */ $autoUpdater = Gears::get('AutoUpdater', $hail); if (IDE_HACKS) { // Just for the sake of auto-completion: $autoUpdater = new \Airship\Engine\Continuum($hail); } $state->logger->info('Automatic update started'); try { $autoUpdater->doUpdateCheck(); } catch (\Throwable $ex) { $state->logger->critical('Tree update failed: ' . \get_class($ex), \Airship\throwableToArray($ex)); exit(255); } $state->logger->info('Automatic update concluded'); \Airship\clear_cache(); exit(0);
/** * Handle user authentication * * @param array $post */ protected function processLogin(array $post = []) { $state = State::instance(); if (empty($post['username']) || empty($post['passphrase'])) { $this->lens('login', ['post_response' => ['message' => \__('Please fill out the form entirely'), 'status' => 'error']]); } $airBrake = Gears::get('AirBrake'); if (IDE_HACKS) { $airBrake = new AirBrake(); } if ($airBrake->failFast($post['username'], $_SERVER['REMOTE_ADDR'])) { $this->lens('login', ['post_response' => ['message' => \__('You are doing that too fast. Please wait a few seconds and try again.'), 'status' => 'error']]); } elseif (!$airBrake->getFastExit()) { $delay = $airBrake->getDelay($post['username'], $_SERVER['REMOTE_ADDR']); if ($delay > 0) { \usleep($delay * 1000); } } try { $userID = $this->airship_auth->login($post['username'], new HiddenString($post['passphrase'])); } catch (InvalidMessage $e) { $this->log('InvalidMessage Exception on Login; probable cause: password column was corrupted', LogLevel::CRITICAL, ['exception' => \Airship\throwableToArray($e)]); $this->lens('login', ['post_response' => ['message' => \__('Incorrect username or passphrase. Please try again.'), 'status' => 'error']]); } if (!empty($userID)) { $userID = (int) $userID; $user = $this->acct->getUserAccount($userID); if ($user['enable_2factor']) { if (empty($post['two_factor'])) { $post['two_factor'] = ''; } $gauth = $this->twoFactorPreamble($userID); $checked = $gauth->validateCode($post['two_factor'], \time()); if (!$checked) { $fails = $airBrake->getFailedLoginAttempts($post['username'], $_SERVER['REMOTE_ADDR']) + 1; // Instead of the password, seal a timestamped and // signed message saying the password was correct. // We use a signature with a key local to this Airship // so attackers can't just spam a string constant to // make the person decrypting these strings freak out // and assume the password was compromised. // // False positives are bad. This gives the sysadmin a // surefire way to reliably verify that a log entry is // due to two-factor authentication failing. $message = '**Note: The password was correct; ' . ' invalid 2FA token was provided.** ' . (new \DateTime('now'))->format(\AIRSHIP_DATE_FORMAT); $signed = Base64UrlSafe::encode(Asymmetric::sign($message, $state->keyring['notary.online_signing_key'], true)); $airBrake->registerLoginFailure($post['username'], $_SERVER['REMOTE_ADDR'], $fails, new HiddenString($signed . $message)); $this->lens('login', ['post_response' => ['message' => \__('Incorrect username or passphrase. Please try again.'), 'status' => 'error']]); } } if ($user['session_canary']) { $_SESSION['session_canary'] = $user['session_canary']; } elseif ($this->config('password-reset.logout')) { $_SESSION['session_canary'] = $this->acct->createSessionCanary($userID); } // Regenerate session ID: Session::regenerate(true); $_SESSION['userid'] = (int) $userID; if (!empty($post['remember'])) { $autoPilot = Gears::getName('AutoPilot'); if (IDE_HACKS) { $autoPilot = new AutoPilot(); } $httpsOnly = (bool) $autoPilot::isHTTPSConnection(); Cookie::setcookie('airship_token', Symmetric::encrypt($this->airship_auth->createAuthToken($userID), $state->keyring['cookie.encrypt_key']), \time() + ($state->universal['long-term-auth-expire'] ?? self::DEFAULT_LONGTERMAUTH_EXPIRE), '/', $state->universal['session_config']['cookie_domain'] ?? '', $httpsOnly ?? false, true); } \Airship\redirect($this->airship_cabin_prefix); } else { $fails = $airBrake->getFailedLoginAttempts($post['username'], $_SERVER['REMOTE_ADDR']) + 1; // If the server is setup (with an EncryptionPublicKey) and the // number of failures is above the log threshold, this will // encrypt the password guess with the public key so that only // the person in possession of the secret key can decrypt it. $airBrake->registerLoginFailure($post['username'], $_SERVER['REMOTE_ADDR'], $fails, new HiddenString($post['passphrase'])); $this->lens('login', ['post_response' => ['message' => \__('Incorrect username or passphrase. Please try again.'), 'status' => 'error']]); } }
/** * Send the HTTP request, return the * * @param string $root * @return array */ protected function getChannelUpdates(string $root) : array { $state = State::instance(); if (IDE_HACKS) { $state->hail = new Hail(new Client()); } foreach ($this->getChannelURLs() as $url) { $initiated = new \DateTime('now'); $response = $state->hail->postSignedJSON($url . API::get('fetch_keys') . '/' . $root, $this->channelPublicKey); try { // We use a separate method for parsing this update: return $this->parseChannelUpdateResponse($response, $initiated); } catch (CouldNotUpdate $ex) { // Log the error message: $this->log($ex->getMessage(), LogLevel::ALERT, \Airship\throwableToArray($ex)); // continue; } } // When all else fails, TransferException throw new TransferException(\__("All else has failed.")); }
/** * Return true if the Merkle roots match. * * Dear future security auditors: This is important. * * This employs challenge-response authentication: * @ref https://github.com/paragonie/airship/issues/13 * * @param Channel $channel * @param MerkleTree $originalTree * @param TreeUpdate[] ...$updates * @return bool * @throws CouldNotUpdate */ protected function verifyResponseWithPeers(Channel $channel, MerkleTree $originalTree, TreeUpdate ...$updates) : bool { $state = State::instance(); $nodes = $this->updatesToNodes($updates); $tree = $originalTree->getExpandedTree(...$nodes); $maxUpdateIndex = \count($updates) - 1; $expectedRoot = $updates[$maxUpdateIndex]->getRoot(); if (!\hash_equals($tree->getRoot(), $expectedRoot)) { // Calculated root did not match. self::$continuumLogger->store(LogLevel::EMERGENCY, 'Calculated Merkle root did not match the update.', [$tree->getRoot(), $expectedRoot]); throw new CouldNotUpdate(\__('Calculated Merkle root did not match the update.')); } if ($state->universal['auto-update']['ignore-peer-verification']) { // The user has expressed no interest in verification return true; } $peers = $channel->getPeerList(); $numPeers = \count($peers); /** * These numbers are negotiable in future versions. * * If P is the set of trusted peer notaries (where ||P|| is the number * of trusted peer notaries): * * 1. At least 1 must return 'success'. * 2. At least ln(||P||) must return 'success'. * 3. At most e * ln(||P||) can timeout. * 4. If any peer disagrees with what we see, our * result is discarded as invalid. * * The most harm a malicious peer can do is DoS if they * are selected. */ $minSuccess = $channel->getAppropriatePeerSize(); $maxFailure = (int) \min(\floor($minSuccess * M_E), $numPeers - 1); if ($maxFailure < 1) { $maxFailure = 1; } \Airship\secure_shuffle($peers); $success = $networkError = 0; /** * If any peers give a different answer, we're under attack. * If too many peers don't respond, assume they're being DDoS'd. * If enough peers respond in absolute agreement, we're good. */ for ($i = 0; $i < $numPeers; ++$i) { try { if (!$this->checkWithPeer($peers[$i], $tree->getRoot())) { // Merkle root mismatch? Abort. return false; } ++$success; } catch (TransferException $ex) { self::$continuumLogger->store(LogLevel::EMERGENCY, 'A transfer exception occurred', \Airship\throwableToArray($ex)); ++$networkError; } if ($success >= $minSuccess) { // We have enough good responses. return true; } elseif ($networkError >= $maxFailure) { // We can't give a confident response here. return false; } } self::$continuumLogger->store(LogLevel::EMERGENCY, 'We ran out of peers.', [$numPeers, $minSuccess, $maxFailure]); // Fail closed: return false; }
/** * We're going to view a page's history * * @route pages/{string}/history/diff/{string}/{string} * @param string $cabin * @param string $leftUnique * @param string $rightUnique */ public function pageHistoryDiff(string $cabin, string $leftUnique, string $rightUnique) { try { $left = $this->pg->getPageVersionByUniqueId($leftUnique); $right = $this->pg->getPageVersionByUniqueId($rightUnique); if ($left['page'] !== $right['page']) { throw new CustomPageNotFoundException(\__('Unique IDs for different pages.')); } } catch (CustomPageNotFoundException $ex) { $this->log('Page not found', LogLevel::NOTICE, ['exception' => \Airship\throwableToArray($ex)]); \Airship\redirect($this->airship_cabin_prefix . '/pages/' . \trim($cabin, '/')); return; } $this->setTemplateExtraData($cabin); $this->lens('pages/page_history_diff', ['left' => $left, 'right' => $right]); }
/** * Grab post data, but only if the CSRF token is valid * * @param InputFilterContainer $filterContainer - Type filter for POST data * @param bool $ignoreCSRFToken - Don't validate CSRF tokens * * @return array|bool * @throws SecurityAlert */ protected function post(InputFilterContainer $filterContainer = null, bool $ignoreCSRFToken = false) { if ($this->airship_http_method !== 'POST' || empty($_POST)) { return false; } if ($ignoreCSRFToken) { if ($filterContainer) { try { return $filterContainer($_POST); } catch (\TypeError $ex) { $this->log('Input validation threw a TypeError', LogLevel::ALERT, \Airship\throwableToArray($ex)); return false; } } return $_POST; } if ($this->airship_csrf->check()) { if ($filterContainer) { try { return $filterContainer($_POST); } catch (\TypeError $ex) { $this->log('Input validation threw a TypeError', LogLevel::ALERT, \Airship\throwableToArray($ex)); return false; } } return $_POST; } $state = State::instance(); if ($state->universal['debug']) { // This is only thrown during development, to be noisy. throw new SecurityAlert(\__('CSRF validation failed')); } $this->log('CSRF validation failed', LogLevel::ALERT); return false; }
/** * Process automatic updates: * * 1. Check if a new update is available. * 2. Download the upload file, store in a temporary file. * 3. Verify the signature (via Halite). * 4. Verify the update is recorded in Keyggdrasil. * 5. If all is well, run the update script. */ public function autoUpdate() { $debugArgs = ['supplier' => $this->supplier->getName(), 'name' => $this->name]; try { /** * @var UpdateInfo[] */ $updateInfoArray = $this->updateCheck($this->supplier->getName(), $this->name, $this->manifest['version']); foreach ($updateInfoArray as $updateInfo) { if (!$this->checkVersionSettings($updateInfo, $this->manifest['version'])) { $this->log('Skipping Motif update', LogLevel::INFO, ['info' => $updateInfo->getResponse(), 'new_version' => $updateInfo->getVersion(), 'current_version' => $this->manifest['version']]); continue; } /** * @var UpdateFile */ $updateFile = $this->downloadUpdateFile($updateInfo); $this->log('Downloaded Motif update file', LogLevel::DEBUG, $debugArgs); if ($this->bypassSecurityAndJustInstall) { $this->log('Motif update verification bypassed', LogLevel::ALERT, $debugArgs); $this->install($updateInfo, $updateFile); return; } /** * Don't proceed unless we've verified the signatures * and the relevant entries in Keyggdrasil */ if ($this->verifyUpdateSignature($updateInfo, $updateFile)) { if ($this->checkKeyggdrasil($updateInfo, $updateFile)) { $this->install($updateInfo, $updateFile); } else { $this->log('Keyggdrasil check failed for this Motif', LogLevel::ERROR, $debugArgs); self::$continuumLogger->store(LogLevel::ALERT, 'Motif update failed -- checksum not registered in Keyggdrasil', $this->getLogContext($updateInfo, $updateFile)); } } else { $this->log('Signature check failed for this Motif', LogLevel::ALERT, $debugArgs); self::$continuumLogger->store(LogLevel::ALERT, 'Motif update failed -- invalid signature', $this->getLogContext($updateInfo, $updateFile)); } } } catch (NoAPIResponse $ex) { // We should log this. $this->log('Automatic update failure: NO API Response.', LogLevel::CRITICAL, \Airship\throwableToArray($ex)); self::$continuumLogger->store(LogLevel::ALERT, 'Motif update failed -- no API Response', ['action' => 'UPDATE', 'name' => $this->name, 'supplier' => $this->supplier->getName(), 'type' => $this->type]); } }
/** * Trigger the package install process */ public function updatePackage() { $expected = ['package', 'supplier', 'type', 'version']; if (!\Airship\all_keys_exist($expected, $_POST)) { \Airship\json_response(['status' => 'ERROR', 'message' => \__('Incomplete request.')]); } try { $filter = new SkyportFilter(); $_POST = $filter($_POST); } catch (\TypeError $ex) { $this->log("Input violation", LogLevel::ALERT, \Airship\throwableToArray($ex)); \Airship\json_response(['status' => 'ERROR', 'message' => \__('Invalid input.')]); } /** * @security We need to guarantee RCE isn't possible: */ $args = \implode(' ', [\escapeshellarg(Util::charWhitelist($_POST['type'], Util::PRINTABLE_ASCII)), \escapeshellarg(Util::charWhitelist($_POST['supplier'], Util::PRINTABLE_ASCII) . '/' . Util::charWhitelist($_POST['package'], Util::PRINTABLE_ASCII)), \escapeshellarg(Util::charWhitelist($_POST['version'], Util::PRINTABLE_ASCII))]); $output = \shell_exec('php -dphar.readonly=0 ' . ROOT . '/CommandLine/update_one.sh ' . $args); \Airship\json_response(['status' => 'OK', 'message' => $output]); }
/** * Are any updates available? * * @param string $supplier * @param string $packageName * @param string $minVersion * @param string $apiEndpoint * * @return UpdateInfo[] * * @throws \Airship\Alerts\Hail\NoAPIResponse */ public function updateCheck(string $supplier = '', string $packageName = '', string $minVersion = '', string $apiEndpoint = 'version') : array { if (empty($supplier)) { $supplier = $this->supplier->getName(); } $channelsConfigured = $this->supplier->getChannels(); if (empty($channelsConfigured)) { throw new NoAPIResponse(\trk('errors.hail.no_channel_configured')); } foreach ($channelsConfigured as $channelName) { $channel = $this->getChannel($channelName); $publicKey = $channel->getPublicKey(); foreach ($channel->getAllURLs() as $ch) { try { $response = $this->hail->postSignedJSON($ch . API::get($apiEndpoint), $publicKey, ['type' => $this->type, 'supplier' => $supplier, 'package' => $packageName, 'minimum' => $minVersion]); if ($response['status'] === 'error') { $this->log($response['error'], LogLevel::ERROR, ['response' => $response, 'channel' => $ch, 'supplier' => $supplier, 'type' => $this->type, 'package' => $packageName]); continue; } $updates = []; foreach ($response['versions'] as $update) { $updates[] = new UpdateInfo($update, $ch, $publicKey, $supplier, $packageName); } if (empty($updates)) { $this->log('No updates found.', LogLevel::DEBUG, ['type' => \get_class($this), 'supplier' => $supplier, 'package' => $packageName, 'channelName' => $channelName, 'channel' => $ch]); return []; } return $this->sortUpdatesByVersion(...$updates); } catch (SignatureFailed $ex) { // Log? Definitely suppress, however. $this->log('Automatic update - signature failure. (' . \get_class($ex) . ')', LogLevel::ALERT, ['exception' => \Airship\throwableToArray($ex), 'channelName' => $channelName, 'channel' => $ch]); } catch (TransferException $ex) { // Log? Definitely suppress, however. $this->log('Automatic update failure. (' . \get_class($ex) . ')', LogLevel::WARNING, ['exception' => \Airship\throwableToArray($ex), 'channelName' => $channelName, 'channel' => $ch]); } } } throw new NoAPIResponse(\trk('errors.hail.no_channel_responded')); }
/** * Let's do an automatic login * * @param string $token * @param string $uid_idx * @param string $token_idx * @return bool * @throws LongTermAuthAlert (only in debug mode) * @throws \TypeError */ protected function doAutoLogin(string $token, string $uid_idx, string $token_idx) : bool { if (!$this->airship_auth instanceof Authentication) { $this->tightenSecurityBolt(); } $state = State::instance(); try { $userId = $this->airship_auth->loginByToken($token); \Sodium\memzero($token); if (!$this->verifySessionCanary($userId, false)) { return false; } // Regenerate session ID: Session::regenerate(true); // Set session variable $_SESSION[$uid_idx] = $userId; $autoPilot = Gears::getName('AutoPilot'); if (IDE_HACKS) { // We're using getName(), this is just to fool IDEs. $autoPilot = new AutoPilot(); } $httpsOnly = (bool) $autoPilot::isHTTPSConnection(); // Rotate the authentication token: Cookie::setcookie($token_idx, Symmetric::encrypt($this->airship_auth->rotateToken($token, $userId), $state->keyring['cookie.encrypt_key']), \time() + ($state->universal['long-term-auth-expire'] ?? self::DEFAULT_LONGTERMAUTH_EXPIRE), '/', '', $httpsOnly ?? false, true); return true; } catch (LongTermAuthAlert $e) { $state = State::instance(); // Let's wipe our long-term authentication cookies Cookie::setcookie($token_idx, null, 0, '/', '', $httpsOnly ?? false, true); // Let's log this incident if (\property_exists($this, 'log')) { $this->log($e->getMessage(), LogLevel::CRITICAL, ['exception' => \Airship\throwableToArray($e)]); } else { $state->logger->log(LogLevel::CRITICAL, $e->getMessage(), ['exception' => \Airship\throwableToArray($e)]); } // In debug mode, re-throw the exception: if ($state->universal['debug']) { throw $e; } } return false; }