function get() { $o .= '<h3>Probe Diagnostic</h3>'; $o .= '<form action="probe" method="get">'; $o .= 'Lookup address: <input type="text" style="width: 250px;" name="addr" value="' . $_GET['addr'] . '" />'; $o .= '<input type="submit" name="submit" value="Submit" /></form>'; $o .= '<br /><br />'; if (x($_GET, 'addr')) { $channel = \App::get_channel(); $addr = trim($_GET['addr']); $do_import = intval($_GET['import']) && is_site_admin() ? true : false; $j = \Zotlabs\Zot\Finger::run($addr, $channel, false); // $res = zot_finger($addr,$channel,false); $o .= '<pre>'; if (!$j['success']) { $o .= sprintf(t('Fetching URL returns error: %1$s'), $res['error'] . "\r\n\r\n"); $o .= "<strong>https connection failed. Trying again with auto failover to http.</strong>\r\n\r\n"; $j = \Zotlabs\Zot\Finger::run($addr, $channel, true); if (!$j['success']) { $o .= sprintf(t('Fetching URL returns error: %1$s'), $res['error'] . "\r\n\r\n"); } } if ($do_import && $j) { $x = import_xchan($j); } if ($j && $j['permissions'] && $j['permissions']['iv']) { $j['permissions'] = json_decode(crypto_unencapsulate($j['permissions'], $channel['channel_prvkey']), true); } $o .= str_replace("\n", '<br />', print_r($j, true)); $o .= '</pre>'; } return $o; }
function GetHublocs($address) { // Try and find a hubloc for the person attempting to auth. // Since we're matching by address, we have to return all entries // some of which may be from re-installed hubs; and we'll need to // try each sequentially to see if one can pass the test $x = q("select * from hubloc left join xchan on xchan_hash = hubloc_hash \n\t\t\twhere 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 \n\t\t\t\t\twhere hubloc_addr = '%s' order by hubloc_id desc", dbesc($address)); } } if (!$x) { logger('mod_zot: auth: unable to finger ' . $address); $this->Debug('no hubloc found for ' . $address . ' and probing failed.'); $this->Finalise(); } return $x; }
function gprobe_run($argv, $argc) { cli_startup(); $a = get_app(); if ($argc != 2) { return; } $url = hex2bin($argv[1]); $r = q("select * from xchan where xchan_addr = '%s' limit 1", dbesc($url)); if (!$r) { $x = zot_finger($url, null); if ($x['success']) { $j = json_decode($x['body'], true); $y = import_xchan($j); } } return; }
public static function run($argc, $argv) { if ($argc != 2) { return; } $url = hex2bin($argv[1]); if (!strpos($url, '@')) { return; } $r = q("select * from xchan where xchan_addr = '%s' limit 1", dbesc($url)); if (!$r) { $j = \Zotlabs\Zot\Finger::run($url, null); if ($j['success']) { $y = import_xchan($j); } } return; }
function GetHublocs($address) { // Try and find a hubloc for the person attempting to auth $x = q("select * from hubloc left join xchan on xchan_hash = hubloc_hash \n\t\t\twhere 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 \n\t\t\t\t\twhere hubloc_addr = '%s' order by hubloc_id desc", dbesc($address)); } } if (!$x) { logger('mod_zot: auth: unable to finger ' . $address); $this->reply_die('no hubloc found for ' . $address . ' and probing failed.'); } return $x; }
/** * @brief * * Given an update record, probe the channel, grab a zot-info packet and refresh/sync the data. * * Ignore updating records marked as deleted. * * If successful, sets ud_last in the DB to the current datetime for this * reddress/webbie. * * @param array $ud Entry from update table */ function update_directory_entry($ud) { logger('update_directory_entry: ' . print_r($ud, true), LOGGER_DATA); if ($ud['ud_addr'] && !($ud['ud_flags'] & UPDATE_FLAGS_DELETED)) { $success = false; $x = zot_finger($ud['ud_addr'], ''); if ($x['success']) { $j = json_decode($x['body'], true); if ($j) { $success = true; } $y = import_xchan($j, 0, $ud); } if (!$success) { q("update updates set ud_last = '%s' where ud_addr = '%s'", dbesc(datetime_convert()), dbesc($ud['ud_addr'])); } } }
function mail_post(&$a) { if (!local_user()) { return; } $replyto = x($_REQUEST, 'replyto') ? notags(trim($_REQUEST['replyto'])) : ''; $subject = x($_REQUEST, 'subject') ? notags(trim($_REQUEST['subject'])) : ''; $body = x($_REQUEST, 'body') ? escape_tags(trim($_REQUEST['body'])) : ''; $recipient = x($_REQUEST, 'messageto') ? notags(trim($_REQUEST['messageto'])) : ''; $rstr = x($_REQUEST, 'messagerecip') ? notags(trim($_REQUEST['messagerecip'])) : ''; $expires = x($_REQUEST, 'expires') ? datetime_convert(date_default_timezone_get(), 'UTC', $_REQUEST['expires']) : NULL_DATE; // If we have a raw string for a recipient which hasn't been auto-filled, // it means they probably aren't in our address book, hence we don't know // if we have permission to send them private messages. // finger them and find out before we try and send it. if (!$recipient) { $channel = $a->get_channel(); $ret = zot_finger($rstr, $channel); if (!$ret['success']) { notice(t('Unable to lookup recipient.') . EOL); return; } $j = json_decode($ret['body'], true); logger('message_post: lookup: ' . $url . ' ' . print_r($j, true)); if (!($j['success'] && $j['guid'])) { notice(t('Unable to communicate with requested channel.')); return; } $x = import_xchan($j); if (!$x['success']) { notice(t('Cannot verify requested channel.')); return; } $recipient = $x['hash']; $their_perms = 0; $global_perms = get_perms(); if ($j['permissions']['data']) { $permissions = crypto_unencapsulate($j['permissions'], $channel['channel_prvkey']); if ($permissions) { $permissions = json_decode($permissions); } logger('decrypted permissions: ' . print_r($permissions, true), LOGGER_DATA); } else { $permissions = $j['permissions']; } foreach ($permissions as $k => $v) { if ($v) { $their_perms = $their_perms | intval($global_perms[$k][1]); } } if (!($their_perms & PERMS_W_MAIL)) { notice(t('Selected channel has private message restrictions. Send failed.')); return; } } // if(feature_enabled(local_user(),'richtext')) { // $body = fix_mce_lf($body); // } if (!$recipient) { notice('No recipient found.'); $a->argc = 2; $a->argv[1] = 'new'; return; } // We have a local_user, let send_message use the session channel and save a lookup $ret = send_message(0, $recipient, $body, $subject, $replyto, $expires); if (!$ret['success']) { notice($ret['message']); } goaway(z_root() . '/message'); }
function init() { $result = array('success' => false); $url = $_REQUEST['url']; $access_token = $_REQUEST['t']; $valid = 0; // we probably don't need the realm as we will find out in the probe. // What we may want to die is throw an error if you're trying to register in a different realm // so this configuration issue can be discovered. $realm = $_REQUEST['realm']; if (!$realm) { $realm = DIRECTORY_REALM; } if ($realm === DIRECTORY_REALM) { $valid = 1; } else { $token = get_config('system', 'realm_token'); if ($token && $access_token != $token) { $result['message'] = 'This realm requires an access token'; return; } $valid = 1; } $dirmode = intval(get_config('system', 'directory_mode')); if ($dirmode == DIRECTORY_MODE_NORMAL) { $ret['message'] = t('This site is not a directory server'); json_return_and_die($ret); } $m = null; if ($url) { $m = parse_url($url); if (!$m || !@dns_get_record($m['host'], DNS_A + DNS_CNAME + DNS_PTR) && !filter_var($m['host'], FILTER_VALIDATE_IP)) { $result['message'] = 'unparseable url'; json_return_and_die($result); } $j = \Zotlabs\Zot\Finger::run('[system]@' . $m['host']); if ($j['success'] && $j['guid']) { $x = import_xchan($j); if ($x['success']) { $result['success'] = true; } } if (!$result['success']) { $valid = 0; } q("update site set site_valid = %d where site_url = '%s' limit 1", intval($valid), strtolower($url)); json_return_and_die($result); } else { // We can put this in the sql without the condition after 31 august 2015 assuming // most directory servers will have updated by then // This just makes sure it happens if I forget $sql_extra = datetime_convert() > datetime_convert('UTC', 'UTC', '2015-08-31') ? ' and site_valid = 1 ' : ''; if ($dirmode == DIRECTORY_MODE_STANDALONE) { $r = array(array('site_url' => z_root())); } else { $r = q("select site_url from site where site_flags in ( 1, 2 ) and site_realm = '%s' and site_type = %d {$sql_extra} ", dbesc(get_directory_realm()), intval(SITE_TYPE_ZOT)); } if ($r) { $result['success'] = true; $result['directories'] = array(); foreach ($r as $rr) { $result['directories'][] = $rr['site_url']; } json_return_and_die($result); } } json_return_and_die($result); }
/** * poco_load * * xchan is your connection * We will load their friend list, and store in xlink_xchan your connection hash and xlink_link the hash for each connection * If xchan isn't provided we will load the list of people from url who have indicated they are willing to be friends with * new folks and add them to xlink with no xlink_xchan. * * Old behaviour: (documentation only): * Given a contact-id (minimum), load the PortableContacts friend list for that contact, * and add the entries to the gcontact (Global Contact) table, or update existing entries * if anything (name or photo) has changed. * We use normalised urls for comparison which ignore http vs https and www.domain vs domain * * Once the global contact is stored add (if necessary) the contact linkage which associates * the given uid, cid to the global contact entry. There can be many uid/cid combinations * pointing to the same global contact id. * * @param string $xchan * @param string $url */ function poco_load($xchan = '', $url = null) { if ($xchan && !$url) { $r = q("select xchan_connurl from xchan where xchan_hash = '%s' limit 1", dbesc($xchan)); if ($r) { $url = $r[0]['xchan_connurl']; } } if (!$url) { logger('poco_load: no url'); return; } $url = $url . '?f=&fields=displayName,hash,urls,photos'; logger('poco_load: ' . $url, LOGGER_DEBUG); $s = z_fetch_url($url); if (!$s['success']) { if ($s['return_code'] == 401) { logger('poco_load: protected'); } elseif ($s['return_code'] == 404) { logger('poco_load: nothing found'); } else { logger('poco_load: returns ' . print_r($s, true)); } return; } $j = json_decode($s['body'], true); if (!$j) { logger('poco_load: unable to json_decode returned data.'); return; } logger('poco_load: ' . print_r($j, true), LOGGER_DATA); if ($xchan) { if (array_key_exists('chatrooms', $j) && is_array($j['chatrooms'])) { foreach ($j['chatrooms'] as $room) { if (!$room['url'] || !$room['desc']) { continue; } $r = q("select * from xchat where xchat_url = '%s' and xchat_xchan = '%s' limit 1", dbesc($room['url']), dbesc($xchan)); if ($r) { q("update xchat set xchat_edited = '%s' where xchat_id = %d", dbesc(datetime_convert()), intval($r[0]['xchat_id'])); } else { $x = q("insert into xchat ( xchat_url, xchat_desc, xchat_xchan, xchat_edited )\n\t\t\t\t\t\tvalues ( '%s', '%s', '%s', '%s' ) ", dbesc(escape_tags($room['url'])), dbesc(escape_tags($room['desc'])), dbesc($xchan), dbesc(datetime_convert())); } } } q("delete from xchat where xchat_edited < %s - INTERVAL %s and xchat_xchan = '%s' ", db_utcnow(), db_quoteinterval('7 DAY'), dbesc($xchan)); } if (!(x($j, 'entry') && is_array($j['entry']))) { logger('poco_load: no entries'); return; } $total = 0; foreach ($j['entry'] as $entry) { $profile_url = ''; $profile_photo = ''; $address = ''; $name = ''; $hash = ''; $rating = 0; $name = $entry['displayName']; $hash = $entry['hash']; if (x($entry, 'urls') && is_array($entry['urls'])) { foreach ($entry['urls'] as $url) { if ($url['type'] == 'profile') { $profile_url = $url['value']; continue; } if ($url['type'] == 'zot' || $url['type'] == 'diaspora' || $url['type'] == 'friendica') { $network = $url['type']; $address = str_replace('acct:', '', $url['value']); continue; } } } if (x($entry, 'photos') && is_array($entry['photos'])) { foreach ($entry['photos'] as $photo) { if ($photo['type'] == 'profile') { $profile_photo = $photo['value']; continue; } } } if (!$name || !$profile_url || !$profile_photo || !$hash || !$address) { logger('poco_load: missing data'); continue; } $x = q("select xchan_hash from xchan where xchan_hash = '%s' limit 1", dbesc($hash)); // We've never seen this person before. Import them. if ($x !== false && !count($x)) { if ($address) { if ($network === 'zot') { $j = Zotlabs\Zot\Finger::run($address, null); if ($j['success']) { import_xchan($j); } $x = q("select xchan_hash from xchan where xchan_hash = '%s' limit 1", dbesc($hash)); if (!$x) { continue; } } else { $x = import_author_diaspora(array('address' => $address)); if (!$x) { continue; } } } else { continue; } } $total++; $r = q("select * from xlink where xlink_xchan = '%s' and xlink_link = '%s' and xlink_static = 0 limit 1", dbesc($xchan), dbesc($hash)); if (!$r) { q("insert into xlink ( xlink_xchan, xlink_link, xlink_updated, xlink_static ) values ( '%s', '%s', '%s', 0 ) ", dbesc($xchan), dbesc($hash), dbesc(datetime_convert())); } else { q("update xlink set xlink_updated = '%s' where xlink_id = %d", dbesc(datetime_convert()), intval($r[0]['xlink_id'])); } } logger("poco_load: loaded {$total} entries", LOGGER_DEBUG); q("delete from xlink where xlink_xchan = '%s' and xlink_updated < %s - INTERVAL %s and xlink_static = 0", dbesc($xchan), db_utcnow(), db_quoteinterval('2 DAY')); }
function new_contact($uid, $url, $channel, $interactive = false, $confirm = false) { $result = array('success' => false, 'message' => ''); $a = get_app(); $is_red = false; $is_http = strpos($url, '://') !== false ? true : false; if ($is_http && substr($url, -1, 1) === '/') { $url = substr($url, 0, -1); } if (!allowed_url($url)) { $result['message'] = t('Channel is blocked on this site.'); return $result; } if (!$url) { $result['message'] = t('Channel location missing.'); return $result; } // check service class limits $r = q("select count(*) as total from abook where abook_channel = %d and abook_self = 0 ", intval($uid)); if ($r) { $total_channels = $r[0]['total']; } if (!service_class_allows($uid, 'total_channels', $total_channels)) { $result['message'] = upgrade_message(); return $result; } $arr = array('url' => $url, 'channel' => array()); call_hooks('follow', $arr); if ($arr['channel']['success']) { $ret = $arr['channel']; } elseif (!$is_http) { $ret = zot_finger($url, $channel); } if ($ret && $ret['success']) { $is_red = true; $j = json_decode($ret['body'], true); } $my_perms = get_channel_default_perms($uid); $role = get_pconfig($uid, 'system', 'permissions_role'); if ($role) { $x = get_role_perms($role); if ($x['perms_follow']) { $my_perms = $x['perms_follow']; } } if ($is_red && $j) { logger('follow: ' . $url . ' ' . print_r($j, true), LOGGER_DEBUG); if (!($j['success'] && $j['guid'])) { $result['message'] = t('Response from remote channel was incomplete.'); logger('mod_follow: ' . $result['message']); return $result; } // Premium channel, set confirm before callback to avoid recursion if (array_key_exists('connect_url', $j) && $interactive && !$confirm) { goaway(zid($j['connect_url'])); } // do we have an xchan and hubloc? // If not, create them. $x = import_xchan($j); if (array_key_exists('deleted', $j) && intval($j['deleted'])) { $result['message'] = t('Channel was deleted and no longer exists.'); return $result; } if (!$x['success']) { return $x; } $xchan_hash = $x['hash']; $their_perms = 0; $global_perms = get_perms(); if (array_key_exists('permissions', $j) && array_key_exists('data', $j['permissions'])) { $permissions = crypto_unencapsulate(array('data' => $j['permissions']['data'], 'key' => $j['permissions']['key'], 'iv' => $j['permissions']['iv']), $channel['channel_prvkey']); if ($permissions) { $permissions = json_decode($permissions, true); } logger('decrypted permissions: ' . print_r($permissions, true), LOGGER_DATA); } else { $permissions = $j['permissions']; } foreach ($permissions as $k => $v) { if ($v) { $their_perms = $their_perms | intval($global_perms[$k][1]); } } } else { $their_perms = 0; $xchan_hash = ''; $r = q("select * from xchan where xchan_hash = '%s' or xchan_url = '%s' limit 1", dbesc($url), dbesc($url)); if (!$r) { // attempt network auto-discovery if (strpos($url, '@') && !$is_http) { $r = discover_by_webbie($url); } elseif ($is_http) { $r = discover_by_url($url); $r['allowed'] = intval(get_config('system', 'feed_contacts')); } if ($r) { $r['channel_id'] = $uid; call_hooks('follow_allow', $r); if (!$r['allowed']) { $result['message'] = t('Protocol disabled.'); return $result; } $r = q("select * from xchan where xchan_hash = '%s' or xchan_url = '%s' limit 1", dbesc($url), dbesc($url)); } } if ($r) { $xchan_hash = $r[0]['xchan_hash']; $their_perms = 0; } } if (!$xchan_hash) { $result['message'] = t('Channel discovery failed.'); logger('follow: ' . $result['message']); return $result; } if (local_channel() && $uid == local_channel()) { $aid = get_account_id(); $hash = get_observer_hash(); $ch = $a->get_channel(); $default_group = $ch['channel_default_group']; } else { $r = q("select * from channel where channel_id = %d limit 1", intval($uid)); if (!$r) { $result['message'] = t('local account not found.'); return $result; } $aid = $r[0]['channel_account_id']; $hash = $r[0]['channel_hash']; $default_group = $r[0]['channel_default_group']; } if ($is_http) { $r = q("select count(*) as total from abook where abook_account = %d and abook_feed = 1 ", intval($aid)); if ($r) { $total_feeds = $r[0]['total']; } if (!service_class_allows($uid, 'total_feeds', $total_feeds)) { $result['message'] = upgrade_message(); return $result; } } if ($hash == $xchan_hash) { $result['message'] = t('Cannot connect to yourself.'); return $result; } $r = q("select abook_xchan from abook where abook_xchan = '%s' and abook_channel = %d limit 1", dbesc($xchan_hash), intval($uid)); if ($r) { $x = q("update abook set abook_their_perms = %d where abook_id = %d", intval($their_perms), intval($r[0]['abook_id'])); } else { $closeness = get_pconfig($uid, 'system', 'new_abook_closeness'); if ($closeness === false) { $closeness = 80; } $r = q("insert into abook ( abook_account, abook_channel, abook_closeness, abook_xchan, abook_feed, abook_their_perms, abook_my_perms, abook_created, abook_updated )\n\t\t\tvalues( %d, %d, %d, '%s', %d, %d, %d, '%s', '%s' ) ", intval($aid), intval($uid), intval($closeness), dbesc($xchan_hash), intval($is_http ? 1 : 0), intval($is_http ? $their_perms | PERMS_R_STREAM | PERMS_A_REPUBLISH : $their_perms), intval($my_perms), dbesc(datetime_convert()), dbesc(datetime_convert())); } if (!$r) { logger('mod_follow: abook creation failed'); } $r = q("select abook.*, xchan.* from abook left join xchan on abook_xchan = xchan_hash \n\t\twhere abook_xchan = '%s' and abook_channel = %d limit 1", dbesc($xchan_hash), intval($uid)); if ($r) { $result['abook'] = $r[0]; proc_run('php', 'include/notifier.php', 'permission_update', $result['abook']['abook_id']); } $arr = array('channel_id' => $uid, 'abook' => $result['abook']); call_hooks('follow', $arr); /** If there is a default group for this channel, add this member to it */ if ($default_group) { require_once 'include/group.php'; $g = group_rec_byhash($uid, $default_group); if ($g) { group_add_member($uid, '', $xchan_hash, $g['id']); } } $result['success'] = true; return $result; }
function new_contact($uid, $url, $channel, $interactive = false, $confirm = false) { $result = array('success' => false, 'message' => ''); $is_red = false; $is_http = strpos($url, '://') !== false ? true : false; if ($is_http && substr($url, -1, 1) === '/') { $url = substr($url, 0, -1); } if (!allowed_url($url)) { $result['message'] = t('Channel is blocked on this site.'); return $result; } if (!$url) { $result['message'] = t('Channel location missing.'); return $result; } // check service class limits $r = q("select count(*) as total from abook where abook_channel = %d and abook_self = 0 ", intval($uid)); if ($r) { $total_channels = $r[0]['total']; } if (!service_class_allows($uid, 'total_channels', $total_channels)) { $result['message'] = upgrade_message(); return $result; } $arr = array('url' => $url, 'channel' => array()); call_hooks('follow', $arr); if ($arr['channel']['success']) { $ret = $arr['channel']; } elseif (!$is_http) { $ret = Zotlabs\Zot\Finger::run($url, $channel); } if ($ret && is_array($ret) && $ret['success']) { $is_red = true; $j = $ret; } $my_perms = get_channel_default_perms($uid); $role = get_pconfig($uid, 'system', 'permissions_role'); if ($role) { $x = \Zotlabs\Access\PermissionRoles::role_perms($role); if ($x['perms_connect']) { $my_perms = $x['perms_connect']; } } if ($is_red && $j) { logger('follow: ' . $url . ' ' . print_r($j, true), LOGGER_DEBUG); if (!($j['success'] && $j['guid'])) { $result['message'] = t('Response from remote channel was incomplete.'); logger('mod_follow: ' . $result['message']); return $result; } // Premium channel, set confirm before callback to avoid recursion if (array_key_exists('connect_url', $j) && $interactive && !$confirm) { goaway(zid($j['connect_url'])); } // do we have an xchan and hubloc? // If not, create them. $x = import_xchan($j); if (array_key_exists('deleted', $j) && intval($j['deleted'])) { $result['message'] = t('Channel was deleted and no longer exists.'); return $result; } if (!$x['success']) { return $x; } $xchan_hash = $x['hash']; if (array_key_exists('permissions', $j) && array_key_exists('data', $j['permissions'])) { $permissions = crypto_unencapsulate(array('data' => $j['permissions']['data'], 'key' => $j['permissions']['key'], 'iv' => $j['permissions']['iv']), $channel['channel_prvkey']); if ($permissions) { $permissions = json_decode($permissions, true); } logger('decrypted permissions: ' . print_r($permissions, true), LOGGER_DATA); } else { $permissions = $j['permissions']; } if (is_array($permissions) && $permissions) { foreach ($permissions as $k => $v) { set_abconfig($channel['channel_uid'], $xchan_hash, 'their_perms', $k, intval($v)); } } } else { $xchan_hash = ''; $r = q("select * from xchan where xchan_hash = '%s' or xchan_url = '%s' limit 1", dbesc($url), dbesc($url)); if (!$r) { // attempt network auto-discovery $d = discover_by_webbie($url); if (!$d && $is_http) { // try RSS discovery if (get_config('system', 'feed_contacts')) { $d = discover_by_url($url); } else { $result['message'] = t('Protocol disabled.'); return $result; } } if ($d) { $r = q("select * from xchan where xchan_hash = '%s' or xchan_url = '%s' limit 1", dbesc($url), dbesc($url)); } } // if discovery was a success we should have an xchan record in $r if ($r) { $xchan = $r[0]; $xchan_hash = $r[0]['xchan_hash']; $their_perms = 0; } } if (!$xchan_hash) { $result['message'] = t('Channel discovery failed.'); logger('follow: ' . $result['message']); return $result; } $allowed = $is_red || $r[0]['xchan_network'] === 'rss' ? 1 : 0; $x = array('channel_id' => $uid, 'follow_address' => $url, 'xchan' => $r[0], 'allowed' => $allowed, 'singleton' => 0); call_hooks('follow_allow', $x); if (!$x['allowed']) { $result['message'] = t('Protocol disabled.'); return $result; } $singleton = intval($x['singleton']); $aid = $channel['channel_account_id']; $hash = get_observer_hash(); $default_group = $channel['channel_default_group']; if ($xchan['xchan_network'] === 'rss') { // check service class feed limits $r = q("select count(*) as total from abook where abook_account = %d and abook_feed = 1 ", intval($aid)); if ($r) { $total_feeds = $r[0]['total']; } if (!service_class_allows($uid, 'total_feeds', $total_feeds)) { $result['message'] = upgrade_message(); return $result; } } if ($hash == $xchan_hash) { $result['message'] = t('Cannot connect to yourself.'); return $result; } $r = q("select abook_xchan, abook_instance from abook where abook_xchan = '%s' and abook_channel = %d limit 1", dbesc($xchan_hash), intval($uid)); if ($is_http) { // Always set these "remote" permissions for feeds since we cannot interact with them // to negotiate a suitable permission response set_abconfig($uid, $xchan_hash, 'their_perms', 'view_stream', 1); set_abconfig($uid, $xchan_hash, 'their_perms', 'republish', 1); } if ($r) { $abook_instance = $r[0]['abook_instance']; if ($singleton && strpos($abook_instance, z_root()) === false) { if ($abook_instance) { $abook_instance .= ','; } $abook_instance .= z_root(); } $x = q("update abook set abook_instance = '%s' where abook_id = %d", dbesc($abook_instance), intval($r[0]['abook_id'])); } else { $closeness = get_pconfig($uid, 'system', 'new_abook_closeness'); if ($closeness === false) { $closeness = 80; } $r = q("insert into abook ( abook_account, abook_channel, abook_closeness, abook_xchan, abook_feed, abook_created, abook_updated, abook_instance )\n\t\t\tvalues( %d, %d, %d, '%s', %d, '%s', '%s', '%s' ) ", intval($aid), intval($uid), intval($closeness), dbesc($xchan_hash), intval($is_http ? 1 : 0), dbesc(datetime_convert()), dbesc(datetime_convert()), dbesc($singleton ? z_root() : '')); } if (!$r) { logger('mod_follow: abook creation failed'); } $all_perms = \Zotlabs\Access\Permissions::Perms(); if ($all_perms) { foreach ($all_perms as $k => $v) { if (in_array($k, $my_perms)) { set_abconfig($uid, $xchan_hash, 'my_perms', $k, 1); } else { set_abconfig($uid, $xchan_hash, 'my_perms', $k, 0); } } } $r = q("select abook.*, xchan.* from abook left join xchan on abook_xchan = xchan_hash \n\t\twhere abook_xchan = '%s' and abook_channel = %d limit 1", dbesc($xchan_hash), intval($uid)); if ($r) { $result['abook'] = $r[0]; Zotlabs\Daemon\Master::Summon(array('Notifier', 'permission_create', $result['abook']['abook_id'])); } $arr = array('channel_id' => $uid, 'channel' => $channel, 'abook' => $result['abook']); call_hooks('follow', $arr); /** If there is a default group for this channel, add this connection to it */ if ($default_group) { require_once 'include/group.php'; $g = group_rec_byhash($uid, $default_group); if ($g) { group_add_member($uid, '', $xchan_hash, $g['id']); } } $result['success'] = true; return $result; }
function discover_by_webbie($webbie) { require_once 'library/HTML5/Parser.php'; $result = array(); $network = null; $diaspora = false; $gnusoc = false; $dfrn = false; $has_salmon = false; $salmon_key = false; $atom_feed = false; $diaspora_base = ''; $diaspora_guid = ''; $diaspora_key = ''; $webbie = strtolower($webbie); $x = webfinger_rfc7033($webbie, true); if ($x && array_key_exists('links', $x) && $x['links']) { foreach ($x['links'] as $link) { if (array_key_exists('rel', $link)) { // If we discover zot - don't search further; grab the info and get out of // here. if ($link['rel'] === PROTOCOL_ZOT) { logger('discover_by_webbie: zot found for ' . $webbie, LOGGER_DEBUG); if (array_key_exists('zot', $x) && $x['zot']['success']) { $i = import_xchan($x['zot']); } else { $z = z_fetch_url($link['href']); if ($z['success']) { $j = json_decode($z['body'], true); $i = import_xchan($j); return true; } } } if ($link['rel'] == NAMESPACE_DFRN) { $dfrn = $link['href']; } if ($link['rel'] == 'magic-public-key') { if (substr($link['href'], 0, 5) === 'data:') { $salmon_key = convert_salmon_key($link['href']); } } if ($link['rel'] == 'salmon') { $has_salmon = true; $salmon = $link['href']; } if ($link['rel'] == 'http://schemas.google.com/g/2010#updates-from') { $atom_feed = $link['href']; } } } } logger('webfinger: ' . print_r($x, true), LOGGER_DATA, LOG_INFO); $arr = array('address' => $webbie, 'success' => false, 'webfinger' => $x); call_hooks('discover_channel_webfinger', $arr); if ($arr['success']) { return true; } $aliases = array(); // Now let's make some decisions on what we may need // to obtain further info $probe_atom = false; $probe_old = false; $probe_hcard = false; $address = ''; $location = ''; $nickname = ''; $fullname = ''; $avatar = ''; $pubkey = ''; if (is_array($x)) { if (array_key_exists('address', $x)) { $address = $x['address']; } if (array_key_exists('location', $x)) { $location = $x['location']; } if (array_key_exists('nickname', $x)) { $nickname = $x['nickname']; } } if (!$x) { $probe_old = true; } if (!$dfrn && !$has_salmon) { $probe_old = true; } if ($probe_old) { $y = old_webfinger($webbie); if ($y) { logger('old_webfinger: ' . print_r($x, true)); foreach ($y as $link) { if ($link['@attributes']['rel'] === NAMESPACE_DFRN) { $dfrn = unamp($link['@attributes']['href']); } if ($link['@attributes']['rel'] === 'salmon') { $notify = unamp($link['@attributes']['href']); } if ($link['@attributes']['rel'] === NAMESPACE_FEED) { $poll = unamp($link['@attributes']['href']); } if ($link['@attributes']['rel'] === 'http://microformats.org/profile/hcard') { $hcard = unamp($link['@attributes']['href']); } if ($link['@attributes']['rel'] === 'http://webfinger.net/rel/profile-page') { $profile = unamp($link['@attributes']['href']); } if ($link['@attributes']['rel'] === 'http://portablecontacts.net/spec/1.0') { $poco = unamp($link['@attributes']['href']); } if ($link['@attributes']['rel'] === 'http://joindiaspora.com/seed_location') { $diaspora_base = unamp($link['@attributes']['href']); $diaspora = true; } if ($link['@attributes']['rel'] === 'http://joindiaspora.com/guid') { $diaspora_guid = unamp($link['@attributes']['href']); $diaspora = true; } if ($link['@attributes']['rel'] === 'diaspora-public-key') { $diaspora_key = base64_decode(unamp($link['@attributes']['href'])); if (strstr($diaspora_key, 'RSA ')) { $pubkey = rsatopem($diaspora_key); } else { $pubkey = $diaspora_key; } $diaspora = true; } if ($link['@attributes']['rel'] == 'magic-public-key') { if (substr($link['@attributes']['href'], 0, 5) === 'data:') { $salmon_key = convert_salmon_key($link['@attributes']['href']); } } if ($link['@attributes']['rel'] == 'salmon') { $has_salmon = true; $salmon = $link['@attributes']['href']; } if ($link['@attributes']['rel'] == 'http://schemas.google.com/g/2010#updates-from') { $atom_feed = $link['@attributes']['href']; } if ($link['@attributes']['rel'] === 'alias') { $aliases[] = $link['@attributes']['href']; } if ($link['@attributes']['rel'] === 'subject') { $subject = $link['@attributes']['href']; } } } } if ($subject || $aliases) { if (strpos($webbie, '@')) { $rhs = substr($webbie, strpos($webbie, '@') + 1); } else { $m = parse_url($webbie); if ($m) { $rhs = $m['host'] . ($m['port'] ? ':' . $m['port'] : ''); } } $v = array('subject' => $subject, 'aliases' => $aliases); $address = find_webfinger_address($v, $rhs); $location = find_webfinger_location($v, $rhs); if ($address) { $nickname = substr($address, 0, strpos($address, '@')); } } if ($salmon_key && $has_salmon && $atom_feed && !$dfrn && !$diaspora) { $gnusoc = true; $probe_atom = true; } if (!$pubkey) { $pubkey = $salmon_key; } if (($dfrn || $diaspora) && $hcard) { $probe_hcard = true; } if (!$fullname) { $fullname = $nickname; } if ($probe_atom) { $k = z_fetch_url($atom_feed); if ($k['success']) { $feed_meta = feed_meta($k['body']); } if ($feed_meta) { // stash any discovered pubsubhubbub hubs in case we need to follow them // this will save an expensive lookup later if ($feed_meta['hubs'] && $address) { set_xconfig($address, 'system', 'push_hubs', $feed_meta['hubs']); set_xconfig($address, 'system', 'feed_url', $atom_feed); } if ($feed_meta['author']['author_name']) { $fullname = $feed_meta['author']['author_name']; } if (!$avatar) { if ($feed_meta['author']['author_photo']) { $avatar = $feed_meta['author']['author_photo']; } } // for GNU-social over-ride any url aliases we may have picked up in webfinger // The author.uri element in the feed is likely to be more accurate if ($gnusoc && $feed_meta['author']['author_uri']) { $location = $feed_meta['author']['author_uri']; } } } else { if ($probe_hcard) { $vcard = scrape_vcard($hcard); if ($vcard) { logger('vcard: ' . print_r($vcard, true), LOGGER_DATA); if ($vcard['fn']) { $fullname = $vcard['fn']; } if ($vcard['photo'] && strpos($vcard['photo'], 'http') !== 0) { $vcard['photo'] = $diaspora_base . '/' . $vcard['photo']; } if (!$avatar) { $avatar = $vcard['photo']; } } } } if ($profile && !$location) { $location = $profile; } if ($location) { $m = parse_url($location); $base = $m['scheme'] . '://' . $m['host']; $host = $m['host']; } if ($diaspora && $diaspora_base && $diaspora_guid) { if ($dfrn) { $network = 'friendica-over-diaspora'; } else { $network = 'diaspora'; } $base = trim($diaspora_base, '/'); $notify = $base . '/receive'; } else { if ($gnusoc) { $network = 'gnusoc'; $notify = $salmon; } } logger('network: ' . $network); logger('address: ' . $address); logger('fullname: ' . $fullname); logger('pubkey: ' . $pubkey); logger('location: ' . $location); // if we have everything we need, let's create the records if ($network && $address && $fullname && $pubkey && $location) { $r = q("select * from xchan where xchan_hash = '%s' limit 1", dbesc($address)); if ($r) { $r = q("update xchan set xchan_name = '%s', xchan_network = '%s', xchan_name_date = '%s' where xchan_hash = '%s' limit 1", dbesc($fullname), dbesc($network), dbesc(datetime_convert()), dbesc($address)); } else { $r = q("insert into xchan ( xchan_hash, xchan_guid, xchan_pubkey, xchan_addr, xchan_url, xchan_name, xchan_network, xchan_name_date ) values ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s') ", dbesc($address), dbesc($diaspora_guid ? $diaspora_guid : $location), dbesc($pubkey), dbesc($address), dbesc($location), dbesc($fullname), dbesc($network), dbescdate(datetime_convert())); } $r = q("select * from hubloc where hubloc_hash = '%s' limit 1", dbesc($address)); if (!$r) { $r = q("insert into hubloc ( hubloc_guid, hubloc_hash, hubloc_addr, hubloc_network, hubloc_url, hubloc_host, hubloc_callback, hubloc_updated, hubloc_primary ) values ('%s','%s','%s','%s','%s','%s','%s','%s', 1)", dbesc($diaspora_guid ? $diaspora_guid : $location), dbesc($address), dbesc($address), dbesc($network), dbesc($base), dbesc($host), dbesc($notify), dbescdate(datetime_convert())); } $photos = import_xchan_photo($avatar, $address); $r = q("update xchan set xchan_photo_date = '%s', xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s', xchan_photo_mimetype = '%s' where xchan_hash = '%s'", dbescdate(datetime_convert()), dbesc($photos[0]), dbesc($photos[1]), dbesc($photos[2]), dbesc($photos[3]), dbesc($address)); return true; } return false; }
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; }
function mail_post(&$a) { if (!local_channel()) { return; } $replyto = x($_REQUEST, 'replyto') ? notags(trim($_REQUEST['replyto'])) : ''; $subject = x($_REQUEST, 'subject') ? notags(trim($_REQUEST['subject'])) : ''; $body = x($_REQUEST, 'body') ? escape_tags(trim($_REQUEST['body'])) : ''; $recipient = x($_REQUEST, 'messageto') ? notags(trim($_REQUEST['messageto'])) : ''; $rstr = x($_REQUEST, 'messagerecip') ? notags(trim($_REQUEST['messagerecip'])) : ''; $preview = x($_REQUEST, 'preview') ? intval($_REQUEST['preview']) : 0; $expires = x($_REQUEST, 'expires') ? datetime_convert(date_default_timezone_get(), 'UTC', $_REQUEST['expires']) : NULL_DATE; // If we have a raw string for a recipient which hasn't been auto-filled, // it means they probably aren't in our address book, hence we don't know // if we have permission to send them private messages. // finger them and find out before we try and send it. if (!$recipient) { $channel = App::get_channel(); $ret = zot_finger($rstr, $channel); if (!$ret['success']) { notice(t('Unable to lookup recipient.') . EOL); return; } $j = json_decode($ret['body'], true); logger('message_post: lookup: ' . $url . ' ' . print_r($j, true)); if (!($j['success'] && $j['guid'])) { notice(t('Unable to communicate with requested channel.')); return; } $x = import_xchan($j); if (!$x['success']) { notice(t('Cannot verify requested channel.')); return; } $recipient = $x['hash']; $their_perms = 0; $global_perms = get_perms(); if ($j['permissions']['data']) { $permissions = crypto_unencapsulate($j['permissions'], $channel['channel_prvkey']); if ($permissions) { $permissions = json_decode($permissions); } logger('decrypted permissions: ' . print_r($permissions, true), LOGGER_DATA); } else { $permissions = $j['permissions']; } foreach ($permissions as $k => $v) { if ($v) { $their_perms = $their_perms | intval($global_perms[$k][1]); } } if (!($their_perms & PERMS_W_MAIL)) { notice(t('Selected channel has private message restrictions. Send failed.')); // reported issue: let's still save the message and continue. We'll just tell them // that nothing useful is likely to happen. They might have spent hours on it. // return; } } // if(feature_enabled(local_channel(),'richtext')) { // $body = fix_mce_lf($body); // } require_once 'include/text.php'; linkify_tags($a, $body, local_channel()); if ($preview) { } if (!$recipient) { notice('No recipient found.'); App::$argc = 2; App::$argv[1] = 'new'; return; } // We have a local_channel, let send_message use the session channel and save a lookup $ret = send_message(0, $recipient, $body, $subject, $replyto, $expires); if ($ret['success']) { xchan_mail_query($ret['mail']); build_sync_packet(0, array('conv' => array($ret['conv']), 'mail' => array(encode_mail($ret['mail'], true)))); } else { notice($ret['message']); } goaway(z_root() . '/mail/combined'); }
/** * @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 discover_by_webbie($webbie) { require_once 'library/HTML5/Parser.php'; $webbie = strtolower($webbie); $x = webfinger_rfc7033($webbie, true); if ($x && array_key_exists('links', $x) && $x['links']) { foreach ($x['links'] as $link) { if (array_key_exists('rel', $link) && $link['rel'] == 'http://purl.org/zot/protocol') { logger('discover_by_webbie: zot found for ' . $webbie, LOGGER_DEBUG); if (array_key_exists('zot', $x) && $x['zot']['success']) { $i = import_xchan($x['zot']); } else { $z = z_fetch_url($link['href']); if ($z['success']) { $j = json_decode($z['body'], true); $i = import_xchan($j); return true; } } } } } $arr = array('address' => $webbie, 'success' => false); call_hooks('discover_by_webbie', $arr); if ($arr['success']) { return true; } $result = array(); $network = null; $diaspora = false; $diaspora_base = ''; $diaspora_guid = ''; $diaspora_key = ''; $dfrn = false; $x = old_webfinger($webbie); if ($x) { logger('old_webfinger: ' . print_r($x, true)); foreach ($x as $link) { if ($link['@attributes']['rel'] === NAMESPACE_DFRN) { $dfrn = unamp($link['@attributes']['href']); } if ($link['@attributes']['rel'] === 'salmon') { $notify = unamp($link['@attributes']['href']); } if ($link['@attributes']['rel'] === NAMESPACE_FEED) { $poll = unamp($link['@attributes']['href']); } if ($link['@attributes']['rel'] === 'http://microformats.org/profile/hcard') { $hcard = unamp($link['@attributes']['href']); } if ($link['@attributes']['rel'] === 'http://webfinger.net/rel/profile-page') { $profile = unamp($link['@attributes']['href']); } if ($link['@attributes']['rel'] === 'http://portablecontacts.net/spec/1.0') { $poco = unamp($link['@attributes']['href']); } if ($link['@attributes']['rel'] === 'http://joindiaspora.com/seed_location') { $diaspora_base = unamp($link['@attributes']['href']); $diaspora = true; } if ($link['@attributes']['rel'] === 'http://joindiaspora.com/guid') { $diaspora_guid = unamp($link['@attributes']['href']); $diaspora = true; } if ($link['@attributes']['rel'] === 'diaspora-public-key') { $diaspora_key = base64_decode(unamp($link['@attributes']['href'])); if (strstr($diaspora_key, 'RSA ')) { $pubkey = rsatopem($diaspora_key); } else { $pubkey = $diaspora_key; } $diaspora = true; } } if ($diaspora && $diaspora_base && $diaspora_guid) { $guid = $diaspora_guid; $diaspora_base = trim($diaspora_base, '/'); $notify = $diaspora_base . '/receive'; if (strpos($webbie, '@')) { $addr = str_replace('acct:', '', $webbie); $hostname = substr($webbie, strpos($webbie, '@') + 1); } $network = 'diaspora'; // until we get a dfrn layer, we'll use diaspora protocols for Friendica, // but give it a different network so we can go back and fix these when we get proper support. // It really should be just 'friendica' but we also want to distinguish // between Friendica sites that we can use D* protocols with and those we can't. // Some Friendica sites will have Diaspora disabled. if ($dfrn) { $network = 'friendica-over-diaspora'; } if ($hcard) { $vcard = scrape_vcard($hcard); $vcard['nick'] = substr($webbie, 0, strpos($webbie, '@')); if (!$vcard['fn']) { $vcard['fn'] = $webbie; } } $r = q("select * from xchan where xchan_hash = '%s' limit 1", dbesc($addr)); // fix relative urls if ($vcard['photo'] && strpos($vcard['photo'], 'http') !== 0) { $vcard['photo'] = $diaspora_base . '/' . $vcard['photo']; } /** * * Diaspora communications are notoriously unreliable and receiving profile update messages (indeed any messages) * are pretty much random luck. We'll check the timestamp of the xchan_name_date at a higher level and refresh * this record once a month; because if you miss a profile update message and they update their profile photo or name * you're otherwise stuck with stale info until they change their profile again - which could be years from now. * */ if ($r) { $r = q("update xchan set xchan_name = '%s', xchan_network = '%s', xchan_name_date = '%s' where xchan_hash = '%s' limit 1", dbesc($vcard['fn']), dbesc($network), dbesc(datetime_convert()), dbesc($addr)); } else { $r = q("insert into xchan ( xchan_hash, xchan_guid, xchan_pubkey, xchan_addr, xchan_url, xchan_name, xchan_network, xchan_instance_url, xchan_name_date ) values ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s') ", dbesc($addr), dbesc($guid), dbesc($pubkey), dbesc($addr), dbesc($profile), dbesc($vcard['fn']), dbesc($network), dbesc(z_root()), dbescdate(datetime_convert())); } $r = q("select * from hubloc where hubloc_hash = '%s' limit 1", dbesc($webbie)); if (!$r) { $r = q("insert into hubloc ( hubloc_guid, hubloc_hash, hubloc_addr, hubloc_network, hubloc_url, hubloc_host, hubloc_callback, hubloc_updated, hubloc_primary ) values ('%s','%s','%s','%s','%s','%s','%s','%s', 1)", dbesc($guid), dbesc($addr), dbesc($addr), dbesc($network), dbesc(trim($diaspora_base, '/')), dbesc($hostname), dbesc($notify), dbescdate(datetime_convert())); } $photos = import_xchan_photo($vcard['photo'], $addr); $r = q("update xchan set xchan_photo_date = '%s', xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s', xchan_photo_mimetype = '%s' where xchan_hash = '%s'", dbescdate(datetime_convert('UTC', 'UTC', $arr['photo_updated'])), dbesc($photos[0]), dbesc($photos[1]), dbesc($photos[2]), dbesc($photos[3]), dbesc($addr)); return true; } return false; /* $vcard['fn'] = notags($vcard['fn']); $vcard['nick'] = str_replace(' ','',notags($vcard['nick'])); $result['name'] = $vcard['fn']; $result['nick'] = $vcard['nick']; $result['guid'] = $guid; $result['url'] = $profile; $result['hostname'] = $hostname; $result['addr'] = $addr; $result['batch'] = $batch; $result['notify'] = $notify; $result['poll'] = $poll; $result['request'] = $request; $result['confirm'] = $confirm; $result['poco'] = $poco; $result['photo'] = $vcard['photo']; $result['priority'] = $priority; $result['network'] = $network; $result['alias'] = $alias; $result['pubkey'] = $pubkey; logger('probe_url: ' . print_r($result,true), LOGGER_DEBUG); return $result; */ /* Sample Diaspora result. Array ( [name] => Mike Macgirvin [nick] => macgirvin [guid] => a9174a618f8d269a [url] => https://joindiaspora.com/u/macgirvin [hostname] => joindiaspora.com [addr] => macgirvin@joindiaspora.com [batch] => [notify] => https://joindiaspora.com/receive [poll] => https://joindiaspora.com/public/macgirvin.atom [request] => [confirm] => [poco] => [photo] => https://joindiaspora.s3.amazonaws.com/uploads/images/thumb_large_fec4e6eef13ae5e56207.jpg [priority] => [network] => diaspora [alias] => [pubkey] => -----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtihtyIuRDWkDpCA+I1UaQ jI4S7k625+A7EEJm+pL2ZVSJxeCKiFeEgHBQENjLMNNm8l8F6blxgQqE6ZJ9Spa7f tlaXYTRCrfxKzh02L3hR7sNA+JS/nXJaUAIo+IwpIEspmcIRbD9GB7Wv/rr+M28uH 31EeYyDz8QL6InU/bJmnCdFvmEMBQxJOw1ih9tQp7UNJAbUMCje0WYFzBz7sfcaHL OyYcCOqOCBLdGucUoJzTQ9iDBVzB8j1r1JkIHoEb2moUoKUp+tkCylNfd/3IVELF9 7w1Qjmit3m50OrJk2DQOXvCW9KQxaQNdpRPSwhvemIt98zXSeyZ1q/YjjOwG0DWDq AF8aLj3/oQaZndTPy/6tMiZogKaijoxj8xFLuPYDTw5VpKquriVC0z8oxyRbv4t9v 8JZZ9BXqzmayvY3xZGGp8NulrfjW+me2bKh0/df1aHaBwpZdDTXQ6kqAiS2FfsuPN vg57fhfHbL1yJ4oDbNNNeI0kJTGchXqerr8C20khU/cQ2Xt31VyEZtnTB665Ceugv kp3t2qd8UpAVKl430S5Quqx2ymfUIdxdW08CEjnoRNEL3aOWOXfbf4gSVaXmPCR4i LSIeXnd14lQYK/uxW/8cTFjcmddsKxeXysoQxbSa9VdDK+KkpZdgYXYrTTofXs6v+ 4afAEhRaaY+MCAwEAAQ== -----END PUBLIC KEY----- ) */ } }
function chanview_content(&$a) { $observer = $a->get_observer(); $xchan = null; $r = null; if ($_REQUEST['hash']) { $r = q("select * from xchan where xchan_hash = '%s' limit 1", dbesc($_REQUEST['hash'])); } if ($_REQUEST['address']) { $r = q("select * from xchan where xchan_addr = '%s' limit 1", dbesc($_REQUEST['address'])); } elseif (local_channel() && intval($_REQUEST['cid'])) { $r = q("SELECT abook.*, xchan.* \n\t\t\tFROM abook left join xchan on abook_xchan = xchan_hash\n\t\t\tWHERE abook_channel = %d and abook_id = %d LIMIT 1", intval(local_channel()), intval($_REQUEST['cid'])); } elseif ($_REQUEST['url']) { // if somebody re-installed they will have more than one xchan, use the most recent name date as this is // the most useful consistently ascending table item we have. $r = q("select * from xchan where xchan_url = '%s' order by xchan_name_date desc limit 1", dbesc($_REQUEST['url'])); } if ($r) { $a->poi = $r[0]; } // Here, let's see if we have an xchan. If we don't, how we proceed is determined by what // info we do have. If it's a URL, we can offer to visit it directly. If it's a webbie or // address, we can and should try to import it. If it's just a hash, we can't continue, but we // probably wouldn't have a hash if we don't already have an xchan for this channel. if (!$a->poi) { logger('mod_chanview: fallback'); // This is hackish - construct a zot address from the url if ($_REQUEST['url']) { if (preg_match('/https?\\:\\/\\/(.*?)(\\/channel\\/|\\/profile\\/)(.*?)$/ism', $_REQUEST['url'], $matches)) { $_REQUEST['address'] = $matches[3] . '@' . $matches[1]; } logger('mod_chanview: constructed address ' . print_r($matches, true)); } if ($_REQUEST['address']) { $ret = zot_finger($_REQUEST['address'], null); if ($ret['success']) { $j = json_decode($ret['body'], true); if ($j) { import_xchan($j); } $r = q("select * from xchan where xchan_addr = '%s' limit 1", dbesc($_REQUEST['address'])); if ($r) { $a->poi = $r[0]; } } } } if (!$a->poi) { // We don't know who this is, and we can't figure it out from the URL // On the plus side, there's a good chance we know somebody else at that // hub so sending them there with a Zid will probably work anyway. $url = $_REQUEST['url']; if ($observer) { $url = zid($url); } } if ($a->poi) { $url = $a->poi['xchan_url']; if ($observer) { $url = zid($url); } } // let somebody over-ride the iframed viewport presentation // or let's just declare this a failed experiment. // if((! local_channel()) || (get_pconfig(local_channel(),'system','chanview_full'))) goaway($url); // $o = replace_macros(get_markup_template('chanview.tpl'),array( // '$url' => $url, // '$full' => t('toggle full screen mode') // )); // return $o; }
function init() { $ret = array('success' => false, 'url' => '', 'message' => ''); logger('mod_magic: invoked', LOGGER_DEBUG); logger('mod_magic: args: ' . print_r($_REQUEST, true), LOGGER_DATA); $addr = x($_REQUEST, 'addr') ? $_REQUEST['addr'] : ''; $dest = x($_REQUEST, 'dest') ? $_REQUEST['dest'] : ''; $test = x($_REQUEST, 'test') ? intval($_REQUEST['test']) : 0; $rev = x($_REQUEST, 'rev') ? intval($_REQUEST['rev']) : 0; $delegate = x($_REQUEST, 'delegate') ? $_REQUEST['delegate'] : ''; $parsed = parse_url($dest); if (!$parsed) { if ($test) { $ret['message'] .= 'could not parse ' . $dest . EOL; return $ret; } goaway($dest); } $basepath = $parsed['scheme'] . '://' . $parsed['host'] . ($parsed['port'] ? ':' . $parsed['port'] : ''); $x = q("select * from hubloc where hubloc_url = '%s' order by hubloc_connected desc limit 1", dbesc($basepath)); if (!$x) { /* * We have no records for, or prior communications with this hub. * If an address was supplied, let's finger them to create a hub record. * Otherwise we'll use the special address '[system]' which will return * either a system channel or the first available normal channel. We don't * really care about what channel is returned - we need the hub information * from that response so that we can create signed auth packets destined * for that hub. * */ $ret = zot_finger($addr ? $addr : '[system]@' . $parsed['host'], null); if ($ret['success']) { $j = json_decode($ret['body'], true); if ($j) { import_xchan($j); } // Now try again $x = q("select * from hubloc where hubloc_url = '%s' order by hubloc_connected desc limit 1", dbesc($basepath)); } } if (!$x) { if ($rev) { goaway($dest); } else { logger('mod_magic: no channels found for requested hub.' . print_r($_REQUEST, true)); if ($test) { $ret['message'] .= 'This site has no previous connections with ' . $basepath . EOL; return $ret; } notice(t('Hub not found.') . EOL); return; } } // This is ready-made for a plugin that provides a blacklist or "ask me" before blindly authenticating. // By default, we'll proceed without asking. $arr = array('channel_id' => local_channel(), 'xchan' => $x[0], 'destination' => $dest, 'proceed' => true); call_hooks('magic_auth', $arr); $dest = $arr['destination']; if (!$arr['proceed']) { if ($test) { $ret['message'] .= 'cancelled by plugin.' . EOL; return $ret; } goaway($dest); } if (get_observer_hash() && $x[0]['hubloc_url'] === z_root()) { // We are already authenticated on this site and a registered observer. // Just redirect. if ($test) { $ret['success'] = true; $ret['message'] .= 'Local site - you are already authenticated.' . EOL; return $ret; } $delegation_success = false; if ($delegate) { $r = q("select * from channel left join hubloc on channel_hash = hubloc_hash where hubloc_addr = '%s' limit 1", dbesc($delegate)); if ($r && intval($r[0]['channel_id'])) { $allowed = perm_is_allowed($r[0]['channel_id'], get_observer_hash(), 'delegate'); if ($allowed) { $_SESSION['delegate_channel'] = $r[0]['channel_id']; $_SESSION['delegate'] = get_observer_hash(); $_SESSION['account_id'] = intval($r[0]['channel_account_id']); change_channel($r[0]['channel_id']); $delegation_success = true; } } } // FIXME: check and honour local delegation goaway($dest); } if (local_channel()) { $channel = \App::get_channel(); $token = random_string(); $token_sig = base64url_encode(rsa_sign($token, $channel['channel_prvkey'])); $channel['token'] = $token; $channel['token_sig'] = $token_sig; \Zotlabs\Zot\Verify::create('auth', $channel['channel_id'], $token, $x[0]['hubloc_url']); $target_url = $x[0]['hubloc_callback'] . '/?f=&auth=' . urlencode($channel['channel_address'] . '@' . \App::get_hostname()) . '&sec=' . $token . '&dest=' . urlencode($dest) . '&version=' . ZOT_REVISION; if ($delegate) { $target_url .= '&delegate=' . urlencode($delegate); } logger('mod_magic: redirecting to: ' . $target_url, LOGGER_DEBUG); if ($test) { $ret['success'] = true; $ret['url'] = $target_url; $ret['message'] = 'token ' . $token . ' created for channel ' . $channel['channel_id'] . ' for url ' . $x[0]['hubloc_url'] . EOL; return $ret; } goaway($target_url); } if ($test) { $ret['message'] = 'Not authenticated or invalid arguments to mod_magic' . EOL; return $ret; } goaway($dest); }
/** * @brief Registers an unknown hub. * * A communication has been received which has an unknown (to us) sender. * Perform discovery based on our calculated hash of the sender at the * origination address. This will fetch the discovery packet of the sender, * which contains the public key we need to verify our guid and url signatures. * * @param array $arr an associative array which must contain: * * \e string \b guid => guid of conversant * * \e string \b guid_sig => guid signed with conversant's private key * * \e string \b url => URL of the origination hub of this communication * * \e string \b url_sig => URL signed with conversant's private key * * @returns array an associative array with * * \b success boolean true or false * * \b message (optional) error string only if success is false */ function zot_register_hub($arr) { $result = array('success' => false); if ($arr['url'] && $arr['url_sig'] && $arr['guid'] && $arr['guid_sig']) { $guid_hash = make_xchan_hash($arr['guid'], $arr['guid_sig']); $url = $arr['url'] . '/.well-known/zot-info/?f=&guid_hash=' . $guid_hash; logger('zot_register_hub: ' . $url, LOGGER_DEBUG); $x = z_fetch_url($url); logger('zot_register_hub: ' . print_r($x, true), LOGGER_DATA); if ($x['success']) { $record = json_decode($x['body'], true); /* * We now have a key - only continue registration if our signatures are valid * AND the guid and guid sig in the returned packet match those provided in * our current communication. */ if (rsa_verify($arr['guid'], base64url_decode($arr['guid_sig']), $record['key']) && rsa_verify($arr['url'], base64url_decode($arr['url_sig']), $record['key']) && $arr['guid'] === $record['guid'] && $arr['guid_sig'] === $record['guid_sig']) { $c = import_xchan($record); if ($c['success']) { $result['success'] = true; } } else { logger('zot_register_hub: failure to verify returned packet.'); } } } return $result; }
function process_channel_sync_delivery($sender, $arr, $deliveries) { /** @FIXME this will sync red structures (channel, pconfig and abook). Eventually we need to make this application agnostic. */ $result = array(); foreach ($deliveries as $d) { $r = q("select * from channel where channel_hash = '%s' limit 1", dbesc($d['hash'])); if (!$r) { $result[] = array($d['hash'], 'not found'); continue; } $channel = $r[0]; $max_friends = service_class_fetch($channel['channel_id'], 'total_channels'); $max_feeds = account_service_class_fetch($channel['channel_account_id'], 'total_feeds'); if ($channel['channel_hash'] != $sender['hash']) { logger('process_channel_sync_delivery: possible forgery. Sender ' . $sender['hash'] . ' is not ' . $channel['channel_hash']); $result[] = array($d['hash'], 'channel mismatch', $channel['channel_name'], ''); continue; } if (array_key_exists('config', $arr) && is_array($arr['config']) && count($arr['config'])) { foreach ($arr['config'] as $cat => $k) { foreach ($arr['config'][$cat] as $k => $v) { set_pconfig($channel['channel_id'], $cat, $k, $v); } } } if (array_key_exists('channel', $arr) && is_array($arr['channel']) && count($arr['channel'])) { $disallowed = array('channel_id', 'channel_account_id', 'channel_primary', 'channel_prvkey', 'channel_address', 'channel_notifyflags'); $clean = array(); foreach ($arr['channel'] as $k => $v) { if (in_array($k, $disallowed)) { continue; } $clean[$k] = $v; } if (count($clean)) { foreach ($clean as $k => $v) { $r = dbq("UPDATE channel set " . dbesc($k) . " = '" . dbesc($v) . "' where channel_id = " . intval($channel['channel_id'])); } } } if (array_key_exists('abook', $arr) && is_array($arr['abook']) && count($arr['abook'])) { $total_friends = 0; $total_feeds = 0; $r = q("select abook_id, abook_flags from abook where abook_channel = %d", intval($channel['channel_id'])); if ($r) { // don't count yourself $total_friends = count($r) > 0 ? count($r) - 1 : 0; foreach ($r as $rr) { if ($rr['abook_flags'] & ABOOK_FLAG_FEED) { $total_feeds++; } } } $disallowed = array('abook_id', 'abook_account', 'abook_channel'); foreach ($arr['abook'] as $abook) { $clean = array(); if ($abook['abook_xchan'] && $abook['entry_deleted']) { logger('process_channel_sync_delivery: removing abook entry for ' . $abook['abook_xchan']); require_once 'include/Contact.php'; $r = q("select abook_id, abook_flags from abook where abook_xchan = '%s' and abook_channel = %d and not ( abook_flags & %d )>0 limit 1", dbesc($abook['abook_xchan']), intval($channel['channel_id']), intval(ABOOK_FLAG_SELF)); if ($r) { contact_remove($channel['channel_id'], $r[0]['abook_id']); if ($total_friends) { $total_friends--; } if ($r[0]['abook_flags'] & ABOOK_FLAG_FEED) { $total_feeds--; } } continue; } // Perform discovery if the referenced xchan hasn't ever been seen on this hub. // This relies on the undocumented behaviour that red sites send xchan info with the abook if ($abook['abook_xchan'] && $abook['xchan_address']) { $h = zot_get_hublocs($abook['abook_xchan']); if (!$h) { $f = zot_finger($abook['xchan_address'], $channel); if (!$f['success']) { logger('process_channel_sync_delivery: abook not probe-able' . $abook['xchan_address']); continue; } $j = json_decode($f['body'], true); if (!($j['success'] && $j['guid'])) { logger('process_channel_sync_delivery: probe failed.'); continue; } $x = import_xchan($j); if (!$x['success']) { logger('process_channel_sync_delivery: import failed.'); continue; } } } foreach ($abook as $k => $v) { if (in_array($k, $disallowed) || strpos($k, 'abook') !== 0) { continue; } $clean[$k] = $v; } if (!array_key_exists('abook_xchan', $clean)) { continue; } $r = q("select * from abook where abook_xchan = '%s' and abook_channel = %d limit 1", dbesc($clean['abook_xchan']), intval($channel['channel_id'])); // make sure we have an abook entry for this xchan on this system if (!$r) { if ($max_friends !== false && $total_friends > $max_friends) { logger('process_channel_sync_delivery: total_channels service class limit exceeded'); continue; } if ($max_feeds !== false && $clean['abook_flags'] & ABOOK_FLAG_FEED && $total_feeds > $max_feeds) { logger('process_channel_sync_delivery: total_feeds service class limit exceeded'); continue; } q("insert into abook ( abook_xchan, abook_channel ) values ('%s', %d ) ", dbesc($clean['abook_xchan']), intval($channel['channel_id'])); $total_friends++; if ($clean['abook_flags'] & ABOOK_FLAG_FEED) { $total_feeds++; } } if (count($clean)) { foreach ($clean as $k => $v) { if ($k == 'abook_dob') { $v = dbescdate($v); } $r = dbq("UPDATE abook set " . dbesc($k) . " = '" . dbesc($v) . "' where abook_xchan = '" . dbesc($clean['abook_xchan']) . "' and abook_channel = " . intval($channel['channel_id'])); } } } } // sync collections (privacy groups) oh joy... if (array_key_exists('collections', $arr) && is_array($arr['collections']) && count($arr['collections'])) { $x = q("select * from groups where uid = %d", intval($channel['channel_id'])); foreach ($arr['collections'] as $cl) { $found = false; if ($x) { foreach ($x as $y) { if ($cl['collection'] == $y['hash']) { $found = true; break; } } if ($found) { if ($y['name'] != $cl['name'] || $y['visible'] != $cl['visible'] || $y['deleted'] != $cl['deleted']) { q("update groups set name = '%s', visible = %d, deleted = %d where hash = '%s' and uid = %d", dbesc($cl['name']), intval($cl['visible']), intval($cl['deleted']), dbesc($cl['hash']), intval($channel['channel_id'])); } if (intval($cl['deleted']) && !intval($y['deleted'])) { q("delete from group_member where gid = %d", intval($y['id'])); } } } if (!$found) { $r = q("INSERT INTO `groups` ( hash, uid, visible, deleted, name )\n\t\t\t\t\t\tVALUES( '%s', %d, %d, %d, '%s' ) ", dbesc($cl['collection']), intval($channel['channel_id']), intval($cl['visible']), intval($cl['deleted']), dbesc($cl['name'])); } // now look for any collections locally which weren't in the list we just received. // They need to be removed by marking deleted and removing the members. // This shouldn't happen except for clones created before this function was written. if ($x) { $found_local = false; foreach ($x as $y) { foreach ($arr['collections'] as $cl) { if ($cl['collection'] == $y['hash']) { $found_local = true; break; } } if (!$found_local) { q("delete from group_member where gid = %d", intval($y['id'])); q("update groups set deleted = 1 where id = %d and uid = %d", intval($y['id']), intval($channel['channel_id'])); } } } } // reload the group list with any updates $x = q("select * from groups where uid = %d", intval($channel['channel_id'])); // now sync the members if (array_key_exists('collection_members', $arr) && is_array($arr['collection_members']) && count($arr['collection_members'])) { // first sort into groups keyed by the group hash $members = array(); foreach ($arr['collection_members'] as $cm) { if (!array_key_exists($cm['collection'], $members)) { $members[$cm['collection']] = array(); } $members[$cm['collection']][] = $cm['member']; } // our group list is already synchronised if ($x) { foreach ($x as $y) { // for each group, loop on members list we just received foreach ($members[$y['hash']] as $member) { $found = false; $z = q("select xchan from group_member where gid = %d and uid = %d and xchan = '%s' limit 1", intval($y['id']), intval($channel['channel_id']), dbesc($member)); if ($z) { $found = true; } // if somebody is in the group that wasn't before - add them if (!$found) { q("INSERT INTO `group_member` (`uid`, `gid`, `xchan`)\n\t\t\t\t\t\t\t\t\tVALUES( %d, %d, '%s' ) ", intval($channel['channel_id']), intval($y['id']), dbesc($member)); } } // now retrieve a list of members we have on this site $m = q("select xchan from group_member where gid = %d and uid = %d", intval($y['id']), intval($channel['channel_id'])); if ($m) { foreach ($m as $mm) { // if the local existing member isn't in the list we just received - remove them if (!in_array($mm['xchan'], $members[$y['hash']])) { q("delete from group_member where xchan = '%s' and gid = %d and uid = %d", dbesc($mm['xchan']), intval($y['id']), intval($channel['channel_id'])); } } } } } } } if (array_key_exists('profile', $arr) && is_array($arr['profile']) && count($arr['profile'])) { $disallowed = array('id', 'aid', 'uid'); foreach ($arr['profile'] as $profile) { $x = q("select * from profile where profile_guid = '%s' and uid = %d limit 1", dbesc($profile['profile_guid']), intval($channel['channel_id'])); if (!$x) { q("insert into profile ( profile_guid, aid, uid ) values ('%s', %d, %d)", dbesc($profile['profile_guid']), intval($channel['channel_account_id']), intval($channel['channel_id'])); $x = q("select * from profile where profile_guid = '%s' and uid = %d limit 1", dbesc($profile['profile_guid']), intval($channel['channel_id'])); if (!$x) { continue; } } $clean = array(); foreach ($profile as $k => $v) { if (in_array($k, $disallowed)) { continue; } $clean[$k] = $v; /** * @TODO check if these are allowed, otherwise we'll error * We also need to import local photos if a custom photo is selected */ } if (count($clean)) { foreach ($clean as $k => $v) { $r = dbq("UPDATE profile set " . dbesc($k) . " = '" . dbesc($v) . "' where profile_guid = '" . dbesc($profile['profile_guid']) . "' and uid = " . intval($channel['channel_id'])); } } } } $result[] = array($d['hash'], 'channel sync updated', $channel['channel_name'], ''); } return $result; }