zot.php 152 KB
Newer Older
1
<?php
friendica's avatar
friendica committed
2
/**
3
 * @file include/zot.php
redmatrix's avatar
redmatrix committed
4
 * @brief Hubzilla implementation of zot protocol.
friendica's avatar
friendica committed
5
6
7
8
9
10
 *
 * https://github.com/friendica/red/wiki/zot
 * https://github.com/friendica/red/wiki/Zot---A-High-Level-Overview
 *
 */

11
12
require_once('include/crypto.php');
require_once('include/items.php');
redmatrix's avatar
redmatrix committed
13
require_once('include/queue_fn.php');
redmatrix's avatar
redmatrix committed
14
require_once('include/perm_upgrade.php');
15

friendica's avatar
friendica committed
16

17
/**
18
 * @brief Generates a unique string for use as a zot guid.
19
 *
20
21
22
23
 * Generates a unique string for use as a zot guid using our DNS-based url, the
 * channel nickname and some entropy.
 * The entropy ensures uniqueness against re-installs where the same URL and
 * nickname are chosen.
friendica's avatar
doco    
friendica committed
24
 *
25
26
27
28
29
 * @note zot doesn't require this to be unique. Internally we use a whirlpool
 * hash of this guid and the signature of this guid signed with the channel
 * private key. This can be verified and should make the probability of
 * collision of the verified result negligible within the constraints of our
 * immediate universe.
friendica's avatar
doco    
friendica committed
30
 *
31
 * @param string $channel_nick a unique nickname of controlling entity
32
33
 * @returns string
 */
Zot's avatar
Zot committed
34

35
36
function zot_new_uid($channel_nick) {
	$rawstr = z_root() . '/' . $channel_nick . '.' . mt_rand();
37
	return(base64url_encode(hash('whirlpool', $rawstr, true), true));
38
39
}

friendica's avatar
friendica committed
40
/**
41
 * @brief Generates a portable hash identifier for a channel.
friendica's avatar
friendica committed
42
 *
43
44
 * Generates a portable hash identifier for the channel identified by $guid and
 * signed with $guid_sig.
friendica's avatar
friendica committed
45
 *
46
47
 * @note This ID is portable across the network but MUST be calculated locally
 * by verifying the signature and can not be trusted as an identity.
friendica's avatar
friendica committed
48
 *
49
50
 * @param string $guid
 * @param string $guid_sig
friendica's avatar
friendica committed
51
 */
Zot's avatar
Zot committed
52

53
54
function make_xchan_hash($guid, $guid_sig) {
	return base64url_encode(hash('whirlpool', $guid . $guid_sig, true));
friendica's avatar
friendica committed
55
56
}

57
/**
58
59
60
61
 * @brief Given a zot hash, return all distinct hubs.
 *
 * This function is used in building the zot discovery packet and therefore
 * should only be used by channels which are defined on this hub.
62
 *
63
64
65
 * @param string $hash - xchan_hash
 * @returns array of hubloc (hub location structures)
 *  * \b hubloc_id          int
Zot's avatar
Zot committed
66
 *  * \b hubloc_guid        char(191)
67
 *  * \b hubloc_guid_sig    text
Zot's avatar
Zot committed
68
69
 *  * \b hubloc_hash        char(191)
 *  * \b hubloc_addr        char(191)
70
71
 *  * \b hubloc_flags       int
 *  * \b hubloc_status      int
Zot's avatar
Zot committed
72
 *  * \b hubloc_url         char(191)
73
 *  * \b hubloc_url_sig     text
Zot's avatar
Zot committed
74
75
76
 *  * \b hubloc_host        char(191)
 *  * \b hubloc_callback    char(191)
 *  * \b hubloc_connect     char(191)
77
78
79
 *  * \b hubloc_sitekey     text
 *  * \b hubloc_updated     datetime
 *  * \b hubloc_connected   datetime
80
 */
81
function zot_get_hublocs($hash) {
82

83
	/* Only search for active hublocs - e.g. those that haven't been marked deleted */
84

85
	$ret = q("select * from hubloc where hubloc_hash = '%s' and hubloc_deleted = 0 order by hubloc_url ",
86
		dbesc($hash)
87
	);
88

89
	return $ret;
90
}
friendica's avatar
friendica committed
91

friendica's avatar
doco    
friendica committed
92
/**
93
 * @brief Builds a zot notification packet.
friendica's avatar
doco    
friendica committed
94
 *
95
96
 * Builds a zot notification packet that you can either store in the queue with
 * a message array or call zot_zot to immediately zot it to the other side.
friendica's avatar
friendica committed
97
 *
98
99
100
 * @param array $channel
 *   sender channel structure
 * @param string $type
Zot's avatar
Zot committed
101
 *   packet type: one of 'ping', 'pickup', 'purge', 'refresh', 'keychange', 'force_refresh', 'notify', 'auth_check'
102
103
104
105
106
107
108
109
110
 * @param array $recipients
 *   envelope information, array ( 'guid' => string, 'guid_sig' => string ); empty for public posts
 * @param string $remote_key
 *   optional public site key of target hub used to encrypt entire packet
 *   NOTE: remote_key and encrypted packets are required for 'auth_check' packets, optional for all others
 * @param string $secret
 *   random string, required for packets which require verification/callback
 *   e.g. 'pickup', 'purge', 'notify', 'auth_check'. Packet types 'ping', 'force_refresh', and 'refresh' do not require verification
 * @param string $extra
friendica's avatar
doco    
friendica committed
111
 * @returns string json encoded zot packet
friendica's avatar
friendica committed
112
 */
Zot's avatar
zot 1.2    
Zot committed
113
function zot_build_packet($channel, $type = 'notify', $recipients = null, $remote_key = null, $methods = '', $secret = null, $extra = null) {
friendica's avatar
friendica committed
114

115
116
	$sig_method = get_config('system','signature_algorithm','sha256');

Zot's avatar
zot 1.2    
Zot committed
117
	$data = [
friendica's avatar
friendica committed
118
		'type' => $type,
Zot's avatar
zot 1.2    
Zot committed
119
		'sender' => [
friendica's avatar
friendica committed
120
			'guid' => $channel['channel_guid'],
121
			'guid_sig' => base64url_encode(rsa_sign($channel['channel_guid'],$channel['channel_prvkey'],$sig_method)),
friendica's avatar
friendica committed
122
			'url' => z_root(),
123
			'url_sig' => base64url_encode(rsa_sign(z_root(),$channel['channel_prvkey'],$sig_method)),
124
			'sitekey' => get_config('system','pubkey')
Zot's avatar
zot 1.2    
Zot committed
125
		],
friendica's avatar
friendica committed
126
		'callback' => '/post',
Zot's avatar
Zot committed
127
		'version' => Zotlabs\Lib\System::get_zot_revision(),
128
129
		'encryption' => crypto_methods(),
		'signing' => signing_methods()
Zot's avatar
zot 1.2    
Zot committed
130
	];
friendica's avatar
friendica committed
131

132
133
	if ($recipients) {
		for ($x = 0; $x < count($recipients); $x ++)
134
			unset($recipients[$x]['hash']);
135

friendica's avatar
friendica committed
136
		$data['recipients'] = $recipients;
137
	}
friendica's avatar
friendica committed
138

139
	if ($secret) {
140
		$data['secret'] = preg_replace('/[^0-9a-fA-F]/','',$secret);
141
		$data['secret_sig'] = base64url_encode(rsa_sign($secret,$channel['channel_prvkey'],$sig_method));
142
	}
143

144
145
	if ($extra) {
		foreach ($extra as $k => $v)
146
147
148
			$data[$k] = $v;
	}

149
	logger('zot_build_packet: ' . print_r($data,true), LOGGER_DATA, LOG_DEBUG);
friendica's avatar
friendica committed
150

friendica's avatar
friendica committed
151
152
	// Hush-hush ultra top-secret mode

Zot's avatar
zot 1.2    
Zot committed
153
154
155
	if($remote_key) {
		$algorithm = zot_best_algorithm($methods);
		$data = crypto_encapsulate(json_encode($data),$remote_key, $algorithm);
friendica's avatar
friendica committed
156
157
158
159
160
	}

	return json_encode($data);
}

Zot's avatar
zot 1.2    
Zot committed
161
162
163
164
165
166
167
168
169
170
171
/**
 * @brief choose best encryption function from those available on both sites
 * 
 * @param string $methods
 *   comma separated list of encryption methods
 * @return string first match from our site method preferences crypto_methods() array
 * of a method which is common to both sites; or 'aes256cbc' if no matches are found.
 */

function zot_best_algorithm($methods) {

Zot's avatar
Zot committed
172
173
174
175
176
	$x = [ 'methods' => $methods, 'result' => '' ];
	call_hooks('zot_best_algorithm',$x);
	if($x['result'])
		return $x['result'];

Zot's avatar
zot 1.2    
Zot committed
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
	if($methods) {
		$x = explode(',',$methods);
		if($x) {
			$y = crypto_methods();
			if($y) {
				foreach($y as $yv) {
					$yv = trim($yv);
					if(in_array($yv,$x)) {
						return($yv);
					}
				}
			}
		}
	}

	return 'aes256cbc';
}



friendica's avatar
doco    
friendica committed
197
/**
198
199
200
 * @brief
 *
 * @see z_post_url()
friendica's avatar
doco    
friendica committed
201
 *
202
203
204
 * @param string $url
 * @param array $data
 * @return array see z_post_url() for returned data format
friendica's avatar
doco    
friendica committed
205
 */
206
207
function zot_zot($url, $data) {
	return z_post_url($url, array('data' => $data));
friendica's avatar
friendica committed
208
209
}

friendica's avatar
doco    
friendica committed
210
/**
211
212
 * @brief Refreshes after permission changed or friending, etc.
 *
Zot's avatar
zot 1.2    
Zot committed
213
214
215
 * The top half of this function is similar to \Zotlabs\Zot\Finger::run() and could potentially be
 * consolidated.
 *
216
 * zot_refresh is typically invoked when somebody has changed permissions of a channel and they are notified
217
218
 * to fetch new permissions via a finger/discovery operation. This may result in a new connection
 * (abook entry) being added to a local channel and it may result in auto-permissions being granted.
219
220
221
222
223
 *
 * Friending in zot is accomplished by sending a refresh packet to a specific channel which indicates a
 * permission change has been made by the sender which affects the target channel. The hub controlling
 * the target channel does targetted discovery (a zot-finger request requesting permissions for the local
 * channel). These are decoded here, and if necessary and abook structure (addressbook) is created to store
224
 * the permissions assigned to this channel.
225
 *
226
227
 * Initially these abook structures are created with a 'pending' flag, so that no reverse permissions are
 * implied until this is approved by the owner channel. A channel can also auto-populate permissions in
228
 * return and send back a refresh packet of its own. This is used by forum and group communication channels
229
 * so that friending and membership in the channel's "club" is automatic.
friendica's avatar
doco    
friendica committed
230
 *
friendica's avatar
doco    
friendica committed
231
232
 * @param array $them => xchan structure of sender
 * @param array $channel => local channel structure of target recipient, required for "friending" operations
233
 * @param array $force default false
friendica's avatar
doco    
friendica committed
234
 *
235
 * @returns boolean true if successful, else false
friendica's avatar
doco    
friendica committed
236
 */
Zot's avatar
zot 1.2    
Zot committed
237

238
function zot_refresh($them, $channel = null, $force = false) {
friendica's avatar
doco    
friendica committed
239

240
	if (array_key_exists('xchan_network', $them) && ($them['xchan_network'] !== 'zot')) {
friendica's avatar
friendica committed
241
242
243
244
		logger('zot_refresh: not got zot. ' . $them['xchan_name']);
		return true;
	}

245
	logger('zot_refresh: them: ' . print_r($them,true), LOGGER_DATA, LOG_DEBUG);
246
	if ($channel)
247
		logger('zot_refresh: channel: ' . print_r($channel,true), LOGGER_DATA, LOG_DEBUG);
248

249
250
	$url = null;

251
	if ($them['hubloc_url']) {
252
		$url = $them['hubloc_url'];
Zot's avatar
zot 1.2    
Zot committed
253
254
	}
	else {
255
256
		$r = null;

257
258
		// if they re-installed the server we could end up with the wrong record - pointing to the old install.
		// We'll order by reverse id to try and pick off the newest one first and hopefully end up with the
259
		// correct hubloc. If this doesn't work we may have to re-write this section to try them all.
260

261
		if(array_key_exists('xchan_addr',$them) && $them['xchan_addr']) {
262
			$r = q("select hubloc_url, hubloc_primary from hubloc where hubloc_addr = '%s' order by hubloc_id desc",
263
264
265
266
				dbesc($them['xchan_addr'])
			);
		}
		if(! $r) {
267
			$r = q("select hubloc_url, hubloc_primary from hubloc where hubloc_hash = '%s' order by hubloc_id desc",
268
269
270
				dbesc($them['xchan_hash'])
			);
		}
271

272
273
		if ($r) {
			foreach ($r as $rr) {
274
				if (intval($rr['hubloc_primary'])) {
275
276
277
278
					$url = $rr['hubloc_url'];
					break;
				}
			}
279
			if (! $url)
280
281
				$url = $r[0]['hubloc_url'];
		}
282
	}
283
	if (! $url) {
284
		logger('zot_refresh: no url');
friendica's avatar
friendica committed
285
		return false;
286
	}
287

288
289
	$token = random_string();

Zot's avatar
zot 1.2    
Zot committed
290
	$postvars = [];
291

292
293
	$postvars['token'] = $token;

294
295
296
297
298
299
	if($channel) {
		$postvars['target']     = $channel['channel_guid'];
		$postvars['target_sig'] = $channel['channel_guid_sig'];
		$postvars['key']        = $channel['channel_pubkey'];
	}

300
	if (array_key_exists('xchan_addr',$them) && $them['xchan_addr'])
301
		$postvars['address'] = $them['xchan_addr'];
302
	if (array_key_exists('xchan_hash',$them) && $them['xchan_hash'])
303
		$postvars['guid_hash'] = $them['xchan_hash'];
304
	if (array_key_exists('xchan_guid',$them) && $them['xchan_guid']
305
306
307
		&& array_key_exists('xchan_guid_sig',$them) && $them['xchan_guid_sig']) {
		$postvars['guid'] = $them['xchan_guid'];
		$postvars['guid_sig'] = $them['xchan_guid_sig'];
308

309
310
	}

friendica's avatar
friendica committed
311
	$rhs = '/.well-known/zot-info';
312

313
314
	logger('zot_refresh: ' . $url, LOGGER_DATA, LOG_INFO);

Zot's avatar
typo    
Zot committed
315

316
	$result = z_post_url($url . $rhs,$postvars);
317
318

	if ($result['success']) {
319

320
		$j = json_decode($result['body'],true);
321

322
		if (! (($j) && ($j['success']))) {
friendica's avatar
friendica committed
323
			logger('zot_refresh: result not decodable');
324
			return false;
friendica's avatar
friendica committed
325
		}
326

327
328
		logger('zot-info: ' . print_r($result,true), LOGGER_DATA, LOG_DEBUG);

329
330
331
332
		$signed_token = ((is_array($j) && array_key_exists('signed_token',$j)) ? $j['signed_token'] : null);
		if($signed_token) {
			$valid = rsa_verify('token.' . $token,base64url_decode($signed_token),$j['key']);
			if(! $valid) {
333
				logger('invalid signed token: ' . $url . $rhs, LOGGER_NORMAL, LOG_ERR);
334
335
336
337
				return false;
			}
		}
		else {
338
			logger('No signed token from '  . $url . $rhs, LOGGER_NORMAL, LOG_WARNING);
339
			return false;
340
341
		}

342
		$x = import_xchan($j, (($force) ? UPDATE_FLAGS_FORCED : UPDATE_FLAGS_UPDATED));
343

344
345
		if(! $x['success'])
			return false;
346

347
		if($channel) {
348
			if($j['permissions']['data']) {
Zot's avatar
zot 1.2    
Zot committed
349
350
				$permissions = crypto_unencapsulate(
					[
351
352
					'data' => $j['permissions']['data'],
					'key'  => $j['permissions']['key'],
Zot's avatar
Zot committed
353
					'iv'   => $j['permissions']['iv'],
Zot's avatar
zot 1.2    
Zot committed
354
355
					'alg'  => $j['permissions']['alg']
					],
Zot's avatar
Zot committed
356
					$channel['channel_prvkey']);
357
				if($permissions)
358
					$permissions = json_decode($permissions,true);
359
				logger('decrypted permissions: ' . print_r($permissions,true), LOGGER_DATA, LOG_DEBUG);
360
361
			}
			else
362
				$permissions = $j['permissions'];
363

364
365
			$connected_set = false;

friendica's avatar
friendica committed
366
			if($permissions && is_array($permissions)) {
redmatrix's avatar
redmatrix committed
367
368
				$old_read_stream_perm = get_abconfig($channel['channel_id'],$x['hash'],'their_perms','view_stream');

friendica's avatar
friendica committed
369
				foreach($permissions as $k => $v) {
redmatrix's avatar
redmatrix committed
370
					set_abconfig($channel['channel_id'],$x['hash'],'their_perms',$k,$v);
371
				}
372
			}
friendica's avatar
friendica committed
373

374
			if(array_key_exists('profile',$j) && array_key_exists('next_birthday',$j['profile'])) {
friendica's avatar
friendica committed
375
376
377
				$next_birthday = datetime_convert('UTC','UTC',$j['profile']['next_birthday']);
			}
			else {
378
				$next_birthday = NULL_DATE;
friendica's avatar
friendica committed
379
380
			}

Zot's avatar
Zot committed
381
382
383
384

			// Keep original perms to check if we need to notify them
			$previous_perms = get_all_perms($channel['channel_id'],$x['hash']);

385
			$r = q("select * from abook where abook_xchan = '%s' and abook_channel = %d and abook_self = 0 limit 1",
386
				dbesc($x['hash']),
387
				intval($channel['channel_id'])
388
389
			);

390
391
			if($r) {

392
393
				// connection exists

394
				// if the dob is the same as what we have stored (disregarding the year), keep the one
395
				// we have as we may have updated the year after sending a notification; and resetting
396
				// to the one we just received would cause us to create duplicated events.
397
398
399
400

				if(substr($r[0]['abook_dob'],5) == substr($next_birthday,5))
					$next_birthday = $r[0]['abook_dob'];

redmatrix's avatar
redmatrix committed
401
				$y = q("update abook set abook_dob = '%s'
402
					where abook_xchan = '%s' and abook_channel = %d
redmatrix's avatar
redmatrix committed
403
					and abook_self = 0 ",
Habeas Codice's avatar
Habeas Codice committed
404
					dbescdate($next_birthday),
405
					dbesc($x['hash']),
redmatrix's avatar
redmatrix committed
406
					intval($channel['channel_id'])
407
				);
408

409
410
				if(! $y)
					logger('abook update failed');
411
412
				else {
					// if we were just granted read stream permission and didn't have it before, try to pull in some posts
redmatrix's avatar
redmatrix committed
413
					if((! $old_read_stream_perm) && (intval($permissions['view_stream'])))
redmatrix's avatar
redmatrix committed
414
						Zotlabs\Daemon\Master::Summon(array('Onepoll',$r[0]['abook_id']));
415
				}
416
417
			}
			else {
418

419
				$p = \Zotlabs\Access\Permissions::connect_perms($channel['channel_id']);
redmatrix's avatar
redmatrix committed
420

421
422
				$my_perms  = $p['perms'];
				$automatic = $p['automatic'];
redmatrix's avatar
redmatrix committed
423

424
				// new connection
425

redmatrix's avatar
redmatrix committed
426
427
428
429
430
				if($my_perms) {
					foreach($my_perms as $k => $v) {
						set_abconfig($channel['channel_id'],$x['hash'],'my_perms',$k,$v);
					}
				}
431

432
433
434
435
				$closeness = get_pconfig($channel['channel_id'],'system','new_abook_closeness');
				if($closeness === false)
					$closeness = 80;

436
437
438
439
440
441
442
443
444
445
446
				$y = abook_store_lowlevel(
					[
						'abook_account'   => intval($channel['channel_account_id']),
						'abook_channel'   => intval($channel['channel_id']),
						'abook_closeness' => intval($closeness),
						'abook_xchan'     => $x['hash'],
						'abook_created'   => datetime_convert(),
						'abook_updated'   => datetime_convert(),
						'abook_dob'       => $next_birthday,
						'abook_pending'   => intval(($automatic) ? 0 : 1)
					]
447
				);
448

449
				if($y) {
450
					logger("New introduction received for {$channel['channel_name']}");
451
					$new_perms = get_all_perms($channel['channel_id'],$x['hash']);
redmatrix's avatar
redmatrix committed
452
453

					// Send a clone sync packet and a permissions update if permissions have changed
454

455
					$new_connection = q("select * from abook left join xchan on abook_xchan = xchan_hash where abook_xchan = '%s' and abook_channel = %d and abook_self = 0 order by abook_created desc limit 1",
redmatrix's avatar
redmatrix committed
456
457
						dbesc($x['hash']),
						intval($channel['channel_id'])
458
					);
redmatrix's avatar
redmatrix committed
459

460
					if($new_connection) {
redmatrix's avatar
redmatrix committed
461
						if(! \Zotlabs\Access\Permissions::PermsCompare($new_perms,$previous_perms))
redmatrix's avatar
redmatrix committed
462
							Zotlabs\Daemon\Master::Summon(array('Notifier','permission_create',$new_connection[0]['abook_id']));
Zot's avatar
zot 1.2    
Zot committed
463
464
						Zotlabs\Lib\Enotify::submit(
							[
465
466
467
							'type'       => NOTIFY_INTRO,
							'from_xchan' => $x['hash'],
							'to_xchan'   => $channel['channel_hash'],
Zot's avatar
zot 1.2    
Zot committed
468
469
470
							'link'       => z_root() . '/connedit/' . $new_connection[0]['abook_id']
							]
						);
471

472
						if(intval($permissions['view_stream'])) {
redmatrix's avatar
redmatrix committed
473
							if(intval(get_pconfig($channel['channel_id'],'perm_limits','send_stream') & PERMS_PENDING)
474
								|| (! intval($new_connection[0]['abook_pending'])))
redmatrix's avatar
redmatrix committed
475
								Zotlabs\Daemon\Master::Summon(array('Onepoll',$new_connection[0]['abook_id']));
redmatrix's avatar
redmatrix committed
476
477
						}

478
479

						/** If there is a default group for this channel, add this connection to it */
Zot's avatar
zot 1.2    
Zot committed
480

481
482
483
484
						$default_group = $channel['channel_default_group'];
						if($default_group) {
							require_once('include/group.php');
							$g = group_rec_byhash($channel['channel_id'],$default_group);
485
							if($g)
486
487
488
								group_add_member($channel['channel_id'],'',$x['hash'],$g['id']);
						}

redmatrix's avatar
redmatrix committed
489
490
491
						unset($new_connection[0]['abook_id']);
						unset($new_connection[0]['abook_account']);
						unset($new_connection[0]['abook_channel']);
redmatrix's avatar
redmatrix committed
492

493
						$abconfig = load_abconfig($channel['channel_id'],$new_connection['abook_xchan']);
redmatrix's avatar
redmatrix committed
494
495
496
						if($abconfig)
							$new_connection['abconfig'] = $abconfig;

redmatrix's avatar
redmatrix committed
497
						build_sync_packet($channel['channel_id'], array('abook' => $new_connection));
498
					}
499
				}
500
			}
501
502
503
		}
		return true;
	}
504

505
506
507
	return false;
}

friendica's avatar
doco    
friendica committed
508
/**
509
 * @brief Look up if channel is known and previously verified.
friendica's avatar
doco    
friendica committed
510
 *
511
512
513
514
 * A guid and a url, both signed by the sender, distinguish a known sender at a
 * known location.
 * This function looks these up to see if the channel is known and therefore
 * previously verified. If not, we will need to verify it.
friendica's avatar
friendica committed
515
 *
516
 * @param array $arr an associative array which must contain:
517
518
519
520
 *  * \e string \b guid => guid of conversant
 *  * \e string \b guid_sig => guid signed with conversant's private key
 *  * \e string \b url => URL of the origination hub of this communication
 *  * \e string \b url_sig => URL signed with conversant's private key
521
 * @param boolean $multiple (optional) default false
friendica's avatar
friendica committed
522
 *
523
524
 * @returns array|null null if site is blacklisted or not found, otherwise an
 *  array with an hubloc record
friendica's avatar
doco    
friendica committed
525
 */
Zot's avatar
zot 1.2    
Zot committed
526

527
function zot_gethub($arr, $multiple = false) {
friendica's avatar
got zot    
friendica committed
528

529
	if($arr['guid'] && $arr['guid_sig'] && $arr['url'] && $arr['url_sig']) {
530

531
532
		if(! check_siteallowed($arr['url'])) {
			logger('blacklisted site: ' . $arr['url']);
533
534
535
			return null;
		}

536
		$limit = (($multiple) ? '' : ' limit 1 ');
Zot's avatar
Zot committed
537
		$sitekey = ((array_key_exists('sitekey',$arr) && $arr['sitekey']) ? " and hubloc_sitekey = '" . dbesc(protect_sprintf($arr['sitekey'])) . "' " : '');
538

Zot's avatar
zot 1.2    
Zot committed
539
		$r = q("select hubloc.*, site.site_crypto from hubloc left join site on hubloc_url = site_url
540
				where hubloc_guid = '%s' and hubloc_guid_sig = '%s'
friendica's avatar
friendica committed
541
				and hubloc_url = '%s' and hubloc_url_sig = '%s'
542
				$sitekey $limit",
543
544
545
546
			dbesc($arr['guid']),
			dbesc($arr['guid_sig']),
			dbesc($arr['url']),
			dbesc($arr['url_sig'])
547
		);
548
		if($r) {
549
			logger('zot_gethub: found', LOGGER_DEBUG);
550
			return (($multiple) ? $r : $r[0]);
551
		}
552
	}
friendica's avatar
friendica committed
553
	logger('zot_gethub: not found: ' . print_r($arr,true), LOGGER_DEBUG);
554

555
	return false;
556
557
}

friendica's avatar
friendica committed
558
/**
redmatrix's avatar
typo    
redmatrix committed
559
 * @brief Registers an unknown hub.
friendica's avatar
friendica committed
560
 *
561
 * A communication has been received which has an unknown (to us) sender.
562
563
564
 * Perform discovery based on our calculated hash of the sender at the
 * origination address. This will fetch the discovery packet of the sender,
 * which contains the public key we need to verify our guid and url signatures.
friendica's avatar
friendica committed
565
 *
566
 * @param array $arr an associative array which must contain:
567
568
569
570
 *  * \e string \b guid => guid of conversant
 *  * \e string \b guid_sig => guid signed with conversant's private key
 *  * \e string \b url => URL of the origination hub of this communication
 *  * \e string \b url_sig => URL signed with conversant's private key
friendica's avatar
friendica committed
571
 *
572
 * @returns array an associative array with
573
574
 *  * \b success boolean true or false
 *  * \b message (optional) error string only if success is false
friendica's avatar
friendica committed
575
 */
Zot's avatar
zot 1.2    
Zot committed
576

577
function zot_register_hub($arr) {
friendica's avatar
friendica committed
578

Zot's avatar
zot 1.2    
Zot committed
579
	$result = [ 'success' => false ];
friendica's avatar
friendica committed
580

581
	if($arr['url'] && $arr['url_sig'] && $arr['guid'] && $arr['guid_sig']) {
friendica's avatar
friendica committed
582

583
584
		$sig_methods = ((array_key_exists('signing',$arr) && is_array($arr['signing'])) ? $arr['signing'] : [ 'sha256' ]);

friendica's avatar
friendica committed
585
		$guid_hash = make_xchan_hash($arr['guid'],$arr['guid_sig']);
friendica's avatar
friendica committed
586

friendica's avatar
friendica committed
587
588
		$url = $arr['url'] . '/.well-known/zot-info/?f=&guid_hash=' . $guid_hash;

friendica's avatar
friendica committed
589
		logger('zot_register_hub: ' . $url, LOGGER_DEBUG);
friendica's avatar
friendica committed
590
591
592

		$x = z_fetch_url($url);

593
		logger('zot_register_hub: ' . print_r($x,true), LOGGER_DATA, LOG_DEBUG);
friendica's avatar
friendica committed
594

595
		if($x['success']) {
596
			$record = json_decode($x['body'],true);
friendica's avatar
friendica committed
597

598
599
			/*
			 * We now have a key - only continue registration if our signatures are valid
friendica's avatar
friendica committed
600
601
602
603
			 * AND the guid and guid sig in the returned packet match those provided in
			 * our current communication.
			 */

604
605
606
			foreach($sig_methods as $method) {
				if((rsa_verify($arr['guid'],base64url_decode($arr['guid_sig']),$record['key'],$method))
				&& (rsa_verify($arr['url'],base64url_decode($arr['url_sig']),$record['key'],$method))
friendica's avatar
friendica committed
607
608
				&& ($arr['guid'] === $record['guid'])
				&& ($arr['guid_sig'] === $record['guid_sig'])) {
609
610
611
612
613
614
615
					$c = import_xchan($record);
					if($c['success'])
						$result['success'] = true;
				}
				else {
					logger('zot_register_hub: failure to verify returned packet using ' . $method);
				}
616
			}
617
618
		}
	}
619

friendica's avatar
friendica committed
620
	return $result;
friendica's avatar
friendica committed
621
}
friendica's avatar
friendica committed
622

friendica's avatar
friendica committed
623
/**
624
 * @brief Takes an associative array of a fetched discovery packet and updates
friendica's avatar
friendica committed
625
 *   all internal data structures which need to be updated as a result.
626
 *
friendica's avatar
friendica committed
627
628
 * @param array $arr => json_decoded discovery packet
 * @param int $ud_flags
629
 *    Determines whether to create a directory update record if any changes occur, default is UPDATE_FLAGS_UPDATED
friendica's avatar
friendica committed
630
 *    $ud_flags = UPDATE_FLAGS_FORCED indicates a forced refresh where we unconditionally create a directory update record
631
632
 *      this typically occurs once a month for each channel as part of a scheduled ping to notify the directory
 *      that the channel still exists
633
 * @param array $ud_arr
634
 *    If set [typically by update_directory_entry()] indicates a specific update table row and more particularly
635
 *    contains a particular address (ud_addr) which needs to be updated in that table.
friendica's avatar
friendica committed
636
 *
637
638
639
 * @return associative array
 *   * \e boolean \b success boolean true or false
 *   * \e string \b message (optional) error string only if success is false
friendica's avatar
friendica committed
640
 */
Zot's avatar
zot 1.2    
Zot committed
641

642
function import_xchan($arr,$ud_flags = UPDATE_FLAGS_UPDATED, $ud_arr = null) {
friendica's avatar
friendica committed
643

644
645
	call_hooks('import_xchan', $arr);

friendica's avatar
friendica committed
646
	$ret = array('success' => false);
647
	$dirmode = intval(get_config('system','directory_mode'));
friendica's avatar
friendica committed
648

friendica's avatar
update    
friendica committed
649
	$changed = false;
650
	$what = '';
friendica's avatar
friendica committed
651

652
653
654
655
656
657
	if(! (is_array($arr) && array_key_exists('success',$arr) && $arr['success'])) {
		logger('import_xchan: invalid data packet: ' . print_r($arr,true));
		$ret['message'] = t('Invalid data packet');
		return $ret;
	}

658
659
660
661
	if(! ($arr['guid'] && $arr['guid_sig'])) {
		logger('import_xchan: no identity information provided. ' . print_r($arr,true));
		return $ret;
	}
662

friendica's avatar
friendica committed
663
	$xchan_hash = make_xchan_hash($arr['guid'],$arr['guid_sig']);
664
665
	$arr['hash'] = $xchan_hash;

friendica's avatar
friendica committed
666
667
	$import_photos = false;

668
669
670
671
672
673
674
675
676
677
678
679
680
	$sig_methods = ((array_key_exists('signing',$arr) && is_array($arr['signing'])) ? $arr['signing'] : [ 'sha256' ]);
	$verified = false;

	foreach($sig_methods as $method) {
		if(! rsa_verify($arr['guid'],base64url_decode($arr['guid_sig']),$arr['key'],$method)) {
			logger('import_xchan: Unable to verify channel signature for ' . $arr['address'] . ' using ' . $method);
			continue;
		}
		else {
			$verified = true;
		}
	}
	if(! $verified) {
friendica's avatar
friendica committed
681
682
683
684
		$ret['message'] = t('Unable to verify channel signature');
		return $ret;
	}

685
686
	logger('import_xchan: ' . $xchan_hash, LOGGER_DEBUG);

friendica's avatar
friendica committed
687
688
	$r = q("select * from xchan where xchan_hash = '%s' limit 1",
		dbesc($xchan_hash)
689
	);
friendica's avatar
friendica committed
690

691
	if(! array_key_exists('connect_url', $arr))
692
693
		$arr['connect_url'] = '';

694
695
	if(strpos($arr['address'],'/') !== false)
		$arr['address'] = substr($arr['address'],0,strpos($arr['address'],'/'));
friendica's avatar
friendica committed
696

friendica's avatar
friendica committed
697
	if($r) {
698
		if($r[0]['xchan_photo_date'] != $arr['photo_updated'])
friendica's avatar
friendica committed
699
700
			$import_photos = true;

701
		// if we import an entry from a site that's not ours and either or both of us is off the grid - hide the entry.
702
		/** @TODO: check if we're the same directory realm, which would mean we are allowed to see it */
703

704
		$dirmode = get_config('system','directory_mode');
705

706
		if((($arr['site']['directory_mode'] === 'standalone') || ($dirmode & DIRECTORY_MODE_STANDALONE)) && ($arr['site']['url'] != z_root()))
707
708
			$arr['searchable'] = false;

friendica's avatar
friendica committed
709
		$hidden = (1 - intval($arr['searchable']));
710

711
		$hidden_changed = $adult_changed = $deleted_changed = $pubforum_changed = 0;
friendica's avatar
friendica committed
712

713
714
715
716
717
718
719
720
		if(intval($r[0]['xchan_hidden']) != (1 - intval($arr['searchable'])))
			$hidden_changed = 1;
		if(intval($r[0]['xchan_selfcensored']) != intval($arr['adult_content']))
			$adult_changed = 1;
		if(intval($r[0]['xchan_deleted']) != intval($arr['deleted']))
			$deleted_changed = 1;
		if(intval($r[0]['xchan_pubforum']) != intval($arr['public_forum']))
			$pubforum_changed = 1;
721

722
723
724
725
726
727
728
729
730
731
		if($arr['protocols']) {
			$protocols = implode(',',$arr['protocols']);
			if($protocols !== 'zot') {
				set_xconfig($xchan_hash,'system','protocols',$protocols);
			}
			else {
				del_xconfig($xchan_hash,'system','protocols');
			}
		}

732
733
		if(($r[0]['xchan_name_date'] != $arr['name_updated'])
			|| ($r[0]['xchan_connurl'] != $arr['connections_url'])
friendica's avatar
friendica committed
734
			|| ($r[0]['xchan_addr'] != $arr['address'])
735
			|| ($r[0]['xchan_follow'] != $arr['follow_url'])
736
			|| ($r[0]['xchan_connpage'] != $arr['connect_url'])
737
			|| ($r[0]['xchan_url'] != $arr['url'])
redmatrix's avatar
redmatrix committed
738
			|| $hidden_changed || $adult_changed || $deleted_changed || $pubforum_changed ) {
739
740
			$rup = q("update xchan set xchan_name = '%s', xchan_name_date = '%s', xchan_connurl = '%s', xchan_follow = '%s',
				xchan_connpage = '%s', xchan_hidden = %d, xchan_selfcensored = %d, xchan_deleted = %d, xchan_pubforum = %d,
741
				xchan_addr = '%s', xchan_url = '%s' where xchan_hash = '%s'",
742
				dbesc(($arr['name']) ? $arr['name'] : '-'),
743
				dbesc($arr['name_updated']),
friendica's avatar
friendica committed
744
				dbesc($arr['connections_url']),
745
746
				dbesc($arr['follow_url']),
				dbesc($arr['connect_url']),
747
748
749
750
				intval(1 - intval($arr['searchable'])),
				intval($arr['adult_content']),
				intval($arr['deleted']),
				intval($arr['public_forum']),
friendica's avatar
friendica committed
751
752
				dbesc($arr['address']),
				dbesc($arr['url']),
friendica's avatar
friendica committed
753
754
				dbesc($xchan_hash)
			);
755

756
757
			logger('import_xchan: update: existing: ' . print_r($r[0],true), LOGGER_DATA, LOG_DEBUG);
			logger('import_xchan: update: new: ' . print_r($arr,true), LOGGER_DATA, LOG_DEBUG);
758
			$what .= 'xchan ';
friendica's avatar
update    
friendica committed
759
			$changed = true;
friendica's avatar
friendica committed
760
		}
Zot's avatar
zot 1.2    
Zot committed
761
762
	}
	else {
friendica's avatar
friendica committed
763
		$import_photos = true;
764

765
766
767
		if((($arr['site']['directory_mode'] === 'standalone')
				|| ($dirmode & DIRECTORY_MODE_STANDALONE))
				&& ($arr['site']['url'] != z_root()))
768
769
			$arr['searchable'] = false;

770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
		$x = xchan_store_lowlevel(
			[
				'xchan_hash'           => $xchan_hash, 
				'xchan_guid'           => $arr['guid'],
				'xchan_guid_sig'       => $arr['guid_sig'],
				'xchan_pubkey'         => $arr['key'],
				'xchan_photo_mimetype' => $arr['photo_mimetype'],
				'xchan_photo_l'        => $arr['photo'],
				'xchan_addr'           => $arr['address'],
				'xchan_url'            => $arr['url'],
				'xchan_connurl'        => $arr['connections_url'],
				'xchan_follow'         => $arr['follow_url'],
				'xchan_connpage'       => $arr['connect_url'],
				'xchan_name'           => (($arr['name']) ? $arr['name'] : '-'),
				'xchan_network'        => 'zot',
				'xchan_photo_date'     => $arr['photo_updated'],
				'xchan_name_date'      => $arr['name_updated'],
				'xchan_hidden'         => intval(1 - intval($arr['searchable'])),
				'xchan_selfcensored'   => $arr['adult_content'],
				'xchan_deleted'        => $arr['deleted'],
				'xchan_pubforum'       => $arr['public_forum']
			]
friendica's avatar
friendica committed
792
		);
793

794
		$what .= 'new_xchan';
friendica's avatar
update    
friendica committed
795
		$changed = true;
796
	}
friendica's avatar
friendica committed
797

Zot's avatar
zot 1.2    
Zot committed
798
	if($import_photos) {
friendica's avatar
friendica committed
799

friendica's avatar
friendica committed
800
		require_once('include/photo/photo_driver.php');
friendica's avatar
friendica committed
801

friendica's avatar
friendica committed
802
803
804
805
806
		// see if this is a channel clone that's hosted locally - which we treat different from other xchans/connections

		$local = q("select channel_account_id, channel_id from channel where channel_hash = '%s' limit 1",
			dbesc($xchan_hash)
		);
Zot's avatar
zot 1.2    
Zot committed
807
		if($local) {
808
			$ph = z_fetch_url($arr['photo'], true);
Zot's avatar
zot 1.2    
Zot committed
809
			if($ph['success']) {
810

811
				$hash = import_channel_photo($ph['body'], $arr['photo_mimetype'], $local[0]['channel_account_id'], $local[0]['channel_id']);
812