Exemplo n.º 1
0
 function ValidateSender()
 {
     $hubs = zot_gethub($this->sender, true);
     if (!$hubs) {
         /* Have never seen this guid or this guid coming from this location. Check it and register it. */
         /* (!!) this will validate the sender. */
         $result = zot_register_hub($this->sender);
         if (!$result['success'] || !($hubs = zot_gethub($this->sender, true))) {
             $this->response['message'] = 'Hub not available.';
             json_return_and_die($this->response);
         }
     }
     foreach ($hubs as $hub) {
         update_hub_connected($hub, array_key_exists('sitekey', $this->sender) ? $this->sender['sitekey'] : '');
     }
     $this->validated = true;
 }
Exemplo n.º 2
0
Arquivo: post.php Projeto: Mauru/red
/**
 * @function post_post(&$a)
 *     zot communications and messaging
 *
 *     Sender HTTP posts to this endpoint ($site/post typically) with 'data' parameter set to json zot message packet.
 *     This packet is optionally encrypted, which we will discover if the json has an 'iv' element.
 *     $contents => array( 'alg' => 'aes256cbc', 'iv' => initialisation vector, 'key' => decryption key, 'data' => encrypted data);
 *     $contents->iv and $contents->key are random strings encrypted with this site's RSA public key and then base64url encoded.
 *     Currently only 'aes256cbc' is used, but this is extensible should that algorithm prove inadequate.
 *
 *     Once decrypted, one will find the normal json_encoded zot message packet. 
 * 
 * Defined packet types are: notify, purge, refresh, force_refresh, auth_check, ping, and pickup 
 *
 * Standard packet: (used by notify, purge, refresh, force_refresh, and auth_check)
 *
 * {
 *  "type": "notify",
 *  "sender":{
 *       "guid":"kgVFf_1...",
 *       "guid_sig":"PT9-TApzp...",
 *       "url":"http:\/\/podunk.edu",
 *       "url_sig":"T8Bp7j5...",
 *    },
 *  "recipients": { optional recipient array },
 *  "callback":"\/post",
 *  "version":1,
 *  "secret":"1eaa...",
 *  "secret_sig": "df89025470fac8..."
 * }
 * 
 * Signature fields are all signed with the sender channel private key and base64url encoded.
 * Recipients are arrays of guid and guid_sig, which were previously signed with the recipients private 
 * key and base64url encoded and later obtained via channel discovery. Absence of recipients indicates
 * a public message or visible to all potential listeners on this site.
 *
 * "pickup" packet:
 * The pickup packet is sent in response to a notify packet from another site
 * 
 * {
 *  "type":"pickup",
 *  "url":"http:\/\/example.com",
 *  "callback":"http:\/\/example.com\/post",
 *  "callback_sig":"teE1_fLI...",
 *  "secret":"1eaa...",
 *  "secret_sig":"O7nB4_..."
 * }
 *
 * In the pickup packet, the sig fields correspond to the respective data element signed with this site's system 
 * private key and then base64url encoded.
 * The "secret" is the same as the original secret from the notify packet. 
 *
 * If verification is successful, a json structure is returned
 * containing a success indicator and an array of type 'pickup'.
 * Each pickup element contains the original notify request and a message field whose contents are 
 * dependent on the message type
 *
 * This JSON array is AES encapsulated using the site public key of the site that sent the initial zot pickup packet.
 * Using the above example, this would be example.com.
 * 
 * 
 * {
 * "success":1,
 * "pickup":{
 *   "notify":{
 *     "type":"notify",
 *     "sender":{
 *       "guid":"kgVFf_...",
 *       "guid_sig":"PT9-TApz...",
 *       "url":"http:\/\/z.podunk.edu",
 *       "url_sig":"T8Bp7j5D..."
 *     },
 *     "callback":"\/post",
 *     "version":1,
 *     "secret":"1eaa661..."
 *   },
 *   "message":{
 *     "type":"activity",
 *     "message_id":"*****@*****.**",
 *     "message_top":"*****@*****.**",
 *     "message_parent":"*****@*****.**",
 *     "created":"2012-11-20 04:04:16",
 *     "edited":"2012-11-20 04:04:16",
 *     "title":"",
 *     "body":"Hi Nickordo",
 *     "app":"",
 *     "verb":"post",
 *     "object_type":"",
 *     "target_type":"",
 *     "permalink":"",
 *     "location":"",
 *     "longlat":"",
 *     "owner":{
 *       "name":"Indigo",
 *       "address":"*****@*****.**",
 *       "url":"http:\/\/podunk.edu",
 *       "photo":{
 *         "mimetype":"image\/jpeg",
 *         "src":"http:\/\/podunk.edu\/photo\/profile\/m\/5"
 *       },
 *       "guid":"kgVFf_...",
 *       "guid_sig":"PT9-TAp...",
 *     },
 *     "author":{
 *       "name":"Indigo",
 *       "address":"*****@*****.**",
 *       "url":"http:\/\/podunk.edu",
 *       "photo":{
 *         "mimetype":"image\/jpeg",
 *         "src":"http:\/\/podunk.edu\/photo\/profile\/m\/5"
 *       },
 *       "guid":"kgVFf_...",
 *       "guid_sig":"PT9-TAp..."
 *     }
 *   }
 * }
 *} 
 *
 * Currently defined message types are 'activity', 'mail', 'profile' and 'channel_sync', which each have 
 * different content schemas.
 *
 * Ping packet:
 * A ping packet does not require any parameters except the type. It may or may not be encrypted.
 * 
 * {
 *  "type": "ping"
 * }
 * 
 * On receipt of a ping packet a ping response will be returned:
 *
 * {
 *   "success" : 1,
 *   "site" {
 *       "url":"http:\/\/podunk.edu",
 *       "url_sig":"T8Bp7j5...",
 *       "sitekey": "-----BEGIN PUBLIC KEY-----
 *                  MIICIjANBgkqhkiG9w0BAQE..."
 *    }
 * }
 * 
 * The ping packet can be used to verify that a site has not been re-installed, and to 
 * initiate corrective action if it has. The url_sig is signed with the site private key
 * and base64url encoded - and this should verify with the enclosed sitekey. Failure to
 * verify indicates the site is corrupt or otherwise unable to communicate using zot.
 * This return packet is not otherwise verified, so should be compared with other
 * results obtained from this site which were verified prior to taking action. For instance
 * if you have one verified result with this signature and key, and other records for this 
 * url which have different signatures and keys, it indicates that the site was re-installed
 * and corrective action may commence (remove or mark invalid any entries with different
 * signatures).
 * If you have no records which match this url_sig and key - no corrective action should
 * be taken as this packet may have been returned by an imposter.  
 *
 */
function post_post(&$a)
{
    $encrypted_packet = false;
    $ret = array('success' => false);
    $data = json_decode($_REQUEST['data'], true);
    /**
     * Many message packets will arrive encrypted. The existence of an 'iv' element 
     * tells us we need to unencapsulate the AES-256-CBC content using the site private key
     */
    if (array_key_exists('iv', $data)) {
        $encrypted_packet = true;
        $data = crypto_unencapsulate($data, get_config('system', 'prvkey'));
        logger('mod_zot: decrypt1: ' . $data, LOGGER_DATA);
        $data = json_decode($data, true);
    }
    if (!$data) {
        // possible Bleichenbacher's attack, just treat it as a
        // message we have no handler for. It should fail a bit
        // further along with "no hub". Our public key is public
        // knowledge. There's no reason why anybody should get the
        // encryption wrong unless they're fishing or hacking. If
        // they're developing and made a goof, this can be discovered
        // in the logs of the destination site. If they're fishing or
        // hacking, the bottom line is we can't verify their hub.
        // That's all we're going to tell them.
        $data = array('type' => 'bogus');
    }
    $msgtype = array_key_exists('type', $data) ? $data['type'] : '';
    if ($msgtype === 'ping') {
        // Useful to get a health check on a remote site.
        // This will let us know if any important communication details
        // that we may have stored are no longer valid, regardless of xchan details.
        logger('POST: got ping send pong now back: ' . z_root(), LOGGER_DEBUG);
        $ret['success'] = true;
        $ret['site'] = array();
        $ret['site']['url'] = z_root();
        $ret['site']['url_sig'] = base64url_encode(rsa_sign(z_root(), get_config('system', 'prvkey')));
        $ret['site']['sitekey'] = get_config('system', 'pubkey');
        json_return_and_die($ret);
    }
    if ($msgtype === 'pickup') {
        /**
         * The 'pickup' message arrives with a tracking ID which is associated with a particular outq_hash
         * First verify that that the returned signatures verify, then check that we have an outbound queue item
         * with the correct hash.
         * If everything verifies, find any/all outbound messages in the queue for this hubloc and send them back
         *
         */
        if (!$data['secret'] || !$data['secret_sig']) {
            $ret['message'] = 'no verification signature';
            logger('mod_zot: pickup: ' . $ret['message'], LOGGER_DEBUG);
            json_return_and_die($ret);
        }
        $r = q("select distinct hubloc_sitekey from hubloc where hubloc_url = '%s' and hubloc_callback = '%s' and hubloc_sitekey != '' group by hubloc_sitekey ", dbesc($data['url']), dbesc($data['callback']));
        if (!$r) {
            $ret['message'] = 'site not found';
            logger('mod_zot: pickup: ' . $ret['message']);
            json_return_and_die($ret);
        }
        foreach ($r as $hubsite) {
            // verify the url_sig
            // If the server was re-installed at some point, there could be multiple hubs with the same url and callback.
            // Only one will have a valid key.
            $forgery = true;
            $secret_fail = true;
            $sitekey = $hubsite['hubloc_sitekey'];
            logger('mod_zot: Checking sitekey: ' . $sitekey, LOGGER_DATA);
            if (rsa_verify($data['callback'], base64url_decode($data['callback_sig']), $sitekey)) {
                $forgery = false;
            }
            if (rsa_verify($data['secret'], base64url_decode($data['secret_sig']), $sitekey)) {
                $secret_fail = false;
            }
            if (!$forgery && !$secret_fail) {
                break;
            }
        }
        if ($forgery) {
            $ret['message'] = 'possible site forgery';
            logger('mod_zot: pickup: ' . $ret['message']);
            json_return_and_die($ret);
        }
        if ($secret_fail) {
            $ret['message'] = 'secret validation failed';
            logger('mod_zot: pickup: ' . $ret['message']);
            json_return_and_die($ret);
        }
        /**
         * If we made it to here, the signatures verify, but we still don't know if the tracking ID is valid.
         * It wouldn't be an error if the tracking ID isn't found, because we may have sent this particular
         * queue item with another pickup (after the tracking ID for the other pickup  was verified). 
         */
        $r = q("select outq_posturl from outq where outq_hash = '%s' and outq_posturl = '%s' limit 1", dbesc($data['secret']), dbesc($data['callback']));
        if (!$r) {
            $ret['message'] = 'nothing to pick up';
            logger('mod_zot: pickup: ' . $ret['message']);
            json_return_and_die($ret);
        }
        /**
         * Everything is good if we made it here, so find all messages that are going to this location
         * and send them all.
         */
        $r = q("select * from outq where outq_posturl = '%s'", dbesc($data['callback']));
        if ($r) {
            logger('mod_zot: succesful pickup message received from ' . $data['callback'] . ' ' . count($r) . ' message(s) picked up', LOGGER_DEBUG);
            $ret['success'] = true;
            $ret['pickup'] = array();
            foreach ($r as $rr) {
                $ret['pickup'][] = array('notify' => json_decode($rr['outq_notify'], true), 'message' => json_decode($rr['outq_msg'], true));
                $x = q("delete from outq where outq_hash = '%s' limit 1", dbesc($rr['outq_hash']));
            }
        }
        $encrypted = crypto_encapsulate(json_encode($ret), $sitekey);
        json_return_and_die($encrypted);
        /** pickup: end */
    }
    /**
     * All other message types require us to verify the sender. This is a generic check, so we 
     * will do it once here and bail if anything goes wrong.
     */
    if (array_key_exists('sender', $data)) {
        $sender = $data['sender'];
    }
    /** Check if the sender is already verified here */
    $hub = zot_gethub($sender);
    if (!$hub) {
        /** Have never seen this guid or this guid coming from this location. Check it and register it. */
        // (!!) this will validate the sender
        $result = zot_register_hub($sender);
        if (!$result['success'] || !($hub = zot_gethub($sender))) {
            $ret['message'] = 'Hub not available.';
            logger('mod_zot: no hub');
            json_return_and_die($ret);
        }
    }
    // Update our DB to show when we last communicated successfully with this hub
    // This will allow us to prune dead hubs from using up resources
    $r = q("update hubloc set hubloc_connected = '%s' where hubloc_id = %d limit 1", dbesc(datetime_convert()), intval($hub['hubloc_id']));
    // a dead hub came back to life - reset any tombstones we might have
    if ($hub['hubloc_status'] & HUBLOC_OFFLINE) {
        q("update hubloc set hubloc_status = (hubloc_status ^ %d) where hubloc_id = %d limit 1", intval(HUBLOC_OFFLINE), intval($hub['hubloc_id']));
        if ($r[0]['hubloc_flags'] & HUBLOC_FLAGS_ORPHANCHECK) {
            q("update hubloc set hubloc_flags = (hubloc_flags ^ %d) where hubloc_id = %d limit 1", intval(HUBLOC_FLAGS_ORPHANCHECK), intval($hub['hubloc_id']));
        }
        q("update xchan set xchan_flags = (xchan_flags ^ %d) where (xchan_flags & %d) and xchan_hash = '%s' limit 1", intval(XCHAN_FLAGS_ORPHAN), intval(XCHAN_FLAGS_ORPHAN), dbesc($hub['hubloc_hash']));
    }
    /** 
     * This hub has now been proven to be valid.
     * Any hub with the same URL and a different sitekey cannot be valid.
     * Get rid of them (mark them deleted). There's a good chance they were re-installs.
     *
     */
    q("update hubloc set hubloc_flags = ( hubloc_flags | %d ) where hubloc_url = '%s' and hubloc_sitekey != '%s' ", intval(HUBLOC_FLAGS_DELETED), dbesc($hub['hubloc_url']), dbesc($hub['hubloc_sitekey']));
    // TODO: check which hub is primary and take action if mismatched
    if (array_key_exists('recipients', $data)) {
        $recipients = $data['recipients'];
    }
    if ($msgtype === 'auth_check') {
        /**
         * Requestor visits /magic/?dest=somewhere on their own site with a browser
         * magic redirects them to $destsite/post [with auth args....]
         * $destsite sends an auth_check packet to originator site
         * The auth_check packet is handled here by the originator's site 
         * - the browser session is still waiting
         * inside $destsite/post for everything to verify
         * If everything checks out we'll return a token to $destsite
         * and then $destsite will verify the token, authenticate the browser
         * session and then redirect to the original destination.
         * If authentication fails, the redirection to the original destination
         * will still take place but without authentication.
         */
        logger('mod_zot: auth_check', LOGGER_DEBUG);
        if (!$encrypted_packet) {
            logger('mod_zot: auth_check packet was not encrypted.');
            $ret['message'] .= 'no packet encryption' . EOL;
            json_return_and_die($ret);
        }
        $arr = $data['sender'];
        $sender_hash = make_xchan_hash($arr['guid'], $arr['guid_sig']);
        // garbage collect any old unused notifications
        q("delete from verify where type = 'auth' and created < UTC_TIMESTAMP() - INTERVAL 10 MINUTE");
        $y = q("select xchan_pubkey from xchan where xchan_hash = '%s' limit 1", dbesc($sender_hash));
        // We created a unique hash in mod/magic.php when we invoked remote auth, and stored it in
        // the verify table. It is now coming back to us as 'secret' and is signed by a channel at the other end.
        // First verify their signature. We will have obtained a zot-info packet from them as part of the sender
        // verification.
        if (!$y || !rsa_verify($data['secret'], base64url_decode($data['secret_sig']), $y[0]['xchan_pubkey'])) {
            logger('mod_zot: auth_check: sender not found or secret_sig invalid.');
            $ret['message'] .= 'sender not found or sig invalid ' . print_r($y, true) . EOL;
            json_return_and_die($ret);
        }
        // There should be exactly one recipient, the original auth requestor
        $ret['message'] .= 'recipients ' . print_r($recipients, true) . EOL;
        if ($data['recipients']) {
            $arr = $data['recipients'][0];
            $recip_hash = make_xchan_hash($arr['guid'], $arr['guid_sig']);
            $c = q("select channel_id, channel_account_id, channel_prvkey from channel where channel_hash = '%s' limit 1", dbesc($recip_hash));
            if (!$c) {
                logger('mod_zot: auth_check: recipient channel not found.');
                $ret['message'] .= 'recipient not found.' . EOL;
                json_return_and_die($ret);
            }
            $confirm = base64url_encode(rsa_sign($data['secret'] . $recip_hash, $c[0]['channel_prvkey']));
            // This additionally checks for forged sites since we already stored the expected result in meta
            // and we've already verified that this is them via zot_gethub() and that their key signed our token
            $z = q("select id from verify where channel = %d and type = 'auth' and token = '%s' and meta = '%s' limit 1", intval($c[0]['channel_id']), dbesc($data['secret']), dbesc($data['sender']['url']));
            if (!$z) {
                logger('mod_zot: auth_check: verification key not found.');
                $ret['message'] .= 'verification key not found' . EOL;
                json_return_and_die($ret);
            }
            $r = q("delete from verify where id = %d limit 1", intval($z[0]['id']));
            $u = q("select account_service_class from account where account_id = %d limit 1", intval($c[0]['channel_account_id']));
            logger('mod_zot: auth_check: success', LOGGER_DEBUG);
            $ret['success'] = true;
            $ret['confirm'] = $confirm;
            if ($u && $u[0]['account_service_class']) {
                $ret['service_class'] = $u[0]['account_service_class'];
            }
            // Set "do not track" flag if this site or this channel's profile is restricted
            if (intval(get_config('system', 'block_public'))) {
                $ret['DNT'] = true;
            }
            if (!perm_is_allowed($c[0]['channel_id'], '', 'view_profile')) {
                $ret['DNT'] = true;
            }
            if (get_pconfig($c[0]['channel_id'], 'system', 'do_not_track')) {
                $ret['DNT'] = true;
            }
            json_return_and_die($ret);
        }
        json_return_and_die($ret);
    }
    if ($msgtype === 'purge') {
        if ($recipients) {
            // basically this means "unfriend"
            foreach ($recipients as $recip) {
                $r = q("select channel.*,xchan.* from channel \n\t\t\t\t\tleft join xchan on channel_hash = xchan_hash\n\t\t\t\t\twhere channel_guid = '%s' and channel_guid_sig = '%s' limit 1", dbesc($recip['guid']), dbesc($recip['guid_sig']));
                if ($r) {
                    $r = q("select abook_id from abook where uid = %d and abook_xchan = '%s' limit 1", intval($r[0]['channel_id']), dbesc(make_xchan_hash($sender['guid'], $sender['guid_sig'])));
                    if ($r) {
                        contact_remove($r[0]['channel_id'], $r[0]['abook_id']);
                    }
                }
            }
        } else {
            // Unfriend everybody - basically this means the channel has committed suicide
            $arr = $data['sender'];
            $sender_hash = make_xchan_hash($arr['guid'], $arr['guid_sig']);
            require_once 'include/Contact.php';
            remove_all_xchan_resources($sender_hash);
            $ret['success'] = true;
            json_return_and_die($ret);
        }
    }
    if ($msgtype === 'refresh' || $msgtype === 'force_refresh') {
        // remote channel info (such as permissions or photo or something)
        // has been updated. Grab a fresh copy and sync it.
        // The difference between refresh and force_refresh is that
        // force_refresh unconditionally creates a directory update record,
        // even if no changes were detected upon processing.
        if ($recipients) {
            // This would be a permissions update, typically for one connection
            foreach ($recipients as $recip) {
                $r = q("select channel.*,xchan.* from channel \n\t\t\t\t\tleft join xchan on channel_hash = xchan_hash\n\t\t\t\t\twhere channel_guid = '%s' and channel_guid_sig = '%s' limit 1", dbesc($recip['guid']), dbesc($recip['guid_sig']));
                $x = zot_refresh(array('xchan_guid' => $sender['guid'], 'xchan_guid_sig' => $sender['guid_sig'], 'hubloc_url' => $sender['url']), $r[0], $msgtype === 'force_refresh' ? true : false);
            }
        } else {
            // system wide refresh
            $x = zot_refresh(array('xchan_guid' => $sender['guid'], 'xchan_guid_sig' => $sender['guid_sig'], 'hubloc_url' => $sender['url']), null, $msgtype === 'force_refresh' ? true : false);
        }
        $ret['success'] = true;
        json_return_and_die($ret);
    }
    if ($msgtype === 'notify') {
        $async = get_config('system', 'queued_fetch');
        if ($async) {
            // add to receive queue
            // qreceive_add($data);
        } else {
            $x = zot_fetch($data);
            $ret['delivery_report'] = $x;
        }
        $ret['success'] = true;
        json_return_and_die($ret);
    }
    // catchall
    json_return_and_die($ret);
}