function prepare($args) { StatusNet::setApi(true); // Send smaller error pages parent::prepare($args); if ($_SERVER['REQUEST_METHOD'] != 'POST') { $this->clientError(_m('This method requires a POST.')); } if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/magic-envelope+xml') { $this->clientError(_m('Salmon requires application/magic-envelope+xml')); } $xml = file_get_contents('php://input'); // Check the signature $salmon = new Salmon(); if (!$salmon->verifyMagicEnv($xml)) { common_log(LOG_DEBUG, "Salmon signature verification failed."); $this->clientError(_m('Salmon signature verification failed.')); } else { $magic_env = new MagicEnvelope(); $env = $magic_env->parse($xml); $xml = $magic_env->unfold($env); } $dom = DOMDocument::loadXML($xml); if ($dom->documentElement->namespaceURI != Activity::ATOM || $dom->documentElement->localName != 'entry') { common_log(LOG_DEBUG, "Got invalid Salmon post: {$xml}"); $this->clientError(_m('Salmon post must be an Atom entry.')); } $this->act = new Activity($dom->documentElement); return true; }
protected function prepare(array $args = array()) { GNUsocial::setApi(true); // Send smaller error pages parent::prepare($args); 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 { 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! $this->activity = new Activity($entry->documentElement); if (empty($this->activity->actor->id)) { common_log(LOG_ERR, "broken actor: " . var_export($this->activity->actor->id, true)); common_log(LOG_ERR, "activity with no actor: " . var_export($this->activity, true)); // TRANS: Exception. throw new Exception(_m('Received a salmon slap from unidentified actor.')); } // ensureProfiles sets $this->actor and $this->oprofile $this->ensureProfiles(); } catch (Exception $e) { common_debug('Salmon envelope parsing failed with: ' . $e->getMessage()); $this->clientError($e->getMessage()); } // Cryptographic verification test if (!$magic_env->verify($this->actor)) { common_log(LOG_DEBUG, "Salmon signature verification failed."); // TRANS: Client error. $this->clientError(_m('Salmon signature verification failed.')); } return true; }
/** * Sign and post the given Atom entry as a Salmon message. * * Side effects: may generate a keypair on-demand for the given user, * which can be very slow on some systems (like those without php5-gmp). * * @param string $endpoint_uri * @param string $xml string representation of payload * @param Profile $user profile whose keys we sign with (must be a local user) * @return boolean success */ public static function post($endpoint_uri, $xml, Profile $actor, Profile $target = null) { if (empty($endpoint_uri)) { common_debug('No endpoint URI for Salmon post to ' . $actor->getUri()); return false; } try { $magic_env = MagicEnvelope::signAsUser($xml, $actor->getUser()); } catch (Exception $e) { common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage()); return false; } // $target is so far only used in Diaspora, so it can be null if (Event::handle('SalmonSlap', array($endpoint_uri, $magic_env, $target))) { return false; //throw new ServerException('Could not distribute salmon slap as no plugin completed the event.'); } return true; }
/** * Sign and post the given Atom entry as a Salmon message. * * Side effects: may generate a keypair on-demand for the given user, * which can be very slow on some systems (like those without php5-gmp). * * @param string $endpoint_uri * @param string $xml string representation of payload * @param User $user local user profile whose keys we sign with * @return boolean success */ public static function post($endpoint_uri, $xml, User $user) { if (empty($endpoint_uri)) { common_debug('No endpoint URI for Salmon post to ' . $user->getUri()); return false; } try { $magic_env = MagicEnvelope::signAsUser($xml, $user); $envxml = $magic_env->toXML(); } catch (Exception $e) { common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage()); return false; } $headers = array('Content-Type: application/magic-envelope+xml'); try { $client = new HTTPClient(); $client->setBody($envxml); $response = $client->post($endpoint_uri, $headers); } catch (HTTP_Request2_Exception $e) { common_log(LOG_ERR, "Salmon post to {$endpoint_uri} failed: " . $e->getMessage()); return false; } // 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; } // Success! return true; }
exit(1); } $notice_id = get_option_value('--notice'); $notice = Notice::getKV('id', $notice_id); $profile = $notice->getProfile(); $entry = $notice->asAtomEntry(true); echo "== Original entry ==\n\n"; print $entry; print "\n\n"; $magic_env = MagicEnvelope::signAsUser($entry, $profile->getUser()); $envxml = $magic_env->toXML(); echo "== Signed envelope ==\n\n"; print $envxml; print "\n\n"; echo "== Testing local verification ==\n\n"; $magic_env = new MagicEnvelope($envxml); $activity = new Activity($magic_env->getPayload()->documentElement); $actprofile = Profile::fromUri($activity->actor->id); $ok = $magic_env->verify($actprofile); if ($ok) { print "OK\n\n"; } else { print "FAIL\n\n"; } if (have_option('--verify')) { $url = 'http://www.madebymonsieur.com/ostatus_discovery/magic_env/validate/'; echo "== Testing remote verification ==\n\n"; print "Sending for verification to {$url} ...\n"; $client = new HTTPClient(); $response = $client->post($url, array(), array('magic_env' => $envxml)); print $response->getStatus() . "\n\n";
/** * Encode the given string as a signed MagicEnvelope XML document, * using the keypair for the given local user profile. We can of * course not sign a remote profile's slap, since we don't have the * private key. * * Side effects: will create and store a keypair on-demand if one * hasn't already been generated for this user. This can be very slow * on some systems. * * @param string $text XML fragment to sign, assumed to be Atom * @param User $user User who cryptographically signs $text * * @return MagicEnvelope object complete with signature * * @throws Exception on bad profile input or key generation problems */ public static function signAsUser($text, User $user) { // Find already stored key $magicsig = Magicsig::getKV('user_id', $user->id); if (!$magicsig instanceof Magicsig) { $magicsig = Magicsig::generate($user); } assert($magicsig instanceof Magicsig); assert($magicsig->privateKey instanceof Crypt_RSA); $magic_env = new MagicEnvelope(); $magic_env->signMessage($text, 'application/atom+xml', $magicsig); return $magic_env; }
/** * Encode the given string as a signed MagicEnvelope XML document, * using the keypair for the given local user profile. We can of * course not sign a remote profile's slap, since we don't have the * private key. * * Side effects: will create and store a keypair on-demand if one * hasn't already been generated for this user. This can be very slow * on some systems. * * @param string $text XML fragment to sign, assumed to be Atom * @param User $user User who cryptographically signs $text * * @return MagicEnvelope object complete with signature * * @throws Exception on bad profile input or key generation problems */ public static function signAsUser($text, User $user) { $magic_env = new MagicEnvelope(null, $user->getProfile()); $magic_env->signMessage($text, 'application/atom+xml'); return $magic_env; }
public function verifyMagicEnv($text) { $magic_env = new MagicEnvelope(); $env = $magic_env->parse($text); return $magic_env->verify($env); }
/** * Test that MagicEnvelope builds the correct plaintext for signing. * @dataProvider provider */ public function testSignatureText(MagicEnvelope $env, $expected) { $text = $env->signingText(); $this->assertEquals($expected, $text, "'{$text}' should be '{$expected}'"); }
public function onSalmonSlap($endpoint_uri, MagicEnvelope $magic_env, Profile $target = null) { $envxml = $magic_env->toXML($target); $headers = array('Content-Type: application/magic-envelope+xml'); try { $client = new HTTPClient(); $client->setBody($envxml); $response = $client->post($endpoint_uri, $headers); } catch (HTTP_Request2_Exception $e) { common_log(LOG_ERR, "Salmon post to {$endpoint_uri} failed: " . $e->getMessage()); return false; } if ($response->getStatus() === 422) { common_debug(sprintf('Salmon (from profile %d) endpoint %s returned status %s. We assume it is a Diaspora seed; will adapt and try again if that plugin is enabled!', $magic_env->getActor()->getID(), $endpoint_uri, $response->getStatus())); return true; } // 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', $magic_env->getActor()->getID(), $endpoint_uri, $response->getStatus(), $response->getBody())); return true; } // Since we completed the salmon slap, we discontinue the event return false; }
public function onSalmonSlap($endpoint_uri, MagicEnvelope $magic_env, Profile $target = null) { $envxml = $magic_env->toXML($target, 'diaspora'); // Diaspora wants another POST format (base64url-encoded POST variable 'xml') $headers = array('Content-Type: application/x-www-form-urlencoded'); // Another way to distinguish Diaspora from GNU social is that a POST with // $headers=array('Content-Type: application/magic-envelope+xml') would return // HTTP status code 422 Unprocessable Entity, at least as of 2015-10-04. try { $client = new HTTPClient(); $client->setBody('xml=' . Magicsig::base64_url_encode($envxml)); $response = $client->post($endpoint_uri, $headers); } catch (HTTP_Request2_Exception $e) { common_log(LOG_ERR, "Diaspora-flavoured Salmon post to {$endpoint_uri} failed: " . $e->getMessage()); return false; } // 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', $magic_env->getActor()->getID(), $endpoint_uri, $response->getStatus(), $response->getBody())); return true; } // Success! return false; }