Merge branch 'testing' into moveaccount

This commit is contained in:
Evan Prodromou 2011-01-17 17:34:03 -05:00
commit 2a59453d4c
21 changed files with 717 additions and 82 deletions

View File

@ -66,6 +66,12 @@ class ApiGroupMembershipAction extends ApiPrivateAuthAction
parent::prepare($args); parent::prepare($args);
$this->group = $this->getTargetGroup($this->arg('id')); $this->group = $this->getTargetGroup($this->arg('id'));
if (empty($this->group)) {
// TRANS: Client error displayed trying to show group membership on a non-existing group.
$this->clientError(_('Group not found.'), 404, $this->format);
return false;
}
$this->profiles = $this->getProfiles(); $this->profiles = $this->getProfiles();
return true; return true;
@ -84,12 +90,6 @@ class ApiGroupMembershipAction extends ApiPrivateAuthAction
{ {
parent::handle($args); parent::handle($args);
if (empty($this->group)) {
// TRANS: Client error displayed trying to show group membership on a non-existing group.
$this->clientError(_('Group not found.'), 404, $this->format);
return false;
}
// XXX: RSS and Atom // XXX: RSS and Atom
switch($this->format) { switch($this->format) {

View File

@ -377,7 +377,7 @@ class ApiStatusesUpdateAction extends ApiAuthAction
function supported($cmd) function supported($cmd)
{ {
static $cmdlist = array('MessageCommand', 'SubCommand', 'UnsubCommand', static $cmdlist = array('MessageCommand', 'SubCommand', 'UnsubCommand',
'FavCommand', 'OnCommand', 'OffCommand'); 'FavCommand', 'OnCommand', 'OffCommand', 'JoinCommand', 'LeaveCommand');
if (in_array(get_class($cmd), $cmdlist)) { if (in_array(get_class($cmd), $cmdlist)) {
return true; return true;

View File

@ -200,8 +200,6 @@ class NewgroupAction extends Action
} }
} }
$mainpage = common_local_url('showgroup', array('nickname' => $nickname));
$cur = common_current_user(); $cur = common_current_user();
// Checked in prepare() above // Checked in prepare() above
@ -215,7 +213,6 @@ class NewgroupAction extends Action
'location' => $location, 'location' => $location,
'aliases' => $aliases, 'aliases' => $aliases,
'userid' => $cur->id, 'userid' => $cur->id,
'mainpage' => $mainpage,
'local' => true)); 'local' => true));
common_redirect($group->homeUrl(), 303); common_redirect($group->homeUrl(), 303);

View File

@ -100,8 +100,6 @@ class SubscribersAction extends GalleryAction
} }
} }
$subscribers->free();
$this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE,
$this->page, 'subscribers', $this->page, 'subscribers',
array('nickname' => $this->user->nickname)); array('nickname' => $this->user->nickname));

View File

@ -106,8 +106,6 @@ class SubscriptionsAction extends GalleryAction
} }
} }
$subscriptions->free();
$this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE,
$this->page, 'subscriptions', $this->page, 'subscriptions',
array('nickname' => $this->user->nickname)); array('nickname' => $this->user->nickname));

View File

@ -487,6 +487,7 @@ class User_group extends Memcached_DataObject
} }
// MAGICALLY put fields into current scope // MAGICALLY put fields into current scope
// @fixme kill extract(); it makes debugging absurdly hard
extract($fields); extract($fields);
@ -498,6 +499,9 @@ class User_group extends Memcached_DataObject
// fill in later... // fill in later...
$uri = null; $uri = null;
} }
if (empty($mainpage)) {
$mainpage = common_local_url('showgroup', array('nickname' => $nickname));
}
$group->nickname = $nickname; $group->nickname = $nickname;
$group->fullname = $fullname; $group->fullname = $fullname;

View File

@ -182,6 +182,9 @@ class Activity
$actorEl = $this->_child($entry, self::ACTOR); $actorEl = $this->_child($entry, self::ACTOR);
if (!empty($actorEl)) { if (!empty($actorEl)) {
// Standalone <activity:actor> elements are a holdover from older
// versions of ActivityStreams. Newer feeds should have this data
// integrated straight into <atom:author>.
$this->actor = new ActivityObject($actorEl); $this->actor = new ActivityObject($actorEl);
@ -196,19 +199,24 @@ class Activity
$this->actor->id = $authorObj->id; $this->actor->id = $authorObj->id;
} }
} }
} else if (!empty($feed) &&
$subjectEl = $this->_child($feed, self::SUBJECT)) {
$this->actor = new ActivityObject($subjectEl);
} else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) { } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) {
// An <atom:author> in the entry overrides any author info on
// the surrounding feed.
$this->actor = new ActivityObject($authorEl); $this->actor = new ActivityObject($authorEl);
} else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR, } else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR,
self::ATOM)) { self::ATOM)) {
// If there's no <atom:author> on the entry, it's safe to assume
// the containing feed's authorship info applies.
$this->actor = new ActivityObject($authorEl); $this->actor = new ActivityObject($authorEl);
} else if (!empty($feed) &&
$subjectEl = $this->_child($feed, self::SUBJECT)) {
// Feed subject is used for things like groups.
// Should actually possibly not be interpreted as an actor...?
$this->actor = new ActivityObject($subjectEl);
} }
$contextEl = $this->_child($entry, self::CONTEXT); $contextEl = $this->_child($entry, self::CONTEXT);

