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.

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