33953c6a284b3253cef2901d09b595ee1f950622
[friendica.git/.git] / mod / dfrn_notify.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2020, Friendica
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  * The dfrn notify endpoint
21  *
22  * @see PDF with dfrn specs: https://github.com/friendica/friendica/blob/stable/spec/dfrn2.pdf
23  */
24
25 use Friendica\App;
26 use Friendica\Core\Logger;
27 use Friendica\Core\System;
28 use Friendica\Database\DBA;
29 use Friendica\DI;
30 use Friendica\Model\Contact;
31 use Friendica\Model\Conversation;
32 use Friendica\Model\User;
33 use Friendica\Protocol\DFRN;
34 use Friendica\Protocol\Diaspora;
35 use Friendica\Util\Network;
36 use Friendica\Util\Strings;
37
38 function dfrn_notify_post(App $a) {
39         Logger::log(__function__, Logger::TRACE);
40
41         $postdata = Network::postdata();
42
43         if (empty($_POST) || !empty($postdata)) {
44                 $data = json_decode($postdata);
45                 if (is_object($data)) {
46                         $nick = $a->argv[1] ?? '';
47
48                         $user = DBA::selectFirst('user', [], ['nickname' => $nick, 'account_expired' => false, 'account_removed' => false]);
49                         if (!DBA::isResult($user)) {
50                                 throw new \Friendica\Network\HTTPException\InternalServerErrorException();
51                         }
52                         dfrn_dispatch_private($user, $postdata);
53                 } elseif (!dfrn_dispatch_public($postdata)) {
54                         require_once 'mod/salmon.php';
55                         salmon_post($a, $postdata);
56                 }
57         }
58
59         $dfrn_id      = (!empty($_POST['dfrn_id'])      ? Strings::escapeTags(trim($_POST['dfrn_id']))   : '');
60         $dfrn_version = (!empty($_POST['dfrn_version']) ? (float) $_POST['dfrn_version']    : 2.0);
61         $challenge    = (!empty($_POST['challenge'])    ? Strings::escapeTags(trim($_POST['challenge'])) : '');
62         $data         = $_POST['data'] ?? '';
63         $key          = $_POST['key'] ?? '';
64         $rino_remote  = (!empty($_POST['rino'])         ? intval($_POST['rino'])            :  0);
65         $dissolve     = (!empty($_POST['dissolve'])     ? intval($_POST['dissolve'])        :  0);
66         $perm         = (!empty($_POST['perm'])         ? Strings::escapeTags(trim($_POST['perm']))      : 'r');
67         $ssl_policy   = (!empty($_POST['ssl_policy'])   ? Strings::escapeTags(trim($_POST['ssl_policy'])): 'none');
68         $page         = (!empty($_POST['page'])         ? intval($_POST['page'])            :  0);
69
70         $forum = (($page == 1) ? 1 : 0);
71         $prv   = (($page == 2) ? 1 : 0);
72
73         $writable = (-1);
74         if ($dfrn_version >= 2.21) {
75                 $writable = (($perm === 'rw') ? 1 : 0);
76         }
77
78         $direction = (-1);
79         if (strpos($dfrn_id, ':') == 1) {
80                 $direction = intval(substr($dfrn_id, 0, 1));
81                 $dfrn_id = substr($dfrn_id, 2);
82         }
83
84         if (!DBA::exists('challenge', ['dfrn-id' => $dfrn_id, 'challenge' => $challenge])) {
85                 Logger::log('could not match challenge to dfrn_id ' . $dfrn_id . ' challenge=' . $challenge);
86                 System::xmlExit(3, 'Could not match challenge');
87         }
88
89         DBA::delete('challenge', ['dfrn-id' => $dfrn_id, 'challenge' => $challenge]);
90
91         $user = DBA::selectFirst('user', ['uid'], ['nickname' => $a->argv[1]]);
92         if (!DBA::isResult($user)) {
93                 Logger::log('User not found for nickname ' . $a->argv[1]);
94                 System::xmlExit(3, 'User not found');
95         }
96
97         // find the local user who owns this relationship.
98         $condition = [];
99         switch ($direction) {
100                 case (-1):
101                         $condition = ["(`issued-id` = ? OR `dfrn-id` = ?) AND `uid` = ?", $dfrn_id, $dfrn_id, $user['uid']];
102                         break;
103                 case 0:
104                         $condition = ['issued-id' => $dfrn_id, 'duplex' => true, 'uid' => $user['uid']];
105                         break;
106                 case 1:
107                         $condition = ['dfrn-id' => $dfrn_id, 'duplex' => true, 'uid' => $user['uid']];
108                         break;
109                 default:
110                         System::xmlExit(3, 'Invalid direction');
111                         break; // NOTREACHED
112         }
113
114         $contact = DBA::selectFirst('contact', ['id'], $condition);
115         if (!DBA::isResult($contact)) {
116                 Logger::log('contact not found for dfrn_id ' . $dfrn_id);
117                 System::xmlExit(3, 'Contact not found');
118         }
119
120         // $importer in this case contains the contact record for the remote contact joined with the user record of our user.
121         $importer = DFRN::getImporter($contact['id'], $user['uid']);
122
123         if ((($writable != (-1)) && ($writable != $importer['writable'])) || ($importer['forum'] != $forum) || ($importer['prv'] != $prv)) {
124                 $fields = ['writable' => ($writable == (-1)) ? $importer['writable'] : $writable,
125                         'forum' => $forum, 'prv' => $prv];
126                 DBA::update('contact', $fields, ['id' => $importer['id']]);
127
128                 if ($writable != (-1)) {
129                         $importer['writable'] = $writable;
130                 }
131                 $importer['forum'] = $page;
132         }
133
134
135         // if contact's ssl policy changed, update our links
136
137         $importer = Contact::updateSslPolicy($importer, $ssl_policy);
138
139         Logger::log('data: ' . $data, Logger::DATA);
140
141         if ($dissolve == 1) {
142                 // Relationship is dissolved permanently
143                 Contact::remove($importer['id']);
144                 Logger::log('relationship dissolved : ' . $importer['name'] . ' dissolved ' . $importer['username']);
145                 System::xmlExit(0, 'relationship dissolved');
146         }
147
148         $rino = DI::config()->get('system', 'rino_encrypt');
149         $rino = intval($rino);
150
151         if (strlen($key)) {
152
153                 // if local rino is lower than remote rino, abort: should not happen!
154                 // but only for $remote_rino > 1, because old code did't send rino version
155                 if ($rino_remote > 1 && $rino < $rino_remote) {
156                         Logger::log("rino version '$rino_remote' is lower than supported '$rino'");
157                         System::xmlExit(0, "rino version '$rino_remote' is lower than supported '$rino'");
158                 }
159
160                 $rawkey = hex2bin(trim($key));
161                 Logger::log('rino: md5 raw key: ' . md5($rawkey), Logger::DATA);
162
163                 $final_key = '';
164
165                 if ($dfrn_version >= 2.1) {
166                         if (($importer['duplex'] && strlen($importer['cprvkey'])) || !strlen($importer['cpubkey'])) {
167                                 openssl_private_decrypt($rawkey, $final_key, $importer['cprvkey']);
168                         } else {
169                                 openssl_public_decrypt($rawkey, $final_key, $importer['cpubkey']);
170                         }
171                 } else {
172                         if (($importer['duplex'] && strlen($importer['cpubkey'])) || !strlen($importer['cprvkey'])) {
173                                 openssl_public_decrypt($rawkey, $final_key, $importer['cpubkey']);
174                         } else {
175                                 openssl_private_decrypt($rawkey, $final_key, $importer['cprvkey']);
176                         }
177                 }
178
179                 switch ($rino_remote) {
180                         case 0:
181                         case 1:
182                                 // we got a key. old code send only the key, without RINO version.
183                                 // we assume RINO 1 if key and no RINO version
184                                 $data = DFRN::aesDecrypt(hex2bin($data), $final_key);
185                                 break;
186                         default:
187                                 Logger::log("rino: invalid sent version '$rino_remote'");
188                                 System::xmlExit(0, "Invalid sent version '$rino_remote'");
189                 }
190
191                 Logger::log('rino: decrypted data: ' . $data, Logger::DATA);
192         }
193
194         Logger::log('Importing post from ' . $importer['addr'] . ' to ' . $importer['nickname'] . ' with the RINO ' . $rino_remote . ' encryption.', Logger::DEBUG);
195
196         $ret = DFRN::import($data, $importer, false, Conversation::PARCEL_LEGACY_DFRN);
197         System::xmlExit($ret, 'Processed');
198
199         // NOTREACHED
200 }
201
202 function dfrn_dispatch_public($postdata)
203 {
204         $msg = Diaspora::decodeRaw($postdata, '', true);
205         if (!$msg) {
206                 // We have to fail silently to be able to hand it over to the salmon parser
207                 return false;
208         }
209
210         // Fetch the corresponding public contact
211         $contact_id = Contact::getIdForURL($msg['author']);
212         if (empty($contact_id)) {
213                 Logger::log('Contact not found for address ' . $msg['author']);
214                 System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found');
215         }
216
217         $importer = DFRN::getImporter($contact_id);
218
219         // This should never fail
220         if (empty($importer)) {
221                 Logger::log('Contact not found for address ' . $msg['author']);
222                 System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found');
223         }
224
225         Logger::log('Importing post from ' . $msg['author'] . ' with the public envelope.', Logger::DEBUG);
226
227         // Now we should be able to import it
228         $ret = DFRN::import($msg['message'], $importer, false, Conversation::PARCEL_DIASPORA_DFRN);
229         System::xmlExit($ret, 'Done');
230 }
231
232 function dfrn_dispatch_private($user, $postdata)
233 {
234         $msg = Diaspora::decodeRaw($postdata, $user['prvkey'] ?? '');
235         if (!$msg) {
236                 System::xmlExit(4, 'Unable to parse message');
237         }
238
239         // Check if the user has got this contact
240         $cid = Contact::getIdForURL($msg['author'], $user['uid']);
241         if (!$cid) {
242                 // Otherwise there should be a public contact
243                 $cid = Contact::getIdForURL($msg['author']);
244                 if (!$cid) {
245                         Logger::log('Contact not found for address ' . $msg['author']);
246                         System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found');
247                 }
248         }
249
250         $importer = DFRN::getImporter($cid, $user['uid']);
251
252         // This should never fail
253         if (empty($importer)) {
254                 Logger::log('Contact not found for address ' . $msg['author']);
255                 System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found');
256         }
257
258         Logger::log('Importing post from ' . $msg['author'] . ' to ' . $user['nickname'] . ' with the private envelope.', Logger::DEBUG);
259
260         // Now we should be able to import it
261         $ret = DFRN::import($msg['message'], $importer, false, Conversation::PARCEL_DIASPORA_DFRN);
262         System::xmlExit($ret, 'Done');
263 }
264
265 function dfrn_notify_content(App $a) {
266
267         if (!empty($_GET['dfrn_id'])) {
268
269                 /*
270                  * initial communication from external contact, $direction is their direction.
271                  * If this is a duplex communication, ours will be the opposite.
272                  */
273
274                 $dfrn_id = Strings::escapeTags(trim($_GET['dfrn_id']));
275                 $rino_remote = (!empty($_GET['rino']) ? intval($_GET['rino']) : 0);
276                 $type = "";
277                 $last_update = "";
278
279                 Logger::log('new notification dfrn_id=' . $dfrn_id);
280
281                 $direction = (-1);
282                 if (strpos($dfrn_id,':') == 1) {
283                         $direction = intval(substr($dfrn_id,0,1));
284                         $dfrn_id = substr($dfrn_id,2);
285                 }
286
287                 $hash = Strings::getRandomHex();
288
289                 $status = 0;
290
291                 DBA::delete('challenge', ["`expire` < ?", time()]);
292
293                 $fields = ['challenge' => $hash, 'dfrn-id' => $dfrn_id, 'expire' => time() + 90,
294                         'type' => $type, 'last_update' => $last_update];
295                 DBA::insert('challenge', $fields);
296
297                 Logger::log('challenge=' . $hash, Logger::DATA);
298
299                 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $a->argv[1]]);
300                 if (!DBA::isResult($user)) {
301                         Logger::log('User not found for nickname ' . $a->argv[1]);
302                         exit();
303                 }
304
305                 $condition = [];
306                 switch ($direction) {
307                         case (-1):
308                                 $condition = ["(`issued-id` = ? OR `dfrn-id` = ?) AND `uid` = ?", $dfrn_id, $dfrn_id, $user['uid']];
309                                 $my_id = $dfrn_id;
310                                 break;
311                         case 0:
312                                 $condition = ['issued-id' => $dfrn_id, 'duplex' => true, 'uid' => $user['uid']];
313                                 $my_id = '1:' . $dfrn_id;
314                                 break;
315                         case 1:
316                                 $condition = ['dfrn-id' => $dfrn_id, 'duplex' => true, 'uid' => $user['uid']];
317                                 $my_id = '0:' . $dfrn_id;
318                                 break;
319                         default:
320                                 $status = 1;
321                                 $my_id = '';
322                                 break;
323                 }
324
325                 $contact = DBA::selectFirst('contact', ['id'], $condition);
326                 if (!DBA::isResult($contact)) {
327                         Logger::log('contact not found for dfrn_id ' . $dfrn_id);
328                         System::xmlExit(3, 'Contact not found');
329                 }
330
331                 // $importer in this case contains the contact record for the remote contact joined with the user record of our user.
332                 $importer = DFRN::getImporter($contact['id'], $user['uid']);
333                 if (empty($importer)) {
334                         Logger::log('No importer data found for user ' . $a->argv[1] . ' and contact ' . $dfrn_id);
335                         exit();
336                 }
337
338                 Logger::log("Remote rino version: ".$rino_remote." for ".$importer["url"], Logger::DATA);
339
340                 $challenge    = '';
341                 $encrypted_id = '';
342                 $id_str       = $my_id . '.' . mt_rand(1000,9999);
343
344                 $prv_key = trim($importer['cprvkey']);
345                 $pub_key = trim($importer['cpubkey']);
346                 $dplx    = intval($importer['duplex']);
347
348                 if (($dplx && strlen($prv_key)) || (strlen($prv_key) && !strlen($pub_key))) {
349                         openssl_private_encrypt($hash, $challenge, $prv_key);
350                         openssl_private_encrypt($id_str, $encrypted_id, $prv_key);
351                 } elseif (strlen($pub_key)) {
352                         openssl_public_encrypt($hash, $challenge, $pub_key);
353                         openssl_public_encrypt($id_str, $encrypted_id, $pub_key);
354                 } else {
355                         /// @TODO these kind of else-blocks are making the code harder to understand
356                         $status = 1;
357                 }
358
359                 $challenge    = bin2hex($challenge);
360                 $encrypted_id = bin2hex($encrypted_id);
361
362
363                 $rino = DI::config()->get('system', 'rino_encrypt');
364                 $rino = intval($rino);
365
366                 Logger::log("Local rino version: ". $rino, Logger::DATA);
367
368                 // if requested rino is lower than enabled local rino, lower local rino version
369                 // if requested rino is higher than enabled local rino, reply with local rino
370                 if ($rino_remote < $rino) {
371                         $rino = $rino_remote;
372                 }
373
374                 if (($importer['rel'] && ($importer['rel'] != Contact::SHARING)) || ($importer['page-flags'] == User::PAGE_FLAGS_COMMUNITY)) {
375                         $perm = 'rw';
376                 } else {
377                         $perm = 'r';
378                 }
379
380                 header("Content-type: text/xml");
381
382                 echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n"
383                         . '<dfrn_notify>' . "\r\n"
384                         . "\t" . '<status>' . $status . '</status>' . "\r\n"
385                         . "\t" . '<dfrn_version>' . DFRN_PROTOCOL_VERSION . '</dfrn_version>' . "\r\n"
386                         . "\t" . '<rino>' . $rino . '</rino>' . "\r\n"
387                         . "\t" . '<perm>' . $perm . '</perm>' . "\r\n"
388                         . "\t" . '<dfrn_id>' . $encrypted_id . '</dfrn_id>' . "\r\n"
389                         . "\t" . '<challenge>' . $challenge . '</challenge>' . "\r\n"
390                         . '</dfrn_notify>' . "\r\n";
391
392                 exit();
393         }
394 }