index.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <?php
  2. /*
  3. ZeroBin - a zero-knowledge paste bin
  4. Please see project page: http://sebsauvage.net/wiki/doku.php?id=php:zerobin
  5. */
  6. $VERSION='Alpha 0.15';
  7. if (version_compare(PHP_VERSION, '5.2.6') < 0) die('ZeroBin requires php 5.2.6 or above to work. Sorry.');
  8. require_once "lib/vizhash_gd_zero.php";
  9. // In case stupid admin has left magic_quotes enabled in php.ini:
  10. if (get_magic_quotes_gpc())
  11. {
  12. function stripslashes_deep($value) { $value = is_array($value) ? array_map('stripslashes_deep', $value) : stripslashes($value); return $value; }
  13. $_POST = array_map('stripslashes_deep', $_POST);
  14. $_GET = array_map('stripslashes_deep', $_GET);
  15. $_COOKIE = array_map('stripslashes_deep', $_COOKIE);
  16. }
  17. // trafic_limiter : Make sure the IP address makes at most 1 request every 10 seconds.
  18. // Will return false if IP address made a call less than 10 seconds ago.
  19. function trafic_limiter_canPass($ip)
  20. {
  21. $tfilename='./data/trafic_limiter.php';
  22. if (!is_file($tfilename))
  23. {
  24. file_put_contents($tfilename,"<?php\n\$GLOBALS['trafic_limiter']=array();\n?>");
  25. chmod($tfilename,0705);
  26. }
  27. require $tfilename;
  28. $tl=$GLOBALS['trafic_limiter'];
  29. if (!empty($tl[$ip]) && ($tl[$ip]+10>=time()))
  30. {
  31. return false;
  32. // FIXME: purge file of expired IPs to keep it small
  33. }
  34. $tl[$ip]=time();
  35. file_put_contents($tfilename, "<?php\n\$GLOBALS['trafic_limiter']=".var_export($tl,true).";\n?>");
  36. return true;
  37. }
  38. /* Convert paste id to storage path.
  39. The idea is to creates subdirectories in order to limit the number of files per directory.
  40. (A high number of files in a single directory can slow things down.)
  41. eg. "f468483c313401e8" will be stored in "data/f4/68/f468483c313401e8"
  42. High-trafic websites may want to deepen the directory structure (like Squid does).
  43. eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/'
  44. */
  45. function dataid2path($dataid)
  46. {
  47. return 'data/'.substr($dataid,0,2).'/'.substr($dataid,2,2).'/';
  48. }
  49. /* Convert paste id to discussion storage path.
  50. eg. 'e3570978f9e4aa90' --> 'data/e3/57/e3570978f9e4aa90.discussion/'
  51. */
  52. function dataid2discussionpath($dataid)
  53. {
  54. return dataid2path($dataid).$dataid.'.discussion/';
  55. }
  56. // Checks if a json string is a proper SJCL encrypted message.
  57. // False if format is incorrect.
  58. function validSJCL($jsonstring)
  59. {
  60. $accepted_keys=array('iv','salt','ct');
  61. // Make sure content is valid json
  62. $decoded = json_decode($jsonstring);
  63. if ($decoded==null) return false;
  64. $decoded = (array)$decoded;
  65. // Make sure required fields are present and that they are base64 data.
  66. foreach($accepted_keys as $k)
  67. {
  68. if (!array_key_exists($k,$decoded)) { return false; }
  69. if (base64_decode($decoded[$k],$strict=true)==null) { return false; }
  70. }
  71. // Make sure no additionnal keys were added.
  72. if (count(array_intersect(array_keys($decoded),$accepted_keys))!=3) { return false; }
  73. // FIXME: Reject data if entropy is too low ?
  74. // Make sure some fields have a reasonable size.
  75. if (strlen($decoded['iv'])>24) return false;
  76. if (strlen($decoded['salt'])>14) return false;
  77. return true;
  78. }
  79. // Delete a paste and its discussion.
  80. // Input: $pasteid : the paste identifier.
  81. function deletePaste($pasteid)
  82. {
  83. // Delete the paste itself
  84. unlink(dataid2path($pasteid).$pasteid);
  85. // Delete discussion if it exists.
  86. $discdir = dataid2discussionpath($pasteid);
  87. if (is_dir($discdir))
  88. {
  89. // Delete all files in discussion directory
  90. $dhandle = opendir($discdir);
  91. while (false !== ($filename = readdir($dhandle)))
  92. {
  93. if (is_file($discdir.$filename)) unlink($discdir.$filename);
  94. }
  95. closedir($dhandle);
  96. // Delete the discussion directory.
  97. rmdir($discdir);
  98. }
  99. }
  100. if (!empty($_POST['data'])) // Create new paste/comment
  101. {
  102. /* POST contains:
  103. data (mandatory) = json encoded SJCL encrypted text (containing keys: iv,salt,ct)
  104. All optional data will go to meta information:
  105. expire (optional) = expiration delay (never,10min,1hour,1day,1month,1year,burn) (default:never)
  106. opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0)
  107. nickname (optional) = son encoded SJCL encrypted text nickname of author of comment (containing keys: iv,salt,ct)
  108. parentid (optional) = in discussion, which comment this comment replies to.
  109. pasteid (optional) = in discussion, which paste this comment belongs to.
  110. */
  111. header('Content-type: application/json');
  112. $error = false;
  113. // Create storage directory if it does not exist.
  114. if (!is_dir('data'))
  115. {
  116. mkdir('data',0705);
  117. file_put_contents('data/.htaccess',"Allow from none\nDeny from all\n");
  118. }
  119. // Make sure last paste from the IP address was more than 10 seconds ago.
  120. if (!trafic_limiter_canPass($_SERVER['REMOTE_ADDR']))
  121. { echo json_encode(array('status'=>1,'message'=>'Please wait 10 seconds between each post.')); exit; }
  122. // Make sure content is not too big.
  123. $data = $_POST['data'];
  124. if (strlen($data)>2000000)
  125. { echo json_encode(array('status'=>1,'message'=>'Paste is limited to 2 Mb of encrypted data.')); exit; }
  126. // Make sure format is correct.
  127. if (!validSJCL($data))
  128. { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; }
  129. // Read additional meta-information.
  130. $meta=array();
  131. // Read expiration date
  132. if (!empty($_POST['expire']))
  133. {
  134. $expire=$_POST['expire'];
  135. if ($expire=='10min') $meta['expire_date']=time()+10*60;
  136. elseif ($expire=='1hour') $meta['expire_date']=time()+60*60;
  137. elseif ($expire=='1day') $meta['expire_date']=time()+24*60*60;
  138. elseif ($expire=='1month') $meta['expire_date']=time()+30*24*60*60; // Well this is not *exactly* one month, it's 30 days.
  139. elseif ($expire=='1year') $meta['expire_date']=time()+365*24*60*60;
  140. elseif ($expire=='burn') $meta['burnafterreading']=true;
  141. }
  142. // Read open discussion flag
  143. if (!empty($_POST['opendiscussion']))
  144. {
  145. $opendiscussion = $_POST['opendiscussion'];
  146. if ($opendiscussion!='0' && $opendiscussion!='1') { $error=true; }
  147. if ($opendiscussion!='0') { $meta['opendiscussion']=true; }
  148. }
  149. // You can't have an open discussion on a "Burn after reading" paste:
  150. if (isset($meta['burnafterreading'])) unset($meta['opendiscussion']);
  151. // Optional nickname for comments
  152. if (!empty($_POST['nickname']))
  153. {
  154. $nick = $_POST['nickname'];
  155. if (!validSJCL($nick))
  156. {
  157. $error=true;
  158. }
  159. else
  160. {
  161. $meta['nickname']=$nick;
  162. // Generation of the anonymous avatar (Vizhash):
  163. // If a nickname is provided, we generate a Vizhash.
  164. // (We assume that if the user did not enter a nickname, he/she wants
  165. // to be anonymous and we will not generate the vizhash.)
  166. $vz = new vizhash16x16();
  167. $pngdata = $vz->generate($_SERVER['REMOTE_ADDR']);
  168. if ($pngdata!='') $meta['vizhash'] = 'data:image/png;base64,'.base64_encode($pngdata);
  169. // Once the avatar is generated, we do not keep the IP address, nor its hash.
  170. }
  171. }
  172. if ($error)
  173. {
  174. echo json_encode(array('status'=>1,'message'=>'Invalid data.'));
  175. exit;
  176. }
  177. // Add post date to meta.
  178. $meta['postdate']=time();
  179. // We just want a small hash to avoid collisions: Half-MD5 (64 bits) will do the trick
  180. $dataid = substr(hash('md5',$data),0,16);
  181. $is_comment = (!empty($_POST['parentid']) && !empty($_POST['pasteid'])); // Is this post a comment ?
  182. $storage = array('data'=>$data);
  183. if (count($meta)>0) $storage['meta'] = $meta; // Add meta-information only if necessary.
  184. if ($is_comment) // The user posts a comment.
  185. {
  186. $pasteid = $_POST['pasteid'];
  187. $parentid = $_POST['parentid'];
  188. if (!preg_match('/[a-f\d]{16}/',$pasteid)) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; }
  189. if (!preg_match('/[a-f\d]{16}/',$parentid)) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; }
  190. unset($storage['expire_date']); // Comment do not expire (it's the paste that expires)
  191. unset($storage['opendiscussion']);
  192. // Make sure paste exists.
  193. $storagedir = dataid2path($pasteid);
  194. if (!is_file($storagedir.$pasteid)) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; }
  195. // Make sure the discussion is opened in this paste.
  196. $paste=json_decode(file_get_contents($storagedir.$pasteid));
  197. if (!$paste->meta->opendiscussion) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; }
  198. $discdir = dataid2discussionpath($pasteid);
  199. $filename = $pasteid.'.'.$dataid.'.'.$parentid;
  200. if (!is_dir($discdir)) mkdir($discdir,$mode=0705,$recursive=true);
  201. if (is_file($discdir.$filename)) // Oups... improbable collision.
  202. {
  203. echo json_encode(array('status'=>1,'message'=>'You are unlucky. Try again.'));
  204. exit;
  205. }
  206. file_put_contents($discdir.$filename,json_encode($storage));
  207. echo json_encode(array('status'=>0,'id'=>$dataid)); // 0 = no error
  208. exit;
  209. }
  210. else // a standard paste.
  211. {
  212. $storagedir = dataid2path($dataid);
  213. if (!is_dir($storagedir)) mkdir($storagedir,$mode=0705,$recursive=true);
  214. if (is_file($storagedir.$dataid)) // Oups... improbable collision.
  215. {
  216. echo json_encode(array('status'=>1,'message'=>'You are unlucky. Try again.'));
  217. exit;
  218. }
  219. // New paste
  220. file_put_contents($storagedir.$dataid,json_encode($storage));
  221. echo json_encode(array('status'=>0,'id'=>$dataid)); // 0 = no error
  222. exit;
  223. }
  224. echo json_encode(array('status'=>1,'message'=>'Server error.'));
  225. exit;
  226. }
  227. $CIPHERDATA='';
  228. $ERRORMESSAGE='';
  229. if (!empty($_SERVER['QUERY_STRING'])) // Display an existing paste.
  230. {
  231. $dataid = $_SERVER['QUERY_STRING'];
  232. if (preg_match('/[a-f\d]{16}/',$dataid)) // Is this a valid paste identifier ?
  233. {
  234. $filename = dataid2path($dataid).$dataid;
  235. if (is_file($filename)) // Check that paste exists.
  236. {
  237. // Get the paste itself.
  238. $paste=json_decode(file_get_contents($filename));
  239. // See if paste has expired.
  240. if (isset($paste->meta->expire_date) && $paste->meta->expire_date<time())
  241. {
  242. deletePaste($dataid); // Delete the paste
  243. $ERRORMESSAGE='Paste does not exist or has expired.';
  244. }
  245. if ($ERRORMESSAGE=='') // If no error, return the paste.
  246. {
  247. // We kindly provide the remaining time before expiration (in seconds)
  248. if ($paste->meta->expire_date) $paste->meta->remaining_time = $paste->meta->expire_date - time();
  249. $messages = array($paste); // The paste itself is the first in the list of encrypted messages.
  250. // If it's a discussion, get all comments.
  251. if (property_exists($paste->meta, 'opendiscussion'))
  252. {
  253. $comments=array();
  254. $datadir = dataid2discussionpath($dataid);
  255. if (!is_dir($datadir)) mkdir($datadir,$mode=0705,$recursive=true);
  256. $dhandle = opendir($datadir);
  257. while (false !== ($filename = readdir($dhandle)))
  258. {
  259. if (is_file($datadir.$filename))
  260. {
  261. $comment=json_decode(file_get_contents($datadir.$filename));
  262. // Filename is in the form pasteid.commentid.parentid:
  263. // - pasteid is the paste this reply belongs to.
  264. // - commentid is the comment identifier itself.
  265. // - parentid is the comment this comment replies to (It can be pasteid)
  266. $items=explode('.',$filename);
  267. $comment->meta->commentid=$items[1]; // Add some meta information not contained in file.
  268. $comment->meta->parentid=$items[2];
  269. $comments[$comment->meta->postdate]=$comment; // Store in table
  270. }
  271. }
  272. closedir($dhandle);
  273. ksort($comments); // Sort comments by date, oldest first.
  274. $messages = array_merge($messages, $comments);
  275. }
  276. $CIPHERDATA = json_encode($messages);
  277. // If the paste was meant to be read only once, delete it.
  278. if (property_exists($paste->meta, 'burnafterreading')) deletePaste($dataid);
  279. }
  280. }
  281. else
  282. {
  283. $ERRORMESSAGE='Paste does not exist or has expired.';
  284. }
  285. }
  286. }
  287. require_once "lib/rain.tpl.class.php";
  288. header('Content-Type: text/html; charset=utf-8');
  289. $page = new RainTPL;
  290. $page->assign('CIPHERDATA',htmlspecialchars($CIPHERDATA,ENT_NOQUOTES)); // We escape it here because ENT_NOQUOTES can't be used in RainTPL templates.
  291. $page->assign('VERSION',$VERSION);
  292. $page->assign('ERRORMESSAGE',$ERRORMESSAGE);
  293. $page->draw('page');
  294. ?>