View File

@ -1437,41 +1437,23 @@ class ApiAction extends Action
{ {
if (empty($id)) { if (empty($id)) {
if (self::is_decimal($this->arg('id'))) { if (self::is_decimal($this->arg('id'))) {
return User_group::staticGet($this->arg('id')); return User_group::staticGet('id', $this->arg('id'));
} else if ($this->arg('id')) { } else if ($this->arg('id')) {
$nickname = common_canonical_nickname($this->arg('id')); return User_group::getForNickname($this->arg('id'));
$local = Local_group::staticGet('nickname', $nickname);
if (empty($local)) {
return null;
} else {
return User_group::staticGet('id', $local->id);
}
} else if ($this->arg('group_id')) { } else if ($this->arg('group_id')) {
// This is to ensure that a non-numeric user_id still // This is to ensure that a non-numeric group_id still
// overrides screen_name even if it doesn't get used // overrides group_name even if it doesn't get used
if (self::is_decimal($this->arg('group_id'))) { if (self::is_decimal($this->arg('group_id'))) {
return User_group::staticGet('id', $this->arg('group_id')); return User_group::staticGet('id', $this->arg('group_id'));
} }
} else if ($this->arg('group_name')) { } else if ($this->arg('group_name')) {
$nickname = common_canonical_nickname($this->arg('group_name')); return User_group::getForNickname($this->arg('group_name'));
$local = Local_group::staticGet('nickname', $nickname);
if (empty($local)) {
return null;
} else {
return User_group::staticGet('id', $local->group_id);
}
} }
} else if (self::is_decimal($id)) { } else if (self::is_decimal($id)) {
return User_group::staticGet($id); return User_group::staticGet('id', $id);
} else { } else {
$nickname = common_canonical_nickname($id); return User_group::getForNickname($id);
$local = Local_group::staticGet('nickname', $nickname);
if (empty($local)) {
return null;
} else {
return User_group::staticGet('id', $local->group_id);
}
} }
} }

View File

@ -76,6 +76,10 @@ class ArrayWrapper
function __call($name, $args) function __call($name, $args)
{ {
$item =& $this->_items[$this->_i]; $item =& $this->_items[$this->_i];
if (!is_object($item)) {
common_log(LOG_ERR, "Invalid entry " . var_export($item, true) . " at index $this->_i of $this->N; calling $name()");
throw new ServerException("Internal error: bad entry in array wrapper list.");
}
return call_user_func_array(array($item, $name), $args); return call_user_func_array(array($item, $name), $args);
} }
} }

View File

@ -95,7 +95,11 @@ function _have_config()
return StatusNet::haveConfig(); return StatusNet::haveConfig();
} }
function __autoload($cls) /**
* Wrapper for class autoloaders.
* This used to be the special function name __autoload(), but that causes bugs with PHPUnit 3.5+
*/
function autoload_sn($cls)
{ {
if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) { if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) {
require_once(INSTALLDIR.'/classes/' . $cls . '.php'); require_once(INSTALLDIR.'/classes/' . $cls . '.php');
@ -111,6 +115,8 @@ function __autoload($cls)
} }
} }
spl_autoload_register('autoload_sn');
// XXX: how many of these could be auto-loaded on use? // XXX: how many of these could be auto-loaded on use?
// XXX: note that these files should not use config options // XXX: note that these files should not use config options
// at compile time since DB config options are not yet loaded. // at compile time since DB config options are not yet loaded.

View File

@ -121,7 +121,7 @@ function mail_notify_from()
$domain = mail_domain(); $domain = mail_domain();
$notifyfrom = '"'.common_config('site', 'name') .'" <noreply@'.$domain.'>'; $notifyfrom = '"'. str_replace('"', '\\"', common_config('site', 'name')) .'" <noreply@'.$domain.'>';
} }
return $notifyfrom; return $notifyfrom;

View File

@ -16,6 +16,12 @@ jQuery(document).ready(function($){
contentSelector : "#notices_primary ol.notices", contentSelector : "#notices_primary ol.notices",
itemSelector : "#notices_primary ol.notices li" itemSelector : "#notices_primary ol.notices li"
},function(){ },function(){
SN.Init.Notices(); // Reply button and attachment magic need to be set up
// for each new notice.
// DO NOT run SN.Init.Notices() which will duplicate stuff.
$(this).find('.notice').each(function() {
SN.U.NoticeReplyTo($(this));
SN.U.NoticeWithAttachment($(this));
});
}); });
}); });

View File

