Refactor ActivityEvent to also have the actor; implement NonActivityHandler

This commit is contained in:
Jeremy Dormitzer 2019-01-22 17:36:21 -05:00
parent 19aa482d78
commit ffc32e312c
18 changed files with 294 additions and 95 deletions

View File

@ -1,6 +1,7 @@
<?php
namespace ActivityPub\Activities;
use ActivityPub\Entities\ActivityPubObject;
use Symfony\Component\EventDispatcher\Event;
class ActivityEvent extends Event
@ -12,9 +13,17 @@ class ActivityEvent extends Event
*/
protected $activity;
protected function __construct( array $activity )
/**
* The actor posting or receiving the activity
*
* @var ActivityPubObject
*/
protected $actor;
protected function __construct( array $activity, ActivityPubObject $actor )
{
$this->activity = $activity;
$this->actor = $actor;
}
/**
@ -29,5 +38,13 @@ class ActivityEvent extends Event
{
$this->activity = $activity;
}
/**
* @return ActivityPubObject The actor
*/
public function getActor()
{
return $this->actor;
}
}
?>

View File

@ -2,31 +2,9 @@
namespace ActivityPub\Activities;
use ActivityPub\Activities\ActivityEvent;
use ActivityPub\Entities\ActivityPubObject;
class InboxActivityEvent extends ActivityEvent
{
const NAME = 'inbox.activity';
/**
* The inbox to which the activity was posted
*
* @var ActivityPubObject
*/
protected $inbox;
public function __construct( array $activity, ActivityPubObject $inbox )
{
parent::__construct( $activity );
$this->inbox = $inbox;
}
/**
* @return ActivityPubObject The inbox
*/
public function getInbox()
{
return $this->inbox;
}
}
?>

View File

@ -2,6 +2,7 @@
namespace ActivityPub\Activities;
use ActivityPub\Activities\OutboxActivityEvent;
use ActivityPub\Objects\IdProvider;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
@ -13,6 +14,11 @@ class NonActivityHandler implements EventSubscriberInterface
* @var ContextProvider
*/
private $contextProvider;
/**
* @var IdProvider
*/
private $idProvider;
const ACTIVITY_TYPES = array(
'Accept', 'Add', 'Announce', 'Arrive',
@ -44,13 +50,27 @@ class NonActivityHandler implements EventSubscriberInterface
/**
* Makes a new Create activity with $object as the object
*
* @param Request $request The current request
* @param array $object The object
* @param ActivityPubObject $actorId The actor creating the object
*
* @return array The Create activity
*/
private function makeCreate( array $object )
private function makeCreate( Request $request, array $object,
ActivityPubObject $actor )
{
// TODO implement me
// if object doesn't have an id, generate one
// generate an id for the Create activity as well
$create = array(
'@context' => $this->contextProvider->getContext(),
'type' => 'Create',
'id' => $this->idProvider->getId( $request, "activities" ),
'actor' => $actor['id'],
'object' => $object,
);
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $field ) {
if ( array_key_exists( $field, $object ) ) {
$create[$field] = $object[$field];
}
}
}
}
?>

View File

@ -2,31 +2,9 @@
namespace ActivityPub\Activities;
use ActivityPub\Activities\ActivityEvent;
use ActivityPub\Entities\ActivityPubObject;
class OutboxActivityEvent extends ActivityEvent
{
const NAME = 'outbox.activity';
/**
* The outbox to which the activity was posted
*
* @var ActivityPubObject
*/
protected $outbox;
public function __construct( array $activity, ActivityPubObject $outbox )
{
parent::__construct( $activity );
$this->outbox = $outbox;
}
/**
* @return ActivityPubObject The outbox
*/
public function getOutbox()
{
return $this->outbox;
}
}
?>

View File

