Esempio n. 1
0
 /**
  * Get the current user ID. Throws a UserNotLoggedIn exception if you aren't logged in.
  *
  * @return int
  * @throws UserNotLoggedIn
  */
 public function getActiveUserId() : int
 {
     if (empty($_SESSION['userid'])) {
         throw new UserNotLoggedIn(\trk('errors.security.not_authenticated'));
     }
     return (int) $_SESSION['userid'];
 }
Esempio n. 2
0
 /**
  * Get the entire API for a specific version
  * 
  * @param string $version
  * @return array
  * @throws InvalidConfig
  */
 public static function getAll(string $version = self::API_VERSION) : array
 {
     switch ($version) {
         case '1.0.0':
             return ['airship_download' => '/airship_download', 'airship_version' => '/airship_version', 'fetch_keys' => '/keyggdrasil', 'version' => '/version', 'download' => '/download'];
         default:
             throw new InvalidConfig(\trk('errors.hail.invalid_api_version', $version));
     }
 }
Esempio n. 3
0
 /**
  * Channel constructor.
  *
  * @param object $parent (Continuum or Keyggdrasil)
  * @param string $name
  * @param array $config
  * @throws \TypeError
  */
 public function __construct($parent, string $name, array $config = [])
 {
     if ($parent instanceof Keyggdrasil || $parent instanceof Continuum) {
         $this->parent = $parent;
     }
     if (!\is1DArray($config['urls'])) {
         throw new \TypeError(\trk('errors.type.expected_1d_array'));
     }
     // The channel should be signing responses at the application layer:
     $this->publicKey = new SignaturePublicKey(\Sodium\hex2bin($config['publickey']));
     $this->name = $name;
     foreach (\array_values($config['urls']) as $index => $url) {
         if (!\is_string($url)) {
             throw new \TypeError(\trk('errors.type.expected_string_array', \gettype($url), $index));
         }
         $this->urls[] = $url;
     }
 }
Esempio n. 4
0
 /**
  * Store a log message -- used by Ledger
  * 
  * @param string $level
  * @param string $message
  * @param string $context (JSON encoded)
  * @return mixed
  * @throws FileNotFound
  * @throws FileAccessDenied
  */
 public function store(string $level, string $message, string $context)
 {
     $now = new \DateTime('now');
     $filename = $now->format($this->fileFormat);
     \touch($this->basedir . DIRECTORY_SEPARATOR . $filename);
     $file = \realpath($this->basedir . DIRECTORY_SEPARATOR . $filename);
     if ($file === false) {
         throw new FileNotFound(\trk('errors.file.cannot_write_file'));
     }
     if (\strpos($file, $this->basedir) === false) {
         throw new FileAccessDenied(\trk('errors.file.lfi'));
     }
     if (!\file_exists($file)) {
         \touch($file);
         \chmod($file, 0770);
     }
     $time = $now->format($this->timeFormat);
     return \file_put_contents($file, $time . "\t" . \preg_replace('#[^a-z]*#', '', $level) . "\t" . \json_encode($message) . "\t" . $context . "\n", FILE_APPEND);
 }
