aboutsummaryrefslogtreecommitdiffstats
path: root/mod/post.php
blob: e03e1321bf5ca0c515c0b45ef9d8be59e79da5e8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
<?php

/**
 * @file mod/post.php
 *
 * @brief Zot endpoint.
 *
 */

require_once('include/zot.php');

/**
 * @brief HTTP POST entry point for Zot.
 *
 * Most access to this endpoint is via the post method.
 * Here we will pick out the magic auth params which arrive as a get request,
 * and the only communications to arrive this way.
 *
 * Magic Auth
 * ==========
 *
 * So-called "magic auth" takes place by a special exchange. On the site where the "channel to be authenticated" lives (e.g. $mysite), 
 * a redirection is made via $mysite/magic to the zot endpoint of the remote site ($remotesite) with special GET parameters.
 *
 * The endpoint is typically  https://$remotesite/post - or whatever was specified as the callback url in prior communications
 * (we will bootstrap an address and fetch a zot info packet if possible where no prior communications exist)
 *
 * Five GET parameters are supplied:
 * * auth => the urlencoded webbie (channel@host.domain) of the channel requesting access
 * * dest => the desired destination URL (urlencoded)
 * * sec  => a random string which is also stored on $mysite for use during the verification phase. 
 * * version => the zot revision
 * * delegate => optional urlencoded webbie of a local channel to invoke delegation rights for
 *
 * When this packet is received, an "auth-check" zot message is sent to $mysite.
 * (e.g. if $_GET['auth'] is foobar@podunk.edu, a zot packet is sent to the podunk.edu zot endpoint, which is typically /post)
 * If no information has been recorded about the requesting identity a zot information packet will be retrieved before
 * continuing.
 *
 * The sender of this packet is an arbitrary/random site channel. The recipients will be a single recipient corresponding
 * to the guid and guid_sig we have associated with the requesting auth identity
 *
 * \code{.json}
 * {
 *   "type":"auth_check",
 *   "sender":{
 *     "guid":"kgVFf_...",
 *     "guid_sig":"PT9-TApz...",
 *     "url":"http:\/\/podunk.edu",
 *     "url_sig":"T8Bp7j..."
 *   },
 *   "recipients":{
 *     {
 *       "guid":"ZHSqb...",
 *       "guid_sig":"JsAAXi..."
 *     }
 *   }
 *   "callback":"\/post",
 *   "version":1,
 *   "secret":"1eaa661",
 *   "secret_sig":"eKV968b1..."
 * }
 * \endcode
 *
 * auth_check messages MUST use encapsulated encryption. This message is sent to the origination site, which checks the 'secret' to see 
 * if it is the same as the 'sec' which it passed originally. It also checks the secret_sig which is the secret signed by the 
 * destination channel's private key and base64url encoded. If everything checks out, a json packet is returned:
 *
 * \code{.json}
 * {
 *   "success":1,
 *   "confirm":"q0Ysovd1u...",
 *   "service_class":(optional)
 *   "level":(optional)
 * }
 * \endcode
 *
 * 'confirm' in this case is the base64url encoded RSA signature of the concatenation of 'secret' with the
 * base64url encoded whirlpool hash of the requestor's guid and guid_sig; signed with the source channel private key. 
 * This prevents a man-in-the-middle from inserting a rogue success packet. Upon receipt and successful 
 * verification of this packet, the destination site will redirect to the original destination URL and indicate a successful remote login. 
 * Service_class can be used by cooperating sites to provide different access rights based on account rights and subscription plans. It is 
 * a string whose contents are not defined by protocol. Example: "basic" or "gold".
 *
 * @param[in,out] App &$a
 */
function post_init(&$a) {

	if (array_key_exists('auth', $_REQUEST)) {

		$ret = array('success' => false, 'message' => '');

		logger('mod_zot: auth request received.');
		$address  = $_REQUEST['auth'];
		$desturl  = $_REQUEST['dest'];
		$sec      = $_REQUEST['sec'];
		$version  = $_REQUEST['version'];
		$delegate = $_REQUEST['delegate'];

		$test     = ((x($_REQUEST, 'test')) ? intval($_REQUEST['test']) : 0);

		// They are authenticating ultimately to the site and not to a particular channel.
		// Any channel will do, providing it's currently active. We just need to have an 
		// identity to attach to the packet we send back. So find one. 

		$c = q("select * from channel where channel_removed = 0 limit 1");

		if (! $c) {
			// nobody here
			logger('mod_zot: auth: unable to find a response channel');
			if ($test) {
				$ret['message'] .= 'no local channels found.' . EOL;
				json_return_and_die($ret);
			}

			goaway($desturl);
		}

		// Try and find a hubloc for the person attempting to auth
		$x = q("select * from hubloc left join xchan on xchan_hash = hubloc_hash where hubloc_addr = '%s' order by hubloc_id desc",
			dbesc($address)
		);

		if (! $x) {
			// finger them if they can't be found. 
			$ret = zot_finger($address, null);
			if ($ret['success']) {
				$j = json_decode($ret['body'], true);
				if ($j)
					import_xchan($j);
				$x = q("select * from hubloc left join xchan on xchan_hash = hubloc_hash where hubloc_addr = '%s' order by hubloc_id desc",
					dbesc($address)
				);
			}
		}
		if(! $x) {
			logger('mod_zot: auth: unable to finger ' . $address);

			if($test) {
				$ret['message'] .= 'no hubloc found for ' . $address . ' and probing failed.' . EOL;
				json_return_and_die($ret);
			}

			goaway($desturl);
		}


		foreach($x as $xx) {
			logger('mod_zot: auth request received from ' . $xx['hubloc_addr'] ); 

			// check credentials and access

			// If they are already authenticated and haven't changed credentials, 
			// we can save an expensive network round trip and improve performance.

			$remote = remote_channel();
			$result = null;
			$remote_service_class = '';
			$remote_level = 0;
			$remote_hub = $xx['hubloc_url'];
			$DNT = 0;

			// Also check that they are coming from the same site as they authenticated with originally.

			$already_authed = ((($remote) && ($xx['hubloc_hash'] == $remote) && ($xx['hubloc_url'] === $_SESSION['remote_hub'])) ? true : false); 
			if($delegate && $delegate !== $_SESSION['delegate_channel'])
				$already_authed = false;

			$j = array();

			if (! $already_authed) {

				// Auth packets MUST use ultra top-secret hush-hush mode - e.g. the entire packet is encrypted using the site private key
				// The actual channel sending the packet ($c[0]) is not important, but this provides a generic zot packet with a sender
				// which can be verified
 
				$p = zot_build_packet($c[0],$type = 'auth_check', array(array('guid' => $xx['hubloc_guid'],'guid_sig' => $xx['hubloc_guid_sig'])), $xx['hubloc_sitekey'], $sec);
				if ($test) {
					$ret['message'] .= 'auth check packet created using sitekey ' . $xx['hubloc_sitekey'] . EOL;
					$ret['message'] .= 'packet contents: ' . $p . EOL;
				}

				$result = zot_zot($xx['hubloc_callback'],$p);

				if (! $result['success']) {
					logger('mod_zot: auth_check callback failed.');
					if ($test) {
						$ret['message'] .= 'auth check request to your site returned .' . print_r($result, true) . EOL;
						continue;
					}
					continue;
				}
				$j = json_decode($result['body'], true);
				if (! $j) {
					logger('mod_zot: auth_check json data malformed.');
					if($test) {
						$ret['message'] .= 'json malformed: ' . $result['body'] . EOL;
						continue;
					}
				}
			}

			if ($test) {
				$ret['message'] .= 'auth check request returned .' . print_r($j, true) . EOL;
			}

			if ($already_authed || $j['success']) {
				if ($j['success']) {
					// legit response, but we do need to check that this wasn't answered by a man-in-middle
					if (! rsa_verify($sec . $xx['xchan_hash'],base64url_decode($j['confirm']),$xx['xchan_pubkey'])) {
						logger('mod_zot: auth: final confirmation failed.');
						if ($test) {
							$ret['message'] .= 'final confirmation failed. ' . $sec . print_r($j,true) . print_r($xx,true);
							continue;
						}

						continue;
					}
					if (array_key_exists('service_class',$j))
						$remote_service_class = $j['service_class'];
					if (array_key_exists('level',$j))
						$remote_level = $j['level'];
					if (array_key_exists('DNT',$j))
						$DNT = $j['DNT'];
				}
				// everything is good... maybe
				if(local_channel()) {

					// tell them to logout if they're logged in locally as anything but the target remote account
					// in which case just shut up because they don't need to be doing this at all.

					if ($a->channel['channel_hash'] != $xx['xchan_hash']) {
						logger('mod_zot: auth: already authenticated locally as somebody else.');
						notice( t('Remote authentication blocked. You are logged into this site locally. Please logout and retry.') . EOL);
						if ($test) {
							$ret['message'] .= 'already logged in locally with a conflicting identity.' . EOL;
							continue;
						}
					}
					continue;
				}

				// log them in

				if ($test) {
					$ret['success'] = true;
					$ret['message'] .= 'Authentication Success!' . EOL;
					json_return_and_die($ret);
				}

				$delegation_success = false;
				if ($delegate) {
					$r = q("select * from channel left join xchan on channel_hash = xchan_hash where xchan_addr = '%s' limit 1",
						dbesc($delegate)
					);
					if ($r && intval($r[0]['channel_id'])) {
						$allowed = perm_is_allowed($r[0]['channel_id'],$xx['xchan_hash'],'delegate');
						if ($allowed) {
							$_SESSION['delegate_channel'] = $r[0]['channel_id'];
							$_SESSION['delegate'] = $xx['xchan_hash'];
							$_SESSION['account_id'] = intval($r[0]['channel_account_id']);
							require_once('include/security.php');
							change_channel($r[0]['channel_id']);
							$delegation_success = true;
						}
					}
				}

				$_SESSION['authenticated'] = 1;
				if (! $delegation_success) {
					$_SESSION['visitor_id'] = $xx['xchan_hash'];
					$_SESSION['my_url'] = $xx['xchan_url'];
					$_SESSION['my_address'] = $address;
					$_SESSION['remote_service_class'] = $remote_service_class;
					$_SESSION['remote_level'] = $remote_level;
					$_SESSION['remote_hub'] = $remote_hub;
					$_SESSION['DNT'] = $DNT;
				}

				$arr = array('xchan' => $xx, 'url' => $desturl, 'session' => $_SESSION);
				call_hooks('magic_auth_success',$arr);
				$a->set_observer($xx);
				require_once('include/security.php');
				$a->set_groups(init_groups_visitor($_SESSION['visitor_id']));
				info(sprintf( t('Welcome %s. Remote authentication successful.'),$xx['xchan_name']));
				logger('mod_zot: auth success from ' . $xx['xchan_addr']); 
			} 
			else {
				if ($test) {
					$ret['message'] .= 'auth failure. ' . print_r($_REQUEST,true) . print_r($j,true) . EOL;
					continue;
				}
				logger('mod_zot: magic-auth failure - not authenticated: ' . $xx['xchan_addr']);
			}

			if ($test) {
				$ret['message'] .= 'auth failure fallthrough ' . print_r($_REQUEST,true) . print_r($j,true) . EOL;
				continue;
			}
		}

		/**
		 * @FIXME we really want to save the return_url in the session before we
		 * visit rmagic. This does however prevent a recursion if you visit
		 * rmagic directly, as it would otherwise send you back here again.
		 * But z_root() probably isn't where you really want to go.
		 */

		if(strstr($desturl,z_root() . '/rmagic'))
			goaway(z_root());

		if ($test) {
			json_return_and_die($ret);
		}

		goaway($desturl);
	}
}


/**
 * @brief zot communications and messaging.
 *
 * Sender HTTP posts to this endpoint ($site/post typically) with 'data' parameter set to json zot message packet.
 * This packet is optionally encrypted, which we will discover if the json has an 'iv' element.
 * $contents => array( 'alg' => 'aes256cbc', 'iv' => initialisation vector, 'key' => decryption key, 'data' => encrypted data);
 * $contents->iv and $contents->key are random strings encrypted with this site's RSA public key and then base64url encoded.
 * Currently only 'aes256cbc' is used, but this is extensible should that algorithm prove inadequate.
 *
 * Once decrypted, one will find the normal json_encoded zot message packet.
 * 
 * Defined packet types are: notify, purge, refresh, force_refresh, auth_check, ping, and pickup 
 *
 * Standard packet: (used by notify, purge, refresh, force_refresh, and auth_check)
 * \code{.json}
 * {
 *   "type": "notify",
 *   "sender":{
 *     "guid":"kgVFf_1...",
 *     "guid_sig":"PT9-TApzp...",
 *     "url":"http:\/\/podunk.edu",
 *     "url_sig":"T8Bp7j5...",
 *   },
 *   "recipients": { optional recipient array },
 *   "callback":"\/post",
 *   "version":1,
 *   "secret":"1eaa...",
 *   "secret_sig": "df89025470fac8..."
 * }
 * \endcode
 *
 * Signature fields are all signed with the sender channel private key and base64url encoded.
 * Recipients are arrays of guid and guid_sig, which were previously signed with the recipients private 
 * key and base64url encoded and later obtained via channel discovery. Absence of recipients indicates
 * a public message or visible to all potential listeners on this site.
 *
 * "pickup" packet:
 * The pickup packet is sent in response to a notify packet from another site
 * \code{.json}
 * {
 *   "type":"pickup",
 *   "url":"http:\/\/example.com",
 *   "callback":"http:\/\/example.com\/post",
 *   "callback_sig":"teE1_fLI...",
 *   "secret":"1eaa...",
 *   "secret_sig":"O7nB4_..."
 * }
 * \endcode
 *
 * In the pickup packet, the sig fields correspond to the respective data
 * element signed with this site's system private key and then base64url encoded.
 * The "secret" is the same as the original secret from the notify packet. 
 *
 * If verification is successful, a json structure is returned containing a
 * success indicator and an array of type 'pickup'.
 * Each pickup element contains the original notify request and a message field
 * whose contents are dependent on the message type.
 *
 * This JSON array is AES encapsulated using the site public key of the site
 * that sent the initial zot pickup packet.
 * Using the above example, this would be example.com.
 *
 * \code{.json}
 * {
 *   "success":1,
 *   "pickup":{
 *     "notify":{
 *       "type":"notify",
 *       "sender":{
 *         "guid":"kgVFf_...",
 *         "guid_sig":"PT9-TApz...",
 *         "url":"http:\/\/z.podunk.edu",
 *         "url_sig":"T8Bp7j5D..."
 *       },
 *       "callback":"\/post",
 *       "version":1,
 *       "secret":"1eaa661..."
 *     },
 *     "message":{
 *       "type":"activity",
 *       "message_id":"10b049ce384cbb2da9467319bc98169ab36290b8bbb403aa0c0accd9cb072e76@podunk.edu",
 *       "message_top":"10b049ce384cbb2da9467319bc98169ab36290b8bbb403aa0c0accd9cb072e76@podunk.edu",
 *       "message_parent":"10b049ce384cbb2da9467319bc98169ab36290b8bbb403aa0c0accd9cb072e76@podunk.edu",
 *       "created":"2012-11-20 04:04:16",
 *       "edited":"2012-11-20 04:04:16",
 *       "title":"",
 *       "body":"Hi Nickordo",
 *       "app":"",
 *       "verb":"post",
 *       "object_type":"",
 *       "target_type":"",
 *       "permalink":"",
 *       "location":"",
 *       "longlat":"",
 *       "owner":{
 *         "name":"Indigo",
 *         "address":"indigo@podunk.edu",
 *         "url":"http:\/\/podunk.edu",
 *         "photo":{
 *           "mimetype":"image\/jpeg",
 *           "src":"http:\/\/podunk.edu\/photo\/profile\/m\/5"
 *         },
 *         "guid":"kgVFf_...",
 *         "guid_sig":"PT9-TAp...",
 *       },
 *       "author":{
 *         "name":"Indigo",
 *         "address":"indigo@podunk.edu",
 *         "url":"http:\/\/podunk.edu",
 *         "photo":{
 *           "mimetype":"image\/jpeg",
 *           "src":"http:\/\/podunk.edu\/photo\/profile\/m\/5"
 *         },
 *         "guid":"kgVFf_...",
 *         "guid_sig":"PT9-TAp..."
 *       }
 *     }
 *   }
 * }
 * \endcode
 *
 * Currently defined message types are 'activity', 'mail', 'profile', 'location'
 * and 'channel_sync', which each have different content schemas.
 *
 * Ping packet:
 * A ping packet does not require any parameters except the type. It may or may
 * not be encrypted.
 *
 * \code{.json}
 * {
 *   "type": "ping"
 * }
 * \endcode
 *
 * On receipt of a ping packet a ping response will be returned:
 *
 * \code{.json}
 * {
 *   "success" : 1,
 *   "site" {
 *     "url": "http:\/\/podunk.edu",
 *     "url_sig": "T8Bp7j5...",
 *     "sitekey": "-----BEGIN PUBLIC KEY-----
 *                 MIICIjANBgkqhkiG9w0BAQE..."
 *   }
 * }
 * \endcode
 *
 * The ping packet can be used to verify that a site has not been re-installed, and to 
 * initiate corrective action if it has. The url_sig is signed with the site private key
 * and base64url encoded - and this should verify with the enclosed sitekey. Failure to
 * verify indicates the site is corrupt or otherwise unable to communicate using zot.
 * This return packet is not otherwise verified, so should be compared with other
 * results obtained from this site which were verified prior to taking action. For instance
 * if you have one verified result with this signature and key, and other records for this 
 * url which have different signatures and keys, it indicates that the site was re-installed
 * and corrective action may commence (remove or mark invalid any entries with different
 * signatures).
 * If you have no records which match this url_sig and key - no corrective action should
 * be taken as this packet may have been returned by an imposter.  
 *
 * @param[in,out] App &$a
 */
function post_post(&$a) {

	$encrypted_packet = false;
	$ret = array('success' => false);

	$data = json_decode($_REQUEST['data'],true);

	/*
	 * Many message packets will arrive encrypted. The existence of an 'iv'
	 * element tells us we need to unencapsulate the AES-256-CBC content using
	 * the site private key.
	 */

	if($data && array_key_exists('iv',$data)) {
		$encrypted_packet = true;
		$data = crypto_unencapsulate($data,get_config('system','prvkey'));
		logger('mod_zot: decrypt1: ' . $data, LOGGER_DATA);
		$data = json_decode($data,true);
	}

	if(! $data) {

		// possible Bleichenbacher's attack, just treat it as a 
		// message we have no handler for. It should fail a bit 
		// further along with "no hub". Our public key is public
		// knowledge. There's no reason why anybody should get the 
		// encryption wrong unless they're fishing or hacking. If 
		// they're developing and made a goof, this can be discovered 
		// in the logs of the destination site. If they're fishing or 
		// hacking, the bottom line is we can't verify their hub. 
		// That's all we're going to tell them.

		$data = array('type' => 'bogus');
	}


	$msgtype = ((array_key_exists('type',$data)) ? $data['type'] : '');

	if($msgtype === 'ping') {

		// Useful to get a health check on a remote site.
		// This will let us know if any important communication details
		// that we may have stored are no longer valid, regardless of xchan details.
		logger('POST: got ping send pong now back: ' . z_root() , LOGGER_DEBUG );
 
		$ret['success'] = true;
		$ret['site'] = array();
		$ret['site']['url'] = z_root();
		$ret['site']['url_sig'] = base64url_encode(rsa_sign(z_root(),get_config('system','prvkey')));
		$ret['site']['sitekey'] = get_config('system','pubkey');
		json_return_and_die($ret);
	}


	if($msgtype === 'pickup') {

		/*
		 * The 'pickup' message arrives with a tracking ID which is associated with a particular outq_hash
		 * First verify that that the returned signatures verify, then check that we have an outbound queue item
		 * with the correct hash.
		 * If everything verifies, find any/all outbound messages in the queue for this hubloc and send them back
		 */

		if((! $data['secret']) || (! $data['secret_sig'])) {
			$ret['message'] = 'no verification signature';
			logger('mod_zot: pickup: ' . $ret['message'], LOGGER_DEBUG);
			json_return_and_die($ret);
		}
		$r = q("select distinct hubloc_sitekey from hubloc where hubloc_url = '%s' and hubloc_callback = '%s' and hubloc_sitekey != '' group by hubloc_sitekey ",
			dbesc($data['url']),
			dbesc($data['callback'])
		);
		if(! $r) {
			$ret['message'] = 'site not found';
			logger('mod_zot: pickup: ' . $ret['message']);
			json_return_and_die($ret);
		}

		foreach ($r as $hubsite) {

			// verify the url_sig
			// If the server was re-installed at some point, there could be multiple hubs with the same url and callback.
			// Only one will have a valid key.

			$forgery = true;
			$secret_fail = true;

			$sitekey = $hubsite['hubloc_sitekey'];

			logger('mod_zot: Checking sitekey: ' . $sitekey, LOGGER_DATA);

			if(rsa_verify($data['callback'],base64url_decode($data['callback_sig']),$sitekey)) {
				$forgery = false;
			}
			if(rsa_verify($data['secret'],base64url_decode($data['secret_sig']),$sitekey)) {
				$secret_fail = false;
			}
			if((! $forgery) && (! $secret_fail))
				break;
		}

		if($forgery) {
			$ret['message'] = 'possible site forgery';
			logger('mod_zot: pickup: ' . $ret['message']);
			json_return_and_die($ret);
		}

		if($secret_fail) {
			$ret['message'] = 'secret validation failed';
			logger('mod_zot: pickup: ' . $ret['message']);
			json_return_and_die($ret);
		}

		/*
		 * If we made it to here, the signatures verify, but we still don't know if the tracking ID is valid.
		 * It wouldn't be an error if the tracking ID isn't found, because we may have sent this particular
		 * queue item with another pickup (after the tracking ID for the other pickup  was verified). 
		 */

		$r = q("select outq_posturl from outq where outq_hash = '%s' and outq_posturl = '%s' limit 1",
			dbesc($data['secret']),
			dbesc($data['callback'])
		);
		if(! $r) {
			$ret['message'] = 'nothing to pick up';
			logger('mod_zot: pickup: ' . $ret['message']);
			json_return_and_die($ret);
		}

		/*
		 * Everything is good if we made it here, so find all messages that are going to this location
		 * and send them all.
		 */

		$r = q("select * from outq where outq_posturl = '%s'",
			dbesc($data['callback'])
		);
		if($r) {
			logger('mod_zot: successful pickup message received from ' . $data['callback'] . ' ' . count($r) . ' message(s) picked up', LOGGER_DEBUG);

			$ret['success'] = true;
			$ret['pickup'] = array();
			foreach($r as $rr) {
				if($rr['outq_msg']) {
					$x = json_decode($rr['outq_msg'],true);

					if(! $x)
						continue;

					if(array_key_exists('message_list',$x)) {
						foreach($x['message_list'] as $xx) {
							$ret['pickup'][] = array('notify' => json_decode($rr['outq_notify'],true),'message' => $xx);
						}
					}
					else
						$ret['pickup'][] = array('notify' => json_decode($rr['outq_notify'],true),'message' => $x);

					$x = q("delete from outq where outq_hash = '%s'",
						dbesc($rr['outq_hash'])
					);
				}
			}
		}

		$encrypted = crypto_encapsulate(json_encode($ret),$sitekey);
		json_return_and_die($encrypted);

		/* pickup: end */
	}


	/*
	 * All other message types require us to verify the sender. This is a generic check, so we 
	 * will do it once here and bail if anything goes wrong.
	 */

	if (array_key_exists('sender',$data)) {
		$sender = $data['sender'];
	}

	/* Check if the sender is already verified here */

	$hubs = zot_gethub($sender,true);

	if (! $hubs) {

		/* Have never seen this guid or this guid coming from this location. Check it and register it. */

		// (!!) this will validate the sender
		$result = zot_register_hub($sender);

		if ((! $result['success']) || (! ($hubs = zot_gethub($sender,true)))) {
			$ret['message'] = 'Hub not available.';
			logger('mod_zot: no hub');
			json_return_and_die($ret);
		}
	}


	foreach($hubs as $hub) {

		// Update our DB to show when we last communicated successfully with this hub
		// This will allow us to prune dead hubs from using up resources

		$r = q("update hubloc set hubloc_connected = '%s' where hubloc_id = %d",
			dbesc(datetime_convert()),
			intval($hub['hubloc_id'])
		);

		// a dead hub came back to life - reset any tombstones we might have

		if(intval($hub['hubloc_error'])) {
			q("update hubloc set hubloc_error = 0 where hubloc_id = %d",
				intval($hub['hubloc_id'])		
			);
			if(intval($r[0]['hubloc_orphancheck'])) {
				q("update hubloc set hubloc_orhpancheck = 0 where hubloc_id = %d",
					intval($hub['hubloc_id'])
				);
			}
			q("update xchan set xchan_orphan = 0 where xchan_orphan = 1 and xchan_hash = '%s'",
				dbesc($hub['hubloc_hash'])
			);
		}


		/*
		 * This hub has now been proven to be valid.
		 * Any hub with the same URL and a different sitekey cannot be valid.
		 * Get rid of them (mark them deleted). There's a good chance they were re-installs.
		 */

	
//		q("update hubloc set hubloc_deleted = 1 where hubloc_url = '%s' and hubloc_sitekey != '%s' ",
//			dbesc($hub['hubloc_url']),
//			dbesc($hub['hubloc_sitekey'])
//		);

		$connecting_url = $hub['hubloc_url'];

	}

	/** @TODO check which hub is primary and take action if mismatched */

	if (array_key_exists('recipients', $data))
		$recipients = $data['recipients'];


	if ($msgtype === 'auth_check') {

		/*
		 * Requestor visits /magic/?dest=somewhere on their own site with a browser
		 * magic redirects them to $destsite/post [with auth args....]
		 * $destsite sends an auth_check packet to originator site
		 * The auth_check packet is handled here by the originator's site 
		 * - the browser session is still waiting
		 * inside $destsite/post for everything to verify
		 * If everything checks out we'll return a token to $destsite
		 * and then $destsite will verify the token, authenticate the browser
		 * session and then redirect to the original destination.
		 * If authentication fails, the redirection to the original destination
		 * will still take place but without authentication.
		 */
		logger('mod_zot: auth_check', LOGGER_DEBUG);

		if (! $encrypted_packet) {
			logger('mod_zot: auth_check packet was not encrypted.');
			$ret['message'] .= 'no packet encryption' . EOL;
			json_return_and_die($ret);
		}

		$arr = $data['sender'];
		$sender_hash = make_xchan_hash($arr['guid'],$arr['guid_sig']);

		// garbage collect any old unused notifications

		// This was and should be 10 minutes but my hosting provider has time lag between the DB and 
		// the web server. We should probably convert this to webserver time rather than DB time so 
		// that the different clocks won't affect it and allow us to keep the time short. 

		q("delete from verify where type = 'auth' and created < %s - INTERVAL %s",
			db_utcnow(), db_quoteinterval('30 MINUTE')
		);

		$y = q("select xchan_pubkey from xchan where xchan_hash = '%s' limit 1",
			dbesc($sender_hash)
		);

		// We created a unique hash in mod/magic.php when we invoked remote auth, and stored it in
		// the verify table. It is now coming back to us as 'secret' and is signed by a channel at the other end.
		// First verify their signature. We will have obtained a zot-info packet from them as part of the sender
		// verification. 

		if ((! $y) || (! rsa_verify($data['secret'], base64url_decode($data['secret_sig']),$y[0]['xchan_pubkey']))) {
			logger('mod_zot: auth_check: sender not found or secret_sig invalid.');
			$ret['message'] .= 'sender not found or sig invalid ' . print_r($y,true) . EOL;
			json_return_and_die($ret);
		}

		// There should be exactly one recipient, the original auth requestor

		$ret['message'] .= 'recipients ' . print_r($recipients,true) . EOL;

		if ($data['recipients']) {

			$arr = $data['recipients'][0];
			$recip_hash = make_xchan_hash($arr['guid'], $arr['guid_sig']);
			$c = q("select channel_id, channel_account_id, channel_prvkey from channel where channel_hash = '%s' limit 1",
				dbesc($recip_hash)
			);
			if (! $c) {
				logger('mod_zot: auth_check: recipient channel not found.');
				$ret['message'] .= 'recipient not found.' . EOL;
				json_return_and_die($ret);
			}

			$confirm = base64url_encode(rsa_sign($data['secret'] . $recip_hash,$c[0]['channel_prvkey']));

			// This additionally checks for forged sites since we already stored the expected result in meta
			// and we've already verified that this is them via zot_gethub() and that their key signed our token

			$z = q("select id from verify where channel = %d and type = 'auth' and token = '%s' and meta = '%s' limit 1",
				intval($c[0]['channel_id']),
				dbesc($data['secret']),
				dbesc($data['sender']['url'])
			);
			if (! $z) {
				logger('mod_zot: auth_check: verification key not found.');
				$ret['message'] .= 'verification key not found' . EOL;
				json_return_and_die($ret);
			}
			$r = q("delete from verify where id = %d",
				intval($z[0]['id'])
			);

			$u = q("select account_service_class from account where account_id = %d limit 1",
				intval($c[0]['channel_account_id'])
			);

			logger('mod_zot: auth_check: success', LOGGER_DEBUG);
			$ret['success'] = true;
			$ret['confirm'] = $confirm;
			if ($u && $u[0]['account_service_class'])
				$ret['service_class'] = $u[0]['account_service_class'];

			// Set "do not track" flag if this site or this channel's profile is restricted
			// in some way

			if (intval(get_config('system','block_public')))
				$ret['DNT'] = true;
			if (! perm_is_allowed($c[0]['channel_id'],'','view_profile'))
				$ret['DNT'] = true;
			if (get_pconfig($c[0]['channel_id'],'system','do_not_track'))
				$ret['DNT'] = true;
			if (get_pconfig($c[0]['channel_id'],'system','hide_online_status'))
				$ret['DNT'] = true;

			json_return_and_die($ret);
		}
		json_return_and_die($ret);
	}

	if ($msgtype === 'request') {
		// request a particular post/conversation by message_id
		$x = zot_process_message_request($data);
		json_return_and_die($x);		
	}

	if ($msgtype === 'purge') {
		if ($recipients) {
			// basically this means "unfriend"
			foreach ($recipients as $recip) {
				$r = q("select channel.*,xchan.* from channel 
					left join xchan on channel_hash = xchan_hash
					where channel_guid = '%s' and channel_guid_sig = '%s' limit 1",
					dbesc($recip['guid']),
					dbesc($recip['guid_sig'])
				);
				if ($r) {
					$r = q("select abook_id from abook where uid = %d and abook_xchan = '%s' limit 1",
						intval($r[0]['channel_id']),
						dbesc(make_xchan_hash($sender['guid'],$sender['guid_sig']))
					);
					if ($r) {
						contact_remove($r[0]['channel_id'],$r[0]['abook_id']);
					}
				}
			}
		} else {
			// Unfriend everybody - basically this means the channel has committed suicide
			$arr = $data['sender'];
			$sender_hash = make_xchan_hash($arr['guid'],$arr['guid_sig']);

			require_once('include/Contact.php');
			remove_all_xchan_resources($sender_hash);	

			$ret['success'] = true;
			json_return_and_die($ret);
		}
	}

	if (($msgtype === 'refresh') || ($msgtype === 'force_refresh')) {

		// remote channel info (such as permissions or photo or something)
		// has been updated. Grab a fresh copy and sync it.
		// The difference between refresh and force_refresh is that 
		// force_refresh unconditionally creates a directory update record,
		// even if no changes were detected upon processing.

		if ($recipients) {

			// This would be a permissions update, typically for one connection

			foreach ($recipients as $recip) {
				$r = q("select channel.*,xchan.* from channel 
					left join xchan on channel_hash = xchan_hash
					where channel_guid = '%s' and channel_guid_sig = '%s' limit 1",
					dbesc($recip['guid']),
					dbesc($recip['guid_sig'])
				);

				$x = zot_refresh(array(
						'xchan_guid'     => $sender['guid'], 
						'xchan_guid_sig' => $sender['guid_sig'],
						'hubloc_url'     => $sender['url']
				), $r[0], (($msgtype === 'force_refresh') ? true : false));
			}
		} else {

			// system wide refresh

			$x = zot_refresh(array(
				'xchan_guid'     => $sender['guid'], 
				'xchan_guid_sig' => $sender['guid_sig'],
				'hubloc_url'     => $sender['url']
			), null, (($msgtype === 'force_refresh') ? true : false));
		}
		$ret['success'] = true;
		json_return_and_die($ret);
	}

	if ($msgtype === 'notify') {

		logger('notify received from ' . $connecting_url);


		$async = get_config('system','queued_fetch');

		if ($async) {
			// add to receive queue
			// qreceive_add($data);
		} else {
			$x = zot_fetch($data);
			$ret['delivery_report'] = $x;
		}

		$ret['success'] = true;
		json_return_and_die($ret);
	}

	// catchall
	json_return_and_die($ret);
}