@ -1,6 +1,8 @@
<?php
namespace ActivityPub\Auth;
use Exception;
use ActivityPub\Objects\ObjectsService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
@ -22,6 +24,11 @@ class AuthListener implements EventSubscriberInterface
*/
private $authFunction;
/**
* @var ObjectsService
*/
private $objectsService;
public static function getSubscribedEvents()
{
return array(
@ -35,9 +42,10 @@ class AuthListener implements EventSubscriberInterface
* @param Callable $authFunction A Callable that should accept
*
*/
public function __construct( Callable $authFunction )
public function __construct( Callable $authFunction, ObjectsService $objectsService )
{
$this->authFunction = $authFunction;
$this->objectsService = $objectsService;
}
public function checkAuth( GetResponseEvent $event )
@ -48,7 +56,11 @@ class AuthListener implements EventSubscriberInterface
}
$actorId = call_user_func( $this->authFunction );
if ( $actorId && ! empty( $actorId ) ) {
$request->attributes->set( 'actor', $actorId );
$actor = $this->objectsService->dereference( $actorId );
if ( ! $actor ) {
throw new Exception( "Actor $actorId does not exist" );
}
$request->attributes->set( 'actor', $actor );
}
}
}

View File

@ -67,8 +67,8 @@ class SignatureListener implements EventSubscriberInterface
return;
}
$owner = $key['owner'];
if ( ! is_string( $owner ) ) {
$owner = $owner['id'];
if ( is_string( $owner ) ) {
$owner = $this->objectsService->dereference( $owner );
}
if ( ! $owner ) {
return;
@ -77,7 +77,6 @@ class SignatureListener implements EventSubscriberInterface
return;
}
$request->attributes->set( 'signed', true );
$request->attributes->set( 'signedBy', $owner );
if ( ! $request->attributes->has( 'actor' ) ) {
$request->attributes->set( 'actor', $owner );
}

View File

@ -25,10 +25,27 @@ class InboxController
*/
public function handle( Request $request )
{
if ( ! $request->attributes->has( 'actor' ) ) {
throw new UnauthorizedHttpException();
}
$actor = $request->attributes->get( 'actor' );
$inboxId = $this->getUriWithoutQuery( $request );
if ( ! $actor->hasField( 'inbox' ) || $actor['inbox']['id'] !== $inboxId ) {
throw new UnauthorizedHttpException();
}
$activity = $request->attributes->get( 'activity' );
$inbox = $request->attributes->get( 'inbox' );
$event = new InboxActivityEvent( $activity, $inbox );
$event = new InboxActivityEvent( $activity, $actor );
$this->eventDispatcher->dispatch( InboxActivityEvent::NAME, $event );
}
private function getUriWithoutQuery( Request $request )
{
$uri = $request->getUri();
$queryPos = strpos( $uri, '?' );
if ( $queryPos !== false ) {
$uri = substr( $uri, 0, $queryPos );
}
return $uri;
}
}
?>

View File

@ -4,6 +4,7 @@ namespace ActivityPub\Controllers;
use ActivityPub\Activities\OutboxActivityEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class OutboxController
{
@ -25,10 +26,27 @@ class OutboxController
*/
public function handle( Request $request )
{
if ( ! $request->attributes->has( 'actor' ) ) {
throw new UnauthorizedHttpException();
}
$actor = $request->attributes->get( 'actor' );
$outboxId = $this->getUriWithoutQuery( $request );
if ( ! $actor->hasField( 'outbox' ) || $actor['outbox']['id'] !== $outboxId ) {
throw new UnauthorizedHttpException();
}
$activity = $request->attributes->get( 'activity' );
$outbox = $request->attributes->get( 'outbox' );
$event = new OutboxActivityEvent( $activity, $outbox );
$event = new OutboxActivityEvent( $activity, $actor );
$this->eventDispatcher->dispatch( OutboxActivityEvent::NAME, $event );
}
private function getUriWithoutQuery( Request $request )
{
$uri = $request->getUri();
$queryPos = strpos( $uri, '?' );
if ( $queryPos !== false ) {
$uri = substr( $uri, 0, $queryPos );
}
return $uri;
}
}
?>

View File