Esempio n. 5
0
                    KeyFactory::save($keys[$index], $path);
                    break;
                case 'EncryptionSecretKey':
                    $kp = KeyFactory::generateEncryptionKeyPair();
                    $keys[$index] = $kp->getSecretKey();
                    KeyFactory::save($keys[$index], $path);
                    break;
                case 'SignatureSecretKey':
                    $kp = KeyFactory::generateSignatureKeyPair();
                    $keys[$index] = $kp->getSecretKey();
                    KeyFactory::save($keys[$index], $path);
                    break;
                case 'EncryptionKeyPair':
                    $keys[$index] = KeyFactory::generateEncryptionKeyPair();
                    KeyFactory::save($keys[$index], $path);
                    break;
                case 'SignatureKeyPair':
                    $keys[$index] = KeyFactory::generateSignatureKeyPair();
                    KeyFactory::save($keys[$index], $path);
                    break;
                default:
                    throw new \Error(\trk('errors.crypto.unknown_key_type', $keyConfig['type']));
            }
        }
    }
    // Now that we have a bunch of Keys stored in $keys, let's load them into
    // our singleton.
    $state->keyring = $keys;
};
$key_management_closure();
unset($key_management_closure);
Esempio n. 6
0
 /**
  * Move a page to a new directory
  *
  * @param int $pageId the page we're changing
  * @param string $url the new URL
  * @param int $destinationDir the new directory
  * @return bool
  * @throws CustomPageCollisionException
  */
 public function movePage(int $pageId, string $url = '', int $destinationDir = 0) : bool
 {
     $this->db->beginTransaction();
     if ($destinationDir > 0) {
         $collision = $this->db->cell('SELECT COUNT(pageid) FROM airship_custom_page WHERE directory = ? AND url = ? AND pageid != ?', $destinationDir, $url, $pageId);
     } else {
         $collision = $this->db->cell('SELECT COUNT(pageid) FROM airship_custom_page WHERE directory IS NULL AND url = ? AND pageid != ?', $url, $pageId);
     }
     if ($collision > 0) {
         // Sorry, but no.
         throw new CustomPageCollisionException(\trk('errors.pages.collision'));
     }
     $this->db->update('airship_custom_page', ['url' => $url, 'directory' => $destinationDir > 0 ? $destinationDir : null], ['pageid' => $pageId]);
     return $this->db->commit();
 }
Esempio n. 7
0
 /**
  * Actually serve the routes. Called by route() above.
  *
  * @param array $route
  * @param array $args
  * @return mixed
  * @throws FallbackLoop
  * @throws \Error
  */
 protected function serve(array $route, array $args = [])
 {
     static $calledOnce = null;
     if (count($route) === 1) {
         $route[] = 'index';
     }
     try {
         $class_name = Gears::getName('Landing__' . $route[0]);
     } catch (GearNotFound $ex) {
         $class_name = '\\Airship\\Cabin\\' . self::$active_cabin . '\\Landing\\' . $route[0];
     }
     $method = $route[1];
     if (!\class_exists($class_name)) {
         $state = State::instance();
         $state->logger->error('Landing Error: Class not found when invoked from router', ['route' => ['class' => $class_name, 'method' => $method]]);
         $calledOnce = true;
         return $this->serveFallback();
     }
     // Load our cabin-specific landing
     $landing = new $class_name();
     if (!$landing instanceof Landing) {
         throw new \Error(\__("%s is not a Landing", "default", $class_name));
     }
     // Dependency injection with a twist
     $landing->airshipEjectFromCockpit($this->lens, $this->databases, self::$patternPrefix);
     // Tighten the Bolts!
     \Airship\tightenBolts($landing);
     if (!\method_exists($landing, $method)) {
         if ($calledOnce) {
             throw new FallbackLoop(\trk('errors.router.fallback_loop'));
         }
         $calledOnce = true;
         return $this->serveFallback();
     }
     return $landing->{$method}(...$args);
 }
Esempio n. 8
0
 /**
  * Update a row in a database table.
  *
  * @param string $table - table name
  * @param array $changes - associative array of which values should be assigned to each field
  * @param array $conditions - WHERE clause
  * @return mixed
  * @throws \TypeError
  */
 public function update(string $table, array $changes, array $conditions)
 {
     if (empty($changes) || empty($conditions)) {
         return null;
     }
     $params = [];
     $queryString = "UPDATE " . $this->escapeIdentifier($table) . " SET ";
     // The first set (pre WHERE)
     $pre = [];
     foreach ($changes as $i => $v) {
         $i = $this->escapeIdentifier($i);
         if ($v === null) {
             $pre[] = " {$i} = NULL";
         } elseif ($v === true) {
             $pre[] = " {$i} = TRUE ";
         } elseif ($v === false) {
             $pre[] = " {$i} = FALSE ";
         } elseif (\is_array($v)) {
             throw new \TypeError(\trk('errors.database.array_passed'));
         } else {
             $pre[] = " {$i} = ?";
             $params[] = $v;
         }
     }
     $queryString .= \implode(', ', $pre);
     $queryString .= " WHERE ";
     // The last set (post WHERE)
     $post = [];
     foreach ($conditions as $i => $v) {
         $i = $this->escapeIdentifier($i);
         if ($v === null) {
             $post[] = " {$i} IS NULL ";
         } elseif ($v === true) {
             $post[] = " {$i} = TRUE ";
         } elseif ($v === false) {
             $post[] = " {$i} = FALSE ";
         } elseif (\is_array($v)) {
             throw new \TypeError(\trk('errors.database.array_passed'));
         } else {
             $post[] = " {$i} = ? ";
             $params[] = $v;
         }
     }
     $queryString .= \implode(' AND ', $post);
     return $this->safeQuery($queryString, $params);
 }
Esempio n. 9
0
 /**
  * Get the channels (cache across all instances of Installer)
  *
  * @param string $name
  * @return Channel
  * @throws NoAPIResponse
  */
 protected function getChannel(string $name) : Channel
 {
     if (empty(self::$channels)) {
         $config = \Airship\loadJSON(ROOT . '/config/channels.json');
         foreach ($config as $chName => $chConfig) {
             self::$channels[$chName] = new Channel($this, $chName, $chConfig);
         }
     }
     if (isset(self::$channels[$name])) {
         return self::$channels[$name];
     }
     throw new NoAPIResponse(\trk('errors.hail.no_channel_configured', ''));
 }
Esempio n. 10
0
 /**
  * Get a relative BLAKE2b hash of an input. Formatted as two lookup
  * directories followed by a cache entry. 'hh/hh/hhhhhhhh...'
  *
  * @param string $preHash The cache identifier (will be hashed)
  * @param bool $asString Return a string?
  * @return string|array
  * @throws InvalidType
  */
 public static function getRelativeHash(string $preHash, bool $asString = false)
 {
     $state = State::instance();
     $cacheKey = $state->keyring['cache.hash_key'];
     if (!$cacheKey instanceof Key) {
         throw new InvalidType(\trk('errors.type.wrong_class', '\\ParagonIE\\Halite\\Key'));
     }
     // We use a keyed hash, with a distinct key per Airship deployment to
     // make collisions unlikely,
     $hash = \Sodium\crypto_generichash($preHash, $cacheKey->getRawKeyMaterial(), self::HASH_SIZE);
     $relHash = [\Sodium\bin2hex($hash[0]), \Sodium\bin2hex($hash[1]), \Sodium\bin2hex(Util::subString($hash, 2))];
     if ($asString) {
         return \implode(DIRECTORY_SEPARATOR, $relHash);
     }
     return $relHash;
 }
Esempio n. 11
0
                        $transportConfig['connection_config']['ssl'] = 'tls';
                        $transportConfig['port'] = !empty($state->universal['email']['smtp']['port']) ? $state->universal['email']['smtp']['port'] : 587;
                    }
                    $transport->setOptions(new \Zend\Mail\Transport\SmtpOptions($transportConfig));
                    break;
                case 'File':
                    $transport = new Zend\Mail\Transport\File();
                    /** @noinspection PhpUnusedParameterInspection */
                    $transport->setOptions(new \Zend\Mail\Transport\FileOptions(['path' => !empty($state->universal['email']['file']['path']) ? $state->universal['email']['file']['path'] : ROOT . '/files/email', 'callback' => function (Zend\Mail\Transport\File $t) : string {
                        return \implode('_', ['Message', \date('YmdHis'), \Airship\uniqueId(12) . '.txt']);
                    }]));
                    break;
                case 'Sendmail':
                    if (!empty($state->universal['email']['sendmail']['parameters'])) {
                        $transport = new Zend\Mail\Transport\Sendmail($state->universal['email']['sendmail']['parameters']);
                    } else {
                        $transport = new Zend\Mail\Transport\Sendmail();
                    }
                    break;
                default:
                    throw new Exception(\trk('errors.email.invalid_transport', \print_r($state->universal['email']['transport'], true)));
            }
        }
        $state->mailer = $transport;
    }
    // Now that our mailer is set up, let's make sure GPGMailer is too.
    $gpgMailer = new GPGMailer($state->mailer, ['homedir' => ROOT . '/files']);
    $state->gpgMailer = $gpgMailer;
};
$email_closure();
unset($email_closure);
Esempio n. 12
0
 /**
  * Grab a blueprint
  *
  * @param string $name
  * @param mixed[] ...$cArgs Constructor arguments
  * @return Blueprint
  * @throws InvalidType
  */
 protected function blueprint(string $name, ...$cArgs) : Blueprint
 {
     if (!empty($cArgs)) {
         $cache = Util::hash($name . ':' . \json_encode($cArgs));
     } else {
         $cArgs = [];
         $cache = Util::hash($name . '[]');
     }
     if (!isset($this->_cache['blueprints'][$cache])) {
         // CACHE MISS. We need to build it, then!
         $db = $this->airshipChooseDB();
         if ($db instanceof DBInterface) {
             \array_unshift($cArgs, $db);
         }
         try {
             $class = Gears::getName('Blueprint__' . $name);
         } catch (GearNotFound $ex) {
             if ($name[0] === '\\') {
                 // If you pass a \Absolute\Namespace, we will just use it.
                 $class = $name;
             } else {
                 // We default to \Current\Application\Namespace\Blueprint\NameHere.
                 $x = \explode('\\', $this->getNamespace());
                 \array_pop($x);
                 $class = \implode('\\', $x) . '\\Blueprint\\' . $name;
             }
         }
         $this->_cache['blueprints'][$cache] = new $class(...$cArgs);
         if (!$this->_cache['blueprints'][$cache] instanceof Blueprint) {
             throw new InvalidType(\trk('errors.type.wrong_class', 'Blueprint'));
         }
         \Airship\tightenBolts($this->_cache['blueprints'][$cache]);
     }
     return $this->_cache['blueprints'][$cache];
 }
Esempio n. 13
0
 /**
  * Get the directory ID for a given path
  *
  * @param string $dir
  * @param int $parent
  * @param string $cabin
  * @return int
  * @throws CustomPageNotFoundException
  */
 public function getDirectoryId(string $dir, int $parent = 0, string $cabin = '') : int
 {
     if (empty($cabin)) {
         $cabin = $this->cabin;
     }
     if (empty($parent)) {
         $res = $this->db->cell("SELECT\n                     directoryid\n                 FROM\n                     airship_custom_dir\n                 WHERE\n                         active\n                     AND cabin = ?\n                     AND url = ?\n                     AND parent IS NULL\n                ", $cabin, $dir);
     } else {
         $res = $this->db->cell("SELECT\n                 directoryid\n             FROM\n                 airship_custom_dir\n             WHERE\n                     active\n                 AND cabin = ?\n                 AND url = ?\n                 AND parent = ?\n            ", $cabin, $dir, $parent);
     }
     if ($res === false) {
         throw new CustomPageNotFoundException(\trk('errors.pages.directory_does_not_exist'));
     }
     return (int) $res;
 }
Esempio n. 14
0
 /**
  * Authenticate a user by a long-term authentication token (e.g. a cookie).
  * 
  * @param string $token
  * @return mixed int
  * @throws LongTermAuthAlert
  */
 public function loginByToken(string $token = '') : int
 {
     $table = $this->db->escapeIdentifier($this->tableConfig['table']['longterm']);
     $f = ['selector' => $this->db->escapeIdentifier($this->tableConfig['fields']['longterm']['selector']), 'userid' => $this->tableConfig['fields']['longterm']['userid'], 'validator' => $this->tableConfig['fields']['longterm']['validator']];
     try {
         $decoded = Base64::decode($token);
     } catch (\RangeException $ex) {
         throw new LongTermAuthAlert(\trk('errors.security.invalid_persistent_token'));
     }
     if ($decoded === false) {
         throw new LongTermAuthAlert(\trk('errors.security.invalid_persistent_token'));
     } elseif (Binary::safeStrlen($decoded) !== self::LONG_TERM_AUTH_BYTES) {
         throw new LongTermAuthAlert(\trk('errors.security.invalid_persistent_token'));
     }
     \Sodium\memzero($token);
     $sel = Binary::safeSubstr($decoded, 0, self::SELECTOR_BYTES);
     $val = CryptoUtil::raw_hash(Binary::safeSubstr($decoded, self::SELECTOR_BYTES));
     \Sodium\memzero($decoded);
     $record = $this->db->row('SELECT * FROM ' . $table . ' WHERE ' . $f['selector'] . ' = ?', Base64::encode($sel));
     if (empty($record)) {
         \Sodium\memzero($val);
         throw new LongTermAuthAlert(\trk('errors.security.invalid_persistent_token'));
     }
     $stored = \Sodium\hex2bin($record[$f['validator']]);
     \Sodium\memzero($record[$f['validator']]);
     if (!\hash_equals($stored, $val)) {
         \Sodium\memzero($val);
         \Sodium\memzero($stored);
         throw new LongTermAuthAlert(\trk('errors.security.invalid_persistent_token'));
     }
     \Sodium\memzero($stored);
     \Sodium\memzero($val);
     $userID = (int) $record[$f['userid']];
     $_SESSION['session_canary'] = $this->db->cell('SELECT session_canary FROM airship_users WHERE userid = ?', $userID);
     return $userID;
 }
Esempio n. 15
0
/**
 * Fetch a query string from the stored queries file
 *
 * @param string $index Which index to replace
 * @param array $params Parameters to be replaced in the query string
 * @param string $cabin Which Cabin are we loading?
 * @param string $driver Which database driver?
 * @return string
 * @throws NotImplementedException
 */
function queryString(string $index, array $params = [], string $cabin = \CABIN_NAME, string $driver = '') : string
{
    static $_cache = [];
    if (empty($driver)) {
        $db = \Airship\get_database();
        $driver = $db->getDriver();
    }
    $cacheKey = Util::hash($cabin . '/' . $driver, \Sodium\CRYPTO_GENERICHASH_BYTES_MIN);
    if (empty($_cache[$cacheKey])) {
        $driver = \preg_replace('/[^a-z]/', '', \strtolower($driver));
        $path = !empty($cabin) ? ROOT . '/Cabin/' . $cabin . '/Queries/' . $driver . '.json' : ROOT . '/Engine/Queries/' . $driver . '.json';
        $_cache[$cacheKey] = \Airship\loadJSON($path);
    }
    $split_key = \explode('.', $index);
    $v = $_cache[$cacheKey];
    foreach ($split_key as $k) {
        if (!\array_key_exists($k, $v)) {
            throw new NotImplementedException(\trk('errors.database.query_not_found', $index));
        }
        $v = $v[$k];
    }
    if (\is_array($v)) {
        throw new NotImplementedException(\trk('errors.database.multiple_candidates', $index));
    }
    $str = $v;
    foreach ($params as $token => $replacement) {
        $str = \str_replace('{{' . $token . '}}', $replacement, $str);
    }
    return $str;
}
Esempio n. 16
0
 /**
  * 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'));
 }
Esempio n. 17
0
 /**
  * Use this to change the configuration settings.
  * Only use this if you know what you are doing.
  *
  * @param array $options
  * @throws InvalidConfig
  */
 public function reconfigure(array $options = [])
 {
     foreach ($options as $opt => $val) {
         switch ($opt) {
             case 'recycleAfter':
             case 'hmacIP':
             case 'expireOld':
             case 'sessionIndex':
                 $this->{$opt} = $val;
                 break;
             default:
                 throw new InvalidConfig(\trk('errors.object.invalid_property', $opt, __CLASS__));
         }
     }
 }