diff --git a/cfg/conf.ini b/cfg/conf.ini new file mode 100644 index 0000000..17687e5 --- /dev/null +++ b/cfg/conf.ini @@ -0,0 +1,31 @@ +; ZeroBin +; +; a zero-knowledge paste bin +; +; @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin +; @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) +; @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License +; @version 0.15 + +; timelimit between calls from the same IP address in seconds +traffic_limit = 10 +traffic_dir = PATH "data" + +; name of data model class to load and directory for storage +; the default model "zerobin_data" stores everything in the filesystem +model = zerobin_data +model_options["dir"] = PATH "data" + +; example of DB configuration for MySQL +;model = zerobin_db +;model_options["dsn"] = "mysql:host=localhost;dbname=zerobin" +;model_options["usr"] = "zerobin" +;model_options["pwd"] = "Z3r0P4ss" +;model_options["opt"][PDO::ATTR_PERSISTENT] = true + +; example of DB configuration for SQLite +;model = zerobin_db +;model_options["dsn"] = "sqlite:" PATH "data"/db.sq3" +;model_options["usr"] = null +;model_options["pwd"] = null +;model_options["opt"] = null diff --git a/index.php b/index.php index ddd28f6..6b4835c 100644 --- a/index.php +++ b/index.php @@ -1,339 +1,16 @@ "); - chmod($tfilename,0705); - } - require $tfilename; - $tl=$GLOBALS['trafic_limiter']; - if (!empty($tl[$ip]) && ($tl[$ip]+10>=time())) - { - return false; - // FIXME: purge file of expired IPs to keep it small - } - $tl[$ip]=time(); - file_put_contents($tfilename, ""); - return true; -} - -/* Convert paste id to storage path. - The idea is to creates subdirectories in order to limit the number of files per directory. - (A high number of files in a single directory can slow things down.) - eg. "f468483c313401e8" will be stored in "data/f4/68/f468483c313401e8" - High-trafic websites may want to deepen the directory structure (like Squid does). - - eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/' -*/ -function dataid2path($dataid) -{ - return 'data/'.substr($dataid,0,2).'/'.substr($dataid,2,2).'/'; -} - -/* Convert paste id to discussion storage path. - eg. 'e3570978f9e4aa90' --> 'data/e3/57/e3570978f9e4aa90.discussion/' -*/ -function dataid2discussionpath($dataid) -{ - return dataid2path($dataid).$dataid.'.discussion/'; -} - -// Checks if a json string is a proper SJCL encrypted message. -// False if format is incorrect. -function validSJCL($jsonstring) -{ - $accepted_keys=array('iv','salt','ct'); - - // Make sure content is valid json - $decoded = json_decode($jsonstring); - if ($decoded==null) return false; - $decoded = (array)$decoded; - - // Make sure required fields are present and that they are base64 data. - foreach($accepted_keys as $k) - { - if (!array_key_exists($k,$decoded)) { return false; } - if (base64_decode($decoded[$k],$strict=true)==null) { return false; } - } - - // Make sure no additionnal keys were added. - if (count(array_intersect(array_keys($decoded),$accepted_keys))!=3) { return false; } - - // FIXME: Reject data if entropy is too low ? - - // Make sure some fields have a reasonable size. - if (strlen($decoded['iv'])>24) return false; - if (strlen($decoded['salt'])>14) return false; - return true; -} - -// Delete a paste and its discussion. -// Input: $pasteid : the paste identifier. -function deletePaste($pasteid) -{ - // Delete the paste itself - unlink(dataid2path($pasteid).$pasteid); - - // Delete discussion if it exists. - $discdir = dataid2discussionpath($pasteid); - if (is_dir($discdir)) - { - // Delete all files in discussion directory - $dhandle = opendir($discdir); - while (false !== ($filename = readdir($dhandle))) - { - if (is_file($discdir.$filename)) unlink($discdir.$filename); - } - closedir($dhandle); - - // Delete the discussion directory. - rmdir($discdir); - } -} - -if (!empty($_POST['data'])) // Create new paste/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,10min,1hour,1day,1month,1year,burn) (default:never) - opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0) - nickname (optional) = son 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. - */ - - header('Content-type: application/json'); - $error = false; - - // Create storage directory if it does not exist. - if (!is_dir('data')) - { - mkdir('data',0705); - file_put_contents('data/.htaccess',"Allow from none\nDeny from all\n"); - } - - // Make sure last paste from the IP address was more than 10 seconds ago. - if (!trafic_limiter_canPass($_SERVER['REMOTE_ADDR'])) - { echo json_encode(array('status'=>1,'message'=>'Please wait 10 seconds between each post.')); exit; } - - // Make sure content is not too big. - $data = $_POST['data']; - if (strlen($data)>2000000) - { echo json_encode(array('status'=>1,'message'=>'Paste is limited to 2 Mb of encrypted data.')); exit; } - - // Make sure format is correct. - if (!validSJCL($data)) - { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; } - - // Read additional meta-information. - $meta=array(); - - // Read expiration date - if (!empty($_POST['expire'])) - { - $expire=$_POST['expire']; - if ($expire=='10min') $meta['expire_date']=time()+10*60; - elseif ($expire=='1hour') $meta['expire_date']=time()+60*60; - elseif ($expire=='1day') $meta['expire_date']=time()+24*60*60; - elseif ($expire=='1month') $meta['expire_date']=time()+30*24*60*60; // Well this is not *exactly* one month, it's 30 days. - elseif ($expire=='1year') $meta['expire_date']=time()+365*24*60*60; - elseif ($expire=='burn') $meta['burnafterreading']=true; - } - - // Read open discussion flag - if (!empty($_POST['opendiscussion'])) - { - $opendiscussion = $_POST['opendiscussion']; - if ($opendiscussion!='0' && $opendiscussion!='1') { $error=true; } - if ($opendiscussion!='0') { $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'])) - { - $nick = $_POST['nickname']; - if (!validSJCL($nick)) - { - $error=true; - } - else - { - $meta['nickname']=$nick; - - // 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.) - $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) - { - echo json_encode(array('status'=>1,'message'=>'Invalid data.')); - exit; - } - - // 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); - - $is_comment = (!empty($_POST['parentid']) && !empty($_POST['pasteid'])); // Is this post a comment ? - $storage = array('data'=>$data); - if (count($meta)>0) $storage['meta'] = $meta; // Add meta-information only if necessary. - - if ($is_comment) // The user posts a comment. - { - $pasteid = $_POST['pasteid']; - $parentid = $_POST['parentid']; - if (!preg_match('/[a-f\d]{16}/',$pasteid)) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; } - if (!preg_match('/[a-f\d]{16}/',$parentid)) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; } - - unset($storage['expire_date']); // Comment do not expire (it's the paste that expires) - unset($storage['opendiscussion']); - - // Make sure paste exists. - $storagedir = dataid2path($pasteid); - if (!is_file($storagedir.$pasteid)) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; } - - // Make sure the discussion is opened in this paste. - $paste=json_decode(file_get_contents($storagedir.$pasteid)); - if (!$paste->meta->opendiscussion) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; } - - $discdir = dataid2discussionpath($pasteid); - $filename = $pasteid.'.'.$dataid.'.'.$parentid; - if (!is_dir($discdir)) mkdir($discdir,$mode=0705,$recursive=true); - if (is_file($discdir.$filename)) // Oups... improbable collision. - { - echo json_encode(array('status'=>1,'message'=>'You are unlucky. Try again.')); - exit; - } - - file_put_contents($discdir.$filename,json_encode($storage)); - echo json_encode(array('status'=>0,'id'=>$dataid)); // 0 = no error - exit; - } - else // a standard paste. - { - $storagedir = dataid2path($dataid); - if (!is_dir($storagedir)) mkdir($storagedir,$mode=0705,$recursive=true); - if (is_file($storagedir.$dataid)) // Oups... improbable collision. - { - echo json_encode(array('status'=>1,'message'=>'You are unlucky. Try again.')); - exit; - } - // New paste - file_put_contents($storagedir.$dataid,json_encode($storage)); - echo json_encode(array('status'=>0,'id'=>$dataid)); // 0 = no error - exit; - } - -echo json_encode(array('status'=>1,'message'=>'Server error.')); -exit; -} - -$CIPHERDATA=''; -$ERRORMESSAGE=''; -if (!empty($_SERVER['QUERY_STRING'])) // Display an existing paste. -{ - $dataid = $_SERVER['QUERY_STRING']; - if (preg_match('/[a-f\d]{16}/',$dataid)) // Is this a valid paste identifier ? - { - $filename = dataid2path($dataid).$dataid; - if (is_file($filename)) // Check that paste exists. - { - // Get the paste itself. - $paste=json_decode(file_get_contents($filename)); - - // See if paste has expired. - if (isset($paste->meta->expire_date) && $paste->meta->expire_datemeta, 'expire_date')) $paste->meta->remaining_time = $paste->meta->expire_date - time(); - - $messages = array($paste); // The paste itself is the first in the list of encrypted messages. - // If it's a discussion, get all comments. - if (property_exists($paste->meta, 'opendiscussion') && $paste->meta->opendiscussion) - { - $comments=array(); - $datadir = dataid2discussionpath($dataid); - if (!is_dir($datadir)) mkdir($datadir,$mode=0705,$recursive=true); - $dhandle = opendir($datadir); - while (false !== ($filename = readdir($dhandle))) - { - if (is_file($datadir.$filename)) - { - $comment=json_decode(file_get_contents($datadir.$filename)); - // Filename is in the form pasteid.commentid.parentid: - // - pasteid is the paste this reply belongs to. - // - commentid is the comment identifier itself. - // - parentid is the comment this comment replies to (It can be pasteid) - $items=explode('.',$filename); - $comment->meta->commentid=$items[1]; // Add some meta information not contained in file. - $comment->meta->parentid=$items[2]; - $comments[$comment->meta->postdate]=$comment; // Store in table - } - } - closedir($dhandle); - ksort($comments); // Sort comments by date, oldest first. - $messages = array_merge($messages, $comments); - } - $CIPHERDATA = json_encode($messages); - - // If the paste was meant to be read only once, delete it. - if (property_exists($paste->meta, 'burnafterreading') && $paste->meta->burnafterreading) deletePaste($dataid); - } - } - else - { - $ERRORMESSAGE='Paste does not exist or has expired.'; - } - } -} - - -require_once "lib/rain.tpl.class.php"; -header('Content-Type: text/html; charset=utf-8'); -$page = new RainTPL; -$page->assign('CIPHERDATA',htmlspecialchars($CIPHERDATA,ENT_NOQUOTES)); // We escape it here because ENT_NOQUOTES can't be used in RainTPL templates. -$page->assign('VERSION',$VERSION); -$page->assign('ERRORMESSAGE',$ERRORMESSAGE); -$page->draw('page'); -?> +// change this, if your php files and data is outside of your webservers document root +define('PATH', ''); +require_once PATH . 'lib/zerobin.php'; +new zerobin; diff --git a/lib/filter.php b/lib/filter.php new file mode 100644 index 0000000..aa66d75 --- /dev/null +++ b/lib/filter.php @@ -0,0 +1,34 @@ + + * PHP tags * True: php tags are enabled into the template * False: php tags are disabled into the template and rendered as html * @@ -92,7 +92,7 @@ class RainTPL{ */ static $php_enabled = false; - + /** * Debug mode flag. * True: debug mode is used, syntax errors are displayed directly in template. Execution of script is not terminated. @@ -257,9 +257,9 @@ class RainTPL{ $tpl_basename = basename( $tpl_name ); // template basename $tpl_basedir = strpos($tpl_name,"/") ? dirname($tpl_name) . '/' : null; // template basedirectory - $tpl_dir = self::$tpl_dir . $tpl_basedir; // template directory + $tpl_dir = PATH . self::$tpl_dir . $tpl_basedir; // template directory $this->tpl['tpl_filename'] = $tpl_dir . $tpl_basename . '.' . self::$tpl_ext; // template filename - $temp_compiled_filename = self::$cache_dir . $tpl_basename . "." . md5( $tpl_dir . serialize(self::$config_name_sum)); + $temp_compiled_filename = PATH . self::$cache_dir . $tpl_basename . "." . md5( $tpl_dir . serialize(self::$config_name_sum)); $this->tpl['compiled_filename'] = $temp_compiled_filename . '.rtpl.php'; // cache filename $this->tpl['cache_filename'] = $temp_compiled_filename . '.s_' . $this->cache_id . '.rtpl.php'; // static cache filename @@ -271,7 +271,7 @@ class RainTPL{ // file doesn't exsist, or the template was updated, Rain will compile the template if( !file_exists( $this->tpl['compiled_filename'] ) || ( self::$check_template_update && filemtime($this->tpl['compiled_filename']) < filemtime( $this->tpl['tpl_filename'] ) ) ){ - $this->compileFile( $tpl_basename, $tpl_basedir, $this->tpl['tpl_filename'], self::$cache_dir, $this->tpl['compiled_filename'] ); + $this->compileFile( $tpl_basename, $tpl_basedir, $this->tpl['tpl_filename'], PATH . self::$cache_dir, $this->tpl['compiled_filename'] ); return true; } $this->tpl['checked'] = true; @@ -285,7 +285,7 @@ class RainTPL{ */ protected function xml_reSubstitution($capture) { return "'; ?>"; - } + } /** * Compile and write the compiled template file @@ -304,11 +304,11 @@ class RainTPL{ $template_code = str_replace( array(""), array("<?","?>"), $template_code ); //xml re-substitution - $template_code = preg_replace_callback ( "/##XML(.*?)XML##/s", array($this, 'xml_reSubstitution'), $template_code ); + $template_code = preg_replace_callback ( "/##XML(.*?)XML##/s", array($this, 'xml_reSubstitution'), $template_code ); //compile template $template_compiled = "" . $this->compileTemplate( $template_code, $tpl_basedir ); - + // fix the php-eating-newline-after-closing-tag-problem $template_compiled = str_replace( "?>\n", "?>\n\n", $template_compiled ); @@ -413,7 +413,7 @@ class RainTPL{ // if the cache is active if( isset($code[ 2 ]) ){ - + //dynamic include $compiled_code .= 'cache( $template = basename("'.$include_var.'") ) )' . @@ -426,7 +426,7 @@ class RainTPL{ '} ?>'; } else{ - + //dynamic include $compiled_code .= 'assign( "key", $key'.$loop_level.' ); $tpl->assign( "value", $value'.$loop_level.' );' ). '$tpl->draw( dirname("'.$include_var.'") . ( substr("'.$include_var.'",-1,1) != "/" ? "/" : "" ) . basename("'.$include_var.'") );'. '?>'; - - + + } } @@ -548,7 +548,7 @@ class RainTPL{ else // parse the function $parsed_function = $function . $this->var_replace( $code[ 2 ], $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level ); - + //if code $compiled_code .= ""; } @@ -582,8 +582,8 @@ class RainTPL{ } return $compiled_code; } - - + + /** * Reduce a path, eg. www/library/../filepath//file => www/filepath/file * @param type $path @@ -611,8 +611,8 @@ class RainTPL{ if( self::$path_replace ){ - $tpl_dir = self::$base_url . self::$tpl_dir . $tpl_basedir; - + $tpl_dir = self::$base_url . PATH . self::$tpl_dir . $tpl_basedir; + // reduce the path $path = $this->reduce_path($tpl_dir); @@ -683,7 +683,7 @@ class RainTPL{ $this->function_check( $tag ); $extra_var = $this->var_replace( $extra_var, null, null, null, null, $loop_level ); - + // check if there's an operator = in the variable tags, if there's this is an initialization so it will not output any value $is_init_variable = preg_match( "/^(\s*?)\=[^=](.*?)$/", $extra_var ); @@ -712,7 +712,7 @@ class RainTPL{ //if there's a function if( $function_var ){ - + // check if there's a function or a static method and separate, function by parameters $function_var = str_replace("::", "@double_dot@", $function_var ); @@ -786,7 +786,7 @@ class RainTPL{ // check if there's an operator = in the variable tags, if there's this is an initialization so it will not output any value $is_init_variable = preg_match( "/^[a-z_A-Z\.\[\](\-\>)]*=[^=]*$/", $extra_var ); - + //function associate to variable $function_var = ( $extra_var and $extra_var[0] == '|') ? substr( $extra_var, 1 ) : null; @@ -805,16 +805,16 @@ class RainTPL{ //transform .$variable in ["$variable"] and .variable in ["variable"] $variable_path = preg_replace('/\.(\${0,1}\w+)/', '["\\1"]', $variable_path ); - + // if is an assignment also assign the variable to $this->var['value'] if( $is_init_variable ) $extra_var = "=\$this->var['{$var_name}']{$variable_path}" . $extra_var; - + //if there's a function if( $function_var ){ - + // check if there's a function or a static method and separate, function by parameters $function_var = str_replace("::", "@double_dot@", $function_var ); @@ -855,13 +855,13 @@ class RainTPL{ $php_var = '$' . $var_name . $variable_path; }else $php_var = '$' . $var_name . $variable_path; - + // compile the variable for php if( isset( $function ) ) $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . ( $params ? "( $function( $php_var, $params ) )" : "$function( $php_var )" ) . $php_right_delimiter; else $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . $php_var . $extra_var . $php_right_delimiter; - + $html = str_replace( $tag, $php_var, $html ); diff --git a/lib/sjcl.php b/lib/sjcl.php new file mode 100644 index 0000000..0b4c381 --- /dev/null +++ b/lib/sjcl.php @@ -0,0 +1,64 @@ + 24) return false; + if (strlen($decoded['salt']) > 14) return false; + + return true; + } +} diff --git a/lib/traffic_limiter.php b/lib/traffic_limiter.php new file mode 100644 index 0000000..c2cc4e8 --- /dev/null +++ b/lib/traffic_limiter.php @@ -0,0 +1,111 @@ + $time) + { + if ($time + 10 < time()) + { + unset($tl[$key]); + } + } + + if (array_key_exists($ip, $tl) && ($tl[$ip] + 10 >= time())) + { + $result = false; + } else { + $tl[$ip] = time(); + $result = true; + } + file_put_contents( + $file, + 'width=16; $this->height=16; - + // Read salt from file (and create it if does not exist). // The salt will make vizhash avatar unique on each ZeroBin installation // to prevent IP checking. - $saltfile = 'data/salt.php'; + $saltfile = PATH . 'data/salt.php'; if (!is_file($saltfile)) file_put_contents($saltfile,'randomSalt().'| */ ?>'); $items=explode('|',file_get_contents($saltfile)); $this->salt = $items[1]; - } - + } + // Generate a 16x16 png corresponding to $text. // Input: $text (string) // Output: PNG data. Or empty string if GD is not available. @@ -61,14 +61,14 @@ class vizhash16x16 $image = $this->degrade($image,$op,array($r0,$g0,$b0),array(0,0,0)); for($i=0; $i<7; $i=$i+1) - { + { $action=$this->getInt(); $color = imagecolorallocate($image, $r,$g,$b); $r = ($r0 + $this->getInt()/25)%256; $g = ($g0 + $this->getInt()/25)%256; $b = ($b0 + $this->getInt()/25)%256; $r0=$r; $g0=$g; $b0=$b; - $this->drawshape($image,$action,$color); + $this->drawshape($image,$action,$color); } $color = imagecolorallocate($image,$this->getInt(),$this->getInt(),$this->getInt()); @@ -78,10 +78,10 @@ class vizhash16x16 $imagedata = ob_get_contents(); ob_end_clean(); imagedestroy($image); - + return $imagedata; - } - + } + // Generate a large random hexadecimal salt. private function randomSalt() { @@ -89,25 +89,25 @@ class vizhash16x16 for($i=0;$i<6;$i++) { $randomSalt.=base_convert(mt_rand(),10,16); } return $randomSalt; } - - + + private function getInt() // Returns a single integer from the $VALUES array (0...255) { - $v= $this->VALUES[$this->VALUES_INDEX]; + $v= $this->VALUES[$this->VALUES_INDEX]; $this->VALUES_INDEX++; $this->VALUES_INDEX %= count($this->VALUES); // Warp around the array return $v; } - private function getX() // Returns a single integer from the array (roughly mapped to image width) + private function getX() // Returns a single integer from the array (roughly mapped to image width) { return $this->width*$this->getInt()/256; } - private function getY() // Returns a single integer from the array (roughly mapped to image height) - { + private function getY() // Returns a single integer from the array (roughly mapped to image height) + { return $this->height*$this->getInt()/256; - } - + } + # Gradient function taken from: # http://www.supportduweb.com/scripts_tutoriaux-code-source-41-gd-faire-un-degrade-en-php-gd-fonction-degrade-imagerie.html private function degrade($img,$direction,$color1,$color2) @@ -129,17 +129,17 @@ class vizhash16x16 } return $img; } - + private function drawshape($image,$action,$color) { switch($action%7) { case 0: - ImageFilledRectangle ($image,$this->getX(),$this->getY(),$this->getX(),$this->getY(),$color); + ImageFilledRectangle ($image,$this->getX(),$this->getY(),$this->getX(),$this->getY(),$color); break; case 1: case 2: - ImageFilledEllipse ($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $color); + ImageFilledEllipse ($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $color); break; case 3: $points = array($this->getX(), $this->getY(), $this->getX(), $this->getY(), $this->getX(), $this->getY(),$this->getX(), $this->getY()); @@ -150,9 +150,9 @@ class vizhash16x16 case 6: $start=$this->getInt()*360/256; $end=$start+$this->getInt()*180/256; ImageFilledArc ($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(),$start,$end,$color,IMG_ARC_PIE); - break; + break; } - } -} + } +} ?> \ No newline at end of file diff --git a/lib/zerobin.php b/lib/zerobin.php new file mode 100644 index 0000000..2e389fe --- /dev/null +++ b/lib/zerobin.php @@ -0,0 +1,406 @@ + 'zerobin_data', + ); + + /** + * @access private + * @var string + */ + private $_data = ''; + + /** + * @access private + * @var string + */ + private $_error = ''; + + /** + * @access private + * @var zerobin_data + */ + private $_model; + + /** + * constructor + * + * initializes and runs ZeroBin + * + * @access public + */ + public function __construct() + { + if (version_compare(PHP_VERSION, '5.2.6') < 0) + die('ZeroBin requires php 5.2.6 or above to work. Sorry.'); + + // In case stupid admin has left magic_quotes enabled in php.ini. + if (get_magic_quotes_gpc()) + { + require_once PATH . 'lib/filter.php'; + $_POST = array_map('filter::stripslashes_deep', $_POST); + $_GET = array_map('filter::stripslashes_deep', $_GET); + $_COOKIE = array_map('filter::stripslashes_deep', $_COOKIE); + } + + // Load config from ini file. + $this->_init(); + + // Create new paste or comment. + if (!empty($_POST['data'])) + { + $this->_create(); + } + // Display an existing paste. + elseif (!empty($_SERVER['QUERY_STRING'])) + { + $this->_read(); + } + + // Display ZeroBin frontend + $this->_view(); + } + + /** + * initialize zerobin + * + * @access private + * @return void + */ + private function _init() + { + $this->_conf = parse_ini_file(PATH . 'cfg/conf.ini'); + $this->_model = $this->_conf['model']; + } + + /** + * get the model, create one if needed + * + * @access private + * @return zerobin_data + */ + private function _model() + { + // if needed, initialize the model + if(is_string($this->_model)) { + require_once PATH . 'lib/' . $this->_model . '.php'; + $this->_model = forward_static_call(array($this->_model, 'getInstance'), $this->_conf['model_options']); + } + return $this->_model; + } + + /** + * 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,10min,1hour,1day,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 + * @return void + */ + private function _create() + { + header('Content-type: application/json'); + $error = false; + + // Make sure last paste from the IP address was more than 10 seconds ago. + require_once PATH . 'lib/traffic_limiter.php'; + traffic_limiter::setLimit($this->_conf['traffic_limit']); + traffic_limiter::setPath($this->_conf['traffic_dir']); + if ( + !traffic_limiter::canPass($_SERVER['REMOTE_ADDR']) + ) $this->_return_message(1, 'Please wait 10 seconds between each post.'); + + // Make sure content is not too big. + $data = $_POST['data']; + if ( + strlen($data) > 2000000 + ) $this->_return_message(1, 'Paste is limited to 2 MB of encrypted data.'); + + // Make sure format is correct. + require_once PATH . 'lib/sjcl.php'; + if (!sjcl::isValid($data)) $this->_return_message(1, 'Invalid data.'); + + // Read additional meta-information. + $meta=array(); + + // Read expiration date + if (!empty($_POST['expire'])) + { + switch ($_POST['expire']) + { + case '10min': + $meta['expire_date'] = time()+10*60; + break; + case '1hour': + $meta['expire_date'] = time()+60*60; + break; + case '1day': + $meta['expire_date'] = time()+24*60*60; + break; + case '1month': + $meta['expire_date'] = strtotime('+1 month'); + break; + case '1year': + $meta['expire_date'] = strtotime('+1 year'); + break; + case 'burn': + $meta['burnafterreading'] = true; + } + } + + // Read open discussion flag. + if (!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 + { + require_once PATH . 'lib/vizhash_gd_zero.php'; + $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 = $_POST['pasteid']; + $parentid = $_POST['parentid']; + if ( + !preg_match('/[a-f\d]{16}/', $pasteid) || + !preg_match('/[a-f\d]{16}/', $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); + } + // The user posts a standard paste. + 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.'); + + // 0 = no error + $this->_return_message(0, $dataid); + } + + $this->_return_message(1, 'Server error.'); + } + + /** + * Read an existing paste or comment. + * + * @access private + * @return void + */ + private function _read() + { + $dataid = $_SERVER['QUERY_STRING']; + + // Is this a valid paste identifier? + if (preg_match('/[a-f\d]{16}/', $dataid)) + { + // Check that paste exists. + if ($this->_model()->exists($dataid)) + { + // Get the paste itself. + $paste = $this->_model()->read($dataid); + + // See if paste has expired. + if ( + isset($paste->meta->expire_date) && + $paste->meta->expire_date < time() + ) + { + // Delete the paste + $this->_model()->delete($dataid); + $this->_error = 'Paste does not exist or has expired.'; + } + // If no error, return the paste. + else + { + // We kindly provide the remaining time before expiration (in seconds) + if ( + property_exists($paste->meta, 'expire_date') + ) $paste->meta->remaining_time = $paste->meta->expire_date - time(); + + // The paste itself is the first in the list of encrypted messages. + $messages = array($paste); + + // If it's a discussion, get all comments. + if ( + property_exists($paste->meta, 'opendiscussion') && + $paste->meta->opendiscussion + ) + { + $messages = array_merge( + $messages, + $this->_model()->readComments($dataid) + ); + } + $this->_data = json_encode($messages); + + // If the paste was meant to be read only once, delete it. + if ( + property_exists($paste->meta, 'burnafterreading') && + $paste->meta->burnafterreading + ) $this->_model()->delete($dataid); + } + } + else + { + $this->_error = 'Paste does not exist or has expired.'; + } + } + } + + /** + * Display ZeroBin frontend. + * + * @access private + * @return void + */ + private function _view() + { + require_once PATH . 'lib/rain.tpl.class.php'; + header('Content-Type: text/html; charset=utf-8'); + $page = new RainTPL; + // We escape it here because ENT_NOQUOTES can't be used in RainTPL templates. + $page->assign('CIPHERDATA', htmlspecialchars($this->_data, ENT_NOQUOTES)); + $page->assign('ERRORMESSAGE', $this->_error); + $page->assign('VERSION', self::VERSION); + $page->draw('page'); + } + + /** + * return JSON encoded message and exit + * + * @access private + * @param bool $status + * @param string $message + * @return void + */ + private function _return_message($status, $message) + { + $result = array('status' => $status); + if ($status) + { + $result['message'] = $message; + } + else + { + $result['id'] = $message; + } + exit(json_encode($result)); + } +} diff --git a/lib/zerobin_data.php b/lib/zerobin_data.php new file mode 100644 index 0000000..8d1e173 --- /dev/null +++ b/lib/zerobin_data.php @@ -0,0 +1,283 @@ +exists($pasteid)) return json_decode( + '{"data":"","meta":{"burnafterreading":true,"postdate":0}}' + ); + return json_decode( + file_get_contents(self::_dataid2path($pasteid) . $pasteid) + ); + } + + /** + * Delete a paste and its discussion. + * + * @access public + * @param string $pasteid + * @return void + */ + public function delete($pasteid) + { + // Delete the paste itself. + unlink(self::_dataid2path($pasteid) . $pasteid); + + // Delete discussion if it exists. + $discdir = self::_dataid2discussionpath($pasteid); + if (is_dir($discdir)) + { + // Delete all files in discussion directory + $dir = dir($discdir); + while (false !== ($filename = $dir->read())) + { + if (is_file($discdir.$filename)) unlink($discdir.$filename); + } + $dir->close(); + + // Delete the discussion directory. + rmdir($discdir); + } + } + + /** + * Test if a paste exists. + * + * @access public + * @param string $dataid + * @return void + */ + public function exists($pasteid) + { + return is_file(self::_dataid2path($pasteid) . $pasteid); + } + + /** + * Create a comment in a paste. + * + * @access public + * @param string $pasteid + * @param string $parentid + * @param string $commentid + * @param array $comment + * @return int|false + */ + public function createComment($pasteid, $parentid, $commentid, $comment) + { + $storagedir = self::_dataid2discussionpath($pasteid); + $filename = $pasteid . '.' . $commentid . '.' . $parentid; + if (is_file($storagedir . $filename)) return false; + if (!is_dir($storagedir)) mkdir($storagedir, 0705, true); + return file_put_contents($storagedir . $filename, json_encode($comment)); + } + + /** + * Read all comments of paste. + * + * @access public + * @param string $pasteid + * @return array + */ + public function readComments($pasteid) + { + $comments = array(); + $discdir = self::_dataid2discussionpath($pasteid); + if (is_dir($discdir)) + { + // Delete all files in discussion directory + $dir = dir($discdir); + while (false !== ($filename = $dir->read())) + { + // Filename is in the form pasteid.commentid.parentid: + // - pasteid is the paste this reply belongs to. + // - commentid is the comment identifier itself. + // - parentid is the comment this comment replies to (It can be pasteid) + if (is_file($discdir.$filename)) + { + $comment = json_decode(file_get_contents($discdir.$filename)); + $items = explode('.', $filename); + // Add some meta information not contained in file. + $comment->meta->commentid=$items[1]; + $comment->meta->parentid=$items[2]; + + // Store in array + $comments[$comment->meta->postdate]=$comment; + } + } + $dir->close(); + + // Sort comments by date, oldest first. + ksort($comments); + } + return $comments; + } + + /** + * Test if a comment exists. + * + * @access public + * @param string $dataid + * @param string $parentid + * @param string $commentid + * @return void + */ + public function existsComment($pasteid, $parentid, $commentid) + { + return is_file( + self::_dataid2discussionpath($pasteid) . + $pasteid . '.' . $dataid . '.' . $parentid + ); + } + + /** + * initialize zerobin + * + * @access private + * @static + * @return void + */ + private static function _init() + { + if (defined('PATH')) self::$_dir = PATH . self::$_dir; + + // Create storage directory if it does not exist. + if (!is_dir(self::$_dir)) + { + mkdir(self::$_dir, 0705); + file_put_contents( + self::$_dir . '.htaccess', + 'Allow from none' . PHP_EOL . + 'Deny from all'. PHP_EOL + ); + } + } + + /** + * Convert paste id to storage path. + * + * The idea is to creates subdirectories in order to limit the number of files per directory. + * (A high number of files in a single directory can slow things down.) + * eg. "f468483c313401e8" will be stored in "data/f4/68/f468483c313401e8" + * High-trafic websites may want to deepen the directory structure (like Squid does). + * + * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/' + * + * @access private + * @static + * @param string $dataid + * @return void + */ + private static function _dataid2path($dataid) + { + return self::$_dir . substr($dataid,0,2) . '/' . substr($dataid,2,2) . '/'; + } + + /** + * Convert paste id to discussion storage path. + * + * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/e3570978f9e4aa90.discussion/' + * + * @access private + * @static + * @param string $dataid + * @return void + */ + private static function _dataid2discussionpath($dataid) + { + return self::_dataid2path($dataid) . $dataid . '.discussion/'; + } +} diff --git a/lib/zerobin_db.php b/lib/zerobin_db.php new file mode 100644 index 0000000..51fbe8d --- /dev/null +++ b/lib/zerobin_db.php @@ -0,0 +1,176 @@ +