@ -82,6 +82,8 @@ class RsaKeypair
*/
public function verify( $data, $signature, $hash = 'sha256' )
{
// TODO this throws a "Signature representative out of range" occasionally
// I have no idea what that means or how to fix it
$rsa = new RSA();
$rsa->setHash( $hash );
$rsa->setSignatureMode(RSA::SIGNATURE_PKCS1);

View File

@ -149,6 +149,21 @@ class ActivityPubObject implements ArrayAccess
return false;
}
/**
* Returns the fields named $field, if it exists
*
* @param string $name The name of the field to get
* @return Field|null
*/
public function getField( string $name )
{
foreach( $this->getFields() as $field ) {
if ( $field->getName() === $name ) {
return $field;
}
}
}
/**
* Returns the value of the field with key $name
*
@ -286,5 +301,26 @@ class ActivityPubObject implements ArrayAccess
'ActivityPubObject fields cannot be directly unset'
);
}
/**
* Returns true if $other has all the same fields as $this
*
* @param ActivityPubObject $other The other object to compare to
* @return bool Whether or not this object has the same fields and values as
* the other
*/
public function equals( ActivityPubObject $other )
{
foreach( $other->getFields() as $otherField ) {
$thisField = $this->getField( $otherField->getName() );
if ( ! $thisField ) {
return false;
}
if ( ! $thisField->equals( $otherField ) ) {
return false;
}
}
return true;
}
}
?>

View File

@ -257,5 +257,23 @@ class Field
{
return $this->lastUpdated;
}
/**
* Returns true if $this is equal to $other
*
* @return bool
*/
public function equals( Field $other )
{
if ( $this->getName() !== $other->getName() ) {
return false;
}
if ( $this->hasValue() ) {
return $other->hasValue() && $other->getValue() === $this->getValue();
} else {
return $other->hasTargetObject() &&
$this->getTargetObject()->equals( $other->getTargetObject() );
}
}
}
?>

View File

@ -50,7 +50,7 @@ class ControllerResolver implements ControllerResolverInterface
if ( $request->getMethod() == Request::METHOD_GET ) {
return array( $this->getObjectController, 'handle' );
} else if ( $request->getMethod() == Request::METHOD_POST ) {
$uri = $request->getUri();
$uri = $this->getUriWithoutQuery( $request );
$actorWithInbox = $this->objectWithField( 'inbox', $uri );
if ( $actorWithInbox ) {
$activity = json_decode( $request->getContent(), true );
@ -58,7 +58,6 @@ class ControllerResolver implements ControllerResolverInterface
throw new BadRequestHttpException( '"type" field not found' );
}
$request->attributes->set( 'activity', $activity );
$request->attributes->set( 'inbox', $actorWithInbox->getFieldValue( 'inbox' ) );
return array( $this->inboxController, 'handle' );
} else {
$actorWithOutbox = $this->objectWithField( 'outbox', $uri );
@ -68,7 +67,6 @@ class ControllerResolver implements ControllerResolverInterface
throw new BadRequestHttpException( '"type" field not found' );
}
$request->attributes->set( 'activity', $activity );
$request->attributes->set( 'outbox', $actorWithOutbox->getFieldValue( 'outbox' ) );
return array( $this->outboxController, 'handle' );
} else {
throw new NotFoundHttpException();
@ -81,5 +79,15 @@ class ControllerResolver implements ControllerResolverInterface
) );
}
}
private function getUriWithoutQuery( Request $request )
{
$uri = $request->getUri();
$queryPos = strpos( $uri, '?' );
if ( $queryPos !== false ) {
$uri = substr( $uri, 0, $queryPos );
}
return $uri;
}
}
?>

View File

@ -0,0 +1,14 @@
<?php
namespace ActivityPub\Test\Activities;
use PHPUnit\Framework\TestCase;
class NonActivityHandlerTest extends TestCase
{
public function testNonActivityHandler()
{
// TODO implement me
$this->assertTrue( false );
}
}
?>

View File

