From 166879b8b04f8656a0ef105c319b8b4a82626bd9 Mon Sep 17 00:00:00 2001 From: zotlabs Date: Mon, 6 Aug 2018 17:43:22 -0700 Subject: bring some Zot6 libraries and interfaces to red/hubzilla --- Zotlabs/Zot6/Finger.php | 146 ++++++++++++++++++++++++ Zotlabs/Zot6/IHandler.php | 18 +++ Zotlabs/Zot6/Receiver.php | 220 +++++++++++++++++++++++++++++++++++ Zotlabs/Zot6/Zot6Handler.php | 266 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 650 insertions(+) create mode 100644 Zotlabs/Zot6/Finger.php create mode 100644 Zotlabs/Zot6/IHandler.php create mode 100644 Zotlabs/Zot6/Receiver.php create mode 100644 Zotlabs/Zot6/Zot6Handler.php (limited to 'Zotlabs/Zot6') diff --git a/Zotlabs/Zot6/Finger.php b/Zotlabs/Zot6/Finger.php new file mode 100644 index 000000000..f1fe41352 --- /dev/null +++ b/Zotlabs/Zot6/Finger.php @@ -0,0 +1,146 @@ + true) or array('success' => false); + */ + + static public function run($webbie, $channel = null, $autofallback = true) { + + $ret = array('success' => false); + + self::$token = random_string(); + + if (strpos($webbie, '@') === false) { + $address = $webbie; + $host = \App::get_hostname(); + } else { + $address = substr($webbie,0,strpos($webbie,'@')); + $host = substr($webbie,strpos($webbie,'@')+1); + if(strpos($host,'/')) + $host = substr($host,0,strpos($host,'/')); + } + + $xchan_addr = $address . '@' . $host; + + if ((! $address) || (! $xchan_addr)) { + logger('zot_finger: no address :' . $webbie); + + return $ret; + } + + logger('using xchan_addr: ' . $xchan_addr, LOGGER_DATA, LOG_DEBUG); + + // potential issue here; the xchan_addr points to the primary hub. + // The webbie we were called with may not, so it might not be found + // unless we query for hubloc_addr instead of xchan_addr + + $r = q("select xchan.*, hubloc.* from xchan + left join hubloc on xchan_hash = hubloc_hash + where xchan_addr = '%s' and hubloc_primary = 1 limit 1", + dbesc($xchan_addr) + ); + + if($r) { + $url = $r[0]['hubloc_url']; + + if($r[0]['hubloc_network'] && $r[0]['hubloc_network'] !== 'zot') { + logger('zot_finger: alternate network: ' . $webbie); + logger('url: ' . $url . ', net: ' . var_export($r[0]['hubloc_network'],true), LOGGER_DATA, LOG_DEBUG); + return $ret; + } + } else { + $url = 'https://' . $host; + } + + $rhs = '/.well-known/zot-info'; + $https = ((strpos($url,'https://') === 0) ? true : false); + + logger('zot_finger: ' . $address . ' at ' . $url, LOGGER_DEBUG); + + if ($channel) { + $postvars = array( + 'address' => $address, + 'target' => $channel['channel_guid'], + 'target_sig' => $channel['channel_guid_sig'], + 'key' => $channel['channel_pubkey'], + 'token' => self::$token + ); + + $headers = []; + $headers['X-Zot-Channel'] = $channel['channel_address'] . '@' . \App::get_hostname(); + $headers['X-Zot-Nonce'] = random_string(); + $xhead = \Zotlabs\Web\HTTPSig::create_sig('',$headers,$channel['channel_prvkey'], + 'acct:' . $channel['channel_address'] . '@' . \App::get_hostname(),false); + + $retries = 0; + + $result = z_post_url($url . $rhs,$postvars,$retries, [ 'headers' => $xhead ]); + + if ((! $result['success']) && ($autofallback)) { + if ($https) { + logger('zot_finger: https failed. falling back to http'); + $result = z_post_url('http://' . $host . $rhs,$postvars, $retries, [ 'headers' => $xhead ]); + } + } + } + else { + $rhs .= '?f=&address=' . urlencode($address) . '&token=' . self::$token; + + $result = z_fetch_url($url . $rhs); + if((! $result['success']) && ($autofallback)) { + if($https) { + logger('zot_finger: https failed. falling back to http'); + $result = z_fetch_url('http://' . $host . $rhs); + } + } + } + + if(! $result['success']) { + logger('zot_finger: no results'); + + return $ret; + } + + $x = json_decode($result['body'], true); + + $verify = \Zotlabs\Web\HTTPSig::verify($result,(($x) ? $x['key'] : '')); + + if($x && (! $verify['header_valid'])) { + $signed_token = ((is_array($x) && array_key_exists('signed_token', $x)) ? $x['signed_token'] : null); + if($signed_token) { + $valid = zot_verify('token.' . self::$token, base64url_decode($signed_token), $x['key']); + if(! $valid) { + logger('invalid signed token: ' . $url . $rhs, LOGGER_NORMAL, LOG_ERR); + + return $ret; + } + } + else { + logger('No signed token from ' . $url . $rhs, LOGGER_NORMAL, LOG_WARNING); + return $ret; + } + } + + return $x; + } + +} diff --git a/Zotlabs/Zot6/IHandler.php b/Zotlabs/Zot6/IHandler.php new file mode 100644 index 000000000..53b6caa89 --- /dev/null +++ b/Zotlabs/Zot6/IHandler.php @@ -0,0 +1,18 @@ +error = false; + $this->validated = false; + $this->messagetype = ''; + $this->response = [ 'success' => false ]; + $this->handler = $handler; + $this->data = null; + $this->rawdata = null; + $this->site_id = null; + $this->prvkey = Config::get('system','prvkey'); + + if($localdata) { + $this->rawdata = $localdata; + } + else { + $this->rawdata = file_get_contents('php://input'); + + // All access to the zot endpoint must use http signatures + + if (! $this->Valid_Httpsig()) { + logger('signature failed'); + $this->error = true; + $this->response['message'] = 'signature invalid'; + return; + } + } + + logger('received raw: ' . print_r($this->rawdata,true), LOGGER_DATA); + + + if ($this->rawdata) { + $this->data = json_decode($this->rawdata,true); + } + else { + $this->error = true; + $this->response['message'] = 'no data'; + } + + logger('received_json: ' . json_encode($this->data,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES), LOGGER_DATA); + + logger('received: ' . print_r($this->data,true), LOGGER_DATA); + + if ($this->data && is_array($this->data)) { + $this->encrypted = ((array_key_exists('encrypted',$this->data) && intval($this->data['encrypted'])) ? true : false); + + if ($this->encrypted && $this->prvkey) { + $uncrypted = crypto_unencapsulate($this->data,$this->prvkey); + if ($uncrypted) { + $this->data = json_decode($uncrypted,true); + } + else { + $this->error = true; + $this->response['message'] = 'no data'; + } + } + } + } + + + function run() { + + if ($this->error) { + // make timing attacks on the decryption engine a bit more difficult + usleep(mt_rand(10000,100000)); + return($this->response); + } + + if ($this->data) { + if (array_key_exists('type',$this->data)) { + $this->messagetype = $this->data['type']; + } + + if (! $this->messagetype) { + $this->error = true; + $this->response['message'] = 'no datatype'; + return $this->response; + } + + $this->sender = ((array_key_exists('sender',$this->data)) ? $this->data['sender'] : null); + $this->recipients = ((array_key_exists('recipients',$this->data)) ? $this->data['recipients'] : null); + $this->site_id = ((array_key_exists('site_id',$this->data)) ? $this->data['site_id'] : null); + } + + if ($this->sender) { + $result = $this->ValidateSender(); + if (! $result) { + $this->error = true; + return $this->response; + } + } + + return $this->Dispatch(); + } + + function ValidateSender() { + + $hub = Libzot::valid_hub($this->sender,$this->site_id); + + if (! $hub) { + $x = Libzot::register_hub($this->sigdata['signer']); + if($x['success']) { + $hub = Libzot::valid_hub($this->sender,$this->site_id); + } + if(! $hub) { + $this->response['message'] = 'sender unknown'; + return false; + } + } + + if (! check_siteallowed($hub['hubloc_url'])) { + $this->response['message'] = 'forbidden'; + return false; + } + + if (! check_channelallowed($this->sender)) { + $this->response['message'] = 'forbidden'; + return false; + } + + Libzot::update_hub_connected($hub,$this->site_id); + + $this->validated = true; + $this->hub = $hub; + return true; + } + + + function Valid_Httpsig() { + + $result = false; + + $this->sigdata = HTTPSig::verify($this->rawdata); + + if ($this->sigdata && $this->sigdata['header_signed'] && $this->sigdata['header_valid']) { + $result = true; + + // It is OK to not have signed content - not all messages provide content. + // But if it is signed, it has to be valid + + if (($this->sigdata['content_signed']) && (! $this->sigdata['content_valid'])) { + $result = false; + } + } + return $result; + } + + function Dispatch() { + + switch ($this->messagetype) { + + case 'request': + $this->response = $this->handler->Request($this->data,$this->hub); + break; + + case 'purge': + $this->response = $this->handler->Purge($this->sender,$this->recipients,$this->hub); + break; + + case 'refresh': + $this->response = $this->handler->Refresh($this->sender,$this->recipients,$this->hub); + break; + + case 'rekey': + $this->response = $this->handler->Rekey($this->sender, $this->data,$this->hub); + break; + + case 'activity': + case 'response': // upstream message + case 'sync': + default: + $this->response = $this->handler->Notify($this->data,$this->hub); + break; + + } + + logger('response_to_return: ' . print_r($this->response,true),LOGGER_DATA); + + if ($this->encrypted) { + $this->EncryptResponse(); + } + + return($this->response); + } + + function EncryptResponse() { + $algorithm = Libzot::best_algorithm($this->hub['site_crypto']); + if ($algorithm) { + $this->response = crypto_encapsulate(json_encode($this->response),$this->hub['hubloc_sitekey'], $algorithm); + } + } + +} + + + diff --git a/Zotlabs/Zot6/Zot6Handler.php b/Zotlabs/Zot6/Zot6Handler.php new file mode 100644 index 000000000..5597921cc --- /dev/null +++ b/Zotlabs/Zot6/Zot6Handler.php @@ -0,0 +1,266 @@ + false ]; + + logger('notify received from ' . $hub['hubloc_url']); + + $x = Libzot::fetch($data); + $ret['delivery_report'] = $x; + + + $ret['success'] = true; + return $ret; + } + + + + /** + * @brief 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. + * + * @param array $sender + * @param array $recipients + * + * @return json_return_and_die() + */ + + static function reply_refresh($sender, $recipients,$hub) { + $ret = array('success' => false); + + if($recipients) { + + // This would be a permissions update, typically for one connection + + foreach ($recipients as $recip) { + $r = q("select channel.*,xchan.* from channel + left join xchan on channel_hash = xchan_hash + where channel_hash ='%s' limit 1", + dbesc($recip) + ); + + $x = Libzot::refresh( [ 'hubloc_id_url' => $hub['hubloc_id_url'] ], $r[0], (($msgtype === 'force_refresh') ? true : false)); + } + } + else { + // system wide refresh + + $x = Libzot::refresh( [ 'hubloc_id_url' => $hub['hubloc_id_url'] ], null, (($msgtype === 'force_refresh') ? true : false)); + } + + $ret['success'] = true; + return $ret; + } + + + + /** + * @brief Process a message request. + * + * If a site receives a comment to a post but finds they have no parent to attach it with, they + * may send a 'request' packet containing the message_id of the missing parent. This is the handler + * for that packet. We will create a message_list array of the entire conversation starting with + * the missing parent and invoke delivery to the sender of the packet. + * + * Zotlabs/Daemon/Deliver.php (for local delivery) and + * mod/post.php???? @fixme (for web delivery) detect the existence of + * this 'message_list' at the destination and split it into individual messages which are + * processed/delivered in order. + * + * + * @param array $data + * @return array + */ + + static function reply_message_request($data,$hub) { + $ret = [ 'success' => false ]; + + $message_id = EMPTY_STR; + + if(array_key_exists('data',$data)) + $ptr = $data['data']; + if(is_array($ptr) && array_key_exists(0,$ptr)) { + $ptr = $ptr[0]; + } + if(is_string($ptr)) { + $message_id = $ptr; + } + if(is_array($ptr) && array_key_exists('id',$ptr)) { + $message_id = $ptr['id']; + } + + if (! $message_id) { + $ret['message'] = 'no message_id'; + logger('no message_id'); + return $ret; + } + + $sender = $hub['hubloc_hash']; + + /* + * Find the local channel in charge of this post (the first and only recipient of the request packet) + */ + + $arr = $data['recipients'][0]; + + $c = q("select * from channel left join xchan on channel_hash = xchan_hash where channel_hash = '%s' limit 1", + dbesc($arr['portable_id']) + ); + if (! $c) { + logger('recipient channel not found.'); + $ret['message'] .= 'recipient not found.' . EOL; + return $ret; + } + + /* + * fetch the requested conversation + */ + + $messages = zot_feed($c[0]['channel_id'],$sender_hash, [ 'message_id' => $data['message_id'], 'encoding' => 'activitystreams' ]); + + return (($messages) ? : [] ); + + } + + static function rekey_request($sender,$data,$hub) { + + $ret = array('success' => false); + + // newsig is newkey signed with oldkey + + // The original xchan will remain. In Zot/Receiver we will have imported the new xchan and hubloc to verify + // the packet authenticity. What we will do now is verify that the keychange operation was signed by the + // oldkey, and if so change all the abook, abconfig, group, and permission elements which reference the + // old xchan_hash. + + if((! $data['old_key']) && (! $data['new_key']) && (! $data['new_sig'])) + return $ret; + + + $old = null; + + if(Libzot::verify($data['old_guid'],$data['old_guid_sig'],$data['old_key'])) { + $oldhash = make_xchan_hash($data['old_guid'],$data['old_key']); + $old = q("select * from xchan where xchan_hash = '%s' limit 1", + dbesc($oldhash) + ); + } + else + return $ret; + + + if(! $old) { + return $ret; + } + + $xchan = $old[0]; + + if(! Libzot::verify($data['new_key'],$data['new_sig'],$xchan['xchan_pubkey'])) { + return $ret; + } + + $r = q("select * from xchan where xchan_hash = '%s' limit 1", + dbesc($sender) + ); + + $newxchan = $r[0]; + + // @todo + // if ! $update create a linked identity + + + xchan_change_key($xchan,$newxchan,$data); + + $ret['success'] = true; + return $ret; + } + + + /** + * @brief + * + * @param array $sender + * @param array $recipients + * + * return json_return_and_die() + */ + + static function reply_purge($sender, $recipients, $hub) { + + $ret = array('success' => false); + + if ($recipients) { + // basically this means "unfriend" + foreach ($recipients as $recip) { + $r = q("select channel.*,xchan.* from channel + left join xchan on channel_hash = xchan_hash + where channel_hash = '%s' and channel_guid_sig = '%s' limit 1", + dbesc($recip) + ); + if ($r) { + $r = q("select abook_id from abook where uid = %d and abook_xchan = '%s' limit 1", + intval($r[0]['channel_id']), + dbesc($sender) + ); + if ($r) { + contact_remove($r[0]['channel_id'],$r[0]['abook_id']); + } + } + } + $ret['success'] = true; + } + else { + + // Unfriend everybody - basically this means the channel has committed suicide + + remove_all_xchan_resources($sender); + + $ret['success'] = true; + } + + return $ret; + } + + + + + + +} -- cgit v1.2.3