/** * @brief * * @param array $argv * @param array $argc */ function directory_run($argv, $argc) { cli_startup(); if ($argc < 2) { return; } $force = false; $pushall = true; if ($argc > 2) { if ($argv[2] === 'force') { $force = true; } if ($argv[2] === 'nopush') { $pushall = false; } } logger('directory update', LOGGER_DEBUG); $dirmode = get_config('system', 'directory_mode'); if ($dirmode === false) { $dirmode = DIRECTORY_MODE_NORMAL; } $x = q("select * from channel where channel_id = %d limit 1", intval($argv[1])); if (!$x) { return; } $channel = $x[0]; if ($dirmode != DIRECTORY_MODE_NORMAL) { // this is an in-memory update and we don't need to send a network packet. local_dir_update($argv[1], $force); q("update channel set channel_dirdate = '%s' where channel_id = %d", dbesc(datetime_convert()), intval($channel['channel_id'])); // Now update all the connections if ($pushall) { proc_run('php', 'include/notifier.php', 'refresh_all', $channel['channel_id']); } return; } // otherwise send the changes upstream $directory = find_upstream_directory($dirmode); $url = $directory['url'] . '/post'; // ensure the upstream directory is updated $packet = zot_build_packet($channel, $force ? 'force_refresh' : 'refresh'); $z = zot_zot($url, $packet); // re-queue if unsuccessful if (!$z['success']) { /** @FIXME we aren't updating channel_dirdate if we have to queue * the directory packet. That means we'll try again on the next poll run. */ $hash = random_string(); q("insert into outq ( outq_hash, outq_account, outq_channel, outq_driver, outq_posturl, outq_async, outq_created, outq_updated, outq_notify, outq_msg ) \n\t\t\tvalues ( '%s', %d, %d, '%s', '%s', %d, '%s', '%s', '%s', '%s' )", dbesc($hash), intval($channel['channel_account_id']), intval($channel['channel_id']), dbesc('zot'), dbesc($url), intval(1), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc($packet), dbesc('')); } else { q("update channel set channel_dirdate = '%s' where channel_id = %d", dbesc(datetime_convert()), intval($channel['channel_id'])); } // Now update all the connections if ($pushall) { proc_run('php', 'include/notifier.php', 'refresh_all', $channel['channel_id']); } }
public static function run($argc, $argv) { if ($argc < 2) { return; } $force = false; $pushall = true; if ($argc > 2) { if ($argv[2] === 'force') { $force = true; } if ($argv[2] === 'nopush') { $pushall = false; } } logger('directory update', LOGGER_DEBUG); $dirmode = get_config('system', 'directory_mode'); if ($dirmode === false) { $dirmode = DIRECTORY_MODE_NORMAL; } $x = q("select * from channel where channel_id = %d limit 1", intval($argv[1])); if (!$x) { return; } $channel = $x[0]; if ($dirmode != DIRECTORY_MODE_NORMAL) { // this is an in-memory update and we don't need to send a network packet. local_dir_update($argv[1], $force); q("update channel set channel_dirdate = '%s' where channel_id = %d", dbesc(datetime_convert()), intval($channel['channel_id'])); // Now update all the connections if ($pushall) { Master::Summon(array('Notifier', 'refresh_all', $channel['channel_id'])); } return; } // otherwise send the changes upstream $directory = find_upstream_directory($dirmode); $url = $directory['url'] . '/post'; // ensure the upstream directory is updated $packet = zot_build_packet($channel, $force ? 'force_refresh' : 'refresh'); $z = zot_zot($url, $packet); // re-queue if unsuccessful if (!$z['success']) { /** @FIXME we aren't updating channel_dirdate if we have to queue * the directory packet. That means we'll try again on the next poll run. */ $hash = random_string(); queue_insert(array('hash' => $hash, 'account_id' => $channel['channel_account_id'], 'channel_id' => $channel['channel_id'], 'posturl' => $url, 'notify' => $packet)); } else { q("update channel set channel_dirdate = '%s' where channel_id = %d", dbesc(datetime_convert()), intval($channel['channel_id'])); } // Now update all the connections if ($pushall) { Master::Summon(array('Notifier', 'refresh_all', $channel['channel_id'])); } }
function queue_run($argv, $argc) { cli_startup(); global $a; require_once 'include/items.php'; require_once 'include/bbcode.php'; if (argc() > 1) { $queue_id = argv(1); } else { $queue_id = 0; } $deadguys = array(); logger('queue: start'); $r = q("DELETE FROM outq WHERE outq_created < UTC_TIMESTAMP() - INTERVAL 3 DAY"); if ($queue_id) { $r = q("SELECT * FROM outq WHERE outq_hash = '%s' LIMIT 1", dbesc($queue_id)); } else { // For the first 12 hours we'll try to deliver every 15 minutes // After that, we'll only attempt delivery once per hour. // This currently only handles the default queue drivers ('zot' or '') which we will group by posturl // so that we don't start off a thousand deliveries for a couple of dead hubs. // The zot driver will deliver everything destined for a single hub once contact is made (*if* contact is made). // Other drivers will have to do something different here and may need their own query. $r = q("SELECT * FROM outq WHERE outq_delivered = 0 and (( outq_created > UTC_TIMESTAMP() - INTERVAL 12 HOUR and outq_updated < UTC_TIMESTAMP() - INTERVAL 15 MINUTE ) OR ( outq_updated < UTC_TIMESTAMP() - INTERVAL 1 HOUR )) group by outq_posturl"); } if (!$r) { return; } foreach ($r as $rr) { if (in_array($rr['outq_posturl'], $deadguys)) { continue; } if ($rr['outq_driver'] === 'post') { $result = z_post_url($rr['outq_posturl'], $rr['outq_msg']); if ($result['success'] && $result['return_code'] < 300) { logger('queue: queue post success to ' . $rr['outq_posturl'], LOGGER_DEBUG); $y = q("delete from outq where outq_hash = '%s' limit 1", dbesc($rr['ouq_hash'])); } else { logger('queue: queue post returned ' . $result['return_code'] . ' from ' . $rr['outq_posturl'], LOGGER_DEBUG); $y = q("update outq set outq_updated = '%s' where outq_hash = '%s' limit 1", dbesc(datetime_convert()), dbesc($rr['outq_hash'])); } continue; } $result = zot_zot($rr['outq_posturl'], $rr['outq_notify']); if ($result['success']) { zot_process_response($rr['outq_posturl'], $result, $rr); } else { $deadguys[] = $rr['outq_posturl']; $y = q("update outq set outq_updated = '%s' where outq_hash = '%s' limit 1", dbesc(datetime_convert()), dbesc($rr['outq_hash'])); } } }
function get() { // This is just a test utility function and may go away once we build these tools into // the address book and directory to do dead site discovery. // The response packet include the current URL and key so we can discover if the server // has been re-installed and clean up (e.g. get rid of) any old hublocs and xchans. // Remember to add '/post' to the url if (!local_channel()) { return; } $url = $_REQUEST['url']; if (!$url) { return; } $m = zot_build_packet(\App::get_channel(), 'ping'); $r = zot_zot($url, $m); return print_r($r, true); }
function deliver_run($argv, $argc) { cli_startup(); $a = get_app(); if ($argc < 2) { return; } logger('deliver: invoked: ' . print_r($argv, true), LOGGER_DATA); for ($x = 1; $x < $argc; $x++) { $r = q("select * from outq where outq_hash = '%s' limit 1", dbesc($argv[$x])); if ($r) { if ($r[0]['outq_driver'] === 'post') { $result = z_post_url($r[0]['outq_posturl'], $r[0]['outq_msg']); if ($result['success'] && $result['return_code'] < 300) { logger('deliver: queue post success to ' . $r[0]['outq_posturl'], LOGGER_DEBUG); $y = q("delete from outq where outq_hash = '%s' limit 1", dbesc($argv[$x])); } else { logger('deliver: queue post returned ' . $result['return_code'] . ' from ' . $r[0]['outq_posturl'], LOGGER_DEBUG); $y = q("update outq set outq_updated = '%s' where outq_hash = '%s' limit 1", dbesc(datetime_convert()), dbesc($argv[$x])); } continue; } if ($r[0]['outq_posturl'] === z_root() . '/post') { logger('deliver: local delivery', LOGGER_DEBUG); // local delivery // we should probably batch these and save a few delivery processes // If there is no outq_msg, this is a refresh_all message which does not require local handling if ($r[0]['outq_msg']) { $msg = array('body' => json_encode(array('pickup' => array(array('notify' => json_decode($r[0]['outq_notify'], true), 'message' => json_decode($r[0]['outq_msg'], true)))))); zot_import($msg, z_root()); $r = q("delete from outq where outq_hash = '%s' limit 1", dbesc($argv[$x])); } } else { logger('deliver: dest: ' . $r[0]['outq_posturl'], LOGGER_DEBUG); $result = zot_zot($r[0]['outq_posturl'], $r[0]['outq_notify']); if ($result['success']) { zot_process_response($r[0]['outq_posturl'], $result, $r[0]); } else { $y = q("update outq set outq_updated = '%s' where outq_hash = '%s' limit 1", dbesc(datetime_convert()), dbesc($argv[$x])); } } } } }
function admin_page_hubloc_post(&$a) { check_form_security_token_redirectOnErr('/admin/hubloc', 'admin_hubloc'); require_once 'include/zot.php'; //prepare for ping if ($_POST['hublocid']) { $hublocid = $_POST['hublocid']; $arrhublocurl = q("SELECT hubloc_url FROM hubloc WHERE hubloc_id = %d ", intval($hublocid)); $hublocurl = $arrhublocurl[0]['hubloc_url'] . '/post'; //perform ping $m = zot_build_packet(\App::get_channel(), 'ping'); $r = zot_zot($hublocurl, $m); //handle results and set the hubloc flags in db to make results visible $r2 = $r['body']; $r3 = $r2['success']; if ($r3['success'] == True) { //set HUBLOC_OFFLINE to 0 logger(' success = true ', LOGGER_DEBUG); } else { //set HUBLOC_OFFLINE to 1 logger(' success = false ', LOGGER_DEBUG); } //unfotunatly zping wont work, I guess return format is not correct //require_once('mod/zping.php'); //$r = zping_content($hublocurl); //logger('zping answer: ' . $r, LOGGER_DEBUG); //in case of repair store new pub key for tested hubloc (all channel with this hubloc) in db //after repair set hubloc flags to 0 } goaway(z_root() . '/admin/hubloc'); }
function Verify($channel, $hubloc) { logger('auth request received from ' . $hubloc['hubloc_addr']); $this->remote = remote_channel(); $this->remote_service_class = ''; $this->remote_level = 0; $this->remote_hub = $hubloc['hubloc_url']; $this->dnt = 0; // 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. // Also check that they are coming from the same site as they authenticated with originally. $already_authed = remote_channel() && $hubloc['hubloc_hash'] == remote_channel() && $hubloc['hubloc_url'] === $_SESSION['remote_hub'] ? true : false; if ($this->delegate && $this->delegate !== $_SESSION['delegate_channel']) { $already_authed = false; } if ($already_authed) { return true; } 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 (\App::$channel['channel_hash'] == $hubloc['xchan_hash']) { return true; } else { logger('already authenticated locally as somebody else.'); notice(t('Remote authentication blocked. You are logged into this site locally. Please logout and retry.') . EOL); if ($this->test) { $this->Debug('already logged in locally with a conflicting identity.'); return false; } } return false; } // 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($channel, $type = 'auth_check', array(array('guid' => $hubloc['hubloc_guid'], 'guid_sig' => $hubloc['hubloc_guid_sig'])), $hubloc['hubloc_sitekey'], $this->sec); $this->Debug('auth check packet created using sitekey ' . $hubloc['hubloc_sitekey']); $this->Debug('packet contents: ' . $p); $result = zot_zot($hubloc['hubloc_callback'], $p); if (!$result['success']) { logger('auth_check callback failed.'); if ($this->test) { $this->Debug('auth check request to your site returned .' . print_r($result, true)); } return false; } $j = json_decode($result['body'], true); if (!$j) { logger('auth_check json data malformed.'); if ($this->test) { $this->Debug('json malformed: ' . $result['body']); } return false; } $this->Debug('auth check request returned .' . print_r($j, true)); if (!$j['success']) { return false; } // legit response, but we do need to check that this wasn't answered by a man-in-middle if (!rsa_verify($this->sec . $hubloc['xchan_hash'], base64url_decode($j['confirm']), $hubloc['xchan_pubkey'])) { logger('final confirmation failed.'); if ($this->test) { $this->Debug('final confirmation failed. ' . $sec . print_r($j, true) . print_r($hubloc, true)); } return false; } if (array_key_exists('service_class', $j)) { $this->remote_service_class = $j['service_class']; } if (array_key_exists('level', $j)) { $this->remote_level = $j['level']; } if (array_key_exists('DNT', $j)) { $this->dnt = $j['DNT']; } // log them in if ($this->test) { // testing only - return the success result $this->test_results['success'] = true; $this->Debug('Authentication Success!'); $this->Finalise(); } $_SESSION['authenticated'] = 1; // check for delegation and if all is well, log them in locally with delegation restrictions $this->delegate_success = false; if ($this->delegate) { $r = q("select * from channel left join xchan on channel_hash = xchan_hash where xchan_addr = '%s' limit 1", dbesc($this->delegate)); if ($r && intval($r[0]['channel_id'])) { $allowed = perm_is_allowed($r[0]['channel_id'], $hubloc['xchan_hash'], 'delegate'); if ($allowed) { $_SESSION['delegate_channel'] = $r[0]['channel_id']; $_SESSION['delegate'] = $hubloc['xchan_hash']; $_SESSION['account_id'] = intval($r[0]['channel_account_id']); require_once 'include/security.php'; // this will set the local_channel authentication in the session change_channel($r[0]['channel_id']); $this->delegate_success = true; } } } if (!$this->delegate_success) { // normal visitor (remote_channel) login session credentials $_SESSION['visitor_id'] = $hubloc['xchan_hash']; $_SESSION['my_url'] = $hubloc['xchan_url']; $_SESSION['my_address'] = $this->address; $_SESSION['remote_service_class'] = $this->remote_service_class; $_SESSION['remote_level'] = $this->remote_level; $_SESSION['remote_hub'] = $this->remote_hub; $_SESSION['DNT'] = $this->dnt; } $arr = array('xchan' => $hubloc, 'url' => $this->desturl, 'session' => $_SESSION); call_hooks('magic_auth_success', $arr); \App::set_observer($hubloc); require_once 'include/security.php'; \App::set_groups(init_groups_visitor($_SESSION['visitor_id'])); info(sprintf(t('Welcome %s. Remote authentication successful.'), $hubloc['xchan_name'])); logger('mod_zot: auth success from ' . $hubloc['xchan_addr']); $this->success = true; return true; }
function queue_deliver($outq, $immediate = false) { $base = null; $h = parse_url($outq['outq_posturl']); if ($h) { $base = $h['scheme'] . '://' . $h['host'] . ($h['port'] ? ':' . $h['port'] : ''); } if ($base && $base !== z_root() && $immediate) { $y = q("select site_update, site_dead from site where site_url = '%s' ", dbesc($base)); if ($y) { if (intval($y[0]['site_dead'])) { remove_queue_by_posturl($outq['outq_posturl']); logger('dead site ignored ' . $base); return; } if ($y[0]['site_update'] < datetime_convert('UTC', 'UTC', 'now - 1 month')) { update_queue_item($outq['outq_hash'], 10); logger('immediate delivery deferred for site ' . $base); return; } } else { // zot sites should all have a site record, unless they've been dead for as long as // your site has existed. Since we don't know for sure what these sites are, // call them unknown q("insert into site (site_url, site_update, site_dead, site_type) values ('%s','%s',0,%d) ", dbesc($base), dbesc(datetime_convert()), intval($outq['outq_driver'] === 'post' ? SITE_TYPE_NOTZOT : SITE_TYPE_UNKNOWN)); } } $arr = array('outq' => $outq, 'base' => $base, 'handled' => false, 'immediate' => $immediate); call_hooks('queue_deliver', $arr); if ($arr['handled']) { return; } // "post" queue driver - used for diaspora and friendica-over-diaspora communications. if ($outq['outq_driver'] === 'post') { $result = z_post_url($outq['outq_posturl'], $outq['outq_msg']); if ($result['success'] && $result['return_code'] < 300) { logger('deliver: queue post success to ' . $outq['outq_posturl'], LOGGER_DEBUG); if ($base) { q("update site set site_update = '%s', site_dead = 0 where site_url = '%s' ", dbesc(datetime_convert()), dbesc($base)); } q("update dreport set dreport_result = '%s', dreport_time = '%s' where dreport_queue = '%s' limit 1", dbesc('accepted for delivery'), dbesc(datetime_convert()), dbesc($outq['outq_hash'])); remove_queue_item($outq['outq_hash']); // server is responding - see if anything else is going to this destination and is piled up // and try to send some more. We're relying on the fact that delivery_loop() results in an // immediate delivery otherwise we could get into a queue loop. if (!$immediate) { $x = q("select outq_hash from outq where outq_posturl = '%s' and outq_delivered = 0", dbesc($outq['outq_posturl'])); $piled_up = array(); if ($x) { foreach ($x as $xx) { $piled_up[] = $xx['outq_hash']; } } if ($piled_up) { delivery_loop($piled_up); } } } else { logger('deliver: queue post returned ' . $result['return_code'] . ' from ' . $outq['outq_posturl'], LOGGER_DEBUG); update_queue_item($outq['outq_posturl']); } return; } // normal zot delivery logger('deliver: dest: ' . $outq['outq_posturl'], LOGGER_DEBUG); $result = zot_zot($outq['outq_posturl'], $outq['outq_notify']); if ($result['success']) { logger('deliver: remote zot delivery succeeded to ' . $outq['outq_posturl']); zot_process_response($outq['outq_posturl'], $result, $outq); } else { logger('deliver: remote zot delivery failed to ' . $outq['outq_posturl']); logger('deliver: remote zot delivery fail data: ' . print_r($result, true), LOGGER_DATA); update_queue_item($outq['outq_hash'], 10); } return; }
function notifier_run($argv, $argc) { cli_startup(); $a = get_app(); require_once "session.php"; require_once "datetime.php"; require_once 'include/items.php'; require_once 'include/bbcode.php'; if ($argc < 3) { return; } logger('notifier: invoked: ' . print_r($argv, true), LOGGER_DEBUG); $cmd = $argv[1]; $item_id = $argv[2]; $extra = $argc > 3 ? $argv[3] : null; if (!$item_id) { return; } require_once 'include/identity.php'; $sys = get_sys_channel(); if ($cmd == 'permission_update') { // Get the recipient $r = q("select abook.*, hubloc.* from abook \n\t\t\tleft join hubloc on hubloc_hash = abook_xchan\n\t\t\twhere abook_id = %d and not ( abook_flags & %d ) > 0 \n\t\t\tand not (hubloc_flags & %d) > 0 and not (hubloc_status & %d) > 0 limit 1", intval($item_id), intval(ABOOK_FLAG_SELF), intval(HUBLOC_FLAGS_DELETED), intval(HUBLOC_OFFLINE)); if ($r) { // Get the sender $s = q("select * from channel left join xchan on channel_hash = xchan_hash where channel_id = %d limit 1", intval($r[0]['abook_channel'])); if ($s) { if ($r[0]['hubloc_network'] === 'diaspora' || $r[0]['hubloc_network'] === 'friendica-over-diaspora') { require_once 'include/diaspora.php'; diaspora_share($s[0], $r[0]); } else { // send a refresh message to each hub they have registered here $h = q("select * from hubloc where hubloc_hash = '%s' \n\t\t\t\t\t\tand not (hubloc_flags & %d) > 0 and not (hubloc_status & %d) > 0", dbesc($r[0]['hubloc_hash']), intval(HUBLOC_FLAGS_DELETED), intval(HUBLOC_OFFLINE)); if ($h) { foreach ($h as $hh) { $data = zot_build_packet($s[0], 'refresh', array(array('guid' => $hh['hubloc_guid'], 'guid_sig' => $hh['hubloc_guid_sig'], 'url' => $hh['hubloc_url']))); if ($data) { $result = zot_zot($hh['hubloc_callback'], $data); // if immediate delivery failed, stick it in the queue to try again later. if (!$result['success']) { $hash = random_string(); q("insert into outq ( outq_hash, outq_account, outq_channel, outq_driver, outq_posturl, outq_async, outq_created, outq_updated, outq_notify, outq_msg ) \n\t\t\t\t\t\t\t\t\t\tvalues ( '%s', %d, %d, '%s', '%s', %d, '%s', '%s', '%s', '%s' )", dbesc($hash), intval($s[0]['channel_account_id']), intval($s[0]['channel_id']), dbesc('zot'), dbesc($hh['hubloc_callback']), intval(1), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc($data), dbesc('')); } } } } } } } return; } $expire = false; $request = false; $mail = false; $fsuggest = false; $top_level = false; $location = false; $recipients = array(); $url_recipients = array(); $normal_mode = true; $packet_type = 'undefined'; if ($cmd === 'mail') { $normal_mode = false; $mail = true; $private = true; $message = q("SELECT * FROM `mail` WHERE `id` = %d LIMIT 1", intval($item_id)); if (!$message) { return; } xchan_mail_query($message[0]); $uid = $message[0]['channel_id']; $recipients[] = $message[0]['from_xchan']; // include clones $recipients[] = $message[0]['to_xchan']; $item = $message[0]; $encoded_item = encode_mail($item); $s = q("select * from channel where channel_id = %d limit 1", intval($item['channel_id'])); if ($s) { $channel = $s[0]; } } elseif ($cmd === 'request') { $channel_id = $item_id; $xchan = $argv[3]; $request_message_id = $argv[4]; $s = q("select * from channel where channel_id = %d limit 1", intval($channel_id)); if ($s) { $channel = $s[0]; } $private = true; $recipients[] = $xchan; $packet_type = 'request'; $normal_mode = false; } elseif ($cmd === 'expire') { // FIXME // This will require a special zot packet containing a list of item message_id's to be expired. // This packet will be public, since we cannot selectively deliver here. // We need the handling on this end to create the array, and the handling on the remote end // to verify permissions (for each item) and process it. Until this is complete, the expire feature will be disabled. return; $normal_mode = false; $expire = true; $items = q("SELECT * FROM item WHERE uid = %d AND ( item_flags & %d )>0\n\t\t\tAND ( item_restrict & %d )>0 AND `changed` > %s - INTERVAL %s", intval($item_id), intval(ITEM_WALL), intval(ITEM_DELETED), db_utcnow(), db_quoteinterval('10 MINUTE')); $uid = $item_id; $item_id = 0; if (!$items) { return; } } elseif ($cmd === 'suggest') { $normal_mode = false; $fsuggest = true; $suggest = q("SELECT * FROM `fsuggest` WHERE `id` = %d LIMIT 1", intval($item_id)); if (!count($suggest)) { return; } $uid = $suggest[0]['uid']; $recipients[] = $suggest[0]['cid']; $item = $suggest[0]; } elseif ($cmd === 'refresh_all') { logger('notifier: refresh_all: ' . $item_id); $s = q("select * from channel where channel_id = %d limit 1", intval($item_id)); if ($s) { $channel = $s[0]; } $uid = $item_id; $recipients = array(); $r = q("select abook_xchan from abook where abook_channel = %d", intval($item_id)); if ($r) { foreach ($r as $rr) { $recipients[] = $rr['abook_xchan']; } } $private = false; $packet_type = 'refresh'; } elseif ($cmd === 'location') { logger('notifier: location: ' . $item_id); $s = q("select * from channel where channel_id = %d limit 1", intval($item_id)); if ($s) { $channel = $s[0]; } $uid = $item_id; $recipients = array(); $r = q("select abook_xchan from abook where abook_channel = %d", intval($item_id)); if ($r) { foreach ($r as $rr) { $recipients[] = $rr['abook_xchan']; } } $encoded_item = array('locations' => zot_encode_locations($channel), 'type' => 'location', 'encoding' => 'zot'); $target_item = array('aid' => $channel['channel_account_id'], 'uid' => $channel['channel_id']); $private = false; $packet_type = 'location'; $location = true; } elseif ($cmd === 'purge_all') { logger('notifier: purge_all: ' . $item_id); $s = q("select * from channel where channel_id = %d limit 1", intval($item_id)); if ($s) { $channel = $s[0]; } $uid = $item_id; $recipients = array(); $r = q("select abook_xchan from abook where abook_channel = %d", intval($item_id)); if ($r) { foreach ($r as $rr) { $recipients[] = $rr['abook_xchan']; } } $private = false; $packet_type = 'purge'; } else { // Normal items // Fetch the target item $r = q("SELECT * FROM item WHERE id = %d and parent != 0 LIMIT 1", intval($item_id)); if (!$r) { return; } xchan_query($r); $r = fetch_post_tags($r); $target_item = $r[0]; $deleted_item = false; if ($target_item['item_restrict'] & ITEM_DELETED) { logger('notifier: target item ITEM_DELETED', LOGGER_DEBUG); $deleted_item = true; } $unforwardable = ITEM_UNPUBLISHED | ITEM_DELAYED_PUBLISH | ITEM_WEBPAGE | ITEM_BUILDBLOCK | ITEM_PDL; if ($target_item['item_restrict'] & $unforwardable) { logger('notifier: target item not forwardable: flags ' . $target_item['item_restrict'], LOGGER_DEBUG); return; } $s = q("select * from channel where channel_id = %d limit 1", intval($target_item['uid'])); if ($s) { $channel = $s[0]; } if ($channel['channel_hash'] !== $target_item['author_xchan'] && $channel['channel_hash'] !== $target_item['owner_xchan']) { logger("notifier: Sending channel {$channel['channel_hash']} is not owner {$target_item['owner_xchan']} or author {$target_item['author_xchan']}"); return; } if ($target_item['id'] == $target_item['parent']) { $parent_item = $target_item; $top_level_post = true; } else { // fetch the parent item $r = q("SELECT * from item where id = %d order by id asc", intval($target_item['parent'])); if (!$r) { return; } xchan_query($r); $r = fetch_post_tags($r); $parent_item = $r[0]; $top_level_post = false; } // avoid looping of discover items 12/4/2014 if ($sys && $parent_item['uid'] == $sys['channel_id']) { return; } $encoded_item = encode_item($target_item); // Send comments to the owner to re-deliver to everybody in the conversation // We only do this if the item in question originated on this site. This prevents looping. // To clarify, a site accepting a new comment is responsible for sending it to the owner for relay. // Relaying should never be initiated on a post that arrived from elsewhere. // We should normally be able to rely on ITEM_ORIGIN, but start_delivery_chain() incorrectly set this // flag on comments for an extended period. So we'll also call comment_local_origin() which looks at // the hostname in the message_id and provides a second (fallback) opinion. $relay_to_owner = !$top_level_post && $target_item['item_flags'] & ITEM_ORIGIN && comment_local_origin($target_item) ? true : false; $uplink = false; // $cmd === 'relay' indicates the owner is sending it to the original recipients // don't allow the item in the relay command to relay to owner under any circumstances, it will loop logger('notifier: relay_to_owner: ' . ($relay_to_owner ? 'true' : 'false'), LOGGER_DATA); logger('notifier: top_level_post: ' . ($top_level_post ? 'true' : 'false'), LOGGER_DATA); logger('notifier: target_item_flags: ' . $target_item['item_flags'] . ' ' . ($target_item['item_flags'] & ITEM_ORIGIN ? 'true' : 'false'), LOGGER_DATA); // tag_deliver'd post which needs to be sent back to the original author if ($cmd === 'uplink' && $parent_item['item_flags'] & ITEM_UPLINK && !$top_level_post) { logger('notifier: uplink'); $uplink = true; } if (($relay_to_owner || $uplink) && $cmd !== 'relay') { logger('notifier: followup relay', LOGGER_DEBUG); $recipients = array($uplink ? $parent_item['source_xchan'] : $parent_item['owner_xchan']); $private = true; if (!$encoded_item['flags']) { $encoded_item['flags'] = array(); } $encoded_item['flags'][] = 'relay'; } else { logger('notifier: normal distribution', LOGGER_DEBUG); if ($cmd === 'relay') { logger('notifier: owner relay'); } // if our parent is a tag_delivery recipient, uplink to the original author causing // a delivery fork. if ($parent_item['item_flags'] & ITEM_UPLINK && !$top_level_post && $cmd !== 'uplink') { logger('notifier: uplinking this item'); proc_run('php', 'include/notifier.php', 'uplink', $item_id); } $private = false; $recipients = collect_recipients($parent_item, $private); // FIXME add any additional recipients such as mentions, etc. // don't send deletions onward for other people's stuff // TODO verify this is needed - copied logic from same place in old code if ($target_item['item_restrict'] & ITEM_DELETED && !($target_item['item_flags'] & ITEM_WALL)) { logger('notifier: ignoring delete notification for non-wall item'); return; } } } $walltowall = $top_level_post && $channel['xchan_hash'] === $target_item['author_xchan'] ? true : false; // Generic delivery section, we have an encoded item and recipients // Now start the delivery process $x = $encoded_item; $x['title'] = 'private'; $x['body'] = 'private'; logger('notifier: encoded item: ' . print_r($x, true), LOGGER_DATA); stringify_array_elms($recipients); if (!$recipients) { return; } // logger('notifier: recipients: ' . print_r($recipients,true)); $env_recips = $private ? array() : null; $details = q("select xchan_hash, xchan_instance_url, xchan_network, xchan_addr, xchan_guid, xchan_guid_sig from xchan where xchan_hash in (" . implode(',', $recipients) . ")"); $recip_list = array(); if ($details) { foreach ($details as $d) { // If the recipient is federated from a traditional network they won't be able to // handle nomadic identity. If we're publishing from a site that they aren't // directly connected with, ignore them. // FIXME: make sure we run through a notifier loop on the hub they're connected // with if this post comes in from a different hub - so that we will deliver to them. // On the down side, these channels will stop working if the hub they connected with // goes down permanently, as they are (doh) not nomadic. if ($d['xchan_instance_url'] && $d['xchan_instance_url'] != z_root()) { continue; } $recip_list[] = $d['xchan_addr'] . ' (' . $d['xchan_hash'] . ')'; if ($private) { $env_recips[] = array('guid' => $d['xchan_guid'], 'guid_sig' => $d['xchan_guid_sig'], 'hash' => $d['xchan_hash']); } } } if ($private && !$env_recips) { // shouldn't happen logger('notifier: private message with no envelope recipients.' . print_r($argv, true)); } logger('notifier: recipients (may be delivered to more if public): ' . print_r($recip_list, true), LOGGER_DEBUG); // Now we have collected recipients (except for external mentions, FIXME) // Let's reduce this to a set of hubs. logger('notifier: hub choice: ' . intval($relay_to_owner) . ' ' . intval($private) . ' ' . $cmd, LOGGER_DEBUG); // FIXME: I think we need to remove the private bit or this clause will never execute. Needs more coffee to think it through. // We may in fact have to send it to clones in case the one we pick recently died. if ($relay_to_owner && !$private && $cmd !== 'relay') { // If sending a followup to the post owner, only send it to one channel clone - to avoid race conditions. // In this case we'll pick the most recently contacted hub, as their primary might be down and the most // recently contacted has the best chance of being alive. // For private posts or uplinks we have to do things differently as only the sending clone will have the recipient list. // We have to send to all clone channels of the owner to find out who has the definitive list. Posts with // item_private set (but no ACL list) will return empty recipients (except for the sender and owner) in // collect_recipients() above. The end result is we should get only one delivery per delivery chain if we // aren't the owner or author. $r = q("select hubloc_guid, hubloc_url, hubloc_sitekey, hubloc_network, hubloc_flags, hubloc_callback, hubloc_host from hubloc \n\t\t\twhere hubloc_hash in (" . implode(',', $recipients) . ") order by hubloc_connected desc limit 1"); } else { $r = q("select hubloc_guid, hubloc_url, hubloc_sitekey, hubloc_network, hubloc_flags, hubloc_callback, hubloc_host from hubloc \n\t\t\twhere hubloc_hash in (" . implode(',', $recipients) . ") and not (hubloc_flags & %d) > 0 and not (hubloc_status & %d) > 0", intval(HUBLOC_FLAGS_DELETED), intval(HUBLOC_OFFLINE)); } if (!$r) { logger('notifier: no hubs'); return; } $hubs = $r; /** * Reduce the hubs to those that are unique. For zot hubs, we need to verify uniqueness by the sitekey, since it may have been * a re-install which has not yet been detected and pruned. * For other networks which don't have or require sitekeys, we'll have to use the URL */ $hublist = array(); // this provides an easily printable list for the logs $dhubs = array(); // delivery hubs where we store our resulting unique array $keys = array(); // array of keys to check uniquness for zot hubs $urls = array(); // array of urls to check uniqueness of hubs from other networks foreach ($hubs as $hub) { if ($hub['hubloc_network'] == 'zot') { if (!in_array($hub['hubloc_sitekey'], $keys)) { $hublist[] = $hub['hubloc_host']; $dhubs[] = $hub; $keys[] = $hub['hubloc_sitekey']; } } else { if (!in_array($hub['hubloc_url'], $urls)) { $hublist[] = $hub['hubloc_host']; $dhubs[] = $hub; $urls[] = $hub['hubloc_url']; } } } logger('notifier: will notify/deliver to these hubs: ' . print_r($hublist, true), LOGGER_DEBUG); $interval = get_config('system', 'delivery_interval') !== false ? intval(get_config('system', 'delivery_interval')) : 2; $deliveries_per_process = intval(get_config('system', 'delivery_batch_count')); if ($deliveries_per_process <= 0) { $deliveries_per_process = 1; } $deliver = array(); foreach ($dhubs as $hub) { if (defined('DIASPORA_RELIABILITY_EMULATION')) { $cointoss = mt_rand(0, 2); if ($cointoss == 2) { continue; } } if ($hub['hubloc_network'] === 'diaspora' || $hub['hubloc_network'] === 'friendica-over-diaspora') { if (!get_config('system', 'diaspora_enabled')) { continue; } require_once 'include/diaspora.php'; diaspora_process_outbound(array('channel' => $channel, 'env_recips' => $env_recips, 'recipients' => $recipients, 'item' => $item, 'target_item' => $target_item, 'hub' => $hub, 'top_level_post' => $top_level_post, 'private' => $private, 'followup' => $followup, 'relay_to_owner' => $relay_to_owner, 'uplink' => $uplink, 'cmd' => $cmd, 'expire' => $expire, 'mail' => $mail, 'location' => $location, 'fsuggest' => $fsuggest, 'request' => $request, 'normal_mode' => $normal_mode, 'packet_type' => $packet_type, 'walltowall' => $walltowall)); continue; } // default: zot protocol $hash = random_string(); if ($packet_type === 'refresh' || $packet_type === 'purge') { $n = zot_build_packet($channel, $packet_type); q("insert into outq ( outq_hash, outq_account, outq_channel, outq_driver, outq_posturl, outq_async, outq_created, outq_updated, outq_notify, outq_msg ) values ( '%s', %d, %d, '%s', '%s', %d, '%s', '%s', '%s', '%s' )", dbesc($hash), intval($channel['channel_account_id']), intval($channel['channel_id']), dbesc('zot'), dbesc($hub['hubloc_callback']), intval(1), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc($n), dbesc('')); } elseif ($packet_type === 'request') { $n = zot_build_packet($channel, 'request', $env_recips, $hub['hubloc_sitekey'], $hash, array('message_id' => $request_message_id)); q("insert into outq ( outq_hash, outq_account, outq_channel, outq_driver, outq_posturl, outq_async, outq_created, outq_updated, outq_notify, outq_msg ) values ( '%s', %d, %d, '%s', '%s', %d, '%s', '%s', '%s', '%s' )", dbesc($hash), intval($channel['channel_account_id']), intval($channel['channel_id']), dbesc('zot'), dbesc($hub['hubloc_callback']), intval(1), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc($n), dbesc('')); } else { $n = zot_build_packet($channel, 'notify', $env_recips, $private ? $hub['hubloc_sitekey'] : null, $hash); q("insert into outq ( outq_hash, outq_account, outq_channel, outq_driver, outq_posturl, outq_async, outq_created, outq_updated, outq_notify, outq_msg ) values ( '%s', %d, %d, '%s', '%s', %d, '%s', '%s', '%s', '%s' )", dbesc($hash), intval($target_item['aid']), intval($target_item['uid']), dbesc('zot'), dbesc($hub['hubloc_callback']), intval(1), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc($n), dbesc(json_encode($encoded_item))); } $deliver[] = $hash; if (count($deliver) >= $deliveries_per_process) { proc_run('php', 'include/deliver.php', $deliver); $deliver = array(); if ($interval) { @time_sleep_until(microtime(true) + (double) $interval); } } } // catch any stragglers if (count($deliver)) { proc_run('php', 'include/deliver.php', $deliver); } logger('notifier: basic loop complete.', LOGGER_DEBUG); if ($normal_mode) { call_hooks('notifier_normal', $target_item); } call_hooks('notifier_end', $target_item); logger('notifer: complete.'); return; }
function post_init(&$a) { // 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) * * Four 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 * * 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 * * * { * "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..." * } * * * 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: * * { * "success":1, * "confirm":"q0Ysovd1u..." * "service_class":(optional) * "level":(optional) * } * * '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". * * * */ 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']; $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 not ( channel_pageflags & %d ) limit 1", intval(PAGE_REMOVED)); 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 limit 1", 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 limit 1", 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); } logger('mod_zot: auth request received from ' . $x[0]['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_user(); $result = null; $remote_service_class = ''; $remote_level = 0; $remote_hub = $x[0]['hubloc_url']; $DNT = 0; // Also check that they are coming from the same site as they authenticated with originally. $already_authed = $remote && $x[0]['hubloc_hash'] == $remote && $x[0]['hubloc_url'] === $_SESSION['remote_hub'] ? true : 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' => $x[0]['hubloc_guid'], 'guid_sig' => $x[0]['hubloc_guid_sig'])), $x[0]['hubloc_sitekey'], $sec); if ($test) { $ret['message'] .= 'auth check packet created using sitekey ' . $x[0]['hubloc_sitekey'] . EOL; $ret['message'] .= 'packet contents: ' . $p . EOL; } $result = zot_zot($x[0]['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; json_return_and_die($ret); } goaway($desturl); } $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; json_return_and_die($ret); } } } 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 . $x[0]['xchan_hash'], base64url_decode($j['confirm']), $x[0]['xchan_pubkey'])) { logger('mod_zot: auth: final confirmation failed.'); if ($test) { $ret['message'] .= 'final confirmation failed. ' . $sec . print_r($j, true) . print_r($x[0], true); json_return_and_die($ret); } goaway($desturl); } 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_user()) { // 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'] != $x[0]['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; json_return_and_die($ret); } } goaway($desturl); } // log them in if ($test) { $ret['success'] = true; $ret['message'] .= 'Authentication Success!' . EOL; json_return_and_die($ret); } $_SESSION['authenticated'] = 1; $_SESSION['visitor_id'] = $x[0]['xchan_hash']; $_SESSION['my_url'] = $x[0]['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' => $x[0], 'url' => $desturl, 'session' => $_SESSION); call_hooks('magic_auth_success', $arr); $a->set_observer($x[0]); require_once 'include/security.php'; $a->set_groups(init_groups_visitor($_SESSION['visitor_id'])); info(sprintf(t('Welcome %s. Remote authentication successful.'), $x[0]['xchan_name'])); logger('mod_zot: auth success from ' . $x[0]['xchan_addr']); q("update hubloc set hubloc_status = (hubloc_status | %d ) where hubloc_id = %d ", intval(HUBLOC_WORKS), intval($x[0]['hubloc_id'])); } else { if ($test) { $ret['message'] .= 'auth failure. ' . print_r($_REQUEST, true) . print_r($j, true) . EOL; json_return_and_die($ret); } logger('mod_zot: magic-auth failure - not authenticated: ' . $x[0]['xchan_addr']); q("update hubloc set hubloc_status = (hubloc_status | %d ) where hubloc_id = %d ", intval(HUBLOC_RECEIVE_ERROR), intval($x[0]['hubloc_id'])); } // 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 ($test) { $ret['message'] .= 'auth failure fallthrough ' . print_r($_REQUEST, true) . print_r($j, true) . EOL; json_return_and_die($ret); } if (strstr($desturl, z_root() . '/rmagic')) { goaway(z_root()); } goaway($desturl); } return; }
/** * @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); } }
function notifier_run($argv, $argc) { cli_startup(); $a = get_app(); require_once "session.php"; require_once "datetime.php"; require_once 'include/items.php'; require_once 'include/bbcode.php'; if ($argc < 3) { return; } logger('notifier: invoked: ' . print_r($argv, true), LOGGER_DEBUG); $cmd = $argv[1]; $item_id = $argv[2]; $extra = $argc > 3 ? $argv[3] : null; if (!$item_id) { return; } require_once 'include/identity.php'; $sys = get_sys_channel(); if ($cmd == 'permission_update') { // Get the recipient $r = q("select abook.*, hubloc.* from abook \n\t\t\tleft join hubloc on hubloc_hash = abook_xchan\n\t\t\twhere abook_id = %d and abook_self = 0\n\t\t\tand not (hubloc_flags & %d) > 0 and not (hubloc_status & %d) > 0 limit 1", intval($item_id), intval(HUBLOC_FLAGS_DELETED), intval(HUBLOC_OFFLINE)); if ($r) { // Get the sender $s = q("select * from channel left join xchan on channel_hash = xchan_hash where channel_id = %d limit 1", intval($r[0]['abook_channel'])); if ($s) { $perm_update = array('sender' => $s[0], 'recipient' => $r[0], 'success' => false); call_hooks('permissions_update', $perm_update); if (!$perm_update['success']) { // send a refresh message to each hub they have registered here $h = q("select * from hubloc where hubloc_hash = '%s' \n\t\t\t\t\t\tand not (hubloc_flags & %d) > 0 and not (hubloc_status & %d) > 0", dbesc($r[0]['hubloc_hash']), intval(HUBLOC_FLAGS_DELETED), intval(HUBLOC_OFFLINE)); if ($h) { foreach ($h as $hh) { $data = zot_build_packet($s[0], 'refresh', array(array('guid' => $hh['hubloc_guid'], 'guid_sig' => $hh['hubloc_guid_sig'], 'url' => $hh['hubloc_url']))); if ($data) { $result = zot_zot($hh['hubloc_callback'], $data); // if immediate delivery failed, stick it in the queue to try again later. if (!$result['success']) { $hash = random_string(); q("insert into outq ( outq_hash, outq_account, outq_channel, outq_driver, outq_posturl, outq_async, outq_created, outq_updated, outq_notify, outq_msg ) \n\t\t\t\t\t\t\t\t\t\tvalues ( '%s', %d, %d, '%s', '%s', %d, '%s', '%s', '%s', '%s' )", dbesc($hash), intval($s[0]['channel_account_id']), intval($s[0]['channel_id']), dbesc('zot'), dbesc($hh['hubloc_callback']), intval(1), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc($data), dbesc('')); } } } } } } } return; } $expire = false; $request = false; $mail = false; $fsuggest = false; $top_level = false; $location = false; $recipients = array(); $url_recipients = array(); $normal_mode = true; $packet_type = 'undefined'; if ($cmd === 'mail') { $normal_mode = false; $mail = true; $private = true; $message = q("SELECT * FROM `mail` WHERE `id` = %d LIMIT 1", intval($item_id)); if (!$message) { return; } xchan_mail_query($message[0]); $uid = $message[0]['channel_id']; $recipients[] = $message[0]['from_xchan']; // include clones $recipients[] = $message[0]['to_xchan']; $item = $message[0]; $encoded_item = encode_mail($item); $s = q("select * from channel where channel_id = %d limit 1", intval($item['channel_id'])); if ($s) { $channel = $s[0]; } } elseif ($cmd === 'request') { $channel_id = $item_id; $xchan = $argv[3]; $request_message_id = $argv[4]; $s = q("select * from channel where channel_id = %d limit 1", intval($channel_id)); if ($s) { $channel = $s[0]; } $private = true; $recipients[] = $xchan; $packet_type = 'request'; $normal_mode = false; } elseif ($cmd === 'expire') { // FIXME // This will require a special zot packet containing a list of item message_id's to be expired. // This packet will be public, since we cannot selectively deliver here. // We need the handling on this end to create the array, and the handling on the remote end // to verify permissions (for each item) and process it. Until this is complete, the expire feature will be disabled. return; $normal_mode = false; $expire = true; $items = q("SELECT * FROM item WHERE uid = %d AND item_wall = 1\n\t\t\tAND item_deleted = 1 AND `changed` > %s - INTERVAL %s", intval($item_id), db_utcnow(), db_quoteinterval('10 MINUTE')); $uid = $item_id; $item_id = 0; if (!$items) { return; } } elseif ($cmd === 'suggest') { $normal_mode = false; $fsuggest = true; $suggest = q("SELECT * FROM `fsuggest` WHERE `id` = %d LIMIT 1", intval($item_id)); if (!count($suggest)) { return; } $uid = $suggest[0]['uid']; $recipients[] = $suggest[0]['cid']; $item = $suggest[0]; } elseif ($cmd === 'refresh_all') { logger('notifier: refresh_all: ' . $item_id); $s = q("select * from channel where channel_id = %d limit 1", intval($item_id)); if ($s) { $channel = $s[0]; } $uid = $item_id; $recipients = array(); $r = q("select abook_xchan from abook where abook_channel = %d", intval($item_id)); if ($r) { foreach ($r as $rr) { $recipients[] = $rr['abook_xchan']; } } $private = false; $packet_type = 'refresh'; } elseif ($cmd === 'location') { logger('notifier: location: ' . $item_id); $s = q("select * from channel where channel_id = %d limit 1", intval($item_id)); if ($s) { $channel = $s[0]; } $uid = $item_id; $recipients = array(); $r = q("select abook_xchan from abook where abook_channel = %d", intval($item_id)); if ($r) { foreach ($r as $rr) { $recipients[] = $rr['abook_xchan']; } } $encoded_item = array('locations' => zot_encode_locations($channel), 'type' => 'location', 'encoding' => 'zot'); $target_item = array('aid' => $channel['channel_account_id'], 'uid' => $channel['channel_id']); $private = false; $packet_type = 'location'; $location = true; } elseif ($cmd === 'purge_all') { logger('notifier: purge_all: ' . $item_id); $s = q("select * from channel where channel_id = %d limit 1", intval($item_id)); if ($s) { $channel = $s[0]; } $uid = $item_id; $recipients = array(); $r = q("select abook_xchan from abook where abook_channel = %d", intval($item_id)); if ($r) { foreach ($r as $rr) { $recipients[] = $rr['abook_xchan']; } } $private = false; $packet_type = 'purge'; } else { // Normal items // Fetch the target item $r = q("SELECT * FROM item WHERE id = %d and parent != 0 LIMIT 1", intval($item_id)); if (!$r) { return; } xchan_query($r); $r = fetch_post_tags($r); $target_item = $r[0]; $deleted_item = false; if (intval($target_item['item_deleted'])) { logger('notifier: target item ITEM_DELETED', LOGGER_DEBUG); $deleted_item = true; } if (intval($target_item['item_type']) != ITEM_TYPE_POST) { logger('notifier: target item not forwardable: type ' . $target_item['item_type'], LOGGER_DEBUG); return; } if (intval($target_item['item_unpublished']) || intval($target_item['item_delayed_publish'])) { logger('notifier: target item not published, so not forwardable', LOGGER_DEBUG); return; } if (strpos($target_item['postopts'], 'nodeliver') !== false) { logger('notifier: target item is undeliverable', LOGGER_DEBUG); return; } $s = q("select * from channel left join xchan on channel_hash = xchan_hash where channel_id = %d limit 1", intval($target_item['uid'])); if ($s) { $channel = $s[0]; } if ($channel['channel_hash'] !== $target_item['author_xchan'] && $channel['channel_hash'] !== $target_item['owner_xchan']) { logger("notifier: Sending channel {$channel['channel_hash']} is not owner {$target_item['owner_xchan']} or author {$target_item['author_xchan']}"); return; } if ($target_item['id'] == $target_item['parent']) { $parent_item = $target_item; $top_level_post = true; } else { // fetch the parent item $r = q("SELECT * from item where id = %d order by id asc", intval($target_item['parent'])); if (!$r) { return; } if (strpos($r[0]['postopts'], 'nodeliver') !== false) { logger('notifier: target item is undeliverable', LOGGER_DEBUG); return; } xchan_query($r); $r = fetch_post_tags($r); $parent_item = $r[0]; $top_level_post = false; } // avoid looping of discover items 12/4/2014 if ($sys && $parent_item['uid'] == $sys['channel_id']) { return; } $encoded_item = encode_item($target_item); // Send comments to the owner to re-deliver to everybody in the conversation // We only do this if the item in question originated on this site. This prevents looping. // To clarify, a site accepting a new comment is responsible for sending it to the owner for relay. // Relaying should never be initiated on a post that arrived from elsewhere. // We should normally be able to rely on ITEM_ORIGIN, but start_delivery_chain() incorrectly set this // flag on comments for an extended period. So we'll also call comment_local_origin() which looks at // the hostname in the message_id and provides a second (fallback) opinion. $relay_to_owner = !$top_level_post && intval($target_item['item_origin']) && comment_local_origin($target_item) ? true : false; $uplink = false; // $cmd === 'relay' indicates the owner is sending it to the original recipients // don't allow the item in the relay command to relay to owner under any circumstances, it will loop logger('notifier: relay_to_owner: ' . ($relay_to_owner ? 'true' : 'false'), LOGGER_DATA); logger('notifier: top_level_post: ' . ($top_level_post ? 'true' : 'false'), LOGGER_DATA); // tag_deliver'd post which needs to be sent back to the original author if ($cmd === 'uplink' && intval($parent_item['item_uplink']) && !$top_level_post) { logger('notifier: uplink'); $uplink = true; } if (($relay_to_owner || $uplink) && $cmd !== 'relay') { logger('notifier: followup relay', LOGGER_DEBUG); $recipients = array($uplink ? $parent_item['source_xchan'] : $parent_item['owner_xchan']); $private = true; if (!$encoded_item['flags']) { $encoded_item['flags'] = array(); } $encoded_item['flags'][] = 'relay'; } else { logger('notifier: normal distribution', LOGGER_DEBUG); if ($cmd === 'relay') { logger('notifier: owner relay'); } // if our parent is a tag_delivery recipient, uplink to the original author causing // a delivery fork. if (intval($parent_item['item_uplink']) && !$top_level_post && $cmd !== 'uplink') { logger('notifier: uplinking this item'); proc_run('php', 'include/notifier.php', 'uplink', $item_id); } $private = false; $recipients = collect_recipients($parent_item, $private); // FIXME add any additional recipients such as mentions, etc. // don't send deletions onward for other people's stuff // TODO verify this is needed - copied logic from same place in old code if (intval($target_item['item_deleted']) && !intval($target_item['item_wall'])) { logger('notifier: ignoring delete notification for non-wall item'); return; } } } $walltowall = $top_level_post && $channel['xchan_hash'] === $target_item['author_xchan'] ? true : false; // Generic delivery section, we have an encoded item and recipients // Now start the delivery process $x = $encoded_item; $x['title'] = 'private'; $x['body'] = 'private'; logger('notifier: encoded item: ' . print_r($x, true), LOGGER_DATA); stringify_array_elms($recipients); if (!$recipients) { return; } // logger('notifier: recipients: ' . print_r($recipients,true)); $env_recips = $private ? array() : null; $details = q("select xchan_hash, xchan_instance_url, xchan_network, xchan_addr, xchan_guid, xchan_guid_sig from xchan where xchan_hash in (" . implode(',', $recipients) . ")"); $recip_list = array(); if ($details) { foreach ($details as $d) { $recip_list[] = $d['xchan_addr'] . ' (' . $d['xchan_hash'] . ')'; if ($private) { $env_recips[] = array('guid' => $d['xchan_guid'], 'guid_sig' => $d['xchan_guid_sig'], 'hash' => $d['xchan_hash']); } if ($d['xchan_network'] === 'mail' && $normal_mode) { $delivery_options = get_xconfig($d['xchan_hash'], 'system', 'delivery_mode'); if (!$delivery_options) { format_and_send_email($channel, $d, $target_item); } } } } if ($private && !$env_recips) { // shouldn't happen logger('notifier: private message with no envelope recipients.' . print_r($argv, true)); } logger('notifier: recipients (may be delivered to more if public): ' . print_r($recip_list, true), LOGGER_DEBUG); // Now we have collected recipients (except for external mentions, FIXME) // Let's reduce this to a set of hubs. $r = q("select * from hubloc where hubloc_hash in (" . implode(',', $recipients) . ") \n\t\tand hubloc_error = 0 and hubloc_deleted = 0"); if (!$r) { logger('notifier: no hubs'); return; } $hubs = $r; $dead_hubs = array(); $dh = q("select site_url from site where site_dead = 1"); if (dh) { foreach ($dh as $dead) { $dead_hubs[] = $dead['site_url']; } } /** * Reduce the hubs to those that are unique. For zot hubs, we need to verify uniqueness by the sitekey, since it may have been * a re-install which has not yet been detected and pruned. * For other networks which don't have or require sitekeys, we'll have to use the URL */ $hublist = array(); // this provides an easily printable list for the logs $dhubs = array(); // delivery hubs where we store our resulting unique array $keys = array(); // array of keys to check uniquness for zot hubs $urls = array(); // array of urls to check uniqueness of hubs from other networks foreach ($hubs as $hub) { if (in_array($hub['hubloc_url'], $dead_hubs)) { logger('skipping dead hub: ' . $hub['hubloc_url'], LOGGER_DEBUG); continue; } if ($hub['hubloc_network'] == 'zot') { if (!in_array($hub['hubloc_sitekey'], $keys)) { $hublist[] = $hub['hubloc_host']; $dhubs[] = $hub; $keys[] = $hub['hubloc_sitekey']; } } else { if (!in_array($hub['hubloc_url'], $urls)) { $hublist[] = $hub['hubloc_host']; $dhubs[] = $hub; $urls[] = $hub['hubloc_url']; } } } logger('notifier: will notify/deliver to these hubs: ' . print_r($hublist, true), LOGGER_DEBUG); $interval = get_config('system', 'delivery_interval') !== false ? intval(get_config('system', 'delivery_interval')) : 2; $deliveries_per_process = intval(get_config('system', 'delivery_batch_count')); if ($deliveries_per_process <= 0) { $deliveries_per_process = 1; } $deliveries = array(); foreach ($dhubs as $hub) { if ($hub['hubloc_network'] !== 'zot') { $narr = array('channel' => $channel, 'env_recips' => $env_recips, 'recipients' => $recipients, 'item' => $item, 'target_item' => $target_item, 'hub' => $hub, 'top_level_post' => $top_level_post, 'private' => $private, 'followup' => $followup, 'relay_to_owner' => $relay_to_owner, 'uplink' => $uplink, 'cmd' => $cmd, 'expire' => $expire, 'mail' => $mail, 'location' => $location, 'fsuggest' => $fsuggest, 'request' => $request, 'normal_mode' => $normal_mode, 'packet_type' => $packet_type, 'walltowall' => $walltowall); call_hooks('notifier_hub', $narr); continue; } // default: zot protocol $hash = random_string(); if ($packet_type === 'refresh' || $packet_type === 'purge') { $n = zot_build_packet($channel, $packet_type); q("insert into outq ( outq_hash, outq_account, outq_channel, outq_driver, outq_posturl, outq_async, outq_created, outq_updated, outq_notify, outq_msg ) values ( '%s', %d, %d, '%s', '%s', %d, '%s', '%s', '%s', '%s' )", dbesc($hash), intval($channel['channel_account_id']), intval($channel['channel_id']), dbesc('zot'), dbesc($hub['hubloc_callback']), intval(1), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc($n), dbesc('')); } elseif ($packet_type === 'request') { $n = zot_build_packet($channel, 'request', $env_recips, $hub['hubloc_sitekey'], $hash, array('message_id' => $request_message_id)); q("insert into outq ( outq_hash, outq_account, outq_channel, outq_driver, outq_posturl, outq_async, outq_created, outq_updated, outq_notify, outq_msg ) values ( '%s', %d, %d, '%s', '%s', %d, '%s', '%s', '%s', '%s' )", dbesc($hash), intval($channel['channel_account_id']), intval($channel['channel_id']), dbesc('zot'), dbesc($hub['hubloc_callback']), intval(1), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc($n), dbesc('')); } else { $n = zot_build_packet($channel, 'notify', $env_recips, $private ? $hub['hubloc_sitekey'] : null, $hash); q("insert into outq ( outq_hash, outq_account, outq_channel, outq_driver, outq_posturl, outq_async, outq_created, outq_updated, outq_notify, outq_msg ) values ( '%s', %d, %d, '%s', '%s', %d, '%s', '%s', '%s', '%s' )", dbesc($hash), intval($target_item['aid']), intval($target_item['uid']), dbesc('zot'), dbesc($hub['hubloc_callback']), intval(1), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc($n), dbesc(json_encode($encoded_item))); // only create delivery reports for normal undeleted items if (array_key_exists('postopts', $target_item) && !$target_item['item_deleted']) { q("insert into dreport ( dreport_mid, dreport_site, dreport_recip, dreport_result, dreport_time, dreport_xchan, dreport_queue ) values ( '%s','%s','%s','%s','%s','%s','%s' ) ", dbesc($target_item['mid']), dbesc($hub['hubloc_host']), dbesc($hub['hubloc_host']), dbesc('queued'), dbesc(datetime_convert()), dbesc($channel['channel_hash']), dbesc($hash)); } } $deliveries[] = $hash; } if ($normal_mode) { $x = q("select * from hook where hook = 'notifier_normal'"); if ($x) { proc_run('php', 'deliver_hooks.php', $target_item['id']); } } if ($deliveries) { $deliver = array(); foreach ($deliveries as $d) { $deliver[] = $d; if (count($deliver) >= $deliveries_per_process) { proc_run('php', 'include/deliver.php', $deliver); $deliver = array(); if ($interval) { @time_sleep_until(microtime(true) + (double) $interval); } } } } // catch any stragglers if ($deliver) { proc_run('php', 'include/deliver.php', $deliver); } logger('notifier: basic loop complete.', LOGGER_DEBUG); call_hooks('notifier_end', $target_item); logger('notifer: complete.'); return; }
function ping_site($url) { $ret = array('success' => false); $sys = get_sys_channel(); $m = zot_build_packet($sys, 'ping'); $r = zot_zot($url . '/post', $m); if (!$r['success']) { $ret['message'] = 'no answer from ' . $url; return $ret; } $packet_result = json_decode($r['body'], true); if (!$packet_result['success']) { $ret['message'] = 'packet failure from ' . $url; return $ret; } if ($packet_result['success']) { $ret['success'] = true; } else { $ret['message'] = 'unknown error from ' . $url; } return $ret; }
function deliver_run($argv, $argc) { cli_startup(); $a = get_app(); if ($argc < 2) { return; } logger('deliver: invoked: ' . print_r($argv, true), LOGGER_DATA); for ($x = 1; $x < $argc; $x++) { $r = q("select * from outq where outq_hash = '%s' limit 1", dbesc($argv[$x])); if ($r) { /** * Check to see if we have any recent communications with this hub (in the last month). * If not, reduce the outq_priority. */ $h = parse_url($r[0]['outq_posturl']); if ($h) { $base = $h['scheme'] . '://' . $h['host'] . ($h['port'] ? ':' . $h['port'] : ''); if ($base !== z_root()) { $y = q("select site_update, site_dead from site where site_url = '%s' ", dbesc($base)); if ($y) { if (intval($y[0]['site_dead'])) { q("delete from outq where outq_posturl = '%s'", dbesc($r[0]['outq_posturl'])); logger('dead site ignored ' . $base); continue; } if ($y[0]['site_update'] < datetime_convert('UTC', 'UTC', 'now - 1 month')) { q("update outq set outq_priority = %d where outq_hash = '%s'", intval($r[0]['outq_priority'] + 10), dbesc($r[0]['outq_hash'])); logger('immediate delivery deferred for site ' . $base); continue; } } } } // "post" queue driver - used for diaspora and friendica-over-diaspora communications. if ($r[0]['outq_driver'] === 'post') { $result = z_post_url($r[0]['outq_posturl'], $r[0]['outq_msg']); if ($result['success'] && $result['return_code'] < 300) { logger('deliver: queue post success to ' . $r[0]['outq_posturl'], LOGGER_DEBUG); $y = q("delete from outq where outq_hash = '%s'", dbesc($argv[$x])); } else { logger('deliver: queue post returned ' . $result['return_code'] . ' from ' . $r[0]['outq_posturl'], LOGGER_DEBUG); $y = q("update outq set outq_updated = '%s' where outq_hash = '%s'", dbesc(datetime_convert()), dbesc($argv[$x])); } continue; } $notify = json_decode($r[0]['outq_notify'], true); // Check if this is a conversation request packet. It won't have outq_msg // but will be an encrypted packet - so will need to be handed off to // web delivery rather than processed inline. $sendtoweb = false; if (array_key_exists('iv', $notify) && !$r[0]['outq_msg']) { $sendtoweb = true; } if ($r[0]['outq_posturl'] === z_root() . '/post' && !$sendtoweb) { logger('deliver: local delivery', LOGGER_DEBUG); // local delivery // we should probably batch these and save a few delivery processes if ($r[0]['outq_msg']) { $m = json_decode($r[0]['outq_msg'], true); if (array_key_exists('message_list', $m)) { foreach ($m['message_list'] as $mm) { $msg = array('body' => json_encode(array('success' => true, 'pickup' => array(array('notify' => $notify, 'message' => $mm))))); zot_import($msg, z_root()); } } else { $msg = array('body' => json_encode(array('success' => true, 'pickup' => array(array('notify' => $notify, 'message' => $m))))); zot_import($msg, z_root()); } $r = q("delete from outq where outq_hash = '%s'", dbesc($argv[$x])); } } else { logger('deliver: dest: ' . $r[0]['outq_posturl'], LOGGER_DEBUG); $result = zot_zot($r[0]['outq_posturl'], $r[0]['outq_notify']); if ($result['success']) { logger('deliver: remote zot delivery succeeded to ' . $r[0]['outq_posturl']); zot_process_response($r[0]['outq_posturl'], $result, $r[0]); } else { logger('deliver: remote zot delivery failed to ' . $r[0]['outq_posturl']); $y = q("update outq set outq_updated = '%s' where outq_hash = '%s'", dbesc(datetime_convert()), dbesc($argv[$x])); } } } } }
function deliver_run($argv, $argc) { cli_startup(); $a = get_app(); if ($argc < 2) { return; } logger('deliver: invoked: ' . print_r($argv, true), LOGGER_DATA); for ($x = 1; $x < $argc; $x++) { $dresult = null; $r = q("select * from outq where outq_hash = '%s' limit 1", dbesc($argv[$x])); if ($r) { /** * Check to see if we have any recent communications with this hub (in the last month). * If not, reduce the outq_priority. */ $h = parse_url($r[0]['outq_posturl']); if ($h) { $base = $h['scheme'] . '://' . $h['host'] . ($h['port'] ? ':' . $h['port'] : ''); if ($base !== z_root()) { $y = q("select site_update, site_dead from site where site_url = '%s' ", dbesc($base)); if ($y) { if (intval($y[0]['site_dead'])) { q("delete from outq where outq_posturl = '%s'", dbesc($r[0]['outq_posturl'])); logger('dead site ignored ' . $base); continue; } if ($y[0]['site_update'] < datetime_convert('UTC', 'UTC', 'now - 1 month')) { q("update outq set outq_priority = %d where outq_hash = '%s'", intval($r[0]['outq_priority'] + 10), dbesc($r[0]['outq_hash'])); logger('immediate delivery deferred for site ' . $base); continue; } } else { // zot sites should all have a site record, unless they've been dead for as long as // your site has existed. Since we don't know for sure what these sites are, // call them unknown q("insert into site (site_url, site_update, site_dead, site_type) values ('%s','%s',0,%d) ", dbesc($base), dbesc(datetime_convert()), intval($r[0]['outq_driver'] === 'post' ? SITE_TYPE_NOTZOT : SITE_TYPE_UNKNOWN)); } } } // "post" queue driver - used for diaspora and friendica-over-diaspora communications. if ($r[0]['outq_driver'] === 'post') { $result = z_post_url($r[0]['outq_posturl'], $r[0]['outq_msg']); if ($result['success'] && $result['return_code'] < 300) { logger('deliver: queue post success to ' . $r[0]['outq_posturl'], LOGGER_DEBUG); q("update site set site_update = '%s', site_dead = 0 where site_url = '%s' ", dbesc(datetime_convert()), dbesc($site_url)); q("update dreport set status = '%s', dreport_time = '%s' where dreport_queue = '%s' limit 1", dbesc('accepted for delivery'), dbesc(datetime_convert()), dbesc($argv[$x])); $y = q("delete from outq where outq_hash = '%s'", dbesc($argv[$x])); } else { logger('deliver: queue post returned ' . $result['return_code'] . ' from ' . $r[0]['outq_posturl'], LOGGER_DEBUG); $y = q("update outq set outq_updated = '%s' where outq_hash = '%s'", dbesc(datetime_convert()), dbesc($argv[$x])); } continue; } $notify = json_decode($r[0]['outq_notify'], true); // Check if this is a conversation request packet. It won't have outq_msg // but will be an encrypted packet - so will need to be handed off to // web delivery rather than processed inline. $sendtoweb = false; if (array_key_exists('iv', $notify) && !$r[0]['outq_msg']) { $sendtoweb = true; } if ($r[0]['outq_posturl'] === z_root() . '/post' && !$sendtoweb) { logger('deliver: local delivery', LOGGER_DEBUG); // local delivery // we should probably batch these and save a few delivery processes if ($r[0]['outq_msg']) { $m = json_decode($r[0]['outq_msg'], true); if (array_key_exists('message_list', $m)) { foreach ($m['message_list'] as $mm) { $msg = array('body' => json_encode(array('success' => true, 'pickup' => array(array('notify' => $notify, 'message' => $mm))))); zot_import($msg, z_root()); } } else { $msg = array('body' => json_encode(array('success' => true, 'pickup' => array(array('notify' => $notify, 'message' => $m))))); $dresult = zot_import($msg, z_root()); } $r = q("delete from outq where outq_hash = '%s'", dbesc($argv[$x])); if ($dresult && is_array($dresult)) { foreach ($dresult as $xx) { if (is_array($xx) && array_key_exists('message_id', $xx)) { q("insert into dreport ( dreport_mid, dreport_site, dreport_recip, dreport_result, dreport_time, dreport_xchan ) values ( '%s', '%s','%s','%s','%s','%s' ) ", dbesc($xx['message_id']), dbesc($xx['location']), dbesc($xx['recipient']), dbesc($xx['status']), dbesc(datetime_convert($xx['date'])), dbesc($xx['sender'])); } } } q("delete from dreport where dreport_queue = '%s' limit 1", dbesc($argv[$x])); } } else { logger('deliver: dest: ' . $r[0]['outq_posturl'], LOGGER_DEBUG); $result = zot_zot($r[0]['outq_posturl'], $r[0]['outq_notify']); if ($result['success']) { logger('deliver: remote zot delivery succeeded to ' . $r[0]['outq_posturl']); zot_process_response($r[0]['outq_posturl'], $result, $r[0]); } else { logger('deliver: remote zot delivery failed to ' . $r[0]['outq_posturl']); $y = q("update outq set outq_updated = '%s' where outq_hash = '%s'", dbesc(datetime_convert()), dbesc($argv[$x])); } } } } }
function queue_run($argv, $argc) { cli_startup(); global $a; require_once 'include/items.php'; require_once 'include/bbcode.php'; if (argc() > 1) { $queue_id = argv(1); } else { $queue_id = 0; } $deadguys = array(); logger('queue: start'); $r = q("select outq_posturl from outq where outq_created < %s - INTERVAL %s", db_utcnow(), db_quoteinterval('3 DAY')); if ($r) { foreach ($r as $rr) { $site_url = ''; $h = parse_url($rr['outq_posturl']); $desturl = $h['scheme'] . '://' . $h['host'] . ($h['port'] ? ':' . $h['port'] : ''); q("update site set site_dead = 1 where site_dead = 0 and site_url = '%s' and site_update < %s - INTERVAL %s", dbesc($desturl), db_utcnow(), db_quoteinterval('1 MONTH')); } } $r = q("DELETE FROM outq WHERE outq_created < %s - INTERVAL %s", db_utcnow(), db_quoteinterval('3 DAY')); if ($queue_id) { $r = q("SELECT * FROM outq WHERE outq_hash = '%s' LIMIT 1", dbesc($queue_id)); } else { // For the first 12 hours we'll try to deliver every 15 minutes // After that, we'll only attempt delivery once per hour. // This currently only handles the default queue drivers ('zot' or '') which we will group by posturl // so that we don't start off a thousand deliveries for a couple of dead hubs. // The zot driver will deliver everything destined for a single hub once contact is made (*if* contact is made). // Other drivers will have to do something different here and may need their own query. // Note: this requires some tweaking as new posts to long dead hubs once a day will keep them in the // "every 15 minutes" category. We probably need to prioritise them when inserted into the queue // or just prior to this query based on recent and long-term delivery history. If we have good reason to believe // the site is permanently down, there's no reason to attempt delivery at all, or at most not more than once // or twice a day. // FIXME: can we sort postgres on outq_priority and maintain the 'distinct' ? // The order by max(outq_priority) might be a dodgy query because of the group by. // The desired result is to return a sequence in the order most likely to be delivered in this run. // If a hub has already been sitting in the queue for a few days, they should be delivered last; // hence every failure should drop them further down the priority list. if (ACTIVE_DBTYPE == DBTYPE_POSTGRES) { $prefix = 'DISTINCT ON (outq_posturl)'; $suffix = 'ORDER BY outq_posturl'; } else { $prefix = ''; $suffix = 'GROUP BY outq_posturl ORDER BY max(outq_priority)'; } $r = q("SELECT {$prefix} * FROM outq WHERE outq_delivered = 0 and (( outq_created > %s - INTERVAL %s and outq_updated < %s - INTERVAL %s ) OR ( outq_updated < %s - INTERVAL %s )) {$suffix}", db_utcnow(), db_quoteinterval('12 HOUR'), db_utcnow(), db_quoteinterval('15 MINUTE'), db_utcnow(), db_quoteinterval('1 HOUR')); } if (!$r) { return; } foreach ($r as $rr) { if (in_array($rr['outq_posturl'], $deadguys)) { continue; } if ($rr['outq_driver'] === 'post') { $result = z_post_url($rr['outq_posturl'], $rr['outq_msg']); if ($result['success'] && $result['return_code'] < 300) { logger('queue: queue post success to ' . $rr['outq_posturl'], LOGGER_DEBUG); $y = q("delete from outq where outq_hash = '%s'", dbesc($rr['ouq_hash'])); } else { logger('queue: queue post returned ' . $result['return_code'] . ' from ' . $rr['outq_posturl'], LOGGER_DEBUG); $y = q("update outq set outq_updated = '%s', outq_priority = outq_priority + 10 where outq_hash = '%s'", dbesc(datetime_convert()), dbesc($rr['outq_hash'])); } continue; } $result = zot_zot($rr['outq_posturl'], $rr['outq_notify']); if ($result['success']) { logger('queue: deliver zot success to ' . $rr['outq_posturl'], LOGGER_DEBUG); zot_process_response($rr['outq_posturl'], $result, $rr); } else { $deadguys[] = $rr['outq_posturl']; logger('queue: deliver zot returned ' . $result['return_code'] . ' from ' . $rr['outq_posturl'], LOGGER_DEBUG); $y = q("update outq set outq_updated = '%s', outq_priority = outq_priority + 10 where outq_hash = '%s'", dbesc(datetime_convert()), dbesc($rr['outq_hash'])); } } }
/** * @brief * * We received a notification packet (in mod/post.php) that a message is waiting for us, and we've verified the sender. * Now send back a pickup message, using our message tracking ID ($arr['secret']), which we will sign with our site private key. * The entire pickup message is encrypted with the remote site's public key. * If everything checks out on the remote end, we will receive back a packet containing one or more messages, * which will be processed and delivered before this function ultimately returns. * * @see zot_import() * * @param array $arr * decrypted and json decoded notify packet from remote site * @return array from zot_import() */ function zot_fetch($arr) { logger('zot_fetch: ' . print_r($arr, true), LOGGER_DATA); $url = $arr['sender']['url'] . $arr['callback']; // set $multiple param on zot_gethub() to return all matching hubs // This allows us to recover from re-installs when a redundant (but invalid) hubloc for // this identity is widely dispersed throughout the network. $ret_hubs = zot_gethub($arr['sender'], true); if (!$ret_hubs) { logger('zot_fetch: no hub: ' . print_r($arr['sender'], true)); return; } foreach ($ret_hubs as $ret_hub) { $data = array('type' => 'pickup', 'url' => z_root(), 'callback_sig' => base64url_encode(rsa_sign(z_root() . '/post', get_config('system', 'prvkey'))), 'callback' => z_root() . '/post', 'secret' => $arr['secret'], 'secret_sig' => base64url_encode(rsa_sign($arr['secret'], get_config('system', 'prvkey')))); $datatosend = json_encode(crypto_encapsulate(json_encode($data), $ret_hub['hubloc_sitekey'])); $fetch = zot_zot($url, $datatosend); $result = zot_import($fetch, $arr['sender']['url']); if ($result) { return $result; } } return; }
/** * @brief * * We received a notification packet (in mod/post.php) that a message is waiting for us, and we've verified the sender. * Now send back a pickup message, using our message tracking ID ($arr['secret']), which we will sign with our site private key. * The entire pickup message is encrypted with the remote site's public key. * If everything checks out on the remote end, we will receive back a packet containing one or more messages, * which will be processed and delivered before this function ultimately returns. * * @see zot_import() * * @param array $arr * decrypted and json decoded notify packet from remote site * @return array from zot_import() */ function zot_fetch($arr) { logger('zot_fetch: ' . print_r($arr, true), LOGGER_DATA); $url = $arr['sender']['url'] . $arr['callback']; $ret_hub = zot_gethub($arr['sender']); if (!$ret_hub) { logger('zot_fetch: no hub: ' . print_r($arr['sender'], true)); return; } $data = array('type' => 'pickup', 'url' => z_root(), 'callback_sig' => base64url_encode(rsa_sign(z_root() . '/post', get_config('system', 'prvkey'))), 'callback' => z_root() . '/post', 'secret' => $arr['secret'], 'secret_sig' => base64url_encode(rsa_sign($arr['secret'], get_config('system', 'prvkey')))); $datatosend = json_encode(crypto_encapsulate(json_encode($data), $ret_hub['hubloc_sitekey'])); $fetch = zot_zot($url, $datatosend); $result = zot_import($fetch, $arr['sender']['url']); return $result; }