@ -2,6 +2,9 @@
namespace ActivityPub\Test\Auth;
use ActivityPub\Auth\AuthListener;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Test\TestUtils\TestUtils;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
@ -9,6 +12,20 @@ use PHPUnit\Framework\TestCase;
class AuthListenerTest extends TestCase
{
private $objectsService;
public function setUp()
{
$this->objectsService = $this->createMock( ObjectsService::class );
$this->objectsService->method( 'dereference' )->will( $this->returnValueMap( array(
array( 'https://example.com/actor/1', TestUtils::objectFromArray( array(
'id' => 'https://example.com/actor/1',
) ) ),
array( 'https://example.com/actor/2', TestUtils::objectFromArray( array(
'id' => 'https://example.com/actor/2',
) ) ),
) ) );
}
public function getEvent()
{
@ -28,7 +45,9 @@ class AuthListenerTest extends TestCase
return 'https://example.com/actor/1';
},
'expectedAttributes' => array(
'actor' => 'https://example.com/actor/1',
'actor' => TestUtils::objectFromArray( array(
'id' => 'https://example.com/actor/1',
) ),
),
),
array(
@ -37,10 +56,14 @@ class AuthListenerTest extends TestCase
return 'https://example.com/actor/1';
},
'requestAttributes' => array(
'actor' => 'https://example.com/actor/2',
'actor' => TestUtils::objectFromArray( array(
'id' => 'https://example.com/actor/2',
) ),
),
'expectedAttributes' => array(
'actor' => 'https://example.com/actor/2',
'actor' => TestUtils::objectFromArray( array(
'id' => 'https://example.com/actor/2',
) ),
),
),
array(
@ -58,13 +81,30 @@ class AuthListenerTest extends TestCase
$event->getRequest()->attributes->set( $attribute, $value );
}
}
$authListener = new AuthListener( $testCase['authFunction'] );
$authListener->checkAuth( $event );
$this->assertEquals(
$testCase['expectedAttributes'],
$event->getRequest()->attributes->all(),
"Error on test $testCase[id]"
$authListener = new AuthListener(
$testCase['authFunction'], $this->objectsService
);
$authListener->checkAuth( $event );
foreach ( $testCase['expectedAttributes'] as $expectedKey => $expectedValue ) {
$this->assertTrue(
$event->getRequest()->attributes->has( $expectedKey ),
"Error on test $testCase[id]"
);
if ( $expectedValue instanceof ActivityPubObject ) {
$this->assertTrue(
$expectedValue->equals(
$event->getRequest()->attributes->get( $expectedKey )
),
"Error on test $testCase[id]"
);
} else {
$this->assertEquals(
$expectedValue,
$event->getRequest()->attributes->get( $expectedKey ),
"Error on test $testCase[id]"
);
}
}
}
}
}

View File

@ -16,6 +16,8 @@ use PHPUnit\Framework\TestCase;
class SignatureListenerTest extends TestCase
{
const ACTOR_ID = 'https://example.com/actor/1';
const ACTOR = array( 'id' => self::ACTOR_ID );
const KEY_ID = 'https://example.com/actor/1/key';
const PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3
@ -42,7 +44,8 @@ oYi+1hqp1fIekaxsyQIDAQAB
$objectsService = $this->createMock( ObjectsService::class );
$objectsService->method( 'dereference' )
->will( $this->returnValueMap( array(
array( self::KEY_ID, TestUtils::objectFromArray( self::KEY ) )
array( self::KEY_ID, TestUtils::objectFromArray( self::KEY ) ),
array( self::ACTOR_ID, TestUtils::objectFromArray( self::ACTOR ) ),
) ) );
$this->signatureListener = new SignatureListener(
$httpSignatureService, $objectsService
@ -82,8 +85,9 @@ oYi+1hqp1fIekaxsyQIDAQAB
),
'expectedAttributes' => array(
'signed' => true,
'signedBy' => 'https://example.com/actor/1',
'actor' => 'https://example.com/actor/1',
'actor' => TestUtils::objectFromArray( array(
'id' => 'https://example.com/actor/1',
) ),
),
),
array(
@ -92,12 +96,15 @@ oYi+1hqp1fIekaxsyQIDAQAB
'Authorization' => 'Signature keyId="https://example.com/actor/1/key",algorithm="rsa-sha256",headers="(request-target) host date", signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="',
),
'requestAttributes' => array(
'actor' => 'https://example.com/actor/2',
'actor' => TestUtils::objectFromArray( array(
'id' => 'https://example.com/actor/2',
) ),
),
'expectedAttributes' => array(
'signed' => true,
'signedBy' => 'https://example.com/actor/1',
'actor' => 'https://example.com/actor/2',
'actor' => TestUtils::objectFromArray( array(
'id' => 'https://example.com/actor/2',
) ),
),
),
array(
@ -107,8 +114,9 @@ oYi+1hqp1fIekaxsyQIDAQAB
),
'expectedAttributes' => array(
'signed' => true,
'signedBy' => 'https://example.com/actor/1',
'actor' => 'https://example.com/actor/1',
'actor' => TestUtils::objectFromArray( array(
'id' => 'https://example.com/actor/1',
) ),
),
),
array(
@ -129,11 +137,27 @@ oYi+1hqp1fIekaxsyQIDAQAB
}
}
$this->signatureListener->validateHttpSignature( $event );
$this->assertEquals(
$testCase['expectedAttributes'],
$event->getRequest()->attributes->all(),
"Error on test $testCase[id]"
);
foreach ( $testCase['expectedAttributes'] as $expectedKey => $expectedValue ) {
$this->assertTrue(
$event->getRequest()->attributes->has( $expectedKey ),
"Error on test $testCase[id]"
);
xdebug_break();
if ( $expectedValue instanceof ActivityPubObject ) {
$this->assertTrue(
$expectedValue->equals(
$event->getRequest()->attributes->get( $expectedKey )
),
"Error on test $testCase[id]"
);
} else {
$this->assertEquals(
$expectedValue,
$event->getRequest()->attributes->get( $expectedKey ),
"Error on test $testCase[id]"
);
}
}
}
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace ActivityPub\Test\Controllers;
use PHPUnit\Framework\TestCase;
class InboxControllerTest extends TestCase
{
public function testInboxController()
{
// TODO implement me
$this->assertTrue( false );
}
}
?>

View File

@ -0,0 +1,14 @@
<?php
namespace ActivityPub\Test\Controllers;
use PHPUnit\Framework\TestCase;
class OutboxControllerTest extends TestCase
{
public function testOutboxController()
{
// TODO implement me
$this->assertTrue( false );
}
}
?>

View File

@ -90,13 +90,8 @@ class ControllerResolverTest extends TestCase
);
$controller = $this->controllerResolver->getController( $request );
$this->assertTrue( $request->attributes->has( 'activity' ) );
$this->assertEquals( array( 'type' => 'Foo' ), $request->attributes->get( 'activity' ) );
$this->assertTrue( $request->attributes->has( 'inbox' ) );
$this->assertEquals(
array(
'id' => 'https://example.com/actor/1/inbox',
),
$request->attributes->get( 'inbox' )->asArray()
array( 'type' => 'Foo' ), $request->attributes->get( 'activity' )
);
$this->assertEquals( array( $this->inboxController, 'handle' ), $controller );
}
@ -108,13 +103,8 @@ class ControllerResolverTest extends TestCase
);
$controller = $this->controllerResolver->getController( $request );
$this->assertTrue( $request->attributes->has( 'activity' ) );
$this->assertEquals( array( 'type' => 'Foo' ), $request->attributes->get( 'activity' ) );
$this->assertTrue( $request->attributes->has( 'outbox' ) );
$this->assertEquals(
array(
'id' => 'https://example.com/actor/1/outbox',
),
$request->attributes->get( 'outbox' )->asArray()
array( 'type' => 'Foo' ), $request->attributes->get( 'activity' )
);
$this->assertEquals( array( $this->outboxController, 'handle' ), $controller );
}