From 47c83f4c496f9cb9b09cc25123b862f1402aacab Mon Sep 17 00:00:00 2001 From: Chimo Date: Sat, 6 Jun 2015 00:22:14 -0400 Subject: [PATCH 01/11] Only serve tagprofile HTML if we aren't POSTing via ajax This fixes an issue where POSTing the selftag form in the profile_block sidebar via AJAX would generate an XML response containing both the content from doPost() and showPage(), resulting in invalid XML. These changes make it so that if we're POSTing via AJAX, we serve content from doPost(), otherwise we serve showPage() but never both. --- actions/tagprofile.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/actions/tagprofile.php b/actions/tagprofile.php index 47a66d0be4..871d0e30b0 100644 --- a/actions/tagprofile.php +++ b/actions/tagprofile.php @@ -65,6 +65,15 @@ class TagprofileAction extends FormAction return sprintf(_m('ADDTOLIST','List %s'), $this->target->getNickname()); } + function showPage() + { + // Only serve page content if we aren't POSTing via ajax + // otherwise, we serve XML content from doPost() + if (!$this->isPost() || !$this->boolean('ajax')) { + parent::showPage(); + } + } + function showContent() { $this->elementStart('div', 'entity_profile h-card'); From e212f2ae77bd96f9ea8e90c7e51125ae74800efc Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 6 Jun 2015 13:49:27 +0200 Subject: [PATCH 02/11] Moved Diaspora specific metadata to own plugin --- plugins/Diaspora/DiasporaPlugin.php | 57 ++++++++++++++++++++++++++++ plugins/OStatus/EVENTS.txt | 7 ++++ plugins/OStatus/OStatusPlugin.php | 9 +++-- plugins/OStatus/classes/Magicsig.php | 1 - 4 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 plugins/Diaspora/DiasporaPlugin.php create mode 100644 plugins/OStatus/EVENTS.txt diff --git a/plugins/Diaspora/DiasporaPlugin.php b/plugins/Diaspora/DiasporaPlugin.php new file mode 100644 index 0000000000..1490fce7a3 --- /dev/null +++ b/plugins/Diaspora/DiasporaPlugin.php @@ -0,0 +1,57 @@ +. + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * Diaspora federation protocol plugin for GNU Social + * + * Depends on: + * - OStatus plugin + * - WebFinger plugin + * + * @package ProtocolDiasporaPlugin + * @maintainer Mikael Nordfeldth + */ + +class DiasporaPlugin extends Plugin +{ + const REL_SEED_LOCATION = 'http://joindiaspora.com/seed_location'; + const REL_GUID = 'http://joindiaspora.com/guid'; + const REL_PUBLIC_KEY = 'diaspora-public-key'; + + public function onEndAttachPubkeyToUserXRD(Magicsig $magicsig, XML_XRD $xrd, Profile $target) + { + $xrd->links[] = new XML_XRD_Element_Link(self::REL_PUBLIC_KEY, + base64_encode($magicsig->exportPublicKey())); + } + + public function onPluginVersion(array &$versions) + { + $versions[] = array('name' => 'Diaspora', + 'version' => '0.1', + 'author' => 'Mikael Nordfeldth', + 'homepage' => 'https://gnu.io/social', + // TRANS: Plugin description. + 'rawdescription' => _m('Follow people across social networks that implement '. + 'the Diaspora federation protocol.')); + + return true; + } +} diff --git a/plugins/OStatus/EVENTS.txt b/plugins/OStatus/EVENTS.txt new file mode 100644 index 0000000000..766b7513a3 --- /dev/null +++ b/plugins/OStatus/EVENTS.txt @@ -0,0 +1,7 @@ +StartAttachPubkeyToUserXRD: Runs only for XRD generation where a Magicsig exists for a Profile which is a "person". +@param Magicsig $magicsig crypto stuff related to the profile we're representing +@param XRD $xrd the XRD object which holds all data for the profile we're representing + +EndAttachPubkeyToUserXRD: Runs only for XRD generation where a Magicsig exists for a Profile which is a "person". And only if StartAttachPubkeyToUserXRD didn't abort. +@param Magicsig $magicsig crypto stuff related to the profile we're representing +@param XRD $xrd the XRD object which holds all data for the profile we're representing diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 774a13be82..ba50cb653d 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -1315,11 +1315,14 @@ class OStatusPlugin extends Plugin $magicsig = Magicsig::generate($target->getUser()); } - if ($magicsig instanceof Magicsig) { + if (!$magicsig instanceof Magicsig) { + return false; // value doesn't mean anything, just figured I'd indicate this function didn't do anything + } + if (Event::handle('StartAttachPubkeyToUserXRD', array($magicsig, $xrd, $target))) { $xrd->links[] = new XML_XRD_Element_Link(Magicsig::PUBLICKEYREL, 'data:application/magic-public-key,'. $magicsig->toString()); - $xrd->links[] = new XML_XRD_Element_Link(Magicsig::DIASPORA_PUBLICKEYREL, - base64_encode($magicsig->exportPublicKey())); + // The following event handles plugins like Diaspora which add their own version of the Magicsig pubkey + Event::handle('EndAttachPubkeyToUserXRD', array($magicsig, $xrd, $target)); } } diff --git a/plugins/OStatus/classes/Magicsig.php b/plugins/OStatus/classes/Magicsig.php index 8d2bb4eac9..42a11533b7 100644 --- a/plugins/OStatus/classes/Magicsig.php +++ b/plugins/OStatus/classes/Magicsig.php @@ -36,7 +36,6 @@ require_once 'Crypt/RSA.php'; class Magicsig extends Managed_DataObject { const PUBLICKEYREL = 'magic-public-key'; - const DIASPORA_PUBLICKEYREL = 'diaspora-public-key'; const DEFAULT_KEYLEN = 1024; const DEFAULT_SIGALG = 'RSA-SHA256'; From c5f79fd2f3d01dc29b3870db9151763dd70cb3c1 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 6 Jun 2015 14:33:43 +0200 Subject: [PATCH 03/11] Magicsig gets toFingerprint function. --- plugins/OStatus/classes/Magicsig.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/OStatus/classes/Magicsig.php b/plugins/OStatus/classes/Magicsig.php index 42a11533b7..1b6aa36084 100644 --- a/plugins/OStatus/classes/Magicsig.php +++ b/plugins/OStatus/classes/Magicsig.php @@ -178,18 +178,30 @@ class Magicsig extends Managed_DataObject * @param boolean $full_pair set to true to include the private key. * @return string */ - public function toString($full_pair=false) + public function toString($full_pair=false, $base64url=true) { - $mod = Magicsig::base64_url_encode($this->publicKey->modulus->toBytes()); - $exp = Magicsig::base64_url_encode($this->publicKey->exponent->toBytes()); + $base64_func = $base64url ? 'Magicsig::base64_url_encode' : 'base64_encode'; + $mod = call_user_func($base64_func, $this->publicKey->modulus->toBytes()); + $exp = call_user_func($base64_func, $this->publicKey->exponent->toBytes()); + $private_exp = ''; if ($full_pair && $this->privateKey instanceof Crypt_RSA && $this->privateKey->exponent->toBytes()) { - $private_exp = '.' . Magicsig::base64_url_encode($this->privateKey->exponent->toBytes()); + $private_exp = '.' . call_user_func($base64_func, $this->privateKey->exponent->toBytes()); } return 'RSA.' . $mod . '.' . $exp . $private_exp; } + public function toFingerprint() + { + // This assumes a specific behaviour from toString, to format as such: + // "RSA." + base64(pubkey.modulus_as_bytes) + "." + base64(pubkey.exponent_as_bytes) + // We don't want the base64 string to be the "url encoding" version because it is not + // as common in programming libraries. And we want it to be base64 encoded since ASCII + // representation avoids any problems with NULL etc. in less forgiving languages. + return strtolower(hash('sha256', $this->toString(false, false))); + } + public function exportPublicKey($format=CRYPT_RSA_PUBLIC_FORMAT_PKCS1) { $this->publicKey->setPublicKey(); From 57943cad993e0728aa5ca637f4ceb9049c9efca5 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 6 Jun 2015 14:35:48 +0200 Subject: [PATCH 04/11] Magicsig gets toFingerprint output We give this as a lowercase, sha256 hexadecimal digest of the string: TYPE + "." + BASE64(modulus as bytes) + "." + BASE64(exponent as bytes) Where TYPE in all our cases up until now at least are "RSA" --- plugins/OStatus/classes/Magicsig.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/OStatus/classes/Magicsig.php b/plugins/OStatus/classes/Magicsig.php index 1b6aa36084..890f525862 100644 --- a/plugins/OStatus/classes/Magicsig.php +++ b/plugins/OStatus/classes/Magicsig.php @@ -198,7 +198,8 @@ class Magicsig extends Managed_DataObject // "RSA." + base64(pubkey.modulus_as_bytes) + "." + base64(pubkey.exponent_as_bytes) // We don't want the base64 string to be the "url encoding" version because it is not // as common in programming libraries. And we want it to be base64 encoded since ASCII - // representation avoids any problems with NULL etc. in less forgiving languages. + // representation avoids any problems with NULL etc. in less forgiving languages and also + // just easier to debug... return strtolower(hash('sha256', $this->toString(false, false))); } From 623a7eee5740f7c352d3b569755a6b3828d8699c Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 6 Jun 2015 14:46:17 +0200 Subject: [PATCH 05/11] Diaspora seeds tend to give the key type in 'type' attribute --- plugins/Diaspora/DiasporaPlugin.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/Diaspora/DiasporaPlugin.php b/plugins/Diaspora/DiasporaPlugin.php index 1490fce7a3..ae5fb2f73a 100644 --- a/plugins/Diaspora/DiasporaPlugin.php +++ b/plugins/Diaspora/DiasporaPlugin.php @@ -38,8 +38,11 @@ class DiasporaPlugin extends Plugin public function onEndAttachPubkeyToUserXRD(Magicsig $magicsig, XML_XRD $xrd, Profile $target) { + // So far we've only handled RSA keys, but it can change in the future, + // so be prepared. And remember to change the statically assigned type attribute below! + assert($magicsig->publicKey instanceof Crypt_RSA); $xrd->links[] = new XML_XRD_Element_Link(self::REL_PUBLIC_KEY, - base64_encode($magicsig->exportPublicKey())); + base64_encode($magicsig->exportPublicKey()), 'RSA'); } public function onPluginVersion(array &$versions) From d4fc064e4420d74dbff0587ba2b05d774e85a51b Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 6 Jun 2015 14:49:39 +0200 Subject: [PATCH 06/11] Include the Diaspora GUID string in our XRD metadata --- plugins/Diaspora/DiasporaPlugin.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/Diaspora/DiasporaPlugin.php b/plugins/Diaspora/DiasporaPlugin.php index ae5fb2f73a..66a9759f87 100644 --- a/plugins/Diaspora/DiasporaPlugin.php +++ b/plugins/Diaspora/DiasporaPlugin.php @@ -43,6 +43,11 @@ class DiasporaPlugin extends Plugin assert($magicsig->publicKey instanceof Crypt_RSA); $xrd->links[] = new XML_XRD_Element_Link(self::REL_PUBLIC_KEY, base64_encode($magicsig->exportPublicKey()), 'RSA'); + + // Instead of choosing a random string, we calculate our GUID from the public key + // by fingerprint through a sha256 hash. + $xrd->links[] = new XML_XRD_Element_Link(self::REL_GUID, + strtolower($magicsig->toFingerprint())); } public function onPluginVersion(array &$versions) From 268b901048155bff5b6c076b05d4a6d2d6242d40 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 6 Jun 2015 15:58:08 +0200 Subject: [PATCH 07/11] Maintainer change for Ostatus_profile --- plugins/OStatus/classes/Ostatus_profile.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 4d1b95e2b7..cb961dc96b 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -17,13 +17,12 @@ * along with this program. If not, see . */ -if (!defined('STATUSNET')) { - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * @package OStatusPlugin - * @maintainer Brion Vibber + * @author Brion Vibber + * @maintainer Mikael Nordfeldth */ class Ostatus_profile extends Managed_DataObject { @@ -1746,11 +1745,8 @@ class Ostatus_profile extends Managed_DataObject throw new Exception(_m('Not a valid webfinger address.')); } - $hints = array('webfinger' => $addr); - - $dhints = DiscoveryHints::fromXRD($xrd); - - $hints = array_merge($hints, $dhints); + $hints = array_merge(array('webfinger' => $addr), + DiscoveryHints::fromXRD($xrd)); // If there's an Hcard, let's grab its info if (array_key_exists('hcard', $hints)) { From 4de125dd843359c8a705c7c76eb2aa5f74efce68 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 6 Jun 2015 16:02:25 +0200 Subject: [PATCH 08/11] Moved FeedSubException parent class to own file --- plugins/OStatus/OStatusPlugin.php | 13 ------------- plugins/OStatus/lib/feedsubexception.php | 13 +++++++++++++ 2 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 plugins/OStatus/lib/feedsubexception.php diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index ba50cb653d..7730f2e67c 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -30,19 +30,6 @@ if (!defined('GNUSOCIAL')) { exit(1); } set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/phpseclib'); -class FeedSubException extends Exception -{ - function __construct($msg=null) - { - $type = get_class($this); - if ($msg) { - parent::__construct("$type: $msg"); - } else { - parent::__construct($type); - } - } -} - class OStatusPlugin extends Plugin { /** diff --git a/plugins/OStatus/lib/feedsubexception.php b/plugins/OStatus/lib/feedsubexception.php new file mode 100644 index 0000000000..8d65881647 --- /dev/null +++ b/plugins/OStatus/lib/feedsubexception.php @@ -0,0 +1,13 @@ + Date: Sat, 6 Jun 2015 16:18:22 +0200 Subject: [PATCH 09/11] OStatus update-profile.php script now finds Diaspora salmon URLs --- plugins/OStatus/scripts/update-profile.php | 24 +++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/plugins/OStatus/scripts/update-profile.php b/plugins/OStatus/scripts/update-profile.php index ece1980442..3d72674920 100644 --- a/plugins/OStatus/scripts/update-profile.php +++ b/plugins/OStatus/scripts/update-profile.php @@ -54,10 +54,28 @@ print "\n"; print "Re-running feed discovery for profile URL $oprofile->uri\n"; // @fixme will bork where the URI isn't the profile URL for now $discover = new FeedDiscovery(); -$feedurl = $discover->discoverFromURL($oprofile->uri); +try { + $feedurl = $discover->discoverFromURL($oprofile->uri); + $salmonuri = $discover->getAtomLink(Salmon::REL_SALMON) + ?: $discover->getAtomLink(Salmon::NS_REPLIES); // NS_REPLIES is deprecated +} catch (FeedSubException $e) { + $acct = $oprofile->localProfile()->getAcctUri(); + print "Could not discover feeds HTML response, trying reconstructed acct URI: " . $acct; + $disco = new Discovery(); + $xrd = $disco->lookup($acct); + $hints = DiscoveryHints::fromXRD($xrd); + + if (!array_key_exists('feedurl', $hints)) { + throw new FeedSubNoFeedException($acct); + } + $feedurl = $hints['feedurl']; + $salmonuri = array_key_exists('salmon', $hints) ? $hints['salmon'] : null; + + // get the hub data too and put it in the FeedDiscovery object + $discover->discoverFromFeedUrl($feedurl); +} + $huburi = $discover->getHubLink(); -$salmonuri = $discover->getAtomLink(Salmon::REL_SALMON) - ?: $discover->getAtomLink(Salmon::NS_REPLIES); print " Feed URL: $feedurl\n"; print " Hub URL: $huburi\n"; From faf14197cd7956c5ced768a82acaefd0d0acce89 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 6 Jun 2015 16:57:29 +0200 Subject: [PATCH 10/11] Diaspora doesn't understand our Salmon POST, so send again --- plugins/OStatus/lib/salmon.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php index 83be963c77..2097ffa77f 100644 --- a/plugins/OStatus/lib/salmon.php +++ b/plugins/OStatus/lib/salmon.php @@ -71,7 +71,19 @@ class Salmon common_log(LOG_ERR, "Salmon post to $endpoint_uri failed: " . $e->getMessage()); return false; } - if ($response->getStatus() != 200) { + + // Diaspora wants a slightly different formatting on the POST (other Content-type, so body needs "xml=") + if ($response->getStatus() === 422) { + common_debug(sprintf('Salmon (from profile %d) endpoint %s returned status %s. Diaspora? Will try again! Body: %s', + $user->id, $endpoint_uri, $response->getStatus(), $response->getBody())); + $headers = array('Content-Type: application/x-www-form-urlencoded'); + $client->setBody('xml=' . Magicsig::base64_url_encode($envxml)); + $response = $client->post($endpoint_uri, $headers); + } + + // 200 OK is the best response + // 202 Accepted is what we get from Diaspora for example + if (!in_array($response->getStatus(), array(200, 202))) { common_log(LOG_ERR, sprintf('Salmon (from profile %d) endpoint %s returned status %s: %s', $user->id, $endpoint_uri, $response->getStatus(), $response->getBody())); return false; From 6478034e92fe167d56450702985810eeb78e215a Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 6 Jun 2015 17:14:01 +0200 Subject: [PATCH 11/11] Diaspora-compatible Salmon slap receival We're not all the way there yet, there is something which seems to bugger up profile discovery from their end. --- plugins/OStatus/lib/salmonaction.php | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 6fb3d2f9fe..5193d302f1 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -41,13 +41,27 @@ class SalmonAction extends Action parent::prepare($args); - if (!isset($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/magic-envelope+xml') { - // TRANS: Client error. Do not translate "application/magic-envelope+xml". - $this->clientError(_m('Salmon requires "application/magic-envelope+xml".')); + if (!isset($_SERVER['CONTENT_TYPE'])) { + // TRANS: Client error. Do not translate "Content-type" + $this->clientError(_m('Salmon requires a Content-type header.')); + } + $envxml = null; + switch ($_SERVER['CONTENT_TYPE']) { + case 'application/magic-envelope+xml': + $envxml = file_get_contents('php://input'); + break; + case 'application/x-www-form-urlencoded': + $envxml = Magicsig::base64_url_decode($this->trimmed('xml')); + break; + default: + // TRANS: Client error. Do not translate the quoted "application/[type]" strings. + $this->clientError(_m('Salmon requires "application/magic-envelope+xml". For Diaspora we also accept "application/x-www-form-urlencoded" with an "xml" parameter.', 415)); } try { - $envxml = file_get_contents('php://input'); + if (empty($envxml)) { + throw new ClientException('No magic envelope supplied in POST.'); + } $magic_env = new MagicEnvelope($envxml); // parse incoming XML as a MagicEnvelope $entry = $magic_env->getPayload(); // Not cryptographically verified yet!