diff options
Diffstat (limited to 'Zotlabs')
29 files changed, 664 insertions, 734 deletions
diff --git a/Zotlabs/Daemon/Master.php b/Zotlabs/Daemon/Master.php index 857d47243..67a3acc0a 100644 --- a/Zotlabs/Daemon/Master.php +++ b/Zotlabs/Daemon/Master.php @@ -17,7 +17,22 @@ if(array_search( __file__ , get_included_files()) === 0) { class Master { static public function Summon($arr) { - proc_run('php','Zotlabs/Daemon/Master.php',$arr); + $hookinfo = [ + 'argv'=>$arr + ]; + + call_hooks ('daemon_master_summon',$hookinfo); + + $arr = $hookinfo['argv']; + $argc = count($arr); + + if ((!is_array($arr) || (count($arr) < 1))) { + logger("Summon handled by hook.",LOGGER_DEBUG); + return; + } + + $phpbin = get_config('system','phpbin','php'); + proc_run($phpbin,'Zotlabs/Daemon/Master.php',$arr); } static public function Release($argc,$argv) { @@ -33,6 +48,7 @@ class Master { $argc = count($argv); if ((!is_array($argv) || (count($argv) < 1))) { + logger("Release handled by hook.",LOGGER_DEBUG); return; } diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php index 0808fe33f..8168e7354 100644 --- a/Zotlabs/Lib/Activity.php +++ b/Zotlabs/Lib/Activity.php @@ -3,7 +3,7 @@ namespace Zotlabs\Lib; use Zotlabs\Daemon\Master; -use Zotlabs\Zot6\HTTPSig; +use Zotlabs\Web\HTTPSig; require_once('include/event.php'); @@ -312,6 +312,10 @@ class Activity { } } + if (intval($i['item_private']) === 2) { + $ret['directMessage'] = true; + } + $ret['attributedTo'] = $i['author']['xchan_url']; if($i['id'] != $i['parent']) { @@ -483,6 +487,19 @@ class Activity { $ret['type'] = self::activity_mapper($i['verb']); + if($ret['type'] === 'emojiReaction') { + // There may not be an object for these items for legacy reasons - it should be the conversation parent. + $p = q("select * from item where mid = '%s' and uid = %d", + dbesc($i['parent_mid']), + intval($i['uid']) + ); + if($p) { + xchan_query($p,true); + $p = fetch_post_tags($p,true); + $i['obj'] = self::encode_item($p[0]); + } + } + $ret['id'] = ((strpos($i['mid'],'http') === 0) ? $i['mid'] : z_root() . '/activity/' . urlencode($i['mid'])); @@ -1415,6 +1432,11 @@ class Activity { if($act->recips && (! in_array(ACTIVITY_PUBLIC_INBOX,$act->recips))) $s['item_private'] = 1; + + if (array_key_exists('directMessage',$act->obj) && intval($act->obj['directMessage'])) { + $s['item_private'] = 2; + } + set_iconfig($s,'activitypub','recips',$act->raw_recips); if($parent) { set_iconfig($s,'activitypub','rawmsg',$act->raw,1); @@ -1835,7 +1857,8 @@ class Activity { $s['item_private'] = 1; set_iconfig($s,'activitypub','recips',$act->raw_recips); - // @FIXME: $parent is not defined + + $parent = (($s['parent_mid'] && $s['parent_mid'] === $s['mid']) ? true : false); if($parent) { set_iconfig($s,'activitypub','rawmsg',$act->raw,1); } @@ -1844,6 +1867,265 @@ class Activity { } + static function store($channel,$observer_hash,$act,$item,$fetch_parents = true) { + + $is_sys_channel = is_sys_channel($channel['channel_id']); + + // Mastodon only allows visibility in public timelines if the public inbox is listed in the 'to' field. + // They are hidden in the public timeline if the public inbox is listed in the 'cc' field. + // This is not part of the activitypub protocol - we might change this to show all public posts in pubstream at some point. + + $pubstream = ((is_array($act->obj) && array_key_exists('to', $act->obj) && in_array(ACTIVITY_PUBLIC_INBOX, $act->obj['to'])) ? true : false); + $is_parent = (($item['parent_mid'] && $item['parent_mid'] === $item['mid']) ? true : false); + + if($is_parent && (! perm_is_allowed($channel['channel_id'],$observer_hash,'send_stream') && ! ($is_sys_channel && $pubstream))) { + logger('no permission'); + return; + } + + if(is_array($act->obj)) { + $content = self::get_content($act->obj); + } + if(! $content) { + logger('no content'); + return; + } + + $item['aid'] = $channel['channel_account_id']; + $item['uid'] = $channel['channel_id']; + $s['uuid'] = ''; + + // Friendica sends the diaspora guid in a nonstandard field via AP + if($act->obj['diaspora:guid']) + $s['uuid'] = $act->obj['diaspora:guid']; + + if(! ( $item['author_xchan'] && $item['owner_xchan'])) { + logger('owner or author missing.'); + return; + } + + if($channel['channel_system']) { + if(! MessageFilter::evaluate($item,get_config('system','pubstream_incl'),get_config('system','pubstream_excl'))) { + logger('post is filtered'); + return; + } + } + + $abook = q("select * from abook where abook_xchan = '%s' and abook_channel = %d limit 1", + dbesc($observer_hash), + intval($channel['channel_id']) + ); + + if($abook) { + if(! post_is_importable($item,$abook[0])) { + logger('post is filtered'); + return; + } + } + + + if($act->obj['conversation']) { + set_iconfig($item,'ostatus','conversation',$act->obj['conversation'],1); + } + + // This isn't perfect but the best we can do for now. + + $item['comment_policy'] = 'authenticated'; + + set_iconfig($item,'activitypub','recips',$act->raw_recips); + + if(! $is_parent) { + $p = q("select parent_mid from item where mid = '%s' and uid = %d limit 1", + dbesc($item['parent_mid']), + intval($item['uid']) + ); + if(! $p) { + $a = (($fetch_parents) ? self::fetch_and_store_parents($channel,$act,$item) : false); + if($a) { + $p = q("select parent_mid from item where mid = '%s' and uid = %d limit 1", + dbesc($item['parent_mid']), + intval($item['uid']) + ); + } + else { + logger('could not fetch parents'); + return; + + // @TODO we maybe could accept these is we formatted the body correctly with share_bb() + // or at least provided a link to the object + // if(in_array($act->type,[ 'Like','Dislike' ])) { + // return; + // } + + // @TODO do we actually want that? + // if no parent was fetched, turn into a top-level post + + // turn into a top level post + // $s['parent_mid'] = $s['mid']; + // $s['thr_parent'] = $s['mid']; + } + } + if($p[0]['parent_mid'] !== $item['parent_mid']) { + $item['thr_parent'] = $item['parent_mid']; + } + else { + $item['thr_parent'] = $p[0]['parent_mid']; + } + $item['parent_mid'] = $p[0]['parent_mid']; + } + + $r = q("select id, created, edited from item where mid = '%s' and uid = %d limit 1", + dbesc($item['mid']), + intval($item['uid']) + ); + if($r) { + if($item['edited'] > $r[0]['edited']) { + $item['id'] = $r[0]['id']; + $x = item_store_update($item); + } + else { + return; + } + } + else { + $x = item_store($item); + } + + if(is_array($x) && $x['item_id']) { + if($is_parent) { + if($item['owner_xchan'] === $channel['channel_hash']) { + // We are the owner of this conversation, so send all received comments back downstream + Master::Summon(array('Notifier','comment-import',$x['item_id'])); + } + $r = q("select * from item where id = %d limit 1", + intval($x['item_id']) + ); + if($r) { + send_status_notifications($x['item_id'],$r[0]); + } + } + sync_an_item($channel['channel_id'],$x['item_id']); + } + + } + + static public function fetch_and_store_parents($channel,$act,$item) { + + logger('fetching parents'); + + $p = []; + + $current_act = $act; + $current_item = $item; + + while($current_item['parent_mid'] !== $current_item['mid']) { + $n = ActivityStreams::fetch($current_item['parent_mid'], $channel); + if(! $n) { + break; + } + $a = new ActivityStreams($n); + + logger($a->debug()); + + if(! $a->is_valid()) { + break; + } + + $replies = null; + if(isset($a->obj['replies']['first']['items'])) { + $replies = $a->obj['replies']['first']['items']; + // we already have this one + array_diff($replies, [$current_item['mid']]); + } + + $item = null; + + switch($a->type) { + case 'Create': + case 'Update': + case 'Like': + case 'Dislike': + case 'Announce': + $item = self::decode_note($a); + break; + default: + break; + + } + if(! $item) { + break; + } + + array_unshift($p,[ $a, $item, $replies]); + + if($item['parent_mid'] === $item['mid'] || count($p) > 20) { + break; + } + + $current_act = $a; + $current_item = $item; + } + + if($p) { + foreach($p as $pv) { + self::store($channel,$pv[0]->actor['id'],$pv[0],$pv[1],false); + if($pv[2]) + self::fetch_and_store_replies($channel, $pv[2]); + } + return true; + } + + return false; + } + + static public function fetch_and_store_replies($channel, $arr) { + + logger('fetching replies'); + + $p = []; + + foreach($arr as $url) { + + $n = ActivityStreams::fetch($url, $channel); + if(! $n) { + break; + } + + $a = new ActivityStreams($n); + + if(! $a->is_valid()) { + break; + } + + $item = null; + + switch($a->type) { + case 'Create': + case 'Update': + case 'Like': + case 'Dislike': + case 'Announce': + $item = self::decode_note($a); + break; + default: + break; + } + if(! $item) { + break; + } + + array_unshift($p,[ $a, $item ]); + + } + + if($p) { + foreach($p as $pv) { + self::store($channel,$pv[0]->actor['id'],$pv[0],$pv[1],false); + } + } + + } + static function announce_note($channel,$observer_hash,$act) { $s = []; @@ -1964,25 +2246,21 @@ class Activity { $x = item_store($s); } - if(is_array($x) && $x['item_id']) { - // @FIXME: $parent is not defined - if($parent) { - if($s['owner_xchan'] === $channel['channel_hash']) { - // We are the owner of this conversation, so send all received comments back downstream - Master::Summon(array('Notifier','comment-import',$x['item_id'])); - } - $r = q("select * from item where id = %d limit 1", - intval($x['item_id']) - ); - if($r) { - send_status_notifications($x['item_id'],$r[0]); - } + if($s['owner_xchan'] === $channel['channel_hash']) { + // We are the owner of this conversation, so send all received comments back downstream + Master::Summon(array('Notifier','comment-import',$x['item_id'])); } + $r = q("select * from item where id = %d limit 1", + intval($x['item_id']) + ); + if($r) { + send_status_notifications($x['item_id'],$r[0]); + } + sync_an_item($channel['channel_id'],$x['item_id']); } - } static function like_note($channel,$observer_hash,$act) { diff --git a/Zotlabs/Lib/JSalmon.php b/Zotlabs/Lib/JSalmon.php index f35bf6235..bed748432 100644 --- a/Zotlabs/Lib/JSalmon.php +++ b/Zotlabs/Lib/JSalmon.php @@ -2,7 +2,7 @@ namespace Zotlabs\Lib; -use Zotlabs\Zot6\HTTPSig; +use Zotlabs\Web\HTTPSig; class JSalmon { diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php index 0ad8afc94..2a13744a3 100644 --- a/Zotlabs/Lib/Libzot.php +++ b/Zotlabs/Lib/Libzot.php @@ -2,7 +2,7 @@ namespace Zotlabs\Lib; -use Zotlabs\Zot6\HTTPSig; +use Zotlabs\Web\HTTPSig; use Zotlabs\Access\Permissions; use Zotlabs\Access\PermissionLimits; use Zotlabs\Daemon\Master; diff --git a/Zotlabs/Lib/ThreadItem.php b/Zotlabs/Lib/ThreadItem.php index 9161aa182..5e4600df2 100644 --- a/Zotlabs/Lib/ThreadItem.php +++ b/Zotlabs/Lib/ThreadItem.php @@ -98,7 +98,7 @@ class ThreadItem { $conv = $this->get_conversation(); $observer = $conv->get_observer(); - $lock = ((($item['item_private'] == 1) || (($item['uid'] == local_channel()) && (strlen($item['allow_cid']) || strlen($item['allow_gid']) + $lock = (((intval($item['item_private'])) || (($item['uid'] == local_channel()) && (strlen($item['allow_cid']) || strlen($item['allow_gid']) || strlen($item['deny_cid']) || strlen($item['deny_gid'])))) ? t('Private Message') : false); @@ -110,7 +110,7 @@ class ThreadItem { $shareable = true; $privacy_warning = false; - if(($item['item_private'] == 1) && ($item['owner']['xchan_network'] === 'activitypub')) { + if(intval($item['item_private']) && ($item['owner']['xchan_network'] === 'activitypub')) { $recips = get_iconfig($item['parent'], 'activitypub', 'recips'); if(! in_array($observer['xchan_url'], $recips['to'])) diff --git a/Zotlabs/Lib/ZotURL.php b/Zotlabs/Lib/ZotURL.php index bc14c516a..98d1febe5 100644 --- a/Zotlabs/Lib/ZotURL.php +++ b/Zotlabs/Lib/ZotURL.php @@ -2,7 +2,7 @@ namespace Zotlabs\Lib; -use Zotlabs\Zot6\HTTPSig; +use Zotlabs\Web\HTTPSig; class ZotURL { diff --git a/Zotlabs/Lib/Zotfinger.php b/Zotlabs/Lib/Zotfinger.php index d094fdc8d..2d2e6796b 100644 --- a/Zotlabs/Lib/Zotfinger.php +++ b/Zotlabs/Lib/Zotfinger.php @@ -2,7 +2,7 @@ namespace Zotlabs\Lib; -use Zotlabs\Zot6\HTTPSig; +use Zotlabs\Web\HTTPSig; class Zotfinger { diff --git a/Zotlabs/Module/Apschema.php b/Zotlabs/Module/Apschema.php index d217041f2..12cc0e00a 100644 --- a/Zotlabs/Module/Apschema.php +++ b/Zotlabs/Module/Apschema.php @@ -28,7 +28,8 @@ class Apschema extends \Zotlabs\Web\Controller { 'nomadicHubs' => 'zot:nomadicHubs', 'emojiReaction' => 'zot:emojiReaction', 'expires' => 'zot:expires', - + 'directMessage' => 'zot:directMessage', + 'magicEnv' => [ '@id' => 'zot:magicEnv', '@type' => '@id' @@ -40,8 +41,11 @@ class Apschema extends \Zotlabs\Web\Controller { ], 'ostatus' => 'http://ostatus.org#', - 'conversation' => 'ostatus:conversation' + 'conversation' => 'ostatus:conversation', + 'diaspora' => 'https://diasporafoundation.org/ns/', + 'guid' => 'diaspora:guid' + ] ]; diff --git a/Zotlabs/Module/Cal.php b/Zotlabs/Module/Cal.php index a84116e76..07bee38bd 100644 --- a/Zotlabs/Module/Cal.php +++ b/Zotlabs/Module/Cal.php @@ -161,12 +161,12 @@ class Cal extends Controller { 'end' => $end, 'drop' => $drop, 'allDay' => (($rr['adjust']) ? 0 : 1), - 'title' => htmlentities($rr['summary'], ENT_COMPAT, 'UTF-8', false), + 'title' => html_entity_decode($rr['summary'], ENT_COMPAT, 'UTF-8'), 'editable' => $edit ? true : false, 'item' => $rr, 'plink' => [$rr['plink'], t('Link to source')], - 'description' => htmlentities($rr['description'], ENT_COMPAT, 'UTF-8', false), - 'location' => htmlentities($rr['location'], ENT_COMPAT, 'UTF-8', false), + 'description' => html_entity_decode($rr['description'], ENT_COMPAT, 'UTF-8'), + 'location' => html_entity_decode($rr['location'], ENT_COMPAT, 'UTF-8'), 'allow_cid' => expand_acl($rr['allow_cid']), 'allow_gid' => expand_acl($rr['allow_gid']), 'deny_cid' => expand_acl($rr['deny_cid']), diff --git a/Zotlabs/Module/Cdav.php b/Zotlabs/Module/Cdav.php index de639e281..e2855d2b6 100644 --- a/Zotlabs/Module/Cdav.php +++ b/Zotlabs/Module/Cdav.php @@ -4,6 +4,7 @@ namespace Zotlabs\Module; use App; use Zotlabs\Lib\Apps; use Zotlabs\Web\Controller; +use Zotlabs\Web\HTTPSig; require_once('include/event.php'); @@ -41,7 +42,7 @@ class Cdav extends Controller { continue; } - $sigblock = \Zotlabs\Web\HTTPSig::parse_sigheader($_SERVER[$head]); + $sigblock = HTTPSig::parse_sigheader($_SERVER[$head]); if($sigblock) { $keyId = str_replace('acct:','',$sigblock['keyId']); if($keyId) { @@ -64,7 +65,7 @@ class Cdav extends Controller { continue; if($record) { - $verified = \Zotlabs\Web\HTTPSig::verify('',$record['channel']['channel_pubkey']); + $verified = HTTPSig::verify('',$record['channel']['channel_pubkey']); if(! ($verified && $verified['header_signed'] && $verified['header_valid'])) { $record = null; } @@ -277,11 +278,11 @@ class Cdav extends Controller { $allday = $_REQUEST['allday']; $title = $_REQUEST['title']; - $start = datetime_convert($tz, 'UTC', $_REQUEST['dtstart']); + $start = datetime_convert('UTC', 'UTC', $_REQUEST['dtstart']); $dtstart = new \DateTime($start); if($_REQUEST['dtend']) { - $end = datetime_convert($tz, 'UTC', $_REQUEST['dtend']); + $end = datetime_convert('UTC', 'UTC', $_REQUEST['dtend']); $dtend = new \DateTime($end); } $description = $_REQUEST['description']; @@ -368,10 +369,10 @@ class Cdav extends Controller { $uri = $_REQUEST['uri']; $title = $_REQUEST['title']; - $start = datetime_convert($tz, 'UTC', $_REQUEST['dtstart']); + $start = datetime_convert('UTC', 'UTC', $_REQUEST['dtstart']); $dtstart = new \DateTime($start); if($_REQUEST['dtend']) { - $end = datetime_convert($tz, 'UTC', $_REQUEST['dtend']); + $end = datetime_convert('UTC', 'UTC', $_REQUEST['dtend']); $dtend = new \DateTime($end); } $description = $_REQUEST['description']; @@ -441,10 +442,10 @@ class Cdav extends Controller { $allday = $_REQUEST['allday']; $uri = $_REQUEST['uri']; - $start = datetime_convert($tz, 'UTC', $_REQUEST['dtstart']); + $start = datetime_convert('UTC', 'UTC', $_REQUEST['dtstart']); $dtstart = new \DateTime($start); if($_REQUEST['dtend']) { - $end = datetime_convert($tz, 'UTC', $_REQUEST['dtend']); + $end = datetime_convert('UTC', 'UTC', $_REQUEST['dtend']); $dtend = new \DateTime($end); } diff --git a/Zotlabs/Module/Channel.php b/Zotlabs/Module/Channel.php index 144c2472a..b1639b213 100644 --- a/Zotlabs/Module/Channel.php +++ b/Zotlabs/Module/Channel.php @@ -6,7 +6,7 @@ namespace Zotlabs\Module; use App; use Zotlabs\Web\Controller; use Zotlabs\Lib\PermissionDescription; -use Zotlabs\Zot6\HTTPSig; +use Zotlabs\Web\HTTPSig; use Zotlabs\Lib\Libzot; require_once('include/items.php'); @@ -111,6 +111,17 @@ class Channel extends Controller { // we start loading content profile_load($which,$profile); + + App::$page['htmlhead'] .= '<meta property="og:title" content="' . htmlspecialchars($channel['channel_name']) . '">' . "\r\n"; + App::$page['htmlhead'] .= '<meta property="og:image" content="' . $channel['xchan_photo_l'] . '">' . "\r\n"; + + if(App::$profile['about'] && perm_is_allowed($channel['channel_id'],get_observer_hash(),'view_profile')) { + App::$page['htmlhead'] .= '<meta property="og:description" content="' . htmlspecialchars(App::$profile['about']) . '">' . "\r\n"; + } + else { + App::$page['htmlhead'] .= '<meta property="og:description" content="' . htmlspecialchars(sprintf( t('This is the home page of %s.'), $channel['channel_name'])) . '">' . "\r\n"; + } + } function get($update = 0, $load = false) { diff --git a/Zotlabs/Module/Channel_calendar.php b/Zotlabs/Module/Channel_calendar.php index ac08dfa96..7d75a7e41 100644 --- a/Zotlabs/Module/Channel_calendar.php +++ b/Zotlabs/Module/Channel_calendar.php @@ -21,7 +21,7 @@ class Channel_calendar extends \Zotlabs\Web\Controller { $event_hash = ((x($_POST,'event_hash')) ? $_POST['event_hash'] : ''); $xchan = ((x($_POST,'xchan')) ? dbesc($_POST['xchan']) : ''); - $uid = local_channel(); + $uid = local_channel(); // only allow editing your own events. if(($xchan) && ($xchan !== get_observer_hash())) @@ -34,8 +34,8 @@ class Channel_calendar extends \Zotlabs\Web\Controller { $adjust = intval($_POST['adjust']); - $start = (($adjust) ? datetime_convert($tz, 'UTC', escape_tags($_REQUEST['dtstart'])) : datetime_convert('UTC', 'UTC', escape_tags($_REQUEST['dtstart']))); - $finish = (($adjust) ? datetime_convert($tz, 'UTC', escape_tags($_REQUEST['dtend'])) : datetime_convert('UTC', 'UTC', escape_tags($_REQUEST['dtend']))); + $start = datetime_convert('UTC', 'UTC', escape_tags($_REQUEST['dtstart'])); + $finish = datetime_convert('UTC', 'UTC', escape_tags($_REQUEST['dtend'])); $summary = escape_tags(trim($_POST['summary'])); $desc = escape_tags(trim($_POST['desc'])); @@ -381,12 +381,12 @@ class Channel_calendar extends \Zotlabs\Web\Controller { 'end' => $end, 'drop' => $drop, 'allDay' => (($rr['adjust']) ? 0 : 1), - 'title' => htmlentities($rr['summary'], ENT_COMPAT, 'UTF-8', false), + 'title' => html_entity_decode($rr['summary'], ENT_COMPAT, 'UTF-8'), 'editable' => $edit ? true : false, 'item' => $rr, 'plink' => [$rr['plink'], t('Link to source')], - 'description' => htmlentities($rr['description'], ENT_COMPAT, 'UTF-8', false), - 'location' => htmlentities($rr['location'], ENT_COMPAT, 'UTF-8', false), + 'description' => html_entity_decode($rr['description'], ENT_COMPAT, 'UTF-8'), + 'location' => html_entity_decode($rr['location'], ENT_COMPAT, 'UTF-8'), 'allow_cid' => expand_acl($rr['allow_cid']), 'allow_gid' => expand_acl($rr['allow_gid']), 'deny_cid' => expand_acl($rr['deny_cid']), @@ -402,7 +402,7 @@ class Channel_calendar extends \Zotlabs\Web\Controller { echo ical_wrapper($r); killme(); } - + if (\App::$argv[1] === 'json'){ json_return_and_die($events); } diff --git a/Zotlabs/Module/Dav.php b/Zotlabs/Module/Dav.php index 9f64e2fea..866520461 100644 --- a/Zotlabs/Module/Dav.php +++ b/Zotlabs/Module/Dav.php @@ -8,8 +8,9 @@ namespace Zotlabs\Module; -use \Sabre\DAV as SDAV; -use \Zotlabs\Storage; +use Sabre\DAV as SDAV; +use Zotlabs\Storage; +use Zotlabs\Web\HTTPSig; require_once('include/attach.php'); require_once('include/auth.php'); @@ -46,7 +47,7 @@ class Dav extends \Zotlabs\Web\Controller { continue; } - $sigblock = \Zotlabs\Web\HTTPSig::parse_sigheader($_SERVER[$head]); + $sigblock = HTTPSig::parse_sigheader($_SERVER[$head]); if($sigblock) { $keyId = str_replace('acct:','',$sigblock['keyId']); if($keyId) { @@ -69,7 +70,7 @@ class Dav extends \Zotlabs\Web\Controller { continue; if($record) { - $verified = \Zotlabs\Web\HTTPSig::verify('',$record['channel']['channel_pubkey']); + $verified = HTTPSig::verify('',$record['channel']['channel_pubkey']); if(! ($verified && $verified['header_signed'] && $verified['header_valid'])) { $record = null; } diff --git a/Zotlabs/Module/Dirsearch.php b/Zotlabs/Module/Dirsearch.php index 26cb82044..92b33df0c 100644 --- a/Zotlabs/Module/Dirsearch.php +++ b/Zotlabs/Module/Dirsearch.php @@ -394,7 +394,7 @@ class Dirsearch extends \Zotlabs\Web\Controller { $quoted_string = false; } else - $curr['value'] .= ' ' . trim(q); + $curr['value'] .= ' ' . trim($q); } } } diff --git a/Zotlabs/Module/Events.php b/Zotlabs/Module/Events.php index dcdbfd233..681d6887d 100644 --- a/Zotlabs/Module/Events.php +++ b/Zotlabs/Module/Events.php @@ -668,9 +668,10 @@ class Events extends \Zotlabs\Web\Controller { 'html'=>$html, 'plink' => array($rr['plink'],t('Link to Source'),'',''), ); + } } - + if($export) { header('Content-type: text/calendar'); header('content-disposition: attachment; filename="' . t('calendar') . '-' . $channel['channel_address'] . '.ics"' ); diff --git a/Zotlabs/Module/Getfile.php b/Zotlabs/Module/Getfile.php index 583cf38f0..6d31d23fd 100644 --- a/Zotlabs/Module/Getfile.php +++ b/Zotlabs/Module/Getfile.php @@ -1,6 +1,8 @@ <?php namespace Zotlabs\Module; +use Zotlabs\Web\HTTPSig; + /** * module: getfile * @@ -46,7 +48,7 @@ class Getfile extends \Zotlabs\Web\Controller { continue; } - $sigblock = \Zotlabs\Web\HTTPSig::parse_sigheader($_SERVER[$head]); + $sigblock = HTTPSig::parse_sigheader($_SERVER[$head]); if($sigblock) { $keyId = $sigblock['keyId']; @@ -57,7 +59,7 @@ class Getfile extends \Zotlabs\Web\Controller { ); if($r) { $hubloc = $r[0]; - $verified = \Zotlabs\Web\HTTPSig::verify('',$hubloc['xchan_pubkey']); + $verified = HTTPSig::verify('',$hubloc['xchan_pubkey']); if($verified && $verified['header_signed'] && $verified['header_valid'] && $hash == $hubloc['hubloc_hash']) { $header_verified = true; } diff --git a/Zotlabs/Module/Id.php b/Zotlabs/Module/Id.php index 15abfa2a3..e08568d00 100644 --- a/Zotlabs/Module/Id.php +++ b/Zotlabs/Module/Id.php @@ -12,7 +12,7 @@ namespace Zotlabs\Module; use Zotlabs\Lib\Activity; use Zotlabs\Lib\ActivityStreams; use Zotlabs\Lib\LDSignatures; -use Zotlabs\Zot6\HTTPSig; +use Zotlabs\Web\HTTPSig; use Zotlabs\Web\Controller; use Zotlabs\Lib\Libzot; use Zotlabs\Lib\ThreadListener; diff --git a/Zotlabs/Module/Item.php b/Zotlabs/Module/Item.php index 965cbf173..d03b6ee30 100644 --- a/Zotlabs/Module/Item.php +++ b/Zotlabs/Module/Item.php @@ -9,7 +9,7 @@ use Zotlabs\Daemon\Master; use Zotlabs\Lib\Activity; use Zotlabs\Lib\ActivityStreams; use Zotlabs\Lib\LDSignatures; -use Zotlabs\Zot6\HTTPSig; +use Zotlabs\Web\HTTPSig; use Zotlabs\Lib\Libzot; use Zotlabs\Lib\ThreadListener; use App; @@ -193,6 +193,25 @@ class Item extends Controller { killme(); } + + if(argc() > 1 && argv(1) !== 'drop') { + $x = q("select uid, item_wall, llink, mid from item where mid = '%s' ", + dbesc(z_root() . '/item/' . argv(1)) + ); + if($x) { + foreach($x as $xv) { + if (intval($xv['item_wall'])) { + $c = channelx_by_n($xv['uid']); + if ($c) { + goaway($c['xchan_url'] . '?mid=' . gen_link_id($xv['mid'])); + } + } + } + goaway($x[0]['llink']); + } + http_status_exit(404, 'Not found'); + } + } @@ -551,10 +570,10 @@ class Item extends Controller { $public_policy = $orig_post['public_policy']; $private = $orig_post['item_private']; } - - if($private || $public_policy || $acl->is_private()) - $private = 1; - + + if($public_policy || $acl->is_private()) { + $private = (($private) ? $private : 1); + } $location = $orig_post['location']; $coord = $orig_post['coord']; @@ -631,12 +650,11 @@ class Item extends Controller { $allow_empty = ((array_key_exists('allow_empty',$_REQUEST)) ? intval($_REQUEST['allow_empty']) : 0); - $private = intval($acl->is_private() || ($public_policy)); + $private = (($private) ? $private : intval($acl->is_private() || ($public_policy))); // If this is a comment, set the permissions from the parent. if($parent_item) { - $private = 0; $acl->set($parent_item); $private = intval($acl->is_private() || $parent_item['item_private']); $public_policy = $parent_item['public_policy']; @@ -742,7 +760,12 @@ class Item extends Controller { } } } - + + if(($str_contact_allow) && (! $str_group_allow)) { + // direct message - private between individual channels but not groups + $private = 2; + } + /** * diff --git a/Zotlabs/Module/Lockview.php b/Zotlabs/Module/Lockview.php index d7ed07a53..8c8519c57 100644 --- a/Zotlabs/Module/Lockview.php +++ b/Zotlabs/Module/Lockview.php @@ -76,7 +76,7 @@ class Lockview extends \Zotlabs\Web\Controller { killme(); } - if(($item['item_private'] == 1) && (! strlen($item['allow_cid'])) && (! strlen($item['allow_gid'])) + if(intval($item['item_private']) && (! strlen($item['allow_cid'])) && (! strlen($item['allow_gid'])) && (! strlen($item['deny_cid'])) && (! strlen($item['deny_gid']))) { // if the post is private, but public_policy is blank ("visible to the internet"), and there aren't any diff --git a/Zotlabs/Module/Magic.php b/Zotlabs/Module/Magic.php index e8e960574..6ac656a04 100644 --- a/Zotlabs/Module/Magic.php +++ b/Zotlabs/Module/Magic.php @@ -1,6 +1,8 @@ <?php namespace Zotlabs\Module; +use Zotlabs\Web\HTTPSig; + @require_once('include/zot.php'); @@ -152,10 +154,9 @@ class Magic extends \Zotlabs\Web\Controller { $headers['Accept'] = 'application/x-zot+json' ; $headers['X-Open-Web-Auth'] = random_string(); $headers['Host'] = $parsed['host']; - $headers['Digest'] = 'SHA-256=' . \Zotlabs\Web\HTTPSig::generate_digest($data,false); + $headers['Digest'] = HTTPSig::generate_digest_header($data); - $headers = \Zotlabs\Web\HTTPSig::create_sig('',$headers,$channel['channel_prvkey'], - 'acct:' . $channel['channel_address'] . '@' . \App::get_hostname(),false,true,'sha512'); + $headers = HTTPSig::create_sig($headers,$channel['channel_prvkey'], 'acct:' . channel_reddress($channel),true,'sha512'); $x = z_post_url($basepath . '/owa',$data,$redirects,[ 'headers' => $headers ]); if($x['success']) { diff --git a/Zotlabs/Module/Owa.php b/Zotlabs/Module/Owa.php index cf116a96c..89f83bf8f 100644 --- a/Zotlabs/Module/Owa.php +++ b/Zotlabs/Module/Owa.php @@ -2,6 +2,8 @@ namespace Zotlabs\Module; +use Zotlabs\Web\HTTPSig; + /** * OpenWebAuth verifier and token generator * See https://macgirvin.com/wiki/mike/OpenWebAuth/Home @@ -25,7 +27,7 @@ class Owa extends \Zotlabs\Web\Controller { continue; } - $sigblock = \Zotlabs\Web\HTTPSig::parse_sigheader($_SERVER[$head]); + $sigblock = HTTPSig::parse_sigheader($_SERVER[$head]); if($sigblock) { $keyId = $sigblock['keyId']; @@ -65,7 +67,7 @@ class Owa extends \Zotlabs\Web\Controller { if ($r) { foreach($r as $hubloc) { - $verified = \Zotlabs\Web\HTTPSig::verify(file_get_contents('php://input'),$hubloc['xchan_pubkey']); + $verified = HTTPSig::verify(file_get_contents('php://input'),$hubloc['xchan_pubkey']); if($verified && $verified['header_signed'] && $verified['header_valid']) { logger('OWA header: ' . print_r($verified,true),LOGGER_DATA); logger('OWA success: ' . $hubloc['hubloc_addr'],LOGGER_DATA); diff --git a/Zotlabs/Module/Ping.php b/Zotlabs/Module/Ping.php index 3dabe0f7b..6e8042eaf 100644 --- a/Zotlabs/Module/Ping.php +++ b/Zotlabs/Module/Ping.php @@ -282,8 +282,8 @@ class Ping extends \Zotlabs\Web\Controller { if(strpos($message, $tt['xname']) === 0) $message = substr($message, strlen($tt['xname']) + 1); - $mid = basename($tt['link']); + $mid = ((strpos($mid, 'b64.') === 0) ? @base64url_decode(substr($mid, 4)) : $mid); if(in_array($tt['verb'], [ACTIVITY_LIKE, ACTIVITY_DISLIKE])) { // we need the thread parent @@ -291,7 +291,6 @@ class Ping extends \Zotlabs\Web\Controller { dbesc($mid), intval(local_channel()) ); - $b64mid = ((strpos($r[0]['thr_parent'], 'b64.') === 0) ? $r[0]['thr_parent'] : 'b64.' . base64url_encode($r[0]['thr_parent'])); } else { diff --git a/Zotlabs/Module/Zfinger.php b/Zotlabs/Module/Zfinger.php index 6ed001df5..3a20144a5 100644 --- a/Zotlabs/Module/Zfinger.php +++ b/Zotlabs/Module/Zfinger.php @@ -1,6 +1,7 @@ <?php namespace Zotlabs\Module; +use Zotlabs\Web\HTTPSig; class Zfinger extends \Zotlabs\Web\Controller { @@ -23,10 +24,9 @@ class Zfinger extends \Zotlabs\Web\Controller { $ret = json_encode($x); if($chan) { - $hash = \Zotlabs\Web\HTTPSig::generate_digest($ret,false); - $headers['Digest'] = 'SHA-256=' . $hash; - \Zotlabs\Web\HTTPSig::create_sig('',$headers,$chan['channel_prvkey'], - 'acct:' . $chan['channel_address'] . '@' . \App::get_hostname(),true); + $headers['Digest'] = HTTPSig::generate_digest_header($ret); + $h = HTTPSig::create_sig($headers,$chan['channel_prvkey'],'acct:' . channel_reddress($chan)); + HTTPSig::set_headers($h); } else { foreach($headers as $k => $v) { diff --git a/Zotlabs/Module/Zot_probe.php b/Zotlabs/Module/Zot_probe.php index d0c7e688f..648ed2175 100644 --- a/Zotlabs/Module/Zot_probe.php +++ b/Zotlabs/Module/Zot_probe.php @@ -3,7 +3,7 @@ namespace Zotlabs\Module; use Zotlabs\Lib\Zotfinger; -use Zotlabs\Zot6\HTTPSig; +use Zotlabs\Web\HTTPSig; class Zot_probe extends \Zotlabs\Web\Controller { diff --git a/Zotlabs/Web/HTTPSig.php b/Zotlabs/Web/HTTPSig.php index fe0b9428f..3d050fd9b 100644 --- a/Zotlabs/Web/HTTPSig.php +++ b/Zotlabs/Web/HTTPSig.php @@ -2,11 +2,17 @@ namespace Zotlabs\Web; +use Zotlabs\Lib\ActivityStreams; +use Zotlabs\Lib\Webfinger; +use Zotlabs\Web\HTTPHeaders; +use Zotlabs\Lib\Libzot; + /** - * @brief Implements HTTP Signatures per draft-cavage-http-signatures-07. + * @brief Implements HTTP Signatures per draft-cavage-http-signatures-10. * - * @see https://tools.ietf.org/html/draft-cavage-http-signatures-07 + * @see https://tools.ietf.org/html/draft-cavage-http-signatures-10 */ + class HTTPSig { /** @@ -15,41 +21,32 @@ class HTTPSig { * @see https://tools.ietf.org/html/rfc5843 * * @param string $body The value to create the digest for - * @param boolean $set (optional, default true) - * If set send a Digest HTTP header - * @return string The generated digest of $body + * @param string $alg hash algorithm (one of 'sha256','sha512') + * @return string The generated digest header string for $body */ - static function generate_digest($body, $set = true) { - $digest = base64_encode(hash('sha256', $body, true)); - if($set) { - header('Digest: SHA-256=' . $digest); + static function generate_digest_header($body,$alg = 'sha256') { + + $digest = base64_encode(hash($alg, $body, true)); + switch($alg) { + case 'sha512': + return 'SHA-512=' . $digest; + case 'sha256': + default: + return 'SHA-256=' . $digest; + break; } - return $digest; } - // See draft-cavage-http-signatures-08 - - static function verify($data,$key = '') { - - $body = $data; - $headers = null; - $spoofable = false; - - $result = [ - 'signer' => '', - 'header_signed' => false, - 'header_valid' => false, - 'content_signed' => false, - 'content_valid' => false - ]; + static function find_headers($data,&$body) { // decide if $data arrived via controller submission or curl + if(is_array($data) && $data['header']) { if(! $data['success']) - return $result; + return []; - $h = new \Zotlabs\Web\HTTPHeaders($data['header']); + $h = new HTTPHeaders($data['header']); $headers = $h->fetcharr(); $body = $data['body']; $headers['(request-target)'] = $data['request_target']; @@ -57,9 +54,7 @@ class HTTPSig { else { $headers = []; - $headers['(request-target)'] = - strtolower($_SERVER['REQUEST_METHOD']) . ' ' . - $_SERVER['REQUEST_URI']; + $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI']; $headers['content-type'] = $_SERVER['CONTENT_TYPE']; $headers['content-length'] = $_SERVER['CONTENT_LENGTH']; @@ -71,9 +66,35 @@ class HTTPSig { } } - // logger('SERVER: ' . print_r($_SERVER,true), LOGGER_ALL); + //logger('SERVER: ' . print_r($_SERVER,true), LOGGER_ALL); + + //logger('headers: ' . print_r($headers,true), LOGGER_ALL); + + return $headers; + } + + + // See draft-cavage-http-signatures-10 + + static function verify($data,$key = '') { + + $body = $data; + $headers = null; + + $result = [ + 'signer' => '', + 'portable_id' => '', + 'header_signed' => false, + 'header_valid' => false, + 'content_signed' => false, + 'content_valid' => false + ]; + + + $headers = self::find_headers($data,$body); - // logger('headers: ' . print_r($headers,true), LOGGER_ALL); + if(! $headers) + return $result; $sig_block = null; @@ -85,7 +106,7 @@ class HTTPSig { } if(! $sig_block) { - logger('no signature provided.'); + logger('no signature provided.', LOGGER_DEBUG); return $result; } @@ -103,9 +124,6 @@ class HTTPSig { if(array_key_exists($h,$headers)) { $signed_data .= $h . ': ' . $headers[$h] . "\n"; } - if(strpos($h,'.')) { - $spoofable = true; - } if($h === 'date') { $d = new \DateTime($headers[$h]); $d->setTimeZone(new \DateTimeZone('UTC')); @@ -128,63 +146,89 @@ class HTTPSig { $algorithm = 'sha512'; } - if($key && function_exists($key)) { - $result['signer'] = $sig_block['keyId']; - $key = $key($sig_block['keyId']); - } + if(! array_key_exists('keyId',$sig_block)) + return $result; - if(! $key) { - $result['signer'] = $sig_block['keyId']; - $key = self::get_activitypub_key($sig_block['keyId']); - } + $result['signer'] = $sig_block['keyId']; - if(! $key) + $key = self::get_key($key,$result['signer']); + + if(! ($key && $key['public_key'])) { return $result; + } - $x = rsa_verify($signed_data,$sig_block['signature'],$key,$algorithm); + $x = rsa_verify($signed_data,$sig_block['signature'],$key['public_key'],$algorithm); logger('verified: ' . $x, LOGGER_DEBUG); - if(! $x) + if(! $x) { + logger('verify failed for ' . $result['signer'] . ' alg=' . $algorithm . (($key['public_key']) ? '' : ' no key')); + $sig_block['signature'] = base64_encode($sig_block['signature']); + logger('affected sigblock: ' . print_r($sig_block,true)); + logger('signed_data: ' . print_r($signed_data,true)); + logger('headers: ' . print_r($headers,true)); + logger('server: ' . print_r($_SERVER,true)); return $result; + } - if(! $spoofable) - $result['header_valid'] = true; + $result['portable_id'] = $key['portable_id']; + $result['header_valid'] = true; if(in_array('digest',$signed_headers)) { $result['content_signed'] = true; - $digest = explode('=', $headers['digest']); + $digest = explode('=', $headers['digest'], 2); if($digest[0] === 'SHA-256') $hashalg = 'sha256'; if($digest[0] === 'SHA-512') $hashalg = 'sha512'; - // The explode operation will have stripped the '=' padding, so compare against unpadded base64 - if(rtrim(base64_encode(hash($hashalg,$body,true)),'=') === $digest[1]) { + if(base64_encode(hash($hashalg,$body,true)) === $digest[1]) { $result['content_valid'] = true; } + + logger('Content_Valid: ' . (($result['content_valid']) ? 'true' : 'false')); } + return $result; + } - if(in_array('x-zot-digest',$signed_headers)) { - $result['content_signed'] = true; - $digest = explode('=', $headers['x-zot-digest']); - if($digest[0] === 'SHA-256') - $hashalg = 'sha256'; - if($digest[0] === 'SHA-512') - $hashalg = 'sha512'; + static function get_key($key,$id) { - // The explode operation will have stripped the '=' padding, so compare against unpadded base64 - if(rtrim(base64_encode(hash($hashalg,$_POST['data'],true)),'=') === $digest[1]) { - $result['content_valid'] = true; + if($key) { + if(function_exists($key)) { + return $key($id); } + return [ 'public_key' => $key ]; } - logger('Content_Valid: ' . (($result['content_valid']) ? 'true' : 'false')); + if(strpos($id,'#') === false) { + $key = self::get_webfinger_key($id); + } + + if(! $key) { + $key = self::get_activitystreams_key($id); + } + + return $key; + + } + + + function convertKey($key) { + + if(strstr($key,'RSA ')) { + return rsatopem($key); + } + elseif(substr($key,0,5) === 'data:') { + return convert_salmon_key($key); + } + else { + return $key; + } - return $result; } + /** * @brief * @@ -192,57 +236,131 @@ class HTTPSig { * @return boolean|string * false if no pub key found, otherwise return the pub key */ - function get_activitypub_key($id) { - if(strpos($id,'acct:') === 0) { - $x = q("select xchan_pubkey from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_addr = '%s' limit 1", - dbesc(str_replace('acct:','',$id)) - ); + function get_activitystreams_key($id) { + + // remove fragment + + $url = ((strpos($id,'#')) ? substr($id,0,strpos($id,'#')) : $id); + + $x = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_addr = '%s' or hubloc_id_url = '%s' limit 1", + dbesc(str_replace('acct:','',$url)), + dbesc($url) + ); + + if($x && $x[0]['xchan_pubkey']) { + return [ 'portable_id' => $x[0]['xchan_hash'], 'public_key' => $x[0]['xchan_pubkey'] , 'hubloc' => $x[0] ]; } - else { - $x = q("select xchan_pubkey from xchan where xchan_hash = '%s' and xchan_network = 'activitypub' ", - dbesc($id) - ); + + $r = ActivityStreams::fetch($id); + + if($r) { + if(array_key_exists('publicKey',$r) && array_key_exists('publicKeyPem',$r['publicKey']) && array_key_exists('id',$r['publicKey'])) { + if($r['publicKey']['id'] === $id || $r['id'] === $id) { + $portable_id = ((array_key_exists('owner',$r['publicKey'])) ? $r['publicKey']['owner'] : EMPTY_STR); + return [ 'public_key' => self::convertKey($r['publicKey']['publicKeyPem']), 'portable_id' => $portable_id, 'hubloc' => [] ]; + } + } } + return false; + } + + + function get_webfinger_key($id) { + + $x = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_addr = '%s' or hubloc_id_url = '%s' limit 1", + dbesc(str_replace('acct:','',$id)), + dbesc($id) + ); if($x && $x[0]['xchan_pubkey']) { - return ($x[0]['xchan_pubkey']); + return [ 'portable_id' => $x[0]['xchan_hash'], 'public_key' => $x[0]['xchan_pubkey'] , 'hubloc' => $x[0] ]; } - if(function_exists('as_fetch')) - $r = as_fetch($id); + $wf = Webfinger::exec($id); + $key = [ 'portable_id' => '', 'public_key' => '', 'hubloc' => [] ]; - if($r) { - $j = json_decode($r,true); + if($wf) { + if(array_key_exists('properties',$wf) && array_key_exists('https://w3id.org/security/v1#publicKeyPem',$wf['properties'])) { + $key['public_key'] = self::convertKey($wf['properties']['https://w3id.org/security/v1#publicKeyPem']); + } + if(array_key_exists('links', $wf) && is_array($wf['links'])) { + foreach($wf['links'] as $l) { + if(! (is_array($l) && array_key_exists('rel',$l))) { + continue; + } + if($l['rel'] === 'magic-public-key' && array_key_exists('href',$l) && $key['public_key'] === EMPTY_STR) { + $key['public_key'] = self::convertKey($l['href']); + } + } + } + } + + return (($key['public_key']) ? $key : false); + } + + + function get_zotfinger_key($id) { + + $x = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_addr = '%s' or hubloc_id_url = '%s' limit 1", + dbesc(str_replace('acct:','',$id)), + dbesc($id) + ); + if($x && $x[0]['xchan_pubkey']) { + return [ 'portable_id' => $x[0]['xchan_hash'], 'public_key' => $x[0]['xchan_pubkey'] , 'hubloc' => $x[0] ]; + } - if(array_key_exists('publicKey',$j) && array_key_exists('publicKeyPem',$j['publicKey'])) { - if((array_key_exists('id',$j['publicKey']) && $j['publicKey']['id'] !== $id) && $j['id'] !== $id) - return false; + $wf = Webfinger::exec($id); + $key = [ 'portable_id' => '', 'public_key' => '', 'hubloc' => [] ]; - return($j['publicKey']['publicKeyPem']); + if($wf) { + if(array_key_exists('properties',$wf) && array_key_exists('https://w3id.org/security/v1#publicKeyPem',$wf['properties'])) { + $key['public_key'] = self::convertKey($wf['properties']['https://w3id.org/security/v1#publicKeyPem']); + } + if(array_key_exists('links', $wf) && is_array($wf['links'])) { + foreach($wf['links'] as $l) { + if(! (is_array($l) && array_key_exists('rel',$l))) { + continue; + } + if($l['rel'] === 'http://purl.org/zot/protocol/6.0' && array_key_exists('href',$l) && $l['href'] !== EMPTY_STR) { + $z = \Zotlabs\Lib\Zotfinger::exec($l['href']); + if($z) { + $i = Libzot::import_xchan($z['data']); + if($i['success']) { + $key['portable_id'] = $i['hash']; + + $x = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_id_url = '%s' limit 1", + dbesc($l['href']) + ); + if($x) { + $key['hubloc'] = $x[0]; + } + } + } + } + if($l['rel'] === 'magic-public-key' && array_key_exists('href',$l) && $key['public_key'] === EMPTY_STR) { + $key['public_key'] = self::convertKey($l['href']); + } + } } } - return false; + return (($key['public_key']) ? $key : false); } + /** * @brief * - * @param string $request * @param array $head * @param string $prvkey - * @param string $keyid (optional, default 'Key') - * @param boolean $send_headers (optional, default false) - * If set send a HTTP header + * @param string $keyid (optional, default '') * @param boolean $auth (optional, default false) * @param string $alg (optional, default 'sha256') - * @param string $crypt_key (optional, default null) - * @param string $crypt_algo (optional, default 'aes256ctr') + * @param array $encryption [ 'key', 'algorithm' ] or false * @return array */ - static function create_sig($request, $head, $prvkey, $keyid = 'Key', $send_headers = false, $auth = false, - $alg = 'sha256', $crypt_key = null, $crypt_algo = 'aes256ctr') { + static function create_sig($head, $prvkey, $keyid = EMPTY_STR, $auth = false, $alg = 'sha256', $encryption = false ) { $return_headers = []; @@ -253,14 +371,15 @@ class HTTPSig { $algorithm = 'rsa-sha512'; } - $x = self::sign($request,$head,$prvkey,$alg); + $x = self::sign($head,$prvkey,$alg); - $headerval = 'keyId="' . $keyid . '",algorithm="' . $algorithm - . '",headers="' . $x['headers'] . '",signature="' . $x['signature'] . '"'; + $headerval = 'keyId="' . $keyid . '",algorithm="' . $algorithm . '",headers="' . $x['headers'] . '",signature="' . $x['signature'] . '"'; - if($crypt_key) { - $x = crypto_encapsulate($headerval,$crypt_key,$crypt_algo); - $headerval = 'iv="' . $x['iv'] . '",key="' . $x['key'] . '",alg="' . $x['alg'] . '",data="' . $x['data'] . '"'; + if($encryption) { + $x = crypto_encapsulate($headerval,$encryption['key'],$encryption['algorithm']); + if(is_array($x)) { + $headerval = 'iv="' . $x['iv'] . '",key="' . $x['key'] . '",alg="' . $x['alg'] . '",data="' . $x['data'] . '"'; + } } if($auth) { @@ -272,43 +391,52 @@ class HTTPSig { if($head) { foreach($head as $k => $v) { - if($send_headers) { - header($k . ': ' . $v); - } - else { - $return_headers[] = $k . ': ' . $v; + // strip the request-target virtual header from the output headers + if($k === '(request-target)') { + continue; } + $return_headers[] = $k . ': ' . $v; } } - if($send_headers) { - header($sighead); - } - else { - $return_headers[] = $sighead; - } + $return_headers[] = $sighead; return $return_headers; } /** + * @brief set headers + * + * @param array $headers + * @return void + */ + + + static function set_headers($headers) { + if($headers && is_array($headers)) { + foreach($headers as $h) { + header($h); + } + } + } + + + /** * @brief * - * @param string $request * @param array $head * @param string $prvkey * @param string $alg (optional) default 'sha256' * @return array */ - static function sign($request, $head, $prvkey, $alg = 'sha256') { + + static function sign($head, $prvkey, $alg = 'sha256') { $ret = []; $headers = ''; $fields = ''; - if($request) { - $headers = '(request-target)' . ': ' . trim($request) . "\n"; - $fields = '(request-target)'; - } + + logger('signing: ' . print_r($head,true), LOGGER_DATA); if($head) { foreach($head as $k => $v) { @@ -340,11 +468,8 @@ class HTTPSig { * - \e array \b headers * - \e string \b signature */ - static function parse_sigheader($header) { - if(is_array($header)) { - btlogger('is_array: ' . print_r($header,true)); - } + static function parse_sigheader($header) { $ret = []; $matches = []; @@ -381,6 +506,7 @@ class HTTPSig { * - \e string \b alg * - \e string \b data */ + static function decrypt_sigheader($header, $prvkey = null) { $iv = $key = $alg = $data = null; diff --git a/Zotlabs/Zot/Finger.php b/Zotlabs/Zot/Finger.php index cb38c7f2b..778b701cd 100644 --- a/Zotlabs/Zot/Finger.php +++ b/Zotlabs/Zot/Finger.php @@ -2,6 +2,8 @@ namespace Zotlabs\Zot; +use Zotlabs\Web\HTTPSig; + /** * @brief Finger * @@ -95,8 +97,7 @@ class Finger { $headers['X-Zot-Nonce'] = random_string(); $headers['Host'] = $parsed_host; - $xhead = \Zotlabs\Web\HTTPSig::create_sig('',$headers,$channel['channel_prvkey'], - 'acct:' . $channel['channel_address'] . '@' . \App::get_hostname(),false); + $xhead = HTTPSig::create_sig($headers,$channel['channel_prvkey'],'acct:' . channel_reddress($channel)); $retries = 0; @@ -129,7 +130,7 @@ class Finger { $x = json_decode($result['body'], true); - $verify = \Zotlabs\Web\HTTPSig::verify($result,(($x) ? $x['key'] : '')); + $verify = 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); diff --git a/Zotlabs/Zot6/Finger.php b/Zotlabs/Zot6/Finger.php index f1fe41352..22ce4685d 100644 --- a/Zotlabs/Zot6/Finger.php +++ b/Zotlabs/Zot6/Finger.php @@ -88,8 +88,7 @@ class Finger { $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); + $xhead = HTTPSig::create_sig($headers,$channel['channel_prvkey'],'acct:' . channel_reddress($channel)); $retries = 0; @@ -122,7 +121,7 @@ class Finger { $x = json_decode($result['body'], true); - $verify = \Zotlabs\Web\HTTPSig::verify($result,(($x) ? $x['key'] : '')); + $verify = 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); diff --git a/Zotlabs/Zot6/HTTPSig.php b/Zotlabs/Zot6/HTTPSig.php deleted file mode 100644 index d3a09b858..000000000 --- a/Zotlabs/Zot6/HTTPSig.php +++ /dev/null @@ -1,536 +0,0 @@ -<?php - -namespace Zotlabs\Zot6; - -use Zotlabs\Lib\ActivityStreams; -use Zotlabs\Lib\Webfinger; -use Zotlabs\Web\HTTPHeaders; -use Zotlabs\Lib\Libzot; - -/** - * @brief Implements HTTP Signatures per draft-cavage-http-signatures-10. - * - * @see https://tools.ietf.org/html/draft-cavage-http-signatures-10 - */ - -class HTTPSig { - - /** - * @brief RFC5843 - * - * @see https://tools.ietf.org/html/rfc5843 - * - * @param string $body The value to create the digest for - * @param string $alg hash algorithm (one of 'sha256','sha512') - * @return string The generated digest header string for $body - */ - - static function generate_digest_header($body,$alg = 'sha256') { - - $digest = base64_encode(hash($alg, $body, true)); - switch($alg) { - case 'sha512': - return 'SHA-512=' . $digest; - case 'sha256': - default: - return 'SHA-256=' . $digest; - break; - } - } - - static function find_headers($data,&$body) { - - // decide if $data arrived via controller submission or curl - - if(is_array($data) && $data['header']) { - if(! $data['success']) - return []; - - $h = new HTTPHeaders($data['header']); - $headers = $h->fetcharr(); - $body = $data['body']; - $headers['(request-target)'] = $data['request_target']; - } - - else { - $headers = []; - $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI']; - $headers['content-type'] = $_SERVER['CONTENT_TYPE']; - $headers['content-length'] = $_SERVER['CONTENT_LENGTH']; - - foreach($_SERVER as $k => $v) { - if(strpos($k,'HTTP_') === 0) { - $field = str_replace('_','-',strtolower(substr($k,5))); - $headers[$field] = $v; - } - } - } - - //logger('SERVER: ' . print_r($_SERVER,true), LOGGER_ALL); - - //logger('headers: ' . print_r($headers,true), LOGGER_ALL); - - return $headers; - } - - - // See draft-cavage-http-signatures-10 - - static function verify($data,$key = '') { - - $body = $data; - $headers = null; - - $result = [ - 'signer' => '', - 'portable_id' => '', - 'header_signed' => false, - 'header_valid' => false, - 'content_signed' => false, - 'content_valid' => false - ]; - - - $headers = self::find_headers($data,$body); - - if(! $headers) - return $result; - - $sig_block = null; - - if(array_key_exists('signature',$headers)) { - $sig_block = self::parse_sigheader($headers['signature']); - } - elseif(array_key_exists('authorization',$headers)) { - $sig_block = self::parse_sigheader($headers['authorization']); - } - - if(! $sig_block) { - logger('no signature provided.', LOGGER_DEBUG); - return $result; - } - - // Warning: This log statement includes binary data - // logger('sig_block: ' . print_r($sig_block,true), LOGGER_DATA); - - $result['header_signed'] = true; - - $signed_headers = $sig_block['headers']; - if(! $signed_headers) - $signed_headers = [ 'date' ]; - - $signed_data = ''; - foreach($signed_headers as $h) { - if(array_key_exists($h,$headers)) { - $signed_data .= $h . ': ' . $headers[$h] . "\n"; - } - if($h === 'date') { - $d = new \DateTime($headers[$h]); - $d->setTimeZone(new \DateTimeZone('UTC')); - $dplus = datetime_convert('UTC','UTC','now + 1 day'); - $dminus = datetime_convert('UTC','UTC','now - 1 day'); - $c = $d->format('Y-m-d H:i:s'); - if($c > $dplus || $c < $dminus) { - logger('bad time: ' . $c); - return $result; - } - } - } - $signed_data = rtrim($signed_data,"\n"); - - $algorithm = null; - if($sig_block['algorithm'] === 'rsa-sha256') { - $algorithm = 'sha256'; - } - if($sig_block['algorithm'] === 'rsa-sha512') { - $algorithm = 'sha512'; - } - - if(! array_key_exists('keyId',$sig_block)) - return $result; - - $result['signer'] = $sig_block['keyId']; - - $key = self::get_key($key,$result['signer']); - - if(! ($key && $key['public_key'])) { - return $result; - } - - $x = rsa_verify($signed_data,$sig_block['signature'],$key['public_key'],$algorithm); - - logger('verified: ' . $x, LOGGER_DEBUG); - - if(! $x) { - logger('verify failed for ' . $result['signer'] . ' alg=' . $algorithm . (($key['public_key']) ? '' : ' no key')); - $sig_block['signature'] = base64_encode($sig_block['signature']); - logger('affected sigblock: ' . print_r($sig_block,true)); - logger('signed_data: ' . print_r($signed_data,true)); - logger('headers: ' . print_r($headers,true)); - logger('server: ' . print_r($_SERVER,true)); - return $result; - } - - $result['portable_id'] = $key['portable_id']; - $result['header_valid'] = true; - - if(in_array('digest',$signed_headers)) { - $result['content_signed'] = true; - $digest = explode('=', $headers['digest'], 2); - if($digest[0] === 'SHA-256') - $hashalg = 'sha256'; - if($digest[0] === 'SHA-512') - $hashalg = 'sha512'; - - if(base64_encode(hash($hashalg,$body,true)) === $digest[1]) { - $result['content_valid'] = true; - } - - logger('Content_Valid: ' . (($result['content_valid']) ? 'true' : 'false')); - } - - return $result; - } - - static function get_key($key,$id) { - - if($key) { - if(function_exists($key)) { - return $key($id); - } - return [ 'public_key' => $key ]; - } - - if(strpos($id,'#') === false) { - $key = self::get_webfinger_key($id); - } - - if(! $key) { - $key = self::get_activitystreams_key($id); - } - - return $key; - - } - - - function convertKey($key) { - - if(strstr($key,'RSA ')) { - return rsatopem($key); - } - elseif(substr($key,0,5) === 'data:') { - return convert_salmon_key($key); - } - else { - return $key; - } - - } - - - /** - * @brief - * - * @param string $id - * @return boolean|string - * false if no pub key found, otherwise return the pub key - */ - - function get_activitystreams_key($id) { - - // remove fragment - - $url = ((strpos($id,'#')) ? substr($id,0,strpos($id,'#')) : $id); - - $x = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_addr = '%s' or hubloc_id_url = '%s' limit 1", - dbesc(str_replace('acct:','',$url)), - dbesc($url) - ); - - if($x && $x[0]['xchan_pubkey']) { - return [ 'portable_id' => $x[0]['xchan_hash'], 'public_key' => $x[0]['xchan_pubkey'] , 'hubloc' => $x[0] ]; - } - - $r = ActivityStreams::fetch($id); - - if($r) { - if(array_key_exists('publicKey',$r) && array_key_exists('publicKeyPem',$r['publicKey']) && array_key_exists('id',$r['publicKey'])) { - if($r['publicKey']['id'] === $id || $r['id'] === $id) { - $portable_id = ((array_key_exists('owner',$r['publicKey'])) ? $r['publicKey']['owner'] : EMPTY_STR); - return [ 'public_key' => self::convertKey($r['publicKey']['publicKeyPem']), 'portable_id' => $portable_id, 'hubloc' => [] ]; - } - } - } - return false; - } - - - function get_webfinger_key($id) { - - $x = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_addr = '%s' or hubloc_id_url = '%s' limit 1", - dbesc(str_replace('acct:','',$id)), - dbesc($id) - ); - - if($x && $x[0]['xchan_pubkey']) { - return [ 'portable_id' => $x[0]['xchan_hash'], 'public_key' => $x[0]['xchan_pubkey'] , 'hubloc' => $x[0] ]; - } - - $wf = Webfinger::exec($id); - $key = [ 'portable_id' => '', 'public_key' => '', 'hubloc' => [] ]; - - if($wf) { - if(array_key_exists('properties',$wf) && array_key_exists('https://w3id.org/security/v1#publicKeyPem',$wf['properties'])) { - $key['public_key'] = self::convertKey($wf['properties']['https://w3id.org/security/v1#publicKeyPem']); - } - if(array_key_exists('links', $wf) && is_array($wf['links'])) { - foreach($wf['links'] as $l) { - if(! (is_array($l) && array_key_exists('rel',$l))) { - continue; - } - if($l['rel'] === 'magic-public-key' && array_key_exists('href',$l) && $key['public_key'] === EMPTY_STR) { - $key['public_key'] = self::convertKey($l['href']); - } - } - } - } - - return (($key['public_key']) ? $key : false); - } - - - function get_zotfinger_key($id) { - - $x = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_addr = '%s' or hubloc_id_url = '%s' limit 1", - dbesc(str_replace('acct:','',$id)), - dbesc($id) - ); - if($x && $x[0]['xchan_pubkey']) { - return [ 'portable_id' => $x[0]['xchan_hash'], 'public_key' => $x[0]['xchan_pubkey'] , 'hubloc' => $x[0] ]; - } - - $wf = Webfinger::exec($id); - $key = [ 'portable_id' => '', 'public_key' => '', 'hubloc' => [] ]; - - if($wf) { - if(array_key_exists('properties',$wf) && array_key_exists('https://w3id.org/security/v1#publicKeyPem',$wf['properties'])) { - $key['public_key'] = self::convertKey($wf['properties']['https://w3id.org/security/v1#publicKeyPem']); - } - if(array_key_exists('links', $wf) && is_array($wf['links'])) { - foreach($wf['links'] as $l) { - if(! (is_array($l) && array_key_exists('rel',$l))) { - continue; - } - if($l['rel'] === 'http://purl.org/zot/protocol/6.0' && array_key_exists('href',$l) && $l['href'] !== EMPTY_STR) { - $z = \Zotlabs\Lib\Zotfinger::exec($l['href']); - if($z) { - $i = Libzot::import_xchan($z['data']); - if($i['success']) { - $key['portable_id'] = $i['hash']; - - $x = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_id_url = '%s' limit 1", - dbesc($l['href']) - ); - if($x) { - $key['hubloc'] = $x[0]; - } - } - } - } - if($l['rel'] === 'magic-public-key' && array_key_exists('href',$l) && $key['public_key'] === EMPTY_STR) { - $key['public_key'] = self::convertKey($l['href']); - } - } - } - } - - return (($key['public_key']) ? $key : false); - } - - - /** - * @brief - * - * @param array $head - * @param string $prvkey - * @param string $keyid (optional, default '') - * @param boolean $auth (optional, default false) - * @param string $alg (optional, default 'sha256') - * @param array $encryption [ 'key', 'algorithm' ] or false - * @return array - */ - static function create_sig($head, $prvkey, $keyid = EMPTY_STR, $auth = false, $alg = 'sha256', $encryption = false ) { - - $return_headers = []; - - if($alg === 'sha256') { - $algorithm = 'rsa-sha256'; - } - if($alg === 'sha512') { - $algorithm = 'rsa-sha512'; - } - - $x = self::sign($head,$prvkey,$alg); - - $headerval = 'keyId="' . $keyid . '",algorithm="' . $algorithm . '",headers="' . $x['headers'] . '",signature="' . $x['signature'] . '"'; - - if($encryption) { - $x = crypto_encapsulate($headerval,$encryption['key'],$encryption['algorithm']); - if(is_array($x)) { - $headerval = 'iv="' . $x['iv'] . '",key="' . $x['key'] . '",alg="' . $x['alg'] . '",data="' . $x['data'] . '"'; - } - } - - if($auth) { - $sighead = 'Authorization: Signature ' . $headerval; - } - else { - $sighead = 'Signature: ' . $headerval; - } - - if($head) { - foreach($head as $k => $v) { - // strip the request-target virtual header from the output headers - if($k === '(request-target)') { - continue; - } - $return_headers[] = $k . ': ' . $v; - } - } - $return_headers[] = $sighead; - - return $return_headers; - } - - /** - * @brief set headers - * - * @param array $headers - * @return void - */ - - - static function set_headers($headers) { - if($headers && is_array($headers)) { - foreach($headers as $h) { - header($h); - } - } - } - - - /** - * @brief - * - * @param array $head - * @param string $prvkey - * @param string $alg (optional) default 'sha256' - * @return array - */ - - static function sign($head, $prvkey, $alg = 'sha256') { - - $ret = []; - - $headers = ''; - $fields = ''; - - logger('signing: ' . print_r($head,true), LOGGER_DATA); - - if($head) { - foreach($head as $k => $v) { - $headers .= strtolower($k) . ': ' . trim($v) . "\n"; - if($fields) - $fields .= ' '; - - $fields .= strtolower($k); - } - // strip the trailing linefeed - $headers = rtrim($headers,"\n"); - } - - $sig = base64_encode(rsa_sign($headers,$prvkey,$alg)); - - $ret['headers'] = $fields; - $ret['signature'] = $sig; - - return $ret; - } - - /** - * @brief - * - * @param string $header - * @return array associate array with - * - \e string \b keyID - * - \e string \b algorithm - * - \e array \b headers - * - \e string \b signature - */ - - static function parse_sigheader($header) { - - $ret = []; - $matches = []; - - // if the header is encrypted, decrypt with (default) site private key and continue - - if(preg_match('/iv="(.*?)"/ism',$header,$matches)) - $header = self::decrypt_sigheader($header); - - if(preg_match('/keyId="(.*?)"/ism',$header,$matches)) - $ret['keyId'] = $matches[1]; - if(preg_match('/algorithm="(.*?)"/ism',$header,$matches)) - $ret['algorithm'] = $matches[1]; - if(preg_match('/headers="(.*?)"/ism',$header,$matches)) - $ret['headers'] = explode(' ', $matches[1]); - if(preg_match('/signature="(.*?)"/ism',$header,$matches)) - $ret['signature'] = base64_decode(preg_replace('/\s+/','',$matches[1])); - - if(($ret['signature']) && ($ret['algorithm']) && (! $ret['headers'])) - $ret['headers'] = [ 'date' ]; - - return $ret; - } - - - /** - * @brief - * - * @param string $header - * @param string $prvkey (optional), if not set use site private key - * @return array|string associative array, empty string if failue - * - \e string \b iv - * - \e string \b key - * - \e string \b alg - * - \e string \b data - */ - - static function decrypt_sigheader($header, $prvkey = null) { - - $iv = $key = $alg = $data = null; - - if(! $prvkey) { - $prvkey = get_config('system', 'prvkey'); - } - - $matches = []; - - if(preg_match('/iv="(.*?)"/ism',$header,$matches)) - $iv = $matches[1]; - if(preg_match('/key="(.*?)"/ism',$header,$matches)) - $key = $matches[1]; - if(preg_match('/alg="(.*?)"/ism',$header,$matches)) - $alg = $matches[1]; - if(preg_match('/data="(.*?)"/ism',$header,$matches)) - $data = $matches[1]; - - if($iv && $key && $alg && $data) { - return crypto_unencapsulate([ 'encrypted' => true, 'iv' => $iv, 'key' => $key, 'alg' => $alg, 'data' => $data ] , $prvkey); - } - - return ''; - } - -} diff --git a/Zotlabs/Zot6/Receiver.php b/Zotlabs/Zot6/Receiver.php index ed7c33b3a..9e70ab318 100644 --- a/Zotlabs/Zot6/Receiver.php +++ b/Zotlabs/Zot6/Receiver.php @@ -4,6 +4,7 @@ namespace Zotlabs\Zot6; use Zotlabs\Lib\Config; use Zotlabs\Lib\Libzot; +use Zotlabs\Web\HTTPSig; class Receiver { |