@ -21,7 +21,7 @@
// grab each selector option and see if any fail. // grab each selector option and see if any fail.
function areSelectorsValid(opts){ function areSelectorsValid(opts){
for (var key in opts){ for (var key in opts){
if (key.indexOf && key.indexOf('Selector') && $(opts[key]).length === 0){ if (key.indexOf && (key.indexOf('Selector') != -1) && $(opts[key]).length === 0){
debug('Your ' + key + ' found no elements.'); debug('Your ' + key + ' found no elements.');
return false; return false;
} }

View File

@ -39,11 +39,41 @@ class Magicsig extends Memcached_DataObject
public $__table = 'magicsig'; public $__table = 'magicsig';
/**
* Key to user.id/profile.id for the local user whose key we're storing.
*
* @var int
*/
public $user_id; public $user_id;
/**
* Flattened string representation of the key pair; callers should
* usually use $this->publicKey and $this->privateKey directly,
* which hold live Crypt_RSA key objects.
*
* @var string
*/
public $keypair; public $keypair;
/**
* Crypto algorithm used for this key; currently only RSA-SHA256 is supported.
*
* @var string
*/
public $alg; public $alg;
/**
* Public RSA key; gets serialized in/out via $this->keypair string.
*
* @var Crypt_RSA
*/
public $publicKey; public $publicKey;
/**
* PrivateRSA key; gets serialized in/out via $this->keypair string.
*
* @var Crypt_RSA
*/
public $privateKey; public $privateKey;
public function __construct($alg = 'RSA-SHA256') public function __construct($alg = 'RSA-SHA256')
@ -51,6 +81,13 @@ class Magicsig extends Memcached_DataObject
$this->alg = $alg; $this->alg = $alg;
} }
/**
* Fetch a Magicsig object from the cache or database on a field match.
*
* @param string $k
* @param mixed $v
* @return Magicsig
*/
public /*static*/ function staticGet($k, $v=null) public /*static*/ function staticGet($k, $v=null)
{ {
$obj = parent::staticGet(__CLASS__, $k, $v); $obj = parent::staticGet(__CLASS__, $k, $v);
@ -103,6 +140,14 @@ class Magicsig extends Memcached_DataObject
return array(false, false, false); return array(false, false, false);
} }
/**
* Save this keypair into the database.
*
* Overloads default insert behavior to encode the live key objects
* as a flat string for storage.
*
* @return mixed
*/
function insert() function insert()
{ {
$this->keypair = $this->toString(); $this->keypair = $this->toString();
@ -110,6 +155,14 @@ class Magicsig extends Memcached_DataObject
return parent::insert(); return parent::insert();
} }
/**
* Generate a new keypair for a local user and store in the database.
*
* Warning: this can be very slow on systems without the GMP module.
* Runtimes of 20-30 seconds are not unheard-of.
*
* @param int $user_id id of local user we're creating a key for
*/
public function generate($user_id) public function generate($user_id)
{ {
$rsa = new Crypt_RSA(); $rsa = new Crypt_RSA();
@ -128,6 +181,12 @@ class Magicsig extends Memcached_DataObject
$this->insert(); $this->insert();
} }
/**
* Encode the keypair or public key as a string.
*
* @param boolean $full_pair set to false to leave out the private key.
* @return string
*/
public function toString($full_pair = true) public function toString($full_pair = true)
{ {
$mod = Magicsig::base64_url_encode($this->publicKey->modulus->toBytes()); $mod = Magicsig::base64_url_encode($this->publicKey->modulus->toBytes());
@ -140,6 +199,13 @@ class Magicsig extends Memcached_DataObject
return 'RSA.' . $mod . '.' . $exp . $private_exp; return 'RSA.' . $mod . '.' . $exp . $private_exp;
} }
/**
* Decode a string representation of an RSA public key or keypair
* as a Magicsig object which can be used to sign or verify.
*
* @param string $text
* @return Magicsig
*/
public static function fromString($text) public static function fromString($text)
{ {
$magic_sig = new Magicsig(); $magic_sig = new Magicsig();
@ -168,6 +234,14 @@ class Magicsig extends Memcached_DataObject
return $magic_sig; return $magic_sig;
} }
/**
* Fill out $this->privateKey or $this->publicKey with a Crypt_RSA object
* representing the give key (as mod/exponent pair).
*
* @param string $mod base64-encoded
* @param string $exp base64-encoded exponent
* @param string $type one of 'public' or 'private'
*/
public function loadKey($mod, $exp, $type = 'public') public function loadKey($mod, $exp, $type = 'public')
{ {
common_log(LOG_DEBUG, "Adding ".$type." key: (".$mod .', '. $exp .")"); common_log(LOG_DEBUG, "Adding ".$type." key: (".$mod .', '. $exp .")");
@ -186,11 +260,22 @@ class Magicsig extends Memcached_DataObject
} }
} }
/**
* Returns the name of the crypto algorithm used for this key.
*
* @return string
*/
public function getName() public function getName()
{ {
return $this->alg; return $this->alg;
} }
/**
* Returns the name of a hash function to use for signing with this key.
*
* @return string
* @fixme is this used? doesn't seem to be called by name.
*/
public function getHash() public function getHash()
{ {
switch ($this->alg) { switch ($this->alg) {
@ -200,24 +285,48 @@ class Magicsig extends Memcached_DataObject
} }
} }
/**
* Generate base64-encoded signature for the given byte string
* using our private key.
*
* @param string $bytes as raw byte string
* @return string base64-encoded signature
*/
public function sign($bytes) public function sign($bytes)
{ {
$sig = $this->privateKey->sign($bytes); $sig = $this->privateKey->sign($bytes);
return Magicsig::base64_url_encode($sig); return Magicsig::base64_url_encode($sig);
} }
/**
*
* @param string $signed_bytes as raw byte string
* @param string $signature as base64
* @return boolean
*/
public function verify($signed_bytes, $signature) public function verify($signed_bytes, $signature)
{ {
$signature = Magicsig::base64_url_decode($signature); $signature = Magicsig::base64_url_decode($signature);
return $this->publicKey->verify($signed_bytes, $signature); return $this->publicKey->verify($signed_bytes, $signature);
} }
/**
* URL-encoding-friendly base64 variant encoding.
*
* @param string $input
* @return string
*/
public static function base64_url_encode($input) public static function base64_url_encode($input)
{ {
return strtr(base64_encode($input), '+/', '-_'); return strtr(base64_encode($input), '+/', '-_');
} }
/**
* URL-encoding-friendly base64 variant decoding.
*
* @param string $input
* @return string
*/
public static function base64_url_decode($input) public static function base64_url_decode($input)
{ {
return base64_decode(strtr($input, '-_', '+/')); return base64_decode(strtr($input, '-_', '+/'));

View File

@ -331,6 +331,7 @@ class Ostatus_profile extends Memcached_DataObject
* an acceptable response from the remote site. * an acceptable response from the remote site.
* *
* @param mixed $entry XML string, Notice, or Activity * @param mixed $entry XML string, Notice, or Activity
* @param Profile $actor
* @return boolean success * @return boolean success
*/ */
public function notifyActivity($entry, $actor) public function notifyActivity($entry, $actor)
@ -1778,8 +1779,10 @@ class Ostatus_profile extends Memcached_DataObject
case 'mailto': case 'mailto':
$rest = $match[2]; $rest = $match[2];
$oprofile = Ostatus_profile::ensureWebfinger($rest); $oprofile = Ostatus_profile::ensureWebfinger($rest);
break;
default: default:
common_log("Unrecognized URI protocol for profile: $protocol ($uri)"); common_log(LOG_WARNING,
"Unrecognized URI protocol for profile: $protocol ($uri)");
break; break;
} }
} }

