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.

376 lines
12KB

  1. <?php
  2. /**
  3. * @file mod/dfrn_notify.php
  4. * @brief The dfrn notify endpoint
  5. * @see PDF with dfrn specs: https://github.com/friendica/friendica/blob/master/spec/dfrn2.pdf
  6. */
  7. use Friendica\App;
  8. use Friendica\Core\Config;
  9. use Friendica\Core\Logger;
  10. use Friendica\Core\System;
  11. use Friendica\Database\DBA;
  12. use Friendica\Model\Contact;
  13. use Friendica\Protocol\DFRN;
  14. use Friendica\Protocol\Diaspora;
  15. use Friendica\Util\Strings;
  16. function dfrn_notify_post(App $a) {
  17. Logger::log(__function__, Logger::TRACE);
  18. $postdata = file_get_contents('php://input');
  19. if (empty($_POST) || !empty($postdata)) {
  20. $data = json_decode($postdata);
  21. if (is_object($data)) {
  22. $nick = defaults($a->argv, 1, '');
  23. $user = DBA::selectFirst('user', [], ['nickname' => $nick, 'account_expired' => false, 'account_removed' => false]);
  24. if (!DBA::isResult($user)) {
  25. System::httpExit(500);
  26. }
  27. dfrn_dispatch_private($user, $postdata);
  28. } elseif (!dfrn_dispatch_public($postdata)) {
  29. require_once 'mod/salmon.php';
  30. salmon_post($a, $postdata);
  31. }
  32. }
  33. $dfrn_id = (!empty($_POST['dfrn_id']) ? Strings::escapeTags(trim($_POST['dfrn_id'])) : '');
  34. $dfrn_version = (!empty($_POST['dfrn_version']) ? (float) $_POST['dfrn_version'] : 2.0);
  35. $challenge = (!empty($_POST['challenge']) ? Strings::escapeTags(trim($_POST['challenge'])) : '');
  36. $data = defaults($_POST, 'data', '');
  37. $key = defaults($_POST, 'key', '');
  38. $rino_remote = (!empty($_POST['rino']) ? intval($_POST['rino']) : 0);
  39. $dissolve = (!empty($_POST['dissolve']) ? intval($_POST['dissolve']) : 0);
  40. $perm = (!empty($_POST['perm']) ? Strings::escapeTags(trim($_POST['perm'])) : 'r');
  41. $ssl_policy = (!empty($_POST['ssl_policy']) ? Strings::escapeTags(trim($_POST['ssl_policy'])): 'none');
  42. $page = (!empty($_POST['page']) ? intval($_POST['page']) : 0);
  43. $forum = (($page == 1) ? 1 : 0);
  44. $prv = (($page == 2) ? 1 : 0);
  45. $writable = (-1);
  46. if ($dfrn_version >= 2.21) {
  47. $writable = (($perm === 'rw') ? 1 : 0);
  48. }
  49. $direction = (-1);
  50. if (strpos($dfrn_id, ':') == 1) {
  51. $direction = intval(substr($dfrn_id, 0, 1));
  52. $dfrn_id = substr($dfrn_id, 2);
  53. }
  54. if (!DBA::exists('challenge', ['dfrn-id' => $dfrn_id, 'challenge' => $challenge])) {
  55. Logger::log('could not match challenge to dfrn_id ' . $dfrn_id . ' challenge=' . $challenge);
  56. System::xmlExit(3, 'Could not match challenge');
  57. }
  58. DBA::delete('challenge', ['dfrn-id' => $dfrn_id, 'challenge' => $challenge]);
  59. $user = DBA::selectFirst('user', ['uid'], ['nickname' => $a->argv[1]]);
  60. if (!DBA::isResult($user)) {
  61. Logger::log('User not found for nickname ' . $a->argv[1]);
  62. System::xmlExit(3, 'User not found');
  63. }
  64. // find the local user who owns this relationship.
  65. $condition = [];
  66. switch ($direction) {
  67. case (-1):
  68. $condition = ["(`issued-id` = ? OR `dfrn-id` = ?) AND `uid` = ?", $dfrn_id, $dfrn_id, $user['uid']];
  69. break;
  70. case 0:
  71. $condition = ['issued-id' => $dfrn_id, 'duplex' => true, 'uid' => $user['uid']];
  72. break;
  73. case 1:
  74. $condition = ['dfrn-id' => $dfrn_id, 'duplex' => true, 'uid' => $user['uid']];
  75. break;
  76. default:
  77. System::xmlExit(3, 'Invalid direction');
  78. break; // NOTREACHED
  79. }
  80. $contact = DBA::selectFirst('contact', ['id'], $condition);
  81. if (!DBA::isResult($contact)) {
  82. Logger::log('contact not found for dfrn_id ' . $dfrn_id);
  83. System::xmlExit(3, 'Contact not found');
  84. }
  85. // $importer in this case contains the contact record for the remote contact joined with the user record of our user.
  86. $importer = DFRN::getImporter($contact['id'], $user['uid']);
  87. if ((($writable != (-1)) && ($writable != $importer['writable'])) || ($importer['forum'] != $forum) || ($importer['prv'] != $prv)) {
  88. $fields = ['writable' => ($writable == (-1)) ? $importer['writable'] : $writable,
  89. 'forum' => $forum, 'prv' => $prv];
  90. DBA::update('contact', $fields, ['id' => $importer['id']]);
  91. if ($writable != (-1)) {
  92. $importer['writable'] = $writable;
  93. }
  94. $importer['forum'] = $page;
  95. }
  96. // if contact's ssl policy changed, update our links
  97. $importer = Contact::updateSslPolicy($importer, $ssl_policy);
  98. Logger::log('data: ' . $data, Logger::DATA);
  99. if ($dissolve == 1) {
  100. // Relationship is dissolved permanently
  101. Contact::remove($importer['id']);
  102. Logger::log('relationship dissolved : ' . $importer['name'] . ' dissolved ' . $importer['username']);
  103. System::xmlExit(0, 'relationship dissolved');
  104. }
  105. $rino = Config::get('system', 'rino_encrypt');
  106. $rino = intval($rino);
  107. if (strlen($key)) {
  108. // if local rino is lower than remote rino, abort: should not happen!
  109. // but only for $remote_rino > 1, because old code did't send rino version
  110. if ($rino_remote > 1 && $rino < $rino_remote) {
  111. Logger::log("rino version '$rino_remote' is lower than supported '$rino'");
  112. System::xmlExit(0, "rino version '$rino_remote' is lower than supported '$rino'");
  113. }
  114. $rawkey = hex2bin(trim($key));
  115. Logger::log('rino: md5 raw key: ' . md5($rawkey), Logger::DATA);
  116. $final_key = '';
  117. if ($dfrn_version >= 2.1) {
  118. if (($importer['duplex'] && strlen($importer['cprvkey'])) || !strlen($importer['cpubkey'])) {
  119. openssl_private_decrypt($rawkey, $final_key, $importer['cprvkey']);
  120. } else {
  121. openssl_public_decrypt($rawkey, $final_key, $importer['cpubkey']);
  122. }
  123. } else {
  124. if (($importer['duplex'] && strlen($importer['cpubkey'])) || !strlen($importer['cprvkey'])) {
  125. openssl_public_decrypt($rawkey, $final_key, $importer['cpubkey']);
  126. } else {
  127. openssl_private_decrypt($rawkey, $final_key, $importer['cprvkey']);
  128. }
  129. }
  130. switch ($rino_remote) {
  131. case 0:
  132. case 1:
  133. // we got a key. old code send only the key, without RINO version.
  134. // we assume RINO 1 if key and no RINO version
  135. $data = DFRN::aesDecrypt(hex2bin($data), $final_key);
  136. break;
  137. default:
  138. Logger::log("rino: invalid sent version '$rino_remote'");
  139. System::xmlExit(0, "Invalid sent version '$rino_remote'");
  140. }
  141. Logger::log('rino: decrypted data: ' . $data, Logger::DATA);
  142. }
  143. Logger::log('Importing post from ' . $importer['addr'] . ' to ' . $importer['nickname'] . ' with the RINO ' . $rino_remote . ' encryption.', Logger::DEBUG);
  144. $ret = DFRN::import($data, $importer);
  145. System::xmlExit($ret, 'Processed');
  146. // NOTREACHED
  147. }
  148. function dfrn_dispatch_public($postdata)
  149. {
  150. $msg = Diaspora::decodeRaw([], $postdata, true);
  151. if (!$msg) {
  152. // We have to fail silently to be able to hand it over to the salmon parser
  153. return false;
  154. }
  155. // Fetch the corresponding public contact
  156. $contact = Contact::getDetailsByAddr($msg['author'], 0);
  157. if (!$contact) {
  158. Logger::log('Contact not found for address ' . $msg['author']);
  159. System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found');
  160. }
  161. $importer = DFRN::getImporter($contact['id']);
  162. // This should never fail
  163. if (empty($importer)) {
  164. Logger::log('Contact not found for address ' . $msg['author']);
  165. System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found');
  166. }
  167. Logger::log('Importing post from ' . $msg['author'] . ' with the public envelope.', Logger::DEBUG);
  168. // Now we should be able to import it
  169. $ret = DFRN::import($msg['message'], $importer);
  170. System::xmlExit($ret, 'Done');
  171. }
  172. function dfrn_dispatch_private($user, $postdata)
  173. {
  174. $msg = Diaspora::decodeRaw($user, $postdata);
  175. if (!$msg) {
  176. System::xmlExit(4, 'Unable to parse message');
  177. }
  178. // Check if the user has got this contact
  179. $cid = Contact::getIdForURL($msg['author'], $user['uid']);
  180. if (!$cid) {
  181. // Otherwise there should be a public contact
  182. $cid = Contact::getIdForURL($msg['author']);
  183. if (!$cid) {
  184. Logger::log('Contact not found for address ' . $msg['author']);
  185. System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found');
  186. }
  187. }
  188. $importer = DFRN::getImporter($cid, $user['uid']);
  189. // This should never fail
  190. if (empty($importer)) {
  191. Logger::log('Contact not found for address ' . $msg['author']);
  192. System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found');
  193. }
  194. Logger::log('Importing post from ' . $msg['author'] . ' to ' . $user['nickname'] . ' with the private envelope.', Logger::DEBUG);
  195. // Now we should be able to import it
  196. $ret = DFRN::import($msg['message'], $importer);
  197. System::xmlExit($ret, 'Done');
  198. }
  199. function dfrn_notify_content(App $a) {
  200. if (!empty($_GET['dfrn_id'])) {
  201. /*
  202. * initial communication from external contact, $direction is their direction.
  203. * If this is a duplex communication, ours will be the opposite.
  204. */
  205. $dfrn_id = Strings::escapeTags(trim($_GET['dfrn_id']));
  206. $dfrn_version = (float) $_GET['dfrn_version'];
  207. $rino_remote = (!empty($_GET['rino']) ? intval($_GET['rino']) : 0);
  208. $type = "";
  209. $last_update = "";
  210. Logger::log('new notification dfrn_id=' . $dfrn_id);
  211. $direction = (-1);
  212. if (strpos($dfrn_id,':') == 1) {
  213. $direction = intval(substr($dfrn_id,0,1));
  214. $dfrn_id = substr($dfrn_id,2);
  215. }
  216. $hash = Strings::getRandomHex();
  217. $status = 0;
  218. DBA::delete('challenge', ["`expire` < ?", time()]);
  219. $fields = ['challenge' => $hash, 'dfrn-id' => $dfrn_id, 'expire' => time() + 90,
  220. 'type' => $type, 'last_update' => $last_update];
  221. DBA::insert('challenge', $fields);
  222. Logger::log('challenge=' . $hash, Logger::DATA);
  223. $user = DBA::selectFirst('user', ['uid'], ['nickname' => $a->argv[1]]);
  224. if (!DBA::isResult($user)) {
  225. Logger::log('User not found for nickname ' . $a->argv[1]);
  226. killme();
  227. }
  228. $condition = [];
  229. switch ($direction) {
  230. case (-1):
  231. $condition = ["(`issued-id` = ? OR `dfrn-id` = ?) AND `uid` = ?", $dfrn_id, $dfrn_id, $user['uid']];
  232. $my_id = $dfrn_id;
  233. break;
  234. case 0:
  235. $condition = ['issued-id' => $dfrn_id, 'duplex' => true, 'uid' => $user['uid']];
  236. $my_id = '1:' . $dfrn_id;
  237. break;
  238. case 1:
  239. $condition = ['dfrn-id' => $dfrn_id, 'duplex' => true, 'uid' => $user['uid']];
  240. $my_id = '0:' . $dfrn_id;
  241. break;
  242. default:
  243. $status = 1;
  244. break;
  245. }
  246. $contact = DBA::selectFirst('contact', ['id'], $condition);
  247. if (!DBA::isResult($contact)) {
  248. Logger::log('contact not found for dfrn_id ' . $dfrn_id);
  249. System::xmlExit(3, 'Contact not found');
  250. }
  251. // $importer in this case contains the contact record for the remote contact joined with the user record of our user.
  252. $importer = DFRN::getImporter($contact['id'], $user['uid']);
  253. if (empty($importer)) {
  254. Logger::log('No importer data found for user ' . $a->argv[1] . ' and contact ' . $dfrn_id);
  255. killme();
  256. }
  257. Logger::log("Remote rino version: ".$rino_remote." for ".$importer["url"], Logger::DATA);
  258. $challenge = '';
  259. $encrypted_id = '';
  260. $id_str = $my_id . '.' . mt_rand(1000,9999);
  261. $prv_key = trim($importer['cprvkey']);
  262. $pub_key = trim($importer['cpubkey']);
  263. $dplx = intval($importer['duplex']);
  264. if (($dplx && strlen($prv_key)) || (strlen($prv_key) && !strlen($pub_key))) {
  265. openssl_private_encrypt($hash, $challenge, $prv_key);
  266. openssl_private_encrypt($id_str, $encrypted_id, $prv_key);
  267. } elseif (strlen($pub_key)) {
  268. openssl_public_encrypt($hash, $challenge, $pub_key);
  269. openssl_public_encrypt($id_str, $encrypted_id, $pub_key);
  270. } else {
  271. /// @TODO these kind of else-blocks are making the code harder to understand
  272. $status = 1;
  273. }
  274. $challenge = bin2hex($challenge);
  275. $encrypted_id = bin2hex($encrypted_id);
  276. $rino = Config::get('system', 'rino_encrypt');
  277. $rino = intval($rino);
  278. Logger::log("Local rino version: ". $rino, Logger::DATA);
  279. // if requested rino is lower than enabled local rino, lower local rino version
  280. // if requested rino is higher than enabled local rino, reply with local rino
  281. if ($rino_remote < $rino) {
  282. $rino = $rino_remote;
  283. }
  284. if (($importer['rel'] && ($importer['rel'] != Contact::SHARING)) || ($importer['page-flags'] == Contact::PAGE_COMMUNITY)) {
  285. $perm = 'rw';
  286. } else {
  287. $perm = 'r';
  288. }
  289. header("Content-type: text/xml");
  290. echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n"
  291. . '<dfrn_notify>' . "\r\n"
  292. . "\t" . '<status>' . $status . '</status>' . "\r\n"
  293. . "\t" . '<dfrn_version>' . DFRN_PROTOCOL_VERSION . '</dfrn_version>' . "\r\n"
  294. . "\t" . '<rino>' . $rino . '</rino>' . "\r\n"
  295. . "\t" . '<perm>' . $perm . '</perm>' . "\r\n"
  296. . "\t" . '<dfrn_id>' . $encrypted_id . '</dfrn_id>' . "\r\n"
  297. . "\t" . '<challenge>' . $challenge . '</challenge>' . "\r\n"
  298. . '</dfrn_notify>' . "\r\n";
  299. killme();
  300. }
  301. }