/** * set configuration options of the traffic limiter * * @access public * @static * @param configuration $conf * @return void */ public static function setConfiguration(configuration $conf) { self::setLimit($conf->getKey('limit', 'traffic')); self::setPath($conf->getKey('dir', 'traffic')); if (($option = $conf->getKey('header', 'traffic')) !== null) { $httpHeader = 'HTTP_' . $option; if (array_key_exists($httpHeader, $_SERVER) && !empty($_SERVER[$httpHeader])) { self::$_ipKey = $httpHeader; } } }
public function testTrafficGetsLimited() { $this->assertEquals($this->_path, trafficlimiter::getPath()); $file = 'baz'; $this->assertEquals($this->_path . DIRECTORY_SEPARATOR . $file, trafficlimiter::getPath($file)); trafficlimiter::setLimit(4); $this->assertTrue(trafficlimiter::canPass('127.0.0.1'), 'first request may pass'); sleep(2); $this->assertFalse(trafficlimiter::canPass('127.0.0.1'), 'second request is to fast, may not pass'); sleep(3); $this->assertTrue(trafficlimiter::canPass('127.0.0.1'), 'third request waited long enough and may pass'); $this->assertTrue(trafficlimiter::canPass('2001:1620:2057:dead:beef::cafe:babe'), 'fourth request has different ip and may pass'); $this->assertFalse(trafficlimiter::canPass('127.0.0.1'), 'fifth request is to fast, may not pass'); }
/** * Store new paste or comment * * POST contains: * data (mandatory) = json encoded SJCL encrypted text (containing keys: iv,salt,ct) * * All optional data will go to meta information: * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never) * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0) * nickname (optional) = in discussion, encoded SJCL encrypted text nickname of author of comment (containing keys: iv,salt,ct) * parentid (optional) = in discussion, which comment this comment replies to. * pasteid (optional) = in discussion, which paste this comment belongs to. * * @access private * @param string $data * @return void */ private function _create($data) { header('Content-type: application/json'); $error = false; // Make sure last paste from the IP address was more than X seconds ago. trafficlimiter::setLimit($this->_conf['traffic']['limit']); trafficlimiter::setPath($this->_conf['traffic']['dir']); if (!trafficlimiter::canPass($_SERVER['REMOTE_ADDR'])) { $this->_return_message(1, 'Please wait ' . $this->_conf['traffic']['limit'] . ' seconds between each post.'); } // Make sure content is not too big. $sizelimit = (int) $this->_getMainConfig('sizelimit', 2097152); if (strlen($data) > $sizelimit) { $this->_return_message(1, 'Paste is limited to ' . filter::size_humanreadable($sizelimit) . ' of encrypted data.'); } // Make sure format is correct. if (!sjcl::isValid($data)) { $this->_return_message(1, 'Invalid data.'); } // Read additional meta-information. $meta = array(); // Read expiration date if (!empty($_POST['expire'])) { $selected_expire = (string) $_POST['expire']; if (array_key_exists($selected_expire, $this->_conf['expire_options'])) { $expire = $this->_conf['expire_options'][$selected_expire]; } else { $expire = $this->_conf['expire_options'][$this->_conf['expire']['default']]; } if ($expire > 0) { $meta['expire_date'] = time() + $expire; } } // Destroy the paste when it is read. if (!empty($_POST['burnafterreading'])) { $burnafterreading = $_POST['burnafterreading']; if ($burnafterreading !== '0') { if ($burnafterreading !== '1') { $error = true; } $meta['burnafterreading'] = true; } } // Read open discussion flag. if ($this->_conf['main']['opendiscussion'] && !empty($_POST['opendiscussion'])) { $opendiscussion = $_POST['opendiscussion']; if ($opendiscussion !== '0') { if ($opendiscussion !== '1') { $error = true; } $meta['opendiscussion'] = true; } } // You can't have an open discussion on a "Burn after reading" paste: if (isset($meta['burnafterreading'])) { unset($meta['opendiscussion']); } // Optional nickname for comments if (!empty($_POST['nickname'])) { // Generation of the anonymous avatar (Vizhash): // If a nickname is provided, we generate a Vizhash. // (We assume that if the user did not enter a nickname, he/she wants // to be anonymous and we will not generate the vizhash.) $nick = $_POST['nickname']; if (!sjcl::isValid($nick)) { $error = true; } else { $meta['nickname'] = $nick; $vz = new vizhash16x16(); $pngdata = $vz->generate($_SERVER['REMOTE_ADDR']); if ($pngdata != '') { $meta['vizhash'] = 'data:image/png;base64,' . base64_encode($pngdata); } // Once the avatar is generated, we do not keep the IP address, nor its hash. } } if ($error) { $this->_return_message(1, 'Invalid data.'); } // Add post date to meta. $meta['postdate'] = time(); // We just want a small hash to avoid collisions: // Half-MD5 (64 bits) will do the trick $dataid = substr(hash('md5', $data), 0, 16); $storage = array('data' => $data); // Add meta-information only if necessary. if (count($meta)) { $storage['meta'] = $meta; } // The user posts a comment. if (!empty($_POST['parentid']) && !empty($_POST['pasteid'])) { $pasteid = (string) $_POST['pasteid']; $parentid = (string) $_POST['parentid']; if (!filter::is_valid_paste_id($pasteid) || !filter::is_valid_paste_id($parentid)) { $this->_return_message(1, 'Invalid data.'); } // Comments do not expire (it's the paste that expires) unset($storage['expire_date']); unset($storage['opendiscussion']); // Make sure paste exists. if (!$this->_model()->exists($pasteid)) { $this->_return_message(1, 'Invalid data.'); } // Make sure the discussion is opened in this paste. $paste = $this->_model()->read($pasteid); if (!$paste->meta->opendiscussion) { $this->_return_message(1, 'Invalid data.'); } // Check for improbable collision. if ($this->_model()->existsComment($pasteid, $parentid, $dataid)) { $this->_return_message(1, 'You are unlucky. Try again.'); } // New comment if ($this->_model()->createComment($pasteid, $parentid, $dataid, $storage) === false) { $this->_return_message(1, 'Error saving comment. Sorry.'); } // 0 = no error $this->_return_message(0, $dataid); } else { // Check for improbable collision. if ($this->_model()->exists($dataid)) { $this->_return_message(1, 'You are unlucky. Try again.'); } // New paste if ($this->_model()->create($dataid, $storage) === false) { $this->_return_message(1, 'Error saving paste. Sorry.'); } // Generate the "delete" token. // The token is the hmac of the pasteid signed with the server salt. // The paste can be delete by calling http://myserver.com/zerobin/?pasteid=<pasteid>&deletetoken=<deletetoken> $deletetoken = hash_hmac('sha1', $dataid, serversalt::get()); // 0 = no error $this->_return_message(0, $dataid, array('deletetoken' => $deletetoken)); } $this->_return_message(1, 'Server error.'); }
/** * set the time limit in seconds * * @access public * @static * @param int $limit * @return void */ public static function setLimit($limit) { self::$_limit = $limit; }
/** * Set nickname. * * @access public * @param string $nickname * @throws Exception * @return void */ public function setNickname($nickname) { if (!sjcl::isValid($nickname)) { throw new Exception('Invalid data.', 66); } $this->_data->meta->nickname = $nickname; // Generation of the anonymous avatar (Vizhash): // If a nickname is provided, we generate a Vizhash. // (We assume that if the user did not enter a nickname, he/she wants // to be anonymous and we will not generate the vizhash.) $vh = new vizhash16x16(); $pngdata = $vh->generate(trafficlimiter::getIp()); if ($pngdata != '') { $this->_data->meta->vizhash = 'data:image/png;base64,' . base64_encode($pngdata); } // Once the avatar is generated, we do not keep the IP address, nor its hash. }
/** * Store new paste or comment * * POST contains one or both: * data = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * attachment = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * * All optional data will go to meta information: * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never) * formatter (optional) = format to display the paste as (plaintext,syntaxhighlighting,markdown) (default:syntaxhighlighting) * burnafterreading (optional) = if this paste may only viewed once ? (0/1) (default:0) * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0) * attachmentname = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * nickname (optional) = in discussion, encoded SJCL encrypted text nickname of author of comment (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * parentid (optional) = in discussion, which comment this comment replies to. * pasteid (optional) = in discussion, which paste this comment belongs to. * * @access private * @return string */ private function _create() { $error = false; // Ensure last paste from visitors IP address was more than configured amount of seconds ago. trafficlimiter::setConfiguration($this->_conf); if (!trafficlimiter::canPass()) { return $this->_return_message(1, i18n::_('Please wait %d seconds between each post.', $this->_conf->getKey('limit', 'traffic'))); } $data = $this->_request->getParam('data'); $attachment = $this->_request->getParam('attachment'); $attachmentname = $this->_request->getParam('attachmentname'); // Ensure content is not too big. $sizelimit = $this->_conf->getKey('sizelimit'); if (strlen($data) + strlen($attachment) + strlen($attachmentname) > $sizelimit) { return $this->_return_message(1, i18n::_('Paste is limited to %s of encrypted data.', filter::size_humanreadable($sizelimit))); } // The user posts a comment. $pasteid = $this->_request->getParam('pasteid'); $parentid = $this->_request->getParam('parentid'); if (!empty($pasteid) && !empty($parentid)) { $paste = $this->_model->getPaste($pasteid); if ($paste->exists()) { try { $comment = $paste->getComment($parentid); $nickname = $this->_request->getParam('nickname'); if (!empty($nickname)) { $comment->setNickname($nickname); } $comment->setData($data); $comment->store(); } catch (Exception $e) { return $this->_return_message(1, $e->getMessage()); } $this->_return_message(0, $comment->getId()); } else { $this->_return_message(1, 'Invalid data.'); } } else { $paste = $this->_model->getPaste(); try { $paste->setData($data); if (!empty($attachment)) { $paste->setAttachment($attachment); if (!empty($attachmentname)) { $paste->setAttachmentName($attachmentname); } } $expire = $this->_request->getParam('expire'); if (!empty($expire)) { $paste->setExpiration($expire); } $burnafterreading = $this->_request->getParam('burnafterreading'); if (!empty($burnafterreading)) { $paste->setBurnafterreading($burnafterreading); } $opendiscussion = $this->_request->getParam('opendiscussion'); if (!empty($opendiscussion)) { $paste->setOpendiscussion($opendiscussion); } $formatter = $this->_request->getParam('formatter'); if (!empty($formatter)) { $paste->setFormatter($formatter); } $paste->store(); } catch (Exception $e) { return $this->_return_message(1, $e->getMessage()); } $this->_return_message(0, $paste->getId(), array('deletetoken' => $paste->getDeleteToken())); } }
/** * @runInSeparateProcess */ public function testCreateInvalidTimelimit() { $this->reset(); $_POST = helper::getPaste(); $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; $_SERVER['REQUEST_METHOD'] = 'POST'; $_SERVER['REMOTE_ADDR'] = '::1'; trafficlimiter::canPass(); ob_start(); new zerobin(); $content = ob_get_contents(); $response = json_decode($content, true); $this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste exists after posting data'); }