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'); diff --git a/plugins/Diaspora/DiasporaPlugin.php b/plugins/Diaspora/DiasporaPlugin.php new file mode 100644 index 0000000000..66a9759f87 --- /dev/null +++ b/plugins/Diaspora/DiasporaPlugin.php @@ -0,0 +1,65 @@ +. + */ + +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) + { + // 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()), '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) + { + $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..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 { /** @@ -1315,11 +1302,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..890f525862 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'; @@ -179,18 +178,31 @@ 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 and also + // just easier to debug... + return strtolower(hash('sha256', $this->toString(false, false))); + } + public function exportPublicKey($format=CRYPT_RSA_PUBLIC_FORMAT_PKCS1) { $this->publicKey->setPublicKey(); 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)) { 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 @@ +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; 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! 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";