View File

@ -80,21 +80,53 @@ class MagicEnvelope
throw new Exception(_m('Unable to locate signer public key.')); throw new Exception(_m('Unable to locate signer public key.'));
} }
/**
* The current MagicEnvelope spec as used in StatusNet 0.9.7 and later
* includes both the original data and some signing metadata fields as
* the input plaintext for the signature hash.
*
* @param array $env
* @return string
*/
public function signingText($env) {
return implode('.', array($env['data'], // this field is pre-base64'd
Magicsig::base64_url_encode($env['data_type']),
Magicsig::base64_url_encode($env['encoding']),
Magicsig::base64_url_encode($env['alg'])));
}
/**
*
* @param <type> $text
* @param <type> $mimetype
* @param <type> $keypair
* @return array: associative array of envelope properties
* @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
*/
public function signMessage($text, $mimetype, $keypair) public function signMessage($text, $mimetype, $keypair)
{ {
$signature_alg = Magicsig::fromString($keypair); $signature_alg = Magicsig::fromString($keypair);
$armored_text = Magicsig::base64_url_encode($text); $armored_text = Magicsig::base64_url_encode($text);
$env = array(
return array(
'data' => $armored_text, 'data' => $armored_text,
'encoding' => MagicEnvelope::ENCODING, 'encoding' => MagicEnvelope::ENCODING,
'data_type' => $mimetype, 'data_type' => $mimetype,
'sig' => $signature_alg->sign($armored_text), 'sig' => '',
'alg' => $signature_alg->getName() 'alg' => $signature_alg->getName()
); );
$env['sig'] = $signature_alg->sign($this->signingText($env));
return $env;
} }
/**
* Create an <me:env> XML representation of the envelope.
*
* @param array $env associative array with envelope data
* @return string representation of XML document
* @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
*/
public function toXML($env) { public function toXML($env) {
$xs = new XMLStringer(); $xs = new XMLStringer();
$xs->startXML(); $xs->startXML();
@ -110,6 +142,16 @@ class MagicEnvelope
return $string; return $string;
} }
/**
* Extract the contained XML payload, and insert a copy of the envelope
* signature data as an <me:provenance> section.
*
* @param array $env associative array with envelope data
* @return string representation of modified XML document
*
* @fixme in case of XML parsing errors, this will spew to the error log or output
* @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
*/
public function unfold($env) public function unfold($env)
{ {
$dom = new DOMDocument(); $dom = new DOMDocument();
@ -136,6 +178,14 @@ class MagicEnvelope
return $dom->saveXML(); return $dom->saveXML();
} }
/**
* Find the author URI referenced in the given Atom entry.
*
* @param string $text string containing Atom entry XML
* @return mixed URI string or false if XML parsing fails, or null if no author URI can be found
*
* @fixme XML parsing failures will spew to error logs/output
*/
public function getAuthor($text) { public function getAuthor($text) {
$doc = new DOMDocument(); $doc = new DOMDocument();
if (!$doc->loadXML($text)) { if (!$doc->loadXML($text)) {
@ -153,11 +203,30 @@ class MagicEnvelope
} }
} }
/**
* Check if the author in the Atom entry fragment claims to match
* the given identifier URI.
*
* @param string $text string containing Atom entry XML
* @param string $signer_uri
* @return boolean
*/
public function checkAuthor($text, $signer_uri) public function checkAuthor($text, $signer_uri)
{ {
return ($this->getAuthor($text) == $signer_uri); return ($this->getAuthor($text) == $signer_uri);
} }
/**
* Attempt to verify cryptographic signing for parsed envelope data.
* Requires network access to retrieve public key referenced by the envelope signer.
*
* Details of failure conditions are dumped to output log and not exposed to caller.
*
* @param array $env array representation of magic envelope data, as returned from MagicEnvelope::parse()
* @return boolean
*
* @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
*/
public function verify($env) public function verify($env)
{ {
if ($env['alg'] != 'RSA-SHA256') { if ($env['alg'] != 'RSA-SHA256') {
@ -187,15 +256,35 @@ class MagicEnvelope
return false; return false;
} }
return $verifier->verify($env['data'], $env['sig']); return $verifier->verify($this->signingText($env), $env['sig']);
} }
/**
* Extract envelope data from an XML document containing an <me:env> or <me:provenance> element.
*
* @param string XML source
* @return mixed associative array of envelope data, or false on unrecognized input
*
* @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
* @fixme will spew errors to logs or output in case of XML parse errors
* @fixme may give fatal errors if some elements are missing or invalid XML
* @fixme calling DOMDocument::loadXML statically triggers warnings in strict mode
*/
public function parse($text) public function parse($text)
{ {
$dom = DOMDocument::loadXML($text); $dom = DOMDocument::loadXML($text);
return $this->fromDom($dom); return $this->fromDom($dom);
} }
/**
* Extract envelope data from an XML document containing an <me:env> or <me:provenance> element.
*
* @param DOMDocument $dom
* @return mixed associative array of envelope data, or false on unrecognized input
*
* @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
* @fixme may give fatal errors if some elements are missing
*/
public function fromDom($dom) public function fromDom($dom)
{ {
$env_element = $dom->getElementsByTagNameNS(MagicEnvelope::NS, 'env')->item(0); $env_element = $dom->getElementsByTagNameNS(MagicEnvelope::NS, 'env')->item(0);
@ -218,3 +307,24 @@ class MagicEnvelope
); );
} }
} }
/**
* Variant of MagicEnvelope using the earlier signature form listed in the MagicEnvelope
* spec in early 2010; this was used in StatusNet up through 0.9.6, so for backwards compatiblity
* we still need to accept and sometimes send this format.
*/
class MagicEnvelopeCompat extends MagicEnvelope {
/**
* StatusNet through 0.9.6 used an earlier version of the MagicEnvelope spec
* which used only the input data, without the additional fields, as the plaintext
* for signing.
*
* @param array $env
* @return string
*/
public function signingText($env) {
return $env['data'];
}
}

View File

@ -38,10 +38,12 @@ class Salmon
/** /**
* Sign and post the given Atom entry as a Salmon message. * Sign and post the given Atom entry as a Salmon message.
* *
* @fixme pass through the actor for signing? * Side effects: may generate a keypair on-demand for the given user,
* which can be very slow on some systems.
* *
* @param string $endpoint_uri * @param string $endpoint_uri
* @param string $xml * @param string $xml string representation of payload
* @param Profile $actor local user profile whose keys to sign with
* @return boolean success * @return boolean success
*/ */
public function post($endpoint_uri, $xml, $actor) public function post($endpoint_uri, $xml, $actor)
@ -50,8 +52,9 @@ class Salmon
return false; return false;
} }
foreach ($this->formatClasses() as $class) {
try { try {
$xml = $this->createMagicEnv($xml, $actor); $envelope = $this->createMagicEnv($xml, $actor, $class);
} catch (Exception $e) { } catch (Exception $e) {
common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage()); common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage());
return false; return false;
@ -61,23 +64,53 @@ class Salmon
try { try {
$client = new HTTPClient(); $client = new HTTPClient();
$client->setBody($xml); $client->setBody($envelope);
$response = $client->post($endpoint_uri, $headers); $response = $client->post($endpoint_uri, $headers);
} catch (HTTP_Request2_Exception $e) { } catch (HTTP_Request2_Exception $e) {
common_log(LOG_ERR, "Salmon post to $endpoint_uri failed: " . $e->getMessage()); common_log(LOG_ERR, "Salmon ($class) post to $endpoint_uri failed: " . $e->getMessage());
return false; continue;
} }
if ($response->getStatus() != 200) { if ($response->getStatus() != 200) {
common_log(LOG_ERR, "Salmon at $endpoint_uri returned status " . common_log(LOG_ERR, "Salmon ($class) at $endpoint_uri returned status " .
$response->getStatus() . ': ' . $response->getBody()); $response->getStatus() . ': ' . $response->getBody());
return false; continue;
}
return true;
} }
public function createMagicEnv($text, $actor) // Success!
return true;
}
return false;
}
/**
* List the magic envelope signature class variants in the order we try them.
* Multiples are needed for backwards-compat with StatusNet prior to 0.9.7,
* which used a draft version of the magic envelope spec.
*/
protected function formatClasses() {
return array('MagicEnvelope', 'MagicEnvelopeCompat');
}
/**
* Encode the given string as a signed MagicEnvelope XML document,
* using the keypair for the given local user profile.
*
* 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 Profile $actor Profile of a local user to use as signer
* @param string $class to override the magic envelope signature version, pass a MagicEnvelope subclass here
*
* @return string XML string representation of magic envelope
*
* @throws Exception on bad profile input or key generation problems
* @fixme if signing fails, this seems to return the original text without warning. Is there a reason for this?
*/
public function createMagicEnv($text, $actor, $class='MagicEnvelope')
{ {
$magic_env = new MagicEnvelope(); $magic_env = new $class();
$user = User::staticGet('id', $actor->id); $user = User::staticGet('id', $actor->id);
if ($user->id) { if ($user->id) {
@ -101,12 +134,32 @@ class Salmon
return $magic_env->toXML($env); return $magic_env->toXML($env);
} }
/**
* Check if the given magic envelope is well-formed and correctly signed.
* Needs to have network access to fetch public keys over the web.
* Both current and back-compat signature formats will be checked.
*
* Side effects: exceptions and caching updates may occur during network
* fetches.
*
* @param string $text XML fragment of magic envelope
* @return boolean
*
* @throws Exception on bad profile input or key generation problems
* @fixme could hit fatal errors or spew output on invalid XML
*/
public function verifyMagicEnv($text) public function verifyMagicEnv($text)
{ {
$magic_env = new MagicEnvelope(); foreach ($this->formatClasses() as $class) {
$magic_env = new $class();
$env = $magic_env->parse($text); $env = $magic_env->parse($text);
return $magic_env->verify($env); if ($magic_env->verify($env)) {
return true;
}
}
return false;
} }
} }

View File

@ -0,0 +1,60 @@
<?php
if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
print "This script must be run from the command line\n";
exit();
}
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
define('STATUSNET', true);
require_once INSTALLDIR . '/lib/common.php';
class MagicEnvelopeTest extends PHPUnit_Framework_TestCase
{
/**
* Test that MagicEnvelope builds the correct plaintext for signing.
* @dataProvider provider
*/
public function testSignatureText($env, $expected)
{
$magic = new MagicEnvelope;
$text = $magic->signingText($env);
$this->assertEquals($expected, $text, "'$text' should be '$expected'");
}
static public function provider()
{
return array(
array(
// Sample case given in spec:
// http://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-magicsig-00.html#signing
array(
'data' => 'Tm90IHJlYWxseSBBdG9t',
'data_type' => 'application/atom+xml',
'encoding' => 'base64url',
'alg' => 'RSA-SHA256'
),
'Tm90IHJlYWxseSBBdG9t.YXBwbGljYXRpb24vYXRvbSt4bWw=.YmFzZTY0dXJs.UlNBLVNIQTI1Ng=='
)
);
}
/**
* Test that MagicEnvelope builds the correct plaintext for signing.
* @dataProvider provider
*/
public function testSignatureTextCompat($env, $expected)
{
// Our old code didn't add the extra fields, just used the armored text.
$alt = $env['data'];
$magic = new MagicEnvelopeCompat;
$text = $magic->signingText($env);
$this->assertEquals($alt, $text, "'$text' should be '$alt'");
}
}

View File

@ -72,6 +72,8 @@ class OStatusTester extends TestBase
$base = 'test' . mt_rand(1, 1000000); $base = 'test' . mt_rand(1, 1000000);
$this->pub = new SNTestClient($this->a, 'pub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout); $this->pub = new SNTestClient($this->a, 'pub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout);
$this->sub = new SNTestClient($this->b, 'sub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout); $this->sub = new SNTestClient($this->b, 'sub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout);
$this->group = 'group' . $base;
} }
function run() function run()
@ -163,6 +165,39 @@ class OStatusTester extends TestBase
$this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri())); $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
} }
function testCreateGroup()
{
$this->groupUrl = $this->pub->createGroup($this->group);
$this->assertTrue(!empty($this->groupUrl));
}
function testJoinGroup()
{
#$this->assertFalse($this->sub->inGroup($this->groupUrl));
$this->sub->joinGroup($this->groupUrl);
#$this->assertTrue($this->sub->inGroup($this->groupUrl));
}
function testLocalGroupPost()
{
$post = $this->pub->post("Group post from local to !{$this->group}, should go out over push.");
$this->assertNotEqual('', $post);
$this->sub->assertReceived($post);
}
function testRemoteGroupPost()
{
$post = $this->sub->post("Group post from remote to !{$this->group}, should come in over salmon.");
$this->assertNotEqual('', $post);
$this->pub->assertReceived($post);
}
function testLeaveGroup()
{
#$this->assertTrue($this->sub->inGroup($this->groupUrl));
$this->sub->leaveGroup($this->groupUrl);
#$this->assertFalse($this->sub->inGroup($this->groupUrl));
}
} }
class SNTestClient extends TestBase class SNTestClient extends TestBase
@ -534,6 +569,63 @@ class SNTestClient extends TestBase
return false; return false;
} }
/**
* Create a group on this site.
*
* @param string $nickname
* @param array $options
* @return string: profile URL for the group
*/
function createGroup($nickname, $options=array()) {
$this->log("Creating group as %s on %s: %s",
$this->username,
$this->basepath,
$nickname);
$data = $this->api('statusnet/groups/create', 'json',
array_merge(array('nickname' => $nickname), $options));
$url = $data['url'];
if ($url) {
$this->log(' created as %s', $url);
} else {
$this->log(' failed? %s', var_export($data, true));
}
return $url;
}
function groupInfo($nickname) {
$data = $this->api('statusnet/groups/show', 'json', array(
'id' => $nickname
));
}
/**
* Join a group.
*
* @param string $group nickname or URL
*/
function joinGroup($group) {
$this->post('join ' . $group);
}
/**
* Leave a group.
*
* @param string $group nickname or URL
*/
function leaveGroup($group) {
$this->post('drop ' . $group);
}
/**
*
* @param string $nickname
* @return
*/
function inGroup($nickname) {
// @todo
}
} }
// @fixme switch to commandline.inc? // @fixme switch to commandline.inc?

View File

@ -0,0 +1,92 @@
#!/usr/bin/env php
<?php
/*
* StatusNet - a distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
$longoptions = array('verify', 'slap=', 'notice=');
$helptext = <<<END_OF_HELP
slap.php [options]
Test generation and sending of magic envelopes for Salmon slaps.
--notice=N generate entry for this notice number
--verify send signed magic envelope to Tuomas Koski's test service
--slap=<url> send signed Salmon slap to the destination endpoint
END_OF_HELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
if (!have_option('--notice')) {
print "$helptext";
exit(1);
}
$notice_id = get_option_value('--notice');
$notice = Notice::staticGet('id', $notice_id);
$profile = $notice->getProfile();
$entry = $notice->asAtomEntry(true);
echo "== Original entry ==\n\n";
print $entry;
print "\n\n";
$salmon = new Salmon();
$envelope = $salmon->createMagicEnv($entry, $profile);
echo "== Signed envelope ==\n\n";
print $envelope;
print "\n\n";
echo "== Testing local verification ==\n\n";
$ok = $salmon->verifyMagicEnv($envelope);
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' => $envelope));
print $response->getStatus() . "\n\n";
print $response->getBody() . "\n\n";
}
if (have_option('--slap')) {
$url = get_option_value('--slap');
echo "== Remote salmon slap ==\n\n";
print "Sending signed Salmon slap to $url ...\n";
$ok = $salmon->post($url, $entry, $profile);
if ($ok) {
print "OK\n\n";
} else {
print "FAIL\n\n";
}
}

View File

@ -382,6 +382,29 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase
} }
} }
public function testExample10()
{
global $_example10;
$dom = new DOMDocument();
$dom->loadXML($_example10);
// example 10 is a PuSH item of a post on a group feed, as generated
// by 0.9.7 code after migration away from <activity:actor> to <author>
$feed = $dom->documentElement;
$entry = $dom->getElementsByTagName('entry')->item(0);
$expected = 'http://lazarus.local/mublog/user/557';
// Reading just the entry alone should pick up its own <author>
// as the actor.
$act = new Activity($entry);
$this->assertEquals($act->actor->id, $expected);
// Reading the entry in feed context used to be buggy, picking up
// the feed's <activity:subject> which referred to the group.
// It should now be returning the expected author entry...
$act = new Activity($entry, $feed);
$this->assertEquals($act->actor->id, $expected);
}
} }
$_example1 = <<<EXAMPLE1 $_example1 = <<<EXAMPLE1
@ -792,3 +815,93 @@ $_example9 = <<<EXAMPLE9
</entry> </entry>
</feed> </feed>
EXAMPLE9; EXAMPLE9;
// Sample PuSH entry from a group feed in 0.9.7
// Old <activity:actor> has been removed from entries in this version.
// A bug in the order of input processing meant that we were incorrectly
// reading the feed's <activity:subject> instead of the entry's <author>,
// causing the entry to get rejected as malformed (groups can't post on
// their own; we want to see the actual author's info here).
$_example10 = <<<EXAMPLE10
<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
<generator uri="http://status.net" version="0.9.7alpha1">StatusNet</generator>
<id>http://lazarus.local/mublog/api/statusnet/groups/timeline/22.atom</id>
<title>grouptest316173 timeline</title>
<subtitle>Updates from grouptest316173 on Blaguette!</subtitle>
<logo>http://lazarus.local/mublog/theme/default/default-avatar-profile.png</logo>
<updated>2011-01-06T22:44:18+00:00</updated>
<author>
<activity:object-type>http://activitystrea.ms/schema/1.0/group</activity:object-type>
<uri>http://lazarus.local/mublog/group/22/id</uri>
<name>grouptest316173</name>
<link rel="alternate" type="text/html" href="http://lazarus.local/mublog/group/22/id"/>
<link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://lazarus.local/mublog/theme/default/default-avatar-profile.png"/>
<link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://lazarus.local/mublog/theme/default/default-avatar-stream.png"/>
<link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://lazarus.local/mublog/theme/default/default-avatar-mini.png"/>
<poco:preferredUsername>grouptest316173</poco:preferredUsername>
<poco:displayName>grouptest316173</poco:displayName>
</author>
<activity:subject>
<activity:object-type>http://activitystrea.ms/schema/1.0/group</activity:object-type>
<id>http://lazarus.local/mublog/group/22/id</id>
<title>grouptest316173</title>
<link rel="alternate" type="text/html" href="http://lazarus.local/mublog/group/22/id"/>
<link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://lazarus.local/mublog/theme/default/default-avatar-profile.png"/>
<link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://lazarus.local/mublog/theme/default/default-avatar-stream.png"/>
<link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://lazarus.local/mublog/theme/default/default-avatar-mini.png"/>
<poco:preferredUsername>grouptest316173</poco:preferredUsername>
<poco:displayName>grouptest316173</poco:displayName>
</activity:subject>
<link href="http://lazarus.local/mublog/group/grouptest316173" rel="alternate" type="text/html"/>
<link href="http://lazarus.local/mublog/main/push/hub" rel="hub"/>
<link href="http://lazarus.local/mublog/main/salmon/group/22" rel="salmon"/>
<link href="http://lazarus.local/mublog/main/salmon/group/22" rel="http://salmon-protocol.org/ns/salmon-replies"/>
<link href="http://lazarus.local/mublog/main/salmon/group/22" rel="http://salmon-protocol.org/ns/salmon-mention"/>
<link href="http://lazarus.local/mublog/api/statusnet/groups/timeline/22.atom" rel="self" type="application/atom+xml"/>
<statusnet:group_info member_count="2"></statusnet:group_info>
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<id>http://lazarus.local/mublog/notice/1243</id>
<title>Group post from local to !grouptest316173, should go out over push.</title>
<content type="html">Group post from local to !&lt;span class=&quot;vcard&quot;&gt;&lt;a href=&quot;http://lazarus.local/mublog/group/22/id&quot; class=&quot;url&quot;&gt;&lt;span class=&quot;fn nickname&quot;&gt;grouptest316173&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;, should go out over push.</content>
<link rel="alternate" type="text/html" href="http://lazarus.local/mublog/notice/1243"/>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<published>2011-01-06T22:44:18+00:00</published>
<updated>2011-01-06T22:44:18+00:00</updated>
<author>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<uri>http://lazarus.local/mublog/user/557</uri>
<name>Pubtest316173 Smith</name>
<link rel="alternate" type="text/html" href="http://lazarus.local/mublog/pubtest316173"/>
<link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://lazarus.local/mublog/theme/default/default-avatar-profile.png"/>
<link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://lazarus.local/mublog/theme/default/default-avatar-stream.png"/>
<link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://lazarus.local/mublog/theme/default/default-avatar-mini.png"/>
<poco:preferredUsername>pubtest316173</poco:preferredUsername>
<poco:displayName>Pubtest316173 Smith</poco:displayName>
<poco:note>Stub account for OStatus tests.</poco:note>
<poco:urls>
<poco:type>homepage</poco:type>
<poco:value>http://example.org/pubtest316173</poco:value>
<poco:primary>true</poco:primary>
</poco:urls>
</author>
<link rel="ostatus:conversation" href="http://lazarus.local/mublog/conversation/1131"/>
<link rel="ostatus:attention" href="http://lazarus.local/mublog/group/22/id"/>
<link rel="mentioned" href="http://lazarus.local/mublog/group/22/id"/>
<category term="grouptest316173"></category>
<source>
<id>http://lazarus.local/mublog/api/statuses/user_timeline/557.atom</id>
<title>Pubtest316173 Smith</title>
<link rel="alternate" type="text/html" href="http://lazarus.local/mublog/pubtest316173"/>
<link rel="self" type="application/atom+xml" href="http://lazarus.local/mublog/api/statuses/user_timeline/557.atom"/>
<link rel="license" href="http://creativecommons.org/licenses/by/3.0/"/>
<icon>http://lazarus.local/mublog/theme/default/default-avatar-profile.png</icon>
<updated>2011-01-06T22:44:18+00:00</updated>
</source>
<link rel="self" type="application/atom+xml" href="http://lazarus.local/mublog/api/statuses/show/1243.atom"/>
<link rel="edit" type="application/atom+xml" href="http://lazarus.local/mublog/api/statuses/show/1243.atom"/>
<statusnet:notice_info local_id="1243" source="api"></statusnet:notice_info>
</entry>
</feed>
EXAMPLE10;