diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c4321ad..d964a57a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ * CHANGED: Language selection cookie only transmitted over HTTPS (#472) * CHANGED: Upgrading libraries to: random_compat 2.0.20 * CHANGED: Removed automatic `.ini` configuration file migration (#808) + * CHANGED: Removed configurable `dir` for `traffic` & `purge` limiters (#419) + * CHANGED: Server salt, traffic and purge limiter now stored in the storage backend (#419) * **1.3.5 (2021-04-05)** * ADDED: Translation for Hebrew, Lithuanian, Indonesian and Catalan * ADDED: Make the project info configurable (#681) diff --git a/cfg/conf.sample.php b/cfg/conf.sample.php index a4b7f6b5..d362f3f2 100644 --- a/cfg/conf.sample.php +++ b/cfg/conf.sample.php @@ -143,9 +143,6 @@ limit = 10 ; set the HTTP header containing the visitors IP address, i.e. X_FORWARDED_FOR ; header = "X_FORWARDED_FOR" -; directory to store the traffic limits in -dir = PATH "data" - [purge] ; minimum time limit between two purgings of expired pastes, it is only ; triggered when pastes are created @@ -157,9 +154,6 @@ limit = 300 ; site batchsize = 10 -; directory to store the purge limit in -dir = PATH "data" - [model] ; name of data model class to load and directory for storage ; the default model "Filesystem" stores everything in the filesystem diff --git a/lib/Configuration.php b/lib/Configuration.php index 1185440c..7c4eb106 100644 --- a/lib/Configuration.php +++ b/lib/Configuration.php @@ -80,13 +80,11 @@ class Configuration 'traffic' => array( 'limit' => 10, 'header' => null, - 'dir' => 'data', 'exemptedIp' => null, ), 'purge' => array( 'limit' => 300, 'batchsize' => 10, - 'dir' => 'data', ), 'model' => array( 'class' => 'Filesystem', diff --git a/lib/Controller.php b/lib/Controller.php index 2df522a2..fb919ca1 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -162,7 +162,6 @@ class Controller $this->_model = new Model($this->_conf); $this->_request = new Request; $this->_urlBase = $this->_request->getRequestUri(); - ServerSalt::setPath($this->_conf->getKey('dir', 'traffic')); // set default language $lang = $this->_conf->getKey('languagedefault'); @@ -196,20 +195,17 @@ class Controller */ private function _create() { - try { - // Ensure last paste from visitors IP address was more than configured amount of seconds ago. - TrafficLimiter::setConfiguration($this->_conf); - if (!TrafficLimiter::canPass()) { - $this->_return_message( - 1, I18n::_( - 'Please wait %d seconds between each post.', - $this->_conf->getKey('limit', 'traffic') - ) - ); - return; - } - } catch (Exception $e) { - $this->_return_message(1, I18n::_($e->getMessage())); + // Ensure last paste from visitors IP address was more than configured amount of seconds ago. + ServerSalt::setStore($this->_model->getStore()); + TrafficLimiter::setConfiguration($this->_conf); + TrafficLimiter::setStore($this->_model->getStore()); + if (!TrafficLimiter::canPass()) { + $this->_return_message( + 1, I18n::_( + 'Please wait %d seconds between each post.', + $this->_conf->getKey('limit', 'traffic') + ) + ); return; } diff --git a/lib/Data/AbstractData.php b/lib/Data/AbstractData.php index 077864ec..591b91fd 100644 --- a/lib/Data/AbstractData.php +++ b/lib/Data/AbstractData.php @@ -15,12 +15,12 @@ namespace PrivateBin\Data; /** * AbstractData * - * Abstract model for PrivateBin data access, implemented as a singleton. + * Abstract model for data access, implemented as a singleton. */ abstract class AbstractData { /** - * singleton instance + * Singleton instance * * @access protected * @static @@ -29,9 +29,18 @@ abstract class AbstractData protected static $_instance = null; /** - * enforce singleton, disable constructor + * cache for the traffic limiter * - * Instantiate using {@link getInstance()}, privatebin is a singleton object. + * @access private + * @static + * @var array + */ + protected static $_last_cache = array(); + + /** + * Enforce singleton, disable constructor + * + * Instantiate using {@link getInstance()}, this object implements the singleton pattern. * * @access protected */ @@ -40,9 +49,9 @@ abstract class AbstractData } /** - * enforce singleton, disable cloning + * Enforce singleton, disable cloning * - * Instantiate using {@link getInstance()}, privatebin is a singleton object. + * Instantiate using {@link getInstance()}, this object implements the singleton pattern. * * @access private */ @@ -51,7 +60,7 @@ abstract class AbstractData } /** - * get instance of singleton + * Get instance of singleton * * @access public * @static @@ -130,6 +139,46 @@ abstract class AbstractData */ abstract public function existsComment($pasteid, $parentid, $commentid); + /** + * Purge outdated entries. + * + * @access public + * @param string $namespace + * @param int $time + * @return void + */ + public function purgeValues($namespace, $time) + { + if ($namespace === 'traffic_limiter') { + foreach (self::$_last_cache as $key => $last_submission) { + if ($last_submission <= $time) { + unset(self::$_last_cache[$key]); + } + } + } + } + + /** + * Save a value. + * + * @access public + * @param string $value + * @param string $namespace + * @param string $key + * @return bool + */ + abstract public function setValue($value, $namespace, $key = ''); + + /** + * Load a value. + * + * @access public + * @param string $namespace + * @param string $key + * @return string + */ + abstract public function getValue($namespace, $key = ''); + /** * Returns up to batch size number of paste ids that have expired * diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 607013ba..0c66d330 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -198,21 +198,25 @@ class Database extends AbstractData $opendiscussion = $paste['adata'][2]; $burnafterreading = $paste['adata'][3]; } - return self::_exec( - 'INSERT INTO ' . self::_sanitizeIdentifier('paste') . - ' VALUES(?,?,?,?,?,?,?,?,?)', - array( - $pasteid, - $isVersion1 ? $paste['data'] : Json::encode($paste), - $created, - $expire_date, - (int) $opendiscussion, - (int) $burnafterreading, - Json::encode($meta), - $attachment, - $attachmentname, - ) - ); + try { + return self::_exec( + 'INSERT INTO ' . self::_sanitizeIdentifier('paste') . + ' VALUES(?,?,?,?,?,?,?,?,?)', + array( + $pasteid, + $isVersion1 ? $paste['data'] : Json::encode($paste), + $created, + $expire_date, + (int) $opendiscussion, + (int) $burnafterreading, + Json::encode($meta), + $attachment, + $attachmentname, + ) + ); + } catch (Exception $e) { + return false; + } } /** @@ -229,11 +233,14 @@ class Database extends AbstractData } self::$_cache[$pasteid] = false; - $paste = self::_select( - 'SELECT * FROM ' . self::_sanitizeIdentifier('paste') . - ' WHERE dataid = ?', array($pasteid), true - ); - + try { + $paste = self::_select( + 'SELECT * FROM ' . self::_sanitizeIdentifier('paste') . + ' WHERE dataid = ?', array($pasteid), true + ); + } catch (Exception $e) { + $paste = false; + } if ($paste === false) { return false; } @@ -348,19 +355,23 @@ class Database extends AbstractData $meta[$key] = null; } } - return self::_exec( - 'INSERT INTO ' . self::_sanitizeIdentifier('comment') . - ' VALUES(?,?,?,?,?,?,?)', - array( - $commentid, - $pasteid, - $parentid, - $data, - $meta['nickname'], - $meta[$iconKey], - $meta[$createdKey], - ) - ); + try { + return self::_exec( + 'INSERT INTO ' . self::_sanitizeIdentifier('comment') . + ' VALUES(?,?,?,?,?,?,?)', + array( + $commentid, + $pasteid, + $parentid, + $data, + $meta['nickname'], + $meta[$iconKey], + $meta[$createdKey], + ) + ); + } catch (Exception $e) { + return false; + } } /** @@ -416,13 +427,85 @@ class Database extends AbstractData */ public function existsComment($pasteid, $parentid, $commentid) { - return (bool) self::_select( - 'SELECT dataid FROM ' . self::_sanitizeIdentifier('comment') . - ' WHERE pasteid = ? AND parentid = ? AND dataid = ?', - array($pasteid, $parentid, $commentid), true + try { + return (bool) self::_select( + 'SELECT dataid FROM ' . self::_sanitizeIdentifier('comment') . + ' WHERE pasteid = ? AND parentid = ? AND dataid = ?', + array($pasteid, $parentid, $commentid), true + ); + } catch (Exception $e) { + return false; + } + } + + /** + * Save a value. + * + * @access public + * @param string $value + * @param string $namespace + * @param string $key + * @return bool + */ + public function setValue($value, $namespace, $key = '') + { + if ($namespace === 'traffic_limiter') { + self::$_last_cache[$key] = $value; + try { + $value = Json::encode(self::$_last_cache); + } catch (Exception $e) { + return false; + } + } + return self::_exec( + 'UPDATE ' . self::_sanitizeIdentifier('config') . + ' SET value = ? WHERE id = ?', + array($value, strtoupper($namespace)) ); } + /** + * Load a value. + * + * @access public + * @param string $namespace + * @param string $key + * @return string + */ + public function getValue($namespace, $key = '') + { + $configKey = strtoupper($namespace); + $value = $this->_getConfig($configKey); + if ($value === '') { + // initialize the row, so that setValue can rely on UPDATE queries + self::_exec( + 'INSERT INTO ' . self::_sanitizeIdentifier('config') . + ' VALUES(?,?)', + array($configKey, '') + ); + + // migrate filesystem based salt into database + $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php'; + if ($namespace === 'salt' && is_readable($file)) { + $value = Filesystem::getInstance(array('dir' => 'data'))->getValue('salt'); + $this->setValue($value, 'salt'); + @unlink($file); + return $value; + } + } + if ($value && $namespace === 'traffic_limiter') { + try { + self::$_last_cache = Json::decode($value); + } catch (Exception $e) { + self::$_last_cache = array(); + } + if (array_key_exists($key, self::$_last_cache)) { + return self::$_last_cache[$key]; + } + } + return (string) $value; + } + /** * Returns up to batch size number of paste ids that have expired * @@ -563,16 +646,19 @@ class Database extends AbstractData * @access private * @static * @param string $key - * @throws PDOException * @return string */ private static function _getConfig($key) { - $row = self::_select( - 'SELECT value FROM ' . self::_sanitizeIdentifier('config') . - ' WHERE id = ?', array($key), true - ); - return $row['value']; + try { + $row = self::_select( + 'SELECT value FROM ' . self::_sanitizeIdentifier('config') . + ' WHERE id = ?', array($key), true + ); + } catch (PDOException $e) { + return ''; + } + return $row ? $row['value'] : ''; } /** diff --git a/lib/Data/Filesystem.php b/lib/Data/Filesystem.php index 96ee6915..577ad34f 100644 --- a/lib/Data/Filesystem.php +++ b/lib/Data/Filesystem.php @@ -12,7 +12,8 @@ namespace PrivateBin\Data; -use PrivateBin\Persistence\DataStore; +use Exception; +use PrivateBin\Json; /** * Filesystem @@ -21,6 +22,29 @@ use PrivateBin\Persistence\DataStore; */ class Filesystem extends AbstractData { + /** + * first line in paste or comment files, to protect their contents from browsing exposed data directories + * + * @const string + */ + const PROTECTION_LINE = 'exists($pasteid)) { + if ( + !$this->exists($pasteid) || + !$paste = self::_get(self::_dataid2path($pasteid) . $pasteid . '.php') + ) { return false; } - return self::upgradePreV1Format( - DataStore::get(self::_dataid2path($pasteid) . $pasteid . '.php') - ); + return self::upgradePreV1Format($paste); } /** @@ -127,7 +152,7 @@ class Filesystem extends AbstractData $pastePath = $basePath . '.php'; // convert to PHP protected files if needed if (is_readable($basePath)) { - DataStore::prependRename($basePath, $pastePath); + self::_prependRename($basePath, $pastePath); // convert comments, too $discdir = self::_dataid2discussionpath($pasteid); @@ -136,7 +161,7 @@ class Filesystem extends AbstractData while (false !== ($filename = $dir->read())) { if (substr($filename, -4) !== '.php' && strlen($filename) >= 16) { $commentFilename = $discdir . $filename . '.php'; - DataStore::prependRename($discdir . $filename, $commentFilename); + self::_prependRename($discdir . $filename, $commentFilename); } } $dir->close(); @@ -165,7 +190,7 @@ class Filesystem extends AbstractData if (!is_dir($storagedir)) { mkdir($storagedir, 0700, true); } - return DataStore::store($file, $comment); + return self::_store($file, $comment); } /** @@ -187,7 +212,7 @@ class Filesystem extends AbstractData // - commentid is the comment identifier itself. // - parentid is the comment this comment replies to (It can be pasteid) if (is_file($discdir . $filename)) { - $comment = DataStore::get($discdir . $filename); + $comment = self::_get($discdir . $filename); $items = explode('.', $filename); // Add some meta information not contained in file. $comment['id'] = $items[1]; @@ -223,6 +248,97 @@ class Filesystem extends AbstractData ); } + /** + * Save a value. + * + * @access public + * @param string $value + * @param string $namespace + * @param string $key + * @return bool + */ + public function setValue($value, $namespace, $key = '') + { + switch ($namespace) { + case 'purge_limiter': + return self::_storeString( + self::$_path . DIRECTORY_SEPARATOR . 'purge_limiter.php', + ' 0) { @@ -243,7 +358,7 @@ class Filesystem extends AbstractData for ($i = 0, $max = $batchsize * 10; $i < $max; ++$i) { $firstKey = array_rand($firstLevel); $secondLevel = array_filter( - scandir($mainpath . DIRECTORY_SEPARATOR . $firstLevel[$firstKey]), + scandir(self::$_path . DIRECTORY_SEPARATOR . $firstLevel[$firstKey]), 'self::_isSecondLevelDir' ); @@ -254,7 +369,7 @@ class Filesystem extends AbstractData } $secondKey = array_rand($secondLevel); - $path = $mainpath . DIRECTORY_SEPARATOR . + $path = self::$_path . DIRECTORY_SEPARATOR . $firstLevel[$firstKey] . DIRECTORY_SEPARATOR . $secondLevel[$secondKey]; if (!is_dir($path)) { @@ -314,10 +429,9 @@ class Filesystem extends AbstractData */ private static function _dataid2path($dataid) { - return DataStore::getPath( + return self::$_path . DIRECTORY_SEPARATOR . substr($dataid, 0, 2) . DIRECTORY_SEPARATOR . - substr($dataid, 2, 2) . DIRECTORY_SEPARATOR - ); + substr($dataid, 2, 2) . DIRECTORY_SEPARATOR; } /** @@ -347,7 +461,7 @@ class Filesystem extends AbstractData private static function _isFirstLevelDir($element) { return self::_isSecondLevelDir($element) && - is_dir(DataStore::getPath($element)); + is_dir(self::$_path . DIRECTORY_SEPARATOR . $element); } /** @@ -362,4 +476,97 @@ class Filesystem extends AbstractData { return (bool) preg_match('/^[a-f0-9]{2}$/', $element); } + + /** + * store the data + * + * @access public + * @static + * @param string $filename + * @param array $data + * @return bool + */ + private static function _store($filename, array $data) + { + try { + return self::_storeString( + $filename, + self::PROTECTION_LINE . PHP_EOL . Json::encode($data) + ); + } catch (Exception $e) { + return false; + } + } + + /** + * store a string + * + * @access public + * @static + * @param string $filename + * @param string $data + * @return bool + */ + private static function _storeString($filename, $data) + { + // Create storage directory if it does not exist. + if (!is_dir(self::$_path)) { + if (!@mkdir(self::$_path, 0700)) { + return false; + } + } + $file = self::$_path . DIRECTORY_SEPARATOR . '.htaccess'; + if (!is_file($file)) { + $writtenBytes = 0; + if ($fileCreated = @touch($file)) { + $writtenBytes = @file_put_contents( + $file, + self::HTACCESS_LINE . PHP_EOL, + LOCK_EX + ); + } + if ( + $fileCreated === false || + $writtenBytes === false || + $writtenBytes < strlen(self::HTACCESS_LINE . PHP_EOL) + ) { + return false; + } + } + + $fileCreated = true; + $writtenBytes = 0; + if (!is_file($filename)) { + $fileCreated = @touch($filename); + } + if ($fileCreated) { + $writtenBytes = @file_put_contents($filename, $data, LOCK_EX); + } + if ($fileCreated === false || $writtenBytes === false || $writtenBytes < strlen($data)) { + return false; + } + @chmod($filename, 0640); // protect file from access by other users on the host + return true; + } + + /** + * rename a file, prepending the protection line at the beginning + * + * @access public + * @static + * @param string $srcFile + * @param string $destFile + * @return void + */ + private static function _prependRename($srcFile, $destFile) + { + // don't overwrite already converted file + if (!is_readable($destFile)) { + $handle = fopen($srcFile, 'r', false, stream_context_create()); + file_put_contents($destFile, self::PROTECTION_LINE . PHP_EOL); + file_put_contents($destFile, $handle, FILE_APPEND); + fclose($handle); + } + unlink($srcFile); + } } diff --git a/lib/Data/GoogleCloudStorage.php b/lib/Data/GoogleCloudStorage.php index 1a1d8bf5..2e8e2c5d 100644 --- a/lib/Data/GoogleCloudStorage.php +++ b/lib/Data/GoogleCloudStorage.php @@ -4,11 +4,39 @@ namespace PrivateBin\Data; use Exception; use Google\Cloud\Core\Exception\NotFoundException; +use Google\Cloud\Storage\Bucket; use Google\Cloud\Storage\StorageClient; use PrivateBin\Json; class GoogleCloudStorage extends AbstractData { + /** + * GCS client + * + * @access private + * @static + * @var StorageClient + */ + private static $_client = null; + + /** + * GCS bucket + * + * @access private + * @static + * @var Bucket + */ + private static $_bucket = null; + + /** + * object prefix + * + * @access private + * @static + * @var string + */ + private static $_prefix = 'pastes'; + /** * returns a Google Cloud Storage data backend. * @@ -19,10 +47,12 @@ class GoogleCloudStorage extends AbstractData */ public static function getInstance(array $options) { - $client = null; - $bucket = null; - $prefix = 'pastes'; + // if needed initialize the singleton + if (!(self::$_instance instanceof self)) { + self::$_instance = new self; + } + $bucket = null; if (getenv('PRIVATEBIN_GCS_BUCKET')) { $bucket = getenv('PRIVATEBIN_GCS_BUCKET'); } @@ -30,53 +60,36 @@ class GoogleCloudStorage extends AbstractData $bucket = $options['bucket']; } if (is_array($options) && array_key_exists('prefix', $options)) { - $prefix = $options['prefix']; - } - if (is_array($options) && array_key_exists('client', $options)) { - $client = $options['client']; + self::$_prefix = $options['prefix']; } - if (!(self::$_instance instanceof self)) { - self::$_instance = new self($bucket, $prefix, $client); + if (empty(self::$_client)) { + self::$_client = class_exists('StorageClientStub', false) ? + new \StorageClientStub(array()) : + new StorageClient(array('suppressKeyFileNotice' => true)); } + self::$_bucket = self::$_client->bucket($bucket); + return self::$_instance; } - protected $_client = null; - protected $_bucket = null; - protected $_prefix = 'pastes'; - - public function __construct($bucket, $prefix, $client = null) - { - parent::__construct(); - if ($client == null) { - $this->_client = new StorageClient(array('suppressKeyFileNotice' => true)); - } else { - // use given client for test purposes - $this->_client = $client; - } - - $this->_bucket = $this->_client->bucket($bucket); - if ($prefix != null) { - $this->_prefix = $prefix; - } - } - /** - * returns the google storage object key for $pasteid in $this->_bucket. + * returns the google storage object key for $pasteid in self::$_bucket. + * + * @access private * @param $pasteid string to get the key for * @return string */ private function _getKey($pasteid) { - if ($this->_prefix != '') { - return $this->_prefix . '/' . $pasteid; + if (self::$_prefix != '') { + return self::$_prefix . '/' . $pasteid; } return $pasteid; } /** - * Uploads the payload in the $this->_bucket under the specified key. + * Uploads the payload in the self::$_bucket under the specified key. * The entire payload is stored as a JSON document. The metadata is replicated * as the GCS object's metadata except for the fields attachment, attachmentname * and salt. @@ -85,7 +98,7 @@ class GoogleCloudStorage extends AbstractData * @param $payload array to store * @return bool true if successful, otherwise false. */ - private function upload($key, $payload) + private function _upload($key, $payload) { $metadata = array_key_exists('meta', $payload) ? $payload['meta'] : array(); unset($metadata['attachment'], $metadata['attachmentname'], $metadata['salt']); @@ -93,7 +106,7 @@ class GoogleCloudStorage extends AbstractData $metadata[$k] = strval($v); } try { - $this->_bucket->upload(Json::encode($payload), array( + self::$_bucket->upload(Json::encode($payload), array( 'name' => $key, 'chunkSize' => 262144, 'predefinedAcl' => 'private', @@ -103,7 +116,7 @@ class GoogleCloudStorage extends AbstractData ), )); } catch (Exception $e) { - error_log('failed to upload ' . $key . ' to ' . $this->_bucket->name() . ', ' . + error_log('failed to upload ' . $key . ' to ' . self::$_bucket->name() . ', ' . trim(preg_replace('/\s\s+/', ' ', $e->getMessage()))); return false; } @@ -119,7 +132,7 @@ class GoogleCloudStorage extends AbstractData return false; } - return $this->upload($this->_getKey($pasteid), $paste); + return $this->_upload($this->_getKey($pasteid), $paste); } /** @@ -128,13 +141,13 @@ class GoogleCloudStorage extends AbstractData public function read($pasteid) { try { - $o = $this->_bucket->object($this->_getKey($pasteid)); + $o = self::$_bucket->object($this->_getKey($pasteid)); $data = $o->downloadAsString(); return Json::decode($data); } catch (NotFoundException $e) { return false; } catch (Exception $e) { - error_log('failed to read ' . $pasteid . ' from ' . $this->_bucket->name() . ', ' . + error_log('failed to read ' . $pasteid . ' from ' . self::$_bucket->name() . ', ' . trim(preg_replace('/\s\s+/', ' ', $e->getMessage()))); return false; } @@ -148,9 +161,9 @@ class GoogleCloudStorage extends AbstractData $name = $this->_getKey($pasteid); try { - foreach ($this->_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) { + foreach (self::$_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) { try { - $this->_bucket->object($comment->name())->delete(); + self::$_bucket->object($comment->name())->delete(); } catch (NotFoundException $e) { // ignore if already deleted. } @@ -160,7 +173,7 @@ class GoogleCloudStorage extends AbstractData } try { - $this->_bucket->object($name)->delete(); + self::$_bucket->object($name)->delete(); } catch (NotFoundException $e) { // ignore if already deleted } @@ -171,7 +184,7 @@ class GoogleCloudStorage extends AbstractData */ public function exists($pasteid) { - $o = $this->_bucket->object($this->_getKey($pasteid)); + $o = self::$_bucket->object($this->_getKey($pasteid)); return $o->exists(); } @@ -184,7 +197,7 @@ class GoogleCloudStorage extends AbstractData return false; } $key = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid; - return $this->upload($key, $comment); + return $this->_upload($key, $comment); } /** @@ -195,8 +208,8 @@ class GoogleCloudStorage extends AbstractData $comments = array(); $prefix = $this->_getKey($pasteid) . '/discussion/'; try { - foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $key) { - $comment = JSON::decode($this->_bucket->object($key->name())->downloadAsString()); + foreach (self::$_bucket->objects(array('prefix' => $prefix)) as $key) { + $comment = JSON::decode(self::$_bucket->object($key->name())->downloadAsString()); $comment['id'] = basename($key->name()); $slot = $this->getOpenSlot($comments, (int) $comment['meta']['created']); $comments[$slot] = $comment; @@ -213,10 +226,92 @@ class GoogleCloudStorage extends AbstractData public function existsComment($pasteid, $parentid, $commentid) { $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid; - $o = $this->_bucket->object($name); + $o = self::$_bucket->object($name); return $o->exists(); } + /** + * @inheritDoc + */ + public function purgeValues($namespace, $time) + { + $path = 'config/' . $namespace; + try { + foreach (self::$_bucket->objects(array('prefix' => $path)) as $object) { + $name = $object->name(); + if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') { + continue; + } + $info = $object->info(); + if (key_exists('metadata', $info) && key_exists('value', $info['metadata'])) { + $value = $info['metadata']['value']; + if (is_numeric($value) && intval($value) < $time) { + try { + $object->delete(); + } catch (NotFoundException $e) { + // deleted by another instance. + } + } + } + } + } catch (NotFoundException $e) { + // no objects in the bucket yet + } + } + + /** + * For GoogleCloudStorage, the value will also be stored in the metadata for the + * namespaces traffic_limiter and purge_limiter. + * @inheritDoc + */ + public function setValue($value, $namespace, $key = '') + { + if ($key === '') { + $key = 'config/' . $namespace; + } else { + $key = 'config/' . $namespace . '/' . $key; + } + + $metadata = array('namespace' => $namespace); + if ($namespace != 'salt') { + $metadata['value'] = strval($value); + } + try { + self::$_bucket->upload($value, array( + 'name' => $key, + 'chunkSize' => 262144, + 'predefinedAcl' => 'private', + 'metadata' => array( + 'content-type' => 'application/json', + 'metadata' => $metadata, + ), + )); + } catch (Exception $e) { + error_log('failed to set key ' . $key . ' to ' . self::$_bucket->name() . ', ' . + trim(preg_replace('/\s\s+/', ' ', $e->getMessage()))); + return false; + } + return true; + } + + /** + * @inheritDoc + */ + public function getValue($namespace, $key = '') + { + if ($key === '') { + $key = 'config/' . $namespace; + } else { + $key = 'config/' . $namespace . '/' . $key; + } + try { + $o = self::$_bucket->object($key); + return $o->downloadAsString(); + } catch (NotFoundException $e) { + return ''; + } + } + /** * @inheritDoc */ @@ -225,12 +320,12 @@ class GoogleCloudStorage extends AbstractData $expired = array(); $now = time(); - $prefix = $this->_prefix; + $prefix = self::$_prefix; if ($prefix != '') { - $prefix = $prefix . '/'; + $prefix .= '/'; } try { - foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) { + foreach (self::$_bucket->objects(array('prefix' => $prefix)) as $object) { $metadata = $object->info()['metadata']; if ($metadata != null && array_key_exists('expire_date', $metadata)) { $expire_at = intval($metadata['expire_date']); diff --git a/lib/FormatV2.php b/lib/FormatV2.php index a06aa5d3..d2055f31 100644 --- a/lib/FormatV2.php +++ b/lib/FormatV2.php @@ -52,6 +52,11 @@ class FormatV2 } } + // Make sure adata is an array. + if (!is_array($message['adata'])) { + return false; + } + $cipherParams = $isComment ? $message['adata'] : $message['adata'][0]; // Make sure some fields are base64 data: diff --git a/lib/Json.php b/lib/Json.php index b6567ed5..5f4efcf3 100644 --- a/lib/Json.php +++ b/lib/Json.php @@ -44,13 +44,13 @@ class Json * @static * @param string $input * @throws Exception - * @return array + * @return mixed */ public static function decode($input) { - $array = json_decode($input, true); + $output = json_decode($input, true); self::_detectError(); - return $array; + return $output; } /** diff --git a/lib/Model.php b/lib/Model.php index 5abb1d15..8aebd794 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -54,7 +54,7 @@ class Model */ public function getPaste($pasteId = null) { - $paste = new Paste($this->_conf, $this->_getStore()); + $paste = new Paste($this->_conf, $this->getStore()); if ($pasteId !== null) { $paste->setId($pasteId); } @@ -67,8 +67,9 @@ class Model public function purge() { PurgeLimiter::setConfiguration($this->_conf); + PurgeLimiter::setStore($this->getStore()); if (PurgeLimiter::canPurge()) { - $this->_getStore()->purge($this->_conf->getKey('batchsize', 'purge')); + $this->getStore()->purge($this->_conf->getKey('batchsize', 'purge')); } } @@ -77,7 +78,7 @@ class Model * * @return Data\AbstractData */ - private function _getStore() + public function getStore() { if ($this->_store === null) { $this->_store = forward_static_call( diff --git a/lib/Model/Paste.php b/lib/Model/Paste.php index 6f343938..b855e6d5 100644 --- a/lib/Model/Paste.php +++ b/lib/Model/Paste.php @@ -93,7 +93,7 @@ class Paste extends AbstractModel } $this->_data['meta']['created'] = time(); - $this->_data['meta']['salt'] = serversalt::generate(); + $this->_data['meta']['salt'] = ServerSalt::generate(); // store paste if ( diff --git a/lib/Persistence/AbstractPersistence.php b/lib/Persistence/AbstractPersistence.php index 489836da..4e61a8ee 100644 --- a/lib/Persistence/AbstractPersistence.php +++ b/lib/Persistence/AbstractPersistence.php @@ -12,7 +12,7 @@ namespace PrivateBin\Persistence; -use Exception; +use PrivateBin\Data\AbstractData; /** * AbstractPersistence @@ -22,114 +22,23 @@ use Exception; abstract class AbstractPersistence { /** - * path in which to persist something + * data storage to use to persist something * * @access private * @static - * @var string + * @var AbstractData */ - private static $_path = 'data'; + protected static $_store; /** * set the path * * @access public * @static - * @param string $path + * @param AbstractData $store */ - public static function setPath($path) + public static function setStore(AbstractData $store) { - self::$_path = $path; - } - - /** - * get the path - * - * @access public - * @static - * @param string $filename - * @return string - */ - public static function getPath($filename = null) - { - if (strlen($filename)) { - return self::$_path . DIRECTORY_SEPARATOR . $filename; - } else { - return self::$_path; - } - } - - /** - * checks if the file exists - * - * @access protected - * @static - * @param string $filename - * @return bool - */ - protected static function _exists($filename) - { - self::_initialize(); - return is_file(self::$_path . DIRECTORY_SEPARATOR . $filename); - } - - /** - * prepares path for storage - * - * @access protected - * @static - * @throws Exception - */ - protected static function _initialize() - { - // Create storage directory if it does not exist. - if (!is_dir(self::$_path)) { - if (!@mkdir(self::$_path, 0700)) { - throw new Exception('unable to create directory ' . self::$_path, 10); - } - } - $file = self::$_path . DIRECTORY_SEPARATOR . '.htaccess'; - if (!is_file($file)) { - $writtenBytes = 0; - if ($fileCreated = @touch($file)) { - $writtenBytes = @file_put_contents( - $file, - 'Require all denied' . PHP_EOL, - LOCK_EX - ); - } - if ($fileCreated === false || $writtenBytes === false || $writtenBytes < 19) { - throw new Exception('unable to write to file ' . $file, 11); - } - } - } - - /** - * store the data - * - * @access protected - * @static - * @param string $filename - * @param string $data - * @throws Exception - * @return string - */ - protected static function _store($filename, $data) - { - self::_initialize(); - $file = self::$_path . DIRECTORY_SEPARATOR . $filename; - $fileCreated = true; - $writtenBytes = 0; - if (!is_file($file)) { - $fileCreated = @touch($file); - } - if ($fileCreated) { - $writtenBytes = @file_put_contents($file, $data, LOCK_EX); - } - if ($fileCreated === false || $writtenBytes === false || $writtenBytes < strlen($data)) { - throw new Exception('unable to write to file ' . $file, 13); - } - @chmod($file, 0640); // protect file access - return $file; + self::$_store = $store; } } diff --git a/lib/Persistence/DataStore.php b/lib/Persistence/DataStore.php deleted file mode 100644 index d17e5faf..00000000 --- a/lib/Persistence/DataStore.php +++ /dev/null @@ -1,96 +0,0 @@ -getKey('limit', 'purge')); - self::setPath($conf->getKey('dir', 'purge')); } /** @@ -60,7 +59,6 @@ class PurgeLimiter extends AbstractPersistence * * @access public * @static - * @throws \Exception * @return bool */ public static function canPurge() @@ -71,17 +69,14 @@ class PurgeLimiter extends AbstractPersistence } $now = time(); - $file = 'purge_limiter.php'; - if (self::_exists($file)) { - require self::getPath($file); - $pl = $GLOBALS['purge_limiter']; - if ($pl + self::$_limit >= $now) { - return false; - } + $pl = (int) self::$_store->getValue('purge_limiter'); + if ($pl + self::$_limit >= $now) { + return false; } - - $content = 'setValue((string) $now, 'purge_limiter'); + if (!$hasStored) { + error_log('failed to store the purge limiter, skipping purge cycle to avoid getting stuck in a purge loop'); + } + return $hasStored; } } diff --git a/lib/Persistence/ServerSalt.php b/lib/Persistence/ServerSalt.php index 329a8ef2..1095498c 100644 --- a/lib/Persistence/ServerSalt.php +++ b/lib/Persistence/ServerSalt.php @@ -12,7 +12,7 @@ namespace PrivateBin\Persistence; -use Exception; +use PrivateBin\Data\AbstractData; /** * ServerSalt @@ -26,15 +26,6 @@ use Exception; */ class ServerSalt extends AbstractPersistence { - /** - * file where salt is saved to - * - * @access private - * @static - * @var string - */ - private static $_file = 'salt.php'; - /** * generated salt * @@ -53,8 +44,7 @@ class ServerSalt extends AbstractPersistence */ public static function generate() { - $randomSalt = bin2hex(random_bytes(256)); - return $randomSalt; + return bin2hex(random_bytes(256)); } /** @@ -62,7 +52,6 @@ class ServerSalt extends AbstractPersistence * * @access public * @static - * @throws Exception * @return string */ public static function get() @@ -71,20 +60,14 @@ class ServerSalt extends AbstractPersistence return self::$_salt; } - if (self::_exists(self::$_file)) { - if (is_readable(self::getPath(self::$_file))) { - $items = explode('|', file_get_contents(self::getPath(self::$_file))); - } - if (!isset($items) || !is_array($items) || count($items) != 3) { - throw new Exception('unable to read file ' . self::getPath(self::$_file), 20); - } - self::$_salt = $items[1]; + $salt = self::$_store->getValue('salt'); + if ($salt) { + self::$_salt = $salt; } else { self::$_salt = self::generate(); - self::_store( - self::$_file, - 'setValue(self::$_salt, 'salt')) { + error_log('failed to store the server salt, delete tokens, traffic limiter and user icons won\'t work'); + } } return self::$_salt; } @@ -94,11 +77,11 @@ class ServerSalt extends AbstractPersistence * * @access public * @static - * @param string $path + * @param AbstractData $store */ - public static function setPath($path) + public static function setStore(AbstractData $store) { self::$_salt = ''; - parent::setPath($path); + parent::setStore($store); } } diff --git a/lib/Persistence/TrafficLimiter.php b/lib/Persistence/TrafficLimiter.php index be76b8cd..9e896c1d 100644 --- a/lib/Persistence/TrafficLimiter.php +++ b/lib/Persistence/TrafficLimiter.php @@ -13,7 +13,6 @@ namespace PrivateBin\Persistence; -use Exception; use IPLib\Factory; use PrivateBin\Configuration; @@ -85,7 +84,6 @@ class TrafficLimiter extends AbstractPersistence public static function setConfiguration(Configuration $conf) { self::setLimit($conf->getKey('limit', 'traffic')); - self::setPath($conf->getKey('dir', 'traffic')); self::setExemptedIp($conf->getKey('exemptedIp', 'traffic')); if (($option = $conf->getKey('header', 'traffic')) !== null) { @@ -134,13 +132,7 @@ class TrafficLimiter extends AbstractPersistence return false; } - // Ip-lib throws an exception when something goes wrong, if so we want to catch it and set contained to false - try { - return $address->matches($range); - } catch (Exception $e) { - // If something is wrong with matching the ip, we assume it doesn't match - return false; - } + return $address->matches($range); } /** @@ -150,7 +142,6 @@ class TrafficLimiter extends AbstractPersistence * * @access public * @static - * @throws Exception * @return bool */ public static function canPass() @@ -170,35 +161,20 @@ class TrafficLimiter extends AbstractPersistence } } - $file = 'traffic_limiter.php'; - if (self::_exists($file)) { - require self::getPath($file); - $tl = $GLOBALS['traffic_limiter']; - } else { - $tl = array(); - } - - // purge file of expired hashes to keep it small - $now = time(); - foreach ($tl as $key => $time) { - if ($time + self::$_limit < $now) { - unset($tl[$key]); - } - } - // this hash is used as an array key, hence a shorter algo is used $hash = self::getHash('sha256'); - if (array_key_exists($hash, $tl) && ($tl[$hash] + self::$_limit >= $now)) { + $now = time(); + $tl = (int) self::$_store->getValue('traffic_limiter', $hash); + self::$_store->purgeValues('traffic_limiter', $now - self::$_limit); + if ($tl > 0 && ($tl + self::$_limit >= $now)) { $result = false; } else { - $tl[$hash] = time(); - $result = true; + $tl = time(); + $result = true; + } + if (!self::$_store->setValue((string) $tl, 'traffic_limiter', $hash)) { + error_log('failed to store the traffic limiter, it probably contains outdated information'); } - self::_store( - $file, - '_params = Json::decode( + // it might be a creation or a deletion, the latter is detected below + $this->_operation = 'create'; + $this->_params = Json::decode( file_get_contents(self::$_inputStream) ); break; @@ -125,15 +127,10 @@ class Request } // prepare operation, depending on current parameters - if ( - array_key_exists('ct', $this->_params) && - !empty($this->_params['ct']) - ) { - $this->_operation = 'create'; - } elseif (array_key_exists('pasteid', $this->_params) && !empty($this->_params['pasteid'])) { + if (array_key_exists('pasteid', $this->_params) && !empty($this->_params['pasteid'])) { if (array_key_exists('deletetoken', $this->_params) && !empty($this->_params['deletetoken'])) { $this->_operation = 'delete'; - } else { + } elseif ($this->_operation != 'create') { $this->_operation = 'read'; } } elseif (array_key_exists('jsonld', $this->_params) && !empty($this->_params['jsonld'])) { @@ -172,7 +169,7 @@ class Request $data['meta'] = $meta; } foreach ($required_keys as $key) { - $data[$key] = $this->getParam($key); + $data[$key] = $this->getParam($key, $key == 'v' ? 1 : ''); } // forcing a cast to int or float $data['v'] = $data['v'] + 0; diff --git a/tst/Bootstrap.php b/tst/Bootstrap.php index b5393510..70aafddd 100644 --- a/tst/Bootstrap.php +++ b/tst/Bootstrap.php @@ -1,5 +1,11 @@ _config = $config; + $this->_connection = new ConnectionInterfaceStub(); + } + + public function bucket($name, $userProject = false) + { + if (!key_exists($name, $this->_buckets)) { + $b = new BucketStub($this->_connection, $name, array(), $this); + $this->_buckets[$name] = $b; + } + return $this->_buckets[$name]; + } + + /** + * @throws \Google\Cloud\Core\Exception\NotFoundException + */ + public function deleteBucket($name) + { + if (key_exists($name, $this->_buckets)) { + unset($this->_buckets[$name]); + } else { + throw new NotFoundException(); + } + } + + public function buckets(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function registerStreamWrapper($protocol = null) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function unregisterStreamWrapper($protocol = null) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function signedUrlUploader($uri, $data, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function timestamp(\DateTimeInterface $timestamp, $nanoSeconds = null) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getServiceAccount(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function hmacKeys(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function hmacKey($accessId, $projectId = null, array $metadata = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function createHmacKey($serviceAccountEmail, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function createBucket($name, array $options = array()) + { + if (key_exists($name, $this->_buckets)) { + throw new BadRequestException('already exists'); + } + $b = new BucketStub($this->_connection, $name, array(), $this); + $this->_buckets[$name] = $b; + return $b; + } +} + +/** + * Class BucketStub stubs a GCS bucket. + */ +class BucketStub extends Bucket +{ + public $_objects; + private $_name; + private $_info; + private $_connection; + private $_client; + + public function __construct(ConnectionInterface $connection, $name, array $info = array(), $client = null) + { + $this->_name = $name; + $this->_info = $info; + $this->_connection = $connection; + $this->_objects = array(); + $this->_client = $client; + } + + public function acl() + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function defaultAcl() + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function exists() + { + return true; + } + + public function upload($data, array $options = array()) + { + if (!is_string($data) || !key_exists('name', $options)) { + throw new BadMethodCallException('not supported by this stub'); + } + + $name = $options['name']; + $generation = '1'; + $o = new StorageObjectStub($this->_connection, $name, $this, $generation, $options); + $this->_objects[$options['name']] = $o; + $o->setData($data); + } + + public function uploadAsync($data, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getResumableUploader($data, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getStreamableUploader($data, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function object($name, array $options = array()) + { + if (key_exists($name, $this->_objects)) { + return $this->_objects[$name]; + } else { + return new StorageObjectStub($this->_connection, $name, $this, null, $options); + } + } + + public function objects(array $options = array()) + { + $prefix = key_exists('prefix', $options) ? $options['prefix'] : ''; + + return new CallbackFilterIterator( + new ArrayIterator($this->_objects), + function ($current, $key, $iterator) use ($prefix) { + return substr($key, 0, strlen($prefix)) == $prefix; + } + ); + } + + public function createNotification($topic, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function notification($id) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function notifications(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function delete(array $options = array()) + { + $this->_client->deleteBucket($this->_name); + } + + public function update(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function compose(array $sourceObjects, $name, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function info(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function reload(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function name() + { + return $this->_name; + } + + public static function lifecycle(array $lifecycle = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function currentLifecycle(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function isWritable($file = null) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function iam() + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function lockRetentionPolicy(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function signedUrl($expires, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function generateSignedPostPolicyV4($objectName, $expires, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } +} + +/** + * Class StorageObjectStub stubs a GCS storage object. + */ +class StorageObjectStub extends StorageObject +{ + private $_name; + private $_data; + private $_info; + private $_bucket; + private $_generation; + private $_exists = false; + private $_connection; + + public function __construct(ConnectionInterface $connection, $name, $bucket, $generation = null, array $info = array(), $encryptionKey = null, $encryptionKeySHA256 = null) + { + $this->_name = $name; + $this->_bucket = $bucket; + $this->_generation = $generation; + $this->_info = $info; + $this->_connection = $connection; + $timeCreated = new Datetime(); + $this->_info['metadata']['timeCreated'] = $timeCreated->format('Y-m-d\TH:i:s.u\Z'); + } + + public function acl() + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function exists(array $options = array()) + { + return key_exists($this->_name, $this->_bucket->_objects); + } + + /** + * @throws NotFoundException + */ + public function delete(array $options = array()) + { + if (key_exists($this->_name, $this->_bucket->_objects)) { + unset($this->_bucket->_objects[$this->_name]); + } else { + throw new NotFoundException('key ' . $this->_name . ' not found.'); + } + } + + /** + * @throws NotFoundException + */ + public function update(array $metadata, array $options = array()) + { + if (!$this->_exists) { + throw new NotFoundException('key ' . $this->_name . ' not found.'); + } + $this->_info = $metadata; + } + + public function copy($destination, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function rewrite($destination, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function rename($name, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + /** + * @throws NotFoundException + */ + public function downloadAsString(array $options = array()) + { + if (!$this->_exists) { + throw new NotFoundException('key ' . $this->_name . ' not found.'); + } + return $this->_data; + } + + public function downloadToFile($path, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function downloadAsStream(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function downloadAsStreamAsync(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function signedUrl($expires, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function signedUploadUrl($expires, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function beginSignedUploadSession(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function info(array $options = array()) + { + return key_exists('metadata',$this->_info) ? $this->_info['metadata'] : array(); + } + + public function reload(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function name() + { + return $this->_name; + } + + public function identity() + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function gcsUri() + { + return sprintf( + 'gs://%s/%s', + $this->_bucket->name(), + $this->_name + ); + } + + public function setData($data) + { + $this->_data = $data; + $this->_exists = true; + } +} + +/** + * Class ConnectionInterfaceStub required for the stubs. + */ +class ConnectionInterfaceStub implements ConnectionInterface +{ + public function deleteAcl(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getAcl(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function listAcl(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function insertAcl(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function patchAcl(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function deleteBucket(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getBucket(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function listBuckets(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function insertBucket(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getBucketIamPolicy(array $args) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function setBucketIamPolicy(array $args) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function testBucketIamPermissions(array $args) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function patchBucket(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function deleteObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function copyObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function rewriteObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function composeObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function listObjects(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function patchObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function downloadObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function insertObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getNotification(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function deleteNotification(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function insertNotification(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function listNotifications(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getServiceAccount(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function lockRetentionPolicy(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function createHmacKey(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function deleteHmacKey(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getHmacKey(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function updateHmacKey(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function listHmacKeys(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } +} + +/** + * Class Helper provides unit tests pastes and comments of various formats + */ class Helper { /** @@ -155,7 +741,11 @@ class Helper public static function getPastePost($version = 2, array $meta = array()) { $example = self::getPaste($version, $meta); - $example['meta'] = array('expire' => $example['meta']['expire']); + if ($version == 2) { + $example['meta'] = array('expire' => $example['meta']['expire']); + } else { + unset($example['meta']['postdate']); + } return $example; } diff --git a/tst/ConfigurationTest.php b/tst/ConfigurationTest.php index 246618cc..312b7997 100644 --- a/tst/ConfigurationTest.php +++ b/tst/ConfigurationTest.php @@ -17,8 +17,6 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase $this->_minimalConfig = '[main]' . PHP_EOL . '[model]' . PHP_EOL . '[model_options]'; $this->_options = Configuration::getDefaults(); $this->_options['model_options']['dir'] = PATH . $this->_options['model_options']['dir']; - $this->_options['traffic']['dir'] = PATH . $this->_options['traffic']['dir']; - $this->_options['purge']['dir'] = PATH . $this->_options['purge']['dir']; $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_cfg'; if (!is_dir($this->_path)) { mkdir($this->_path); diff --git a/tst/ConfigurationTestGenerator.php b/tst/ConfigurationTestGenerator.php index 284fa5f2..945fc479 100755 --- a/tst/ConfigurationTestGenerator.php +++ b/tst/ConfigurationTestGenerator.php @@ -428,8 +428,6 @@ class ConfigurationCombinationsTest extends PHPUnit_Framework_TestCase Helper::confBackup(); $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; $this->_model = Filesystem::getInstance(array('dir' => $this->_path)); - ServerSalt::setPath($this->_path); - TrafficLimiter::setPath($this->_path); $this->reset(); } @@ -449,8 +447,6 @@ class ConfigurationCombinationsTest extends PHPUnit_Framework_TestCase if ($this->_model->exists(Helper::getPasteId())) $this->_model->delete(Helper::getPasteId()); $configuration['model_options']['dir'] = $this->_path; - $configuration['traffic']['dir'] = $this->_path; - $configuration['purge']['dir'] = $this->_path; Helper::createIniFile(CONF, $configuration); } diff --git a/tst/ControllerTest.php b/tst/ControllerTest.php index b00f2ce6..5c951273 100644 --- a/tst/ControllerTest.php +++ b/tst/ControllerTest.php @@ -17,6 +17,8 @@ class ControllerTest extends PHPUnit_Framework_TestCase /* Setup Routine */ $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; $this->_data = Filesystem::getInstance(array('dir' => $this->_path)); + ServerSalt::setStore($this->_data); + TrafficLimiter::setStore($this->_data); $this->reset(); } @@ -37,11 +39,8 @@ class ControllerTest extends PHPUnit_Framework_TestCase $this->_data->delete(Helper::getPasteId()); } $options = parse_ini_file(CONF_SAMPLE, true); - $options['purge']['dir'] = $this->_path; - $options['traffic']['dir'] = $this->_path; $options['model_options']['dir'] = $this->_path; Helper::createIniFile(CONF, $options); - ServerSalt::setPath($this->_path); } /** @@ -49,6 +48,8 @@ class ControllerTest extends PHPUnit_Framework_TestCase */ public function testView() { + $_SERVER['QUERY_STRING'] = Helper::getPasteId(); + $_GET[Helper::getPasteId()] = ''; ob_start(); new Controller; $content = ob_get_contents(); @@ -127,28 +128,6 @@ class ControllerTest extends PHPUnit_Framework_TestCase ); } - /** - * @runInSeparateProcess - */ - public function testHtaccess() - { - $htaccess = $this->_path . DIRECTORY_SEPARATOR . '.htaccess'; - @unlink($htaccess); - - $paste = Helper::getPasteJson(); - $file = tempnam(sys_get_temp_dir(), 'FOO'); - file_put_contents($file, $paste); - Request::setInputStream($file); - $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_SERVER['REMOTE_ADDR'] = '::1'; - ob_start(); - new Controller; - ob_end_clean(); - - $this->assertFileExists($htaccess, 'htaccess recreated'); - } - /** * @expectedException Exception * @expectedExceptionCode 2 @@ -493,6 +472,29 @@ class ControllerTest extends PHPUnit_Framework_TestCase $this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste exists after posting data'); } + /** + * @runInSeparateProcess + */ + public function testCreateInvalidFormat() + { + $options = parse_ini_file(CONF, true); + $options['traffic']['limit'] = 0; + Helper::createIniFile(CONF, $options); + $file = tempnam(sys_get_temp_dir(), 'FOO'); + file_put_contents($file, Helper::getPasteJson(1)); + Request::setInputStream($file); + $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_SERVER['REMOTE_ADDR'] = '::1'; + ob_start(); + new Controller; + $content = ob_get_contents(); + ob_end_clean(); + $response = json_decode($content, true); + $this->assertEquals(1, $response['status'], 'outputs error status'); + $this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste exists after posting data'); + } + /** * @runInSeparateProcess */ @@ -541,7 +543,7 @@ class ControllerTest extends PHPUnit_Framework_TestCase ob_end_clean(); $response = json_decode($content, true); $this->assertEquals(1, $response['status'], 'outputs error status'); - $this->assertFalse($this->_data->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'paste exists after posting data'); + $this->assertFalse($this->_data->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists after posting data'); } /** diff --git a/tst/ControllerWithDbTest.php b/tst/ControllerWithDbTest.php index 90dee920..bc8cd7be 100644 --- a/tst/ControllerWithDbTest.php +++ b/tst/ControllerWithDbTest.php @@ -1,6 +1,8 @@ _options['dsn'] = 'sqlite:' . $this->_path . DIRECTORY_SEPARATOR . 'tst.sq3'; $this->_data = Database::getInstance($this->_options); + ServerSalt::setStore($this->_data); + TrafficLimiter::setStore($this->_data); $this->reset(); } diff --git a/tst/ControllerWithGcsTest.php b/tst/ControllerWithGcsTest.php new file mode 100644 index 00000000..39833417 --- /dev/null +++ b/tst/ControllerWithGcsTest.php @@ -0,0 +1,59 @@ +false)); + $handler = HttpHandlerFactory::build($httpClient); + + $name = 'pb-'; + $alphabet = 'abcdefghijklmnopqrstuvwxyz'; + for ($i = 0; $i < 29; ++$i) { + $name .= $alphabet[rand(0, strlen($alphabet) - 1)]; + } + self::$_client = new StorageClientStub(array()); + self::$_bucket = self::$_client->createBucket($name); + } + + public function setUp() + { + /* Setup Routine */ + $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; + if (!is_dir($this->_path)) { + mkdir($this->_path); + } + $this->_options = array( + 'bucket' => self::$_bucket->name(), + 'prefix' => 'pastes', + ); + $this->_data = GoogleCloudStorage::getInstance($this->_options); + ServerSalt::setStore($this->_data); + TrafficLimiter::setStore($this->_data); + $this->reset(); + } + + public function reset() + { + parent::reset(); + // but then inject a db config + $options = parse_ini_file(CONF, true); + $options['model'] = array( + 'class' => 'GoogleCloudStorage', + ); + $options['model_options'] = $this->_options; + Helper::createIniFile(CONF, $options); + } +} diff --git a/tst/Data/DatabaseTest.php b/tst/Data/DatabaseTest.php index 0d48eb6a..1cfc0bea 100644 --- a/tst/Data/DatabaseTest.php +++ b/tst/Data/DatabaseTest.php @@ -2,6 +2,8 @@ use PrivateBin\Controller; use PrivateBin\Data\Database; +use PrivateBin\Data\Filesystem; +use PrivateBin\Persistence\ServerSalt; class DatabaseTest extends PHPUnit_Framework_TestCase { @@ -31,6 +33,19 @@ class DatabaseTest extends PHPUnit_Framework_TestCase } } + public function testSaltMigration() + { + ServerSalt::setStore(Filesystem::getInstance(array('dir' => 'data'))); + $salt = ServerSalt::get(); + $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php'; + $this->assertFileExists($file, 'ServerSalt got initialized and stored on disk'); + $this->assertNotEquals($salt, ''); + ServerSalt::setStore($this->_model); + ServerSalt::get(); + $this->assertFileNotExists($file, 'legacy ServerSalt got removed'); + $this->assertEquals($salt, ServerSalt::get(), 'ServerSalt got preserved & migrated'); + } + public function testDatabaseBasedDataStoreWorks() { $this->_model->delete(Helper::getPasteId()); @@ -287,6 +302,48 @@ class DatabaseTest extends PHPUnit_Framework_TestCase Helper::rmDir($this->_path); } + public function testCorruptMeta() + { + mkdir($this->_path); + $path = $this->_path . DIRECTORY_SEPARATOR . 'meta-test.sq3'; + if (is_file($path)) { + unlink($path); + } + $this->_options['dsn'] = 'sqlite:' . $path; + $this->_options['tbl'] = 'baz_'; + $model = Database::getInstance($this->_options); + $paste = Helper::getPaste(1, array('expire_date' => 1344803344)); + unset($paste['meta']['formatter'], $paste['meta']['opendiscussion'], $paste['meta']['salt']); + $model->delete(Helper::getPasteId()); + + $db = new PDO( + $this->_options['dsn'], + $this->_options['usr'], + $this->_options['pwd'], + $this->_options['opt'] + ); + $statement = $db->prepare('INSERT INTO baz_paste VALUES(?,?,?,?,?,?,?,?,?)'); + $statement->execute( + array( + Helper::getPasteId(), + $paste['data'], + $paste['meta']['postdate'], + $paste['meta']['expire_date'], + 0, + 0, + '{', + null, + null, + ) + ); + $statement->closeCursor(); + + $this->assertTrue($model->exists(Helper::getPasteId()), 'paste exists after storing it'); + $this->assertEquals($paste, $model->read(Helper::getPasteId())); + + Helper::rmDir($this->_path); + } + public function testTableUpgrade() { mkdir($this->_path); diff --git a/tst/Data/FilesystemTest.php b/tst/Data/FilesystemTest.php index 37e03f36..684c2940 100644 --- a/tst/Data/FilesystemTest.php +++ b/tst/Data/FilesystemTest.php @@ -117,6 +117,7 @@ class FilesystemTest extends PHPUnit_Framework_TestCase $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist'); $this->assertFalse($this->_model->create(Helper::getPasteId(), $paste), 'unable to store broken paste'); $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does still not exist'); + $this->assertFalse($this->_model->setValue('foo', 'non existing namespace'), 'rejects setting value in non existing namespace'); } public function testCommentErrorDetection() diff --git a/tst/Data/GoogleCloudStorageTest.php b/tst/Data/GoogleCloudStorageTest.php index 6905f04b..3b101c40 100644 --- a/tst/Data/GoogleCloudStorageTest.php +++ b/tst/Data/GoogleCloudStorageTest.php @@ -1,12 +1,6 @@ _model = GoogleCloudStorage::getInstance(array( 'bucket' => self::$_bucket->name(), 'prefix' => 'pastes', - 'client' => self::$_client, )); + )); } public function tearDown() @@ -46,7 +37,6 @@ class GoogleCloudStorageTest extends PHPUnit_Framework_TestCase foreach (self::$_bucket->objects() as $object) { $object->delete(); } - error_reporting(E_ALL); } public static function tearDownAfterClass() @@ -138,579 +128,55 @@ class GoogleCloudStorageTest extends PHPUnit_Framework_TestCase $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'unable to store broken comment'); $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does still not exist'); } -} -/** - * Class StorageClientStub provides a limited stub for performing the unit test - */ -class StorageClientStub extends StorageClient -{ - private $_config = null; - private $_connection = null; - private $_buckets = array(); - - public function __construct(array $config = array()) + /** + * @throws Exception + */ + public function testKeyValueStore() { - $this->_config = $config; - $this->_connection = new ConnectionInterfaceStub(); - } + $salt = bin2hex(random_bytes(256)); + $this->_model->setValue($salt, 'salt', ''); + $storedSalt = $this->_model->getValue('salt', ''); + $this->assertEquals($salt, $storedSalt); + $this->_model->purgeValues('salt', time() + 60); + $this->assertEquals('', $this->_model->getValue('salt', 'master')); - public function bucket($name, $userProject = false) - { - if (!key_exists($name, $this->_buckets)) { - $b = new BucketStub($this->_connection, $name, array(), $this); - $this->_buckets[$name] = $b; - } - return $this->_buckets[$name]; + $client = hash_hmac('sha512', '127.0.0.1', $salt); + $expire = time(); + $this->_model->setValue(strval($expire), 'traffic_limiter', $client); + $storedExpired = $this->_model->getValue('traffic_limiter', $client); + $this->assertEquals(strval($expire), $storedExpired); + + $this->_model->purgeValues('traffic_limiter', time() - 60); + $this->assertEquals($storedExpired, $this->_model->getValue('traffic_limiter', $client)); + $this->_model->purgeValues('traffic_limiter', time() + 60); + $this->assertEquals('', $this->_model->getValue('traffic_limiter', $client)); + + $purgeAt = $expire + (15 * 60); + $this->_model->setValue(strval($purgeAt), 'purge_limiter', ''); + $storedPurgedAt = $this->_model->getValue('purge_limiter', ''); + $this->assertEquals(strval($purgeAt), $storedPurgedAt); + $this->_model->purgeValues('purge_limiter', $purgeAt + 60); + $this->assertEquals('', $this->_model->getValue('purge_limiter', '')); + $this->assertEquals('', $this->_model->getValue('purge_limiter', 'at')); } /** - * @throws \Google\Cloud\Core\Exception\NotFoundException + * @throws Exception */ - public function deleteBucket($name) + public function testKeyValuePurgeTrafficLimiter() { - if (key_exists($name, $this->_buckets)) { - unset($this->_buckets[$name]); - } else { - throw new NotFoundException(); - } - } + $salt = bin2hex(random_bytes(256)); + $client = hash_hmac('sha512', '127.0.0.1', $salt); + $expire = time(); + $this->_model->setValue(strval($expire), 'traffic_limiter', $client); + $storedExpired = $this->_model->getValue('traffic_limiter', $client); + $this->assertEquals(strval($expire), $storedExpired); - public function buckets(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } + $this->_model->purgeValues('traffic_limiter', time() - 60); + $this->assertEquals($storedExpired, $this->_model->getValue('traffic_limiter', $client)); - public function registerStreamWrapper($protocol = null) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function unregisterStreamWrapper($protocol = null) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function signedUrlUploader($uri, $data, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function timestamp(\DateTimeInterface $timestamp, $nanoSeconds = null) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function getServiceAccount(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function hmacKeys(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function hmacKey($accessId, $projectId = null, array $metadata = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function createHmacKey($serviceAccountEmail, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function createBucket($name, array $options = array()) - { - if (key_exists($name, $this->_buckets)) { - throw new BadRequestException('already exists'); - } - $b = new BucketStub($this->_connection, $name, array(), $this); - $this->_buckets[$name] = $b; - return $b; - } -} - -/** - * Class BucketStub stubs a GCS bucket. - */ -class BucketStub extends Bucket -{ - public $_objects; - private $_name; - private $_info; - private $_connection; - private $_client; - - public function __construct(ConnectionInterface $connection, $name, array $info = array(), $client = null) - { - $this->_name = $name; - $this->_info = $info; - $this->_connection = $connection; - $this->_objects = array(); - $this->_client = $client; - } - - public function acl() - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function defaultAcl() - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function exists() - { - return true; - } - - public function upload($data, array $options = array()) - { - if (!is_string($data) || !key_exists('name', $options)) { - throw new BadMethodCallException('not supported by this stub'); - } - - $name = $options['name']; - $generation = '1'; - $o = new StorageObjectStub($this->_connection, $name, $this, $generation, $options); - $this->_objects[$options['name']] = $o; - $o->setData($data); - } - - public function uploadAsync($data, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function getResumableUploader($data, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function getStreamableUploader($data, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function object($name, array $options = array()) - { - if (key_exists($name, $this->_objects)) { - return $this->_objects[$name]; - } else { - return new StorageObjectStub($this->_connection, $name, $this, null, $options); - } - } - - public function objects(array $options = array()) - { - $prefix = key_exists('prefix', $options) ? $options['prefix'] : ''; - - return new CallbackFilterIterator( - new ArrayIterator($this->_objects), - function ($current, $key, $iterator) use ($prefix) { - return substr($key, 0, strlen($prefix)) == $prefix; - } - ); - } - - public function createNotification($topic, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function notification($id) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function notifications(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function delete(array $options = array()) - { - $this->_client->deleteBucket($this->_name); - } - - public function update(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function compose(array $sourceObjects, $name, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function info(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function reload(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function name() - { - return $this->_name; - } - - public static function lifecycle(array $lifecycle = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function currentLifecycle(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function isWritable($file = null) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function iam() - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function lockRetentionPolicy(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function signedUrl($expires, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function generateSignedPostPolicyV4($objectName, $expires, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } -} - -/** - * Class StorageObjectStub stubs a GCS storage object. - */ -class StorageObjectStub extends StorageObject -{ - private $_name; - private $_data; - private $_info; - private $_bucket; - private $_generation; - private $_exists = false; - private $_connection; - - public function __construct(ConnectionInterface $connection, $name, $bucket, $generation = null, array $info = array(), $encryptionKey = null, $encryptionKeySHA256 = null) - { - $this->_name = $name; - $this->_bucket = $bucket; - $this->_generation = $generation; - $this->_info = $info; - $this->_connection = $connection; - } - - public function acl() - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function exists(array $options = array()) - { - return key_exists($this->_name, $this->_bucket->_objects); - } - - /** - * @throws NotFoundException - */ - public function delete(array $options = array()) - { - if (key_exists($this->_name, $this->_bucket->_objects)) { - unset($this->_bucket->_objects[$this->_name]); - } else { - throw new NotFoundException('key ' . $this->_name . ' not found.'); - } - } - - /** - * @throws NotFoundException - */ - public function update(array $metadata, array $options = array()) - { - if (!$this->_exists) { - throw new NotFoundException('key ' . $this->_name . ' not found.'); - } - $this->_info = $metadata; - } - - public function copy($destination, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function rewrite($destination, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function rename($name, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - /** - * @throws NotFoundException - */ - public function downloadAsString(array $options = array()) - { - if (!$this->_exists) { - throw new NotFoundException('key ' . $this->_name . ' not found.'); - } - return $this->_data; - } - - public function downloadToFile($path, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function downloadAsStream(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function downloadAsStreamAsync(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function signedUrl($expires, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function signedUploadUrl($expires, array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function beginSignedUploadSession(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function info(array $options = array()) - { - return key_exists('metadata',$this->_info) ? $this->_info['metadata'] : array(); - } - - public function reload(array $options = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function name() - { - return $this->_name; - } - - public function identity() - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function gcsUri() - { - return sprintf( - 'gs://%s/%s', - $this->_bucket->name(), - $this->_name - ); - } - - public function setData($data) - { - $this->_data = $data; - $this->_exists = true; - } -} - -/** - * Class ConnectionInterfaceStub required for the stubs. - */ -class ConnectionInterfaceStub implements ConnectionInterface -{ - public function deleteAcl(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function getAcl(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function listAcl(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function insertAcl(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function patchAcl(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function deleteBucket(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function getBucket(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function listBuckets(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function insertBucket(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function getBucketIamPolicy(array $args) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function setBucketIamPolicy(array $args) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function testBucketIamPermissions(array $args) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function patchBucket(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function deleteObject(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function copyObject(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function rewriteObject(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function composeObject(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function getObject(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function listObjects(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function patchObject(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function downloadObject(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function insertObject(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function getNotification(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function deleteNotification(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function insertNotification(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function listNotifications(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function getServiceAccount(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function lockRetentionPolicy(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function createHmacKey(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function deleteHmacKey(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function getHmacKey(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function updateHmacKey(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); - } - - public function listHmacKeys(array $args = array()) - { - throw new BadMethodCallException('not supported by this stub'); + $this->_model->purgeValues('traffic_limiter', time() + 60); + $this->assertEquals('', $this->_model->getValue('traffic_limiter', $client)); } } diff --git a/tst/JsonApiTest.php b/tst/JsonApiTest.php index 9655e609..6d9732a5 100644 --- a/tst/JsonApiTest.php +++ b/tst/JsonApiTest.php @@ -16,7 +16,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase /* Setup Routine */ $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; $this->_model = Filesystem::getInstance(array('dir' => $this->_path)); - ServerSalt::setPath($this->_path); + ServerSalt::setStore($this->_model); $_POST = array(); $_GET = array(); @@ -25,8 +25,6 @@ class JsonApiTest extends PHPUnit_Framework_TestCase $this->_model->delete(Helper::getPasteId()); } $options = parse_ini_file(CONF_SAMPLE, true); - $options['purge']['dir'] = $this->_path; - $options['traffic']['dir'] = $this->_path; $options['model_options']['dir'] = $this->_path; Helper::confBackup(); Helper::createIniFile(CONF, $options); diff --git a/tst/ModelTest.php b/tst/ModelTest.php index d5c4074f..a88e0296 100644 --- a/tst/ModelTest.php +++ b/tst/ModelTest.php @@ -25,7 +25,6 @@ class ModelTest extends PHPUnit_Framework_TestCase if (!is_dir($this->_path)) { mkdir($this->_path); } - ServerSalt::setPath($this->_path); $options = parse_ini_file(CONF_SAMPLE, true); $options['purge']['limit'] = 0; $options['model'] = array( @@ -39,6 +38,7 @@ class ModelTest extends PHPUnit_Framework_TestCase ); Helper::confBackup(); Helper::createIniFile(CONF, $options); + ServerSalt::setStore(Database::getInstance($options['model_options'])); $this->_conf = new Configuration; $this->_model = new Model($this->_conf); $_SERVER['REMOTE_ADDR'] = '::1'; @@ -102,6 +102,58 @@ class ModelTest extends PHPUnit_Framework_TestCase $this->assertEquals(array(), $paste->getComments(), 'comment was deleted with paste'); } + public function testPasteV1() + { + $pasteData = Helper::getPaste(1); + unset($pasteData['meta']['formatter']); + + $path = $this->_path . DIRECTORY_SEPARATOR . 'v1-test.sq3'; + if (is_file($path)) { + unlink($path); + } + $options = parse_ini_file(CONF_SAMPLE, true); + $options['purge']['limit'] = 0; + $options['model'] = array( + 'class' => 'Database', + ); + $options['model_options'] = array( + 'dsn' => 'sqlite:' . $path, + 'usr' => null, + 'pwd' => null, + 'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION), + ); + Helper::createIniFile(CONF, $options); + $model = new Model(new Configuration); + $model->getPaste('0000000000000000')->exists(); // triggers database table creation + $model->getPaste(Helper::getPasteId())->delete(); // deletes the cache + + $db = new PDO( + $options['model_options']['dsn'], + $options['model_options']['usr'], + $options['model_options']['pwd'], + $options['model_options']['opt'] + ); + $statement = $db->prepare('INSERT INTO paste VALUES(?,?,?,?,?,?,?,?,?)'); + $statement->execute( + array( + Helper::getPasteId(), + $pasteData['data'], + $pasteData['meta']['postdate'], + 0, + 0, + 0, + json_encode($pasteData['meta']), + null, + null, + ) + ); + $statement->closeCursor(); + + $paste = $model->getPaste(Helper::getPasteId()); + $this->assertNotEmpty($paste->getDeleteToken(), 'excercise the condition to load the data from storage'); + $this->assertEquals('plaintext', $paste->get()['meta']['formatter'], 'paste got created with default formatter'); + } + public function testCommentDefaults() { $comment = new Comment( @@ -133,6 +185,97 @@ class ModelTest extends PHPUnit_Framework_TestCase $paste->store(); } + /** + * @expectedException Exception + * @expectedExceptionCode 76 + */ + public function testStoreFail() + { + $path = $this->_path . DIRECTORY_SEPARATOR . 'model-store-test.sq3'; + if (is_file($path)) { + unlink($path); + } + $options = parse_ini_file(CONF_SAMPLE, true); + $options['purge']['limit'] = 0; + $options['model'] = array( + 'class' => 'Database', + ); + $options['model_options'] = array( + 'dsn' => 'sqlite:' . $path, + 'usr' => null, + 'pwd' => null, + 'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION), + ); + Helper::createIniFile(CONF, $options); + $model = new Model(new Configuration); + + $pasteData = Helper::getPastePost(); + $model->getPaste(Helper::getPasteId())->delete(); + $model->getPaste(Helper::getPasteId())->exists(); + + $db = new PDO( + $options['model_options']['dsn'], + $options['model_options']['usr'], + $options['model_options']['pwd'], + $options['model_options']['opt'] + ); + $statement = $db->prepare('DROP TABLE paste'); + $statement->execute(); + $statement->closeCursor(); + + $paste = $model->getPaste(); + $paste->setData($pasteData); + $paste->store(); + } + + /** + * @expectedException Exception + * @expectedExceptionCode 70 + */ + public function testCommentStoreFail() + { + $path = $this->_path . DIRECTORY_SEPARATOR . 'model-test.sq3'; + if (is_file($path)) { + unlink($path); + } + $options = parse_ini_file(CONF_SAMPLE, true); + $options['purge']['limit'] = 0; + $options['model'] = array( + 'class' => 'Database', + ); + $options['model_options'] = array( + 'dsn' => 'sqlite:' . $path, + 'usr' => null, + 'pwd' => null, + 'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION), + ); + Helper::createIniFile(CONF, $options); + $model = new Model(new Configuration); + + $pasteData = Helper::getPastePost(); + $commentData = Helper::getCommentPost(); + $model->getPaste(Helper::getPasteId())->delete(); + + $paste = $model->getPaste(); + $paste->setData($pasteData); + $paste->store(); + $paste->exists(); + + $db = new PDO( + $options['model_options']['dsn'], + $options['model_options']['usr'], + $options['model_options']['pwd'], + $options['model_options']['opt'] + ); + $statement = $db->prepare('DROP TABLE comment'); + $statement->execute(); + $statement->closeCursor(); + + $comment = $paste->getComment(Helper::getPasteId()); + $comment->setData($commentData); + $comment->store(); + } + /** * @expectedException Exception * @expectedExceptionCode 69 @@ -195,6 +338,18 @@ class ModelTest extends PHPUnit_Framework_TestCase $paste->get(); } + /** + * @expectedException Exception + * @expectedExceptionCode 75 + */ + public function testInvalidPasteFormat() + { + $pasteData = Helper::getPastePost(); + $pasteData['adata'][1] = 'format does not exist'; + $paste = $this->_model->getPaste(); + $paste->setData($pasteData); + } + /** * @expectedException Exception * @expectedExceptionCode 60 diff --git a/tst/Persistence/PurgeLimiterTest.php b/tst/Persistence/PurgeLimiterTest.php index 391a840b..adb96ffd 100644 --- a/tst/Persistence/PurgeLimiterTest.php +++ b/tst/Persistence/PurgeLimiterTest.php @@ -1,5 +1,6 @@ _path)) { mkdir($this->_path); } - PurgeLimiter::setPath($this->_path); + PurgeLimiter::setStore( + Filesystem::getInstance(array('dir' => $this->_path)) + ); } public function tearDown() diff --git a/tst/Persistence/ServerSaltTest.php b/tst/Persistence/ServerSaltTest.php index ecdc0f83..3db5f7d7 100644 --- a/tst/Persistence/ServerSaltTest.php +++ b/tst/Persistence/ServerSaltTest.php @@ -1,5 +1,6 @@ _path)) { mkdir($this->_path); } - ServerSalt::setPath($this->_path); + ServerSalt::setStore( + Filesystem::getInstance(array('dir' => $this->_path)) + ); $this->_otherPath = $this->_path . DIRECTORY_SEPARATOR . 'foo'; @@ -40,46 +43,46 @@ class ServerSaltTest extends PHPUnit_Framework_TestCase public function testGeneration() { // generating new salt - ServerSalt::setPath($this->_path); + ServerSalt::setStore( + Filesystem::getInstance(array('dir' => $this->_path)) + ); $salt = ServerSalt::get(); // try setting a different path and resetting it - ServerSalt::setPath($this->_otherPath); + ServerSalt::setStore( + Filesystem::getInstance(array('dir' => $this->_otherPath)) + ); $this->assertNotEquals($salt, ServerSalt::get()); - ServerSalt::setPath($this->_path); + ServerSalt::setStore( + Filesystem::getInstance(array('dir' => $this->_path)) + ); $this->assertEquals($salt, ServerSalt::get()); } - /** - * @expectedException Exception - * @expectedExceptionCode 11 - */ public function testPathShenanigans() { // try setting an invalid path chmod($this->_invalidPath, 0000); - ServerSalt::setPath($this->_invalidPath); - ServerSalt::get(); + $store = Filesystem::getInstance(array('dir' => $this->_invalidPath)); + ServerSalt::setStore($store); + $salt = ServerSalt::get(); + ServerSalt::setStore($store); + $this->assertNotEquals($salt, ServerSalt::get()); } - /** - * @expectedException Exception - * @expectedExceptionCode 20 - */ public function testFileRead() { // try setting an invalid file chmod($this->_invalidPath, 0700); file_put_contents($this->_invalidFile, ''); chmod($this->_invalidFile, 0000); - ServerSalt::setPath($this->_invalidPath); - ServerSalt::get(); + $store = Filesystem::getInstance(array('dir' => $this->_invalidPath)); + ServerSalt::setStore($store); + $salt = ServerSalt::get(); + ServerSalt::setStore($store); + $this->assertNotEquals($salt, ServerSalt::get()); } - /** - * @expectedException Exception - * @expectedExceptionCode 13 - */ public function testFileWrite() { // try setting an invalid file @@ -90,19 +93,24 @@ class ServerSaltTest extends PHPUnit_Framework_TestCase } file_put_contents($this->_invalidPath . DIRECTORY_SEPARATOR . '.htaccess', ''); chmod($this->_invalidPath, 0500); - ServerSalt::setPath($this->_invalidPath); - ServerSalt::get(); + $store = Filesystem::getInstance(array('dir' => $this->_invalidPath)); + ServerSalt::setStore($store); + $salt = ServerSalt::get(); + ServerSalt::setStore($store); + $this->assertNotEquals($salt, ServerSalt::get()); } - /** - * @expectedException Exception - * @expectedExceptionCode 10 - */ public function testPermissionShenanigans() { // try creating an invalid path chmod($this->_invalidPath, 0000); - ServerSalt::setPath($this->_invalidPath . DIRECTORY_SEPARATOR . 'baz'); - ServerSalt::get(); + ServerSalt::setStore( + Filesystem::getInstance(array('dir' => $this->_invalidPath . DIRECTORY_SEPARATOR . 'baz')) + ); + $store = Filesystem::getInstance(array('dir' => $this->_invalidPath)); + ServerSalt::setStore($store); + $salt = ServerSalt::get(); + ServerSalt::setStore($store); + $this->assertNotEquals($salt, ServerSalt::get()); } } diff --git a/tst/Persistence/TrafficLimiterTest.php b/tst/Persistence/TrafficLimiterTest.php index 41013016..aedbf889 100644 --- a/tst/Persistence/TrafficLimiterTest.php +++ b/tst/Persistence/TrafficLimiterTest.php @@ -1,5 +1,7 @@ _path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'trafficlimit'; - TrafficLimiter::setPath($this->_path); + $store = Filesystem::getInstance(array('dir' => $this->_path)); + ServerSalt::setStore($store); + TrafficLimiter::setStore($store); } public function tearDown() @@ -19,11 +23,17 @@ class TrafficLimiterTest extends PHPUnit_Framework_TestCase Helper::rmDir($this->_path . DIRECTORY_SEPARATOR); } + public function testHtaccess() + { + $htaccess = $this->_path . DIRECTORY_SEPARATOR . '.htaccess'; + @unlink($htaccess); + $_SERVER['REMOTE_ADDR'] = 'foobar'; + TrafficLimiter::canPass(); + $this->assertFileExists($htaccess, 'htaccess recreated'); + } + public function testTrafficGetsLimited() { - $this->assertEquals($this->_path, TrafficLimiter::getPath()); - $file = 'baz'; - $this->assertEquals($this->_path . DIRECTORY_SEPARATOR . $file, TrafficLimiter::getPath($file)); TrafficLimiter::setLimit(4); $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $this->assertTrue(TrafficLimiter::canPass(), 'first request may pass'); diff --git a/tst/Vizhash16x16Test.php b/tst/Vizhash16x16Test.php index afcda562..abfb8c49 100644 --- a/tst/Vizhash16x16Test.php +++ b/tst/Vizhash16x16Test.php @@ -1,5 +1,6 @@ _path); } $this->_file = $this->_path . DIRECTORY_SEPARATOR . 'vizhash.png'; - ServerSalt::setPath($this->_path); + ServerSalt::setStore(Filesystem::getInstance(array('dir' => $this->_path))); } public function tearDown()