Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

572 lines
17 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. <?php
  2. /**
  3. * @file mod/dfrn_poll.php
  4. */
  5. use Friendica\App;
  6. use Friendica\Core\Config;
  7. use Friendica\Core\L10n;
  8. use Friendica\Core\Logger;
  9. use Friendica\Core\System;
  10. use Friendica\Core\Session;
  11. use Friendica\Database\DBA;
  12. use Friendica\Module\Login;
  13. use Friendica\Protocol\DFRN;
  14. use Friendica\Protocol\OStatus;
  15. use Friendica\Util\Network;
  16. use Friendica\Util\Strings;
  17. use Friendica\Util\XML;
  18. function dfrn_poll_init(App $a)
  19. {
  20. Login::sessionAuth();
  21. $dfrn_id = defaults($_GET, 'dfrn_id' , '');
  22. $type = defaults($_GET, 'type' , 'data');
  23. $last_update = defaults($_GET, 'last_update' , '');
  24. $destination_url = defaults($_GET, 'destination_url', '');
  25. $challenge = defaults($_GET, 'challenge' , '');
  26. $sec = defaults($_GET, 'sec' , '');
  27. $dfrn_version = (float) defaults($_GET, 'dfrn_version' , 2.0);
  28. $quiet = !empty($_GET['quiet']);
  29. // Possibly it is an OStatus compatible server that requests a user feed
  30. $user_agent = defaults($_SERVER, 'HTTP_USER_AGENT', '');
  31. if (($a->argc > 1) && ($dfrn_id == '') && !strstr($user_agent, 'Friendica')) {
  32. $nickname = $a->argv[1];
  33. header("Content-type: application/atom+xml");
  34. echo OStatus::feed($nickname, $last_update, 10);
  35. exit();
  36. }
  37. $direction = -1;
  38. if (strpos($dfrn_id, ':') == 1) {
  39. $direction = intval(substr($dfrn_id, 0, 1));
  40. $dfrn_id = substr($dfrn_id, 2);
  41. }
  42. $hidewall = false;
  43. if (($dfrn_id === '') && empty($_POST['dfrn_id'])) {
  44. if (Config::get('system', 'block_public') && !Session::isAuthenticated()) {
  45. throw new \Friendica\Network\HTTPException\ForbiddenException();
  46. }
  47. $user = '';
  48. if ($a->argc > 1) {
  49. $r = q("SELECT `hidewall`,`nickname` FROM `user` WHERE `user`.`nickname` = '%s' LIMIT 1",
  50. DBA::escape($a->argv[1])
  51. );
  52. if (!$r) {
  53. throw new \Friendica\Network\HTTPException\NotFoundException();
  54. }
  55. $hidewall = ($r[0]['hidewall'] && !local_user());
  56. $user = $r[0]['nickname'];
  57. }
  58. Logger::log('dfrn_poll: public feed request from ' . $_SERVER['REMOTE_ADDR'] . ' for ' . $user);
  59. header("Content-type: application/atom+xml");
  60. echo DFRN::feed('', $user, $last_update, 0, $hidewall);
  61. exit();
  62. }
  63. if (($type === 'profile') && (!strlen($sec))) {
  64. $sql_extra = '';
  65. switch ($direction) {
  66. case -1:
  67. $sql_extra = sprintf(" AND ( `dfrn-id` = '%s' OR `issued-id` = '%s' ) ", DBA::escape($dfrn_id), DBA::escape($dfrn_id));
  68. $my_id = $dfrn_id;
  69. break;
  70. case 0:
  71. $sql_extra = sprintf(" AND `issued-id` = '%s' AND `duplex` = 1 ", DBA::escape($dfrn_id));
  72. $my_id = '1:' . $dfrn_id;
  73. break;
  74. case 1:
  75. $sql_extra = sprintf(" AND `dfrn-id` = '%s' AND `duplex` = 1 ", DBA::escape($dfrn_id));
  76. $my_id = '0:' . $dfrn_id;
  77. break;
  78. default:
  79. $a->internalRedirect();
  80. break; // NOTREACHED
  81. }
  82. $r = q("SELECT `contact`.*, `user`.`username`, `user`.`nickname`
  83. FROM `contact` LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
  84. WHERE `contact`.`blocked` = 0 AND `contact`.`pending` = 0
  85. AND `user`.`nickname` = '%s' $sql_extra LIMIT 1",
  86. DBA::escape($a->argv[1])
  87. );
  88. if (DBA::isResult($r)) {
  89. $s = Network::fetchUrl($r[0]['poll'] . '?dfrn_id=' . $my_id . '&type=profile-check');
  90. Logger::log("dfrn_poll: old profile returns " . $s, Logger::DATA);
  91. if (strlen($s)) {
  92. $xml = XML::parseString($s);
  93. if ((int)$xml->status === 1) {
  94. $_SESSION['authenticated'] = 1;
  95. $_SESSION['visitor_id'] = $r[0]['id'];
  96. $_SESSION['visitor_home'] = $r[0]['url'];
  97. $_SESSION['visitor_handle'] = $r[0]['addr'];
  98. $_SESSION['visitor_visiting'] = $r[0]['uid'];
  99. $_SESSION['my_url'] = $r[0]['url'];
  100. Session::setVisitorsContacts();
  101. if (!$quiet) {
  102. info(L10n::t('%1$s welcomes %2$s', $r[0]['username'], $r[0]['name']) . EOL);
  103. }
  104. // Visitors get 1 day session.
  105. $session_id = session_id();
  106. $expire = time() + 86400;
  107. q("UPDATE `session` SET `expire` = '%s' WHERE `sid` = '%s'",
  108. DBA::escape($expire),
  109. DBA::escape($session_id)
  110. );
  111. }
  112. }
  113. $profile = (count($r) > 0 && isset($r[0]['nickname']) ? $r[0]['nickname'] : '');
  114. if (!empty($destination_url)) {
  115. System::externalRedirect($destination_url);
  116. } else {
  117. $a->internalRedirect('profile/' . $profile);
  118. }
  119. }
  120. $a->internalRedirect();
  121. }
  122. if ($type === 'profile-check' && $dfrn_version < 2.2) {
  123. if ((strlen($challenge)) && (strlen($sec))) {
  124. DBA::delete('profile_check', ["`expire` < ?", time()]);
  125. $r = q("SELECT * FROM `profile_check` WHERE `sec` = '%s' ORDER BY `expire` DESC LIMIT 1",
  126. DBA::escape($sec)
  127. );
  128. if (!DBA::isResult($r)) {
  129. System::xmlExit(3, 'No ticket');
  130. // NOTREACHED
  131. }
  132. $orig_id = $r[0]['dfrn_id'];
  133. if (strpos($orig_id, ':')) {
  134. $orig_id = substr($orig_id, 2);
  135. }
  136. $c = q("SELECT * FROM `contact` WHERE `id` = %d LIMIT 1",
  137. intval($r[0]['cid'])
  138. );
  139. if (!DBA::isResult($c)) {
  140. System::xmlExit(3, 'No profile');
  141. }
  142. $contact = $c[0];
  143. $sent_dfrn_id = hex2bin($dfrn_id);
  144. $challenge = hex2bin($challenge);
  145. $final_dfrn_id = '';
  146. if (($contact['duplex']) && strlen($contact['prvkey'])) {
  147. openssl_private_decrypt($sent_dfrn_id, $final_dfrn_id, $contact['prvkey']);
  148. openssl_private_decrypt($challenge, $decoded_challenge, $contact['prvkey']);
  149. } else {
  150. openssl_public_decrypt($sent_dfrn_id, $final_dfrn_id, $contact['pubkey']);
  151. openssl_public_decrypt($challenge, $decoded_challenge, $contact['pubkey']);
  152. }
  153. $final_dfrn_id = substr($final_dfrn_id, 0, strpos($final_dfrn_id, '.'));
  154. if (strpos($final_dfrn_id, ':') == 1) {
  155. $final_dfrn_id = substr($final_dfrn_id, 2);
  156. }
  157. if ($final_dfrn_id != $orig_id) {
  158. Logger::log('profile_check: ' . $final_dfrn_id . ' != ' . $orig_id, Logger::DEBUG);
  159. // did not decode properly - cannot trust this site
  160. System::xmlExit(3, 'Bad decryption');
  161. }
  162. header("Content-type: text/xml");
  163. echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?><dfrn_poll><status>0</status><challenge>$decoded_challenge</challenge><sec>$sec</sec></dfrn_poll>";
  164. exit();
  165. // NOTREACHED
  166. } else {
  167. // old protocol
  168. switch ($direction) {
  169. case 1:
  170. $dfrn_id = '0:' . $dfrn_id;
  171. break;
  172. case 0:
  173. $dfrn_id = '1:' . $dfrn_id;
  174. break;
  175. default:
  176. break;
  177. }
  178. DBA::delete('profile_check', ["`expire` < ?", time()]);
  179. $r = q("SELECT * FROM `profile_check` WHERE `dfrn_id` = '%s' ORDER BY `expire` DESC",
  180. DBA::escape($dfrn_id));
  181. if (DBA::isResult($r)) {
  182. System::xmlExit(1);
  183. return; // NOTREACHED
  184. }
  185. System::xmlExit(0);
  186. return; // NOTREACHED
  187. }
  188. }
  189. }
  190. function dfrn_poll_post(App $a)
  191. {
  192. $dfrn_id = defaults($_POST, 'dfrn_id' , '');
  193. $challenge = defaults($_POST, 'challenge', '');
  194. $url = defaults($_POST, 'url' , '');
  195. $sec = defaults($_POST, 'sec' , '');
  196. $ptype = defaults($_POST, 'type' , '');
  197. $perm = defaults($_POST, 'perm' , 'r');
  198. $dfrn_version = !empty($_POST['dfrn_version']) ? (float) $_POST['dfrn_version'] : 2.0;
  199. if ($ptype === 'profile-check') {
  200. if (strlen($challenge) && strlen($sec)) {
  201. Logger::log('dfrn_poll: POST: profile-check');
  202. DBA::delete('profile_check', ["`expire` < ?", time()]);
  203. $r = q("SELECT * FROM `profile_check` WHERE `sec` = '%s' ORDER BY `expire` DESC LIMIT 1",
  204. DBA::escape($sec)
  205. );
  206. if (!DBA::isResult($r)) {
  207. System::xmlExit(3, 'No ticket');
  208. // NOTREACHED
  209. }
  210. $orig_id = $r[0]['dfrn_id'];
  211. if (strpos($orig_id, ':')) {
  212. $orig_id = substr($orig_id, 2);
  213. }
  214. $c = q("SELECT * FROM `contact` WHERE `id` = %d LIMIT 1",
  215. intval($r[0]['cid'])
  216. );
  217. if (!DBA::isResult($c)) {
  218. System::xmlExit(3, 'No profile');
  219. }
  220. $contact = $c[0];
  221. $sent_dfrn_id = hex2bin($dfrn_id);
  222. $challenge = hex2bin($challenge);
  223. $final_dfrn_id = '';
  224. if ($contact['duplex'] && strlen($contact['prvkey'])) {
  225. openssl_private_decrypt($sent_dfrn_id, $final_dfrn_id, $contact['prvkey']);
  226. openssl_private_decrypt($challenge, $decoded_challenge, $contact['prvkey']);
  227. } else {
  228. openssl_public_decrypt($sent_dfrn_id, $final_dfrn_id, $contact['pubkey']);
  229. openssl_public_decrypt($challenge, $decoded_challenge, $contact['pubkey']);
  230. }
  231. $final_dfrn_id = substr($final_dfrn_id, 0, strpos($final_dfrn_id, '.'));
  232. if (strpos($final_dfrn_id, ':') == 1) {
  233. $final_dfrn_id = substr($final_dfrn_id, 2);
  234. }
  235. if ($final_dfrn_id != $orig_id) {
  236. Logger::log('profile_check: ' . $final_dfrn_id . ' != ' . $orig_id, Logger::DEBUG);
  237. // did not decode properly - cannot trust this site
  238. System::xmlExit(3, 'Bad decryption');
  239. }
  240. header("Content-type: text/xml");
  241. echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?><dfrn_poll><status>0</status><challenge>$decoded_challenge</challenge><sec>$sec</sec></dfrn_poll>";
  242. exit();
  243. // NOTREACHED
  244. }
  245. }
  246. $direction = -1;
  247. if (strpos($dfrn_id, ':') == 1) {
  248. $direction = intval(substr($dfrn_id, 0, 1));
  249. $dfrn_id = substr($dfrn_id, 2);
  250. }
  251. $r = q("SELECT * FROM `challenge` WHERE `dfrn-id` = '%s' AND `challenge` = '%s' LIMIT 1",
  252. DBA::escape($dfrn_id),
  253. DBA::escape($challenge)
  254. );
  255. if (!DBA::isResult($r)) {
  256. exit();
  257. }
  258. $type = $r[0]['type'];
  259. $last_update = $r[0]['last_update'];
  260. DBA::delete('challenge', ['dfrn-id' => $dfrn_id, 'challenge' => $challenge]);
  261. $sql_extra = '';
  262. switch ($direction) {
  263. case -1:
  264. $sql_extra = sprintf(" AND `issued-id` = '%s' ", DBA::escape($dfrn_id));
  265. break;
  266. case 0:
  267. $sql_extra = sprintf(" AND `issued-id` = '%s' AND `duplex` = 1 ", DBA::escape($dfrn_id));
  268. break;
  269. case 1:
  270. $sql_extra = sprintf(" AND `dfrn-id` = '%s' AND `duplex` = 1 ", DBA::escape($dfrn_id));
  271. break;
  272. default:
  273. $a->internalRedirect();
  274. break; // NOTREACHED
  275. }
  276. $r = q("SELECT * FROM `contact` WHERE `blocked` = 0 AND `pending` = 0 $sql_extra LIMIT 1");
  277. if (!DBA::isResult($r)) {
  278. exit();
  279. }
  280. $contact = $r[0];
  281. $owner_uid = $r[0]['uid'];
  282. $contact_id = $r[0]['id'];
  283. if ($type === 'reputation' && strlen($url)) {
  284. $r = q("SELECT * FROM `contact` WHERE `url` = '%s' AND `uid` = %d LIMIT 1",
  285. DBA::escape($url),
  286. intval($owner_uid)
  287. );
  288. $reputation = 0;
  289. $text = '';
  290. if (DBA::isResult($r)) {
  291. $reputation = $r[0]['rating'];
  292. $text = $r[0]['reason'];
  293. if ($r[0]['id'] == $contact_id) { // inquiring about own reputation not allowed
  294. $reputation = 0;
  295. $text = '';
  296. }
  297. }
  298. echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
  299. <reputation>
  300. <url>$url</url>
  301. <rating>$reputation</rating>
  302. <description>$text</description>
  303. </reputation>
  304. ";
  305. exit();
  306. // NOTREACHED
  307. } else {
  308. // Update the writable flag if it changed
  309. Logger::log('dfrn_poll: post request feed: ' . print_r($_POST, true), Logger::DATA);
  310. if ($dfrn_version >= 2.21) {
  311. if ($perm === 'rw') {
  312. $writable = 1;
  313. } else {
  314. $writable = 0;
  315. }
  316. if ($writable != $contact['writable']) {
  317. q("UPDATE `contact` SET `writable` = %d WHERE `id` = %d",
  318. intval($writable),
  319. intval($contact_id)
  320. );
  321. }
  322. }
  323. header("Content-type: application/atom+xml");
  324. $o = DFRN::feed($dfrn_id, $a->argv[1], $last_update, $direction);
  325. echo $o;
  326. exit();
  327. }
  328. }
  329. function dfrn_poll_content(App $a)
  330. {
  331. $dfrn_id = defaults($_GET, 'dfrn_id' , '');
  332. $type = defaults($_GET, 'type' , 'data');
  333. $last_update = defaults($_GET, 'last_update' , '');
  334. $destination_url = defaults($_GET, 'destination_url', '');
  335. $sec = defaults($_GET, 'sec' , '');
  336. $dfrn_version = !empty($_GET['dfrn_version']) ? (float) $_GET['dfrn_version'] : 2.0;
  337. $quiet = !empty($_GET['quiet']);
  338. $direction = -1;
  339. if (strpos($dfrn_id, ':') == 1) {
  340. $direction = intval(substr($dfrn_id, 0, 1));
  341. $dfrn_id = substr($dfrn_id, 2);
  342. }
  343. if ($dfrn_id != '') {
  344. // initial communication from external contact
  345. $hash = Strings::getRandomHex();
  346. $status = 0;
  347. DBA::delete('challenge', ["`expire` < ?", time()]);
  348. if ($type !== 'profile') {
  349. q("INSERT INTO `challenge` ( `challenge`, `dfrn-id`, `expire` , `type`, `last_update` )
  350. VALUES( '%s', '%s', '%s', '%s', '%s' ) ",
  351. DBA::escape($hash),
  352. DBA::escape($dfrn_id),
  353. intval(time() + 60 ),
  354. DBA::escape($type),
  355. DBA::escape($last_update)
  356. );
  357. }
  358. $sql_extra = '';
  359. switch ($direction) {
  360. case -1:
  361. if ($type === 'profile') {
  362. $sql_extra = sprintf(" AND (`dfrn-id` = '%s' OR `issued-id` = '%s') ", DBA::escape($dfrn_id), DBA::escape($dfrn_id));
  363. } else {
  364. $sql_extra = sprintf(" AND `issued-id` = '%s' ", DBA::escape($dfrn_id));
  365. }
  366. $my_id = $dfrn_id;
  367. break;
  368. case 0:
  369. $sql_extra = sprintf(" AND `issued-id` = '%s' AND `duplex` = 1 ", DBA::escape($dfrn_id));
  370. $my_id = '1:' . $dfrn_id;
  371. break;
  372. case 1:
  373. $sql_extra = sprintf(" AND `dfrn-id` = '%s' AND `duplex` = 1 ", DBA::escape($dfrn_id));
  374. $my_id = '0:' . $dfrn_id;
  375. break;
  376. default:
  377. $a->internalRedirect();
  378. break; // NOTREACHED
  379. }
  380. $nickname = $a->argv[1];
  381. $r = q("SELECT `contact`.*, `user`.`username`, `user`.`nickname`
  382. FROM `contact` LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
  383. WHERE `contact`.`blocked` = 0 AND `contact`.`pending` = 0
  384. AND `user`.`nickname` = '%s' $sql_extra LIMIT 1",
  385. DBA::escape($nickname)
  386. );
  387. if (DBA::isResult($r)) {
  388. $challenge = '';
  389. $encrypted_id = '';
  390. $id_str = $my_id . '.' . mt_rand(1000, 9999);
  391. if (($r[0]['duplex'] && strlen($r[0]['pubkey'])) || !strlen($r[0]['prvkey'])) {
  392. openssl_public_encrypt($hash, $challenge, $r[0]['pubkey']);
  393. openssl_public_encrypt($id_str, $encrypted_id, $r[0]['pubkey']);
  394. } else {
  395. openssl_private_encrypt($hash, $challenge, $r[0]['prvkey']);
  396. openssl_private_encrypt($id_str, $encrypted_id, $r[0]['prvkey']);
  397. }
  398. $challenge = bin2hex($challenge);
  399. $encrypted_id = bin2hex($encrypted_id);
  400. } else {
  401. $status = 1;
  402. $challenge = '';
  403. $encrypted_id = '';
  404. }
  405. if (($type === 'profile') && (strlen($sec))) {
  406. // heluecht: I don't know why we don't fail immediately when the user or contact hadn't been found.
  407. // Since it doesn't make sense to continue from this point on, we now fail here. This should be safe.
  408. if (!DBA::isResult($r)) {
  409. throw new \Friendica\Network\HTTPException\NotFoundException();
  410. }
  411. // URL reply
  412. if ($dfrn_version < 2.2) {
  413. $s = Network::fetchUrl($r[0]['poll']
  414. . '?dfrn_id=' . $encrypted_id
  415. . '&type=profile-check'
  416. . '&dfrn_version=' . DFRN_PROTOCOL_VERSION
  417. . '&challenge=' . $challenge
  418. . '&sec=' . $sec
  419. );
  420. } else {
  421. $s = Network::post($r[0]['poll'], [
  422. 'dfrn_id' => $encrypted_id,
  423. 'type' => 'profile-check',
  424. 'dfrn_version' => DFRN_PROTOCOL_VERSION,
  425. 'challenge' => $challenge,
  426. 'sec' => $sec
  427. ])->getBody();
  428. }
  429. Logger::log("dfrn_poll: sec profile: " . $s, Logger::DATA);
  430. if (strlen($s) && strstr($s, '<?xml')) {
  431. $xml = XML::parseString($s);
  432. Logger::log('dfrn_poll: profile: parsed xml: ' . print_r($xml, true), Logger::DATA);
  433. Logger::log('dfrn_poll: secure profile: challenge: ' . $xml->challenge . ' expecting ' . $hash);
  434. Logger::log('dfrn_poll: secure profile: sec: ' . $xml->sec . ' expecting ' . $sec);
  435. if (((int) $xml->status == 0) && ($xml->challenge == $hash) && ($xml->sec == $sec)) {
  436. $_SESSION['authenticated'] = 1;
  437. $_SESSION['visitor_id'] = $r[0]['id'];
  438. $_SESSION['visitor_home'] = $r[0]['url'];
  439. $_SESSION['visitor_visiting'] = $r[0]['uid'];
  440. $_SESSION['my_url'] = $r[0]['url'];
  441. Session::setVisitorsContacts();
  442. if (!$quiet) {
  443. info(L10n::t('%1$s welcomes %2$s', $r[0]['username'], $r[0]['name']) . EOL);
  444. }
  445. // Visitors get 1 day session.
  446. $session_id = session_id();
  447. $expire = time() + 86400;
  448. q("UPDATE `session` SET `expire` = '%s' WHERE `sid` = '%s'",
  449. DBA::escape($expire),
  450. DBA::escape($session_id)
  451. );
  452. }
  453. }
  454. $profile = ((DBA::isResult($r) && $r[0]['nickname']) ? $r[0]['nickname'] : $nickname);
  455. switch ($destination_url) {
  456. case 'profile':
  457. $a->internalRedirect('profile/' . $profile . '?f=&tab=profile');
  458. break;
  459. case 'photos':
  460. $a->internalRedirect('photos/' . $profile);
  461. break;
  462. case 'status':
  463. case '':
  464. $a->internalRedirect('profile/' . $profile);
  465. break;
  466. default:
  467. $appendix = (strstr($destination_url, '?') ? '&f=&redir=1' : '?f=&redir=1');
  468. $a->redirect($destination_url . $appendix);
  469. break;
  470. }
  471. // NOTREACHED
  472. } else {
  473. // XML reply
  474. header("Content-type: text/xml");
  475. echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n"
  476. . '<dfrn_poll>' . "\r\n"
  477. . "\t" . '<status>' . $status . '</status>' . "\r\n"
  478. . "\t" . '<dfrn_version>' . DFRN_PROTOCOL_VERSION . '</dfrn_version>' . "\r\n"
  479. . "\t" . '<dfrn_id>' . $encrypted_id . '</dfrn_id>' . "\r\n"
  480. . "\t" . '<challenge>' . $challenge . '</challenge>' . "\r\n"
  481. . '</dfrn_poll>' . "\r\n";
  482. exit();
  483. // NOTREACHED
  484. }
  485. }
  486. }