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