diff --git a/phpunit.xml b/phpunit.xml index 2020aa65..0cc9c72c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,5 +22,10 @@ + + + src/ + + \ No newline at end of file diff --git a/src/Call.php b/src/Call.php index 74e8a26d..aba62bab 100644 --- a/src/Call.php +++ b/src/Call.php @@ -82,17 +82,21 @@ class Call private $invocationRequestId; + private $overrideCallerSession; + /** * Constructor * * @param \Thruway\Session $callerSession * @param \Thruway\Message\CallMessage $callMessage * @param Procedure $procedure + * @param Session|null $overrideCallerSession */ public function __construct( Session $callerSession, CallMessage $callMessage, - Procedure $procedure + Procedure $procedure, + Session $overrideCallerSession = null ) { $this->callMessage = $callMessage; @@ -101,6 +105,8 @@ public function __construct( $this->callStart = microtime(true); $this->invocationRequestId = Utils::getUniqueId(); + + $this->overrideCallerSession = $overrideCallerSession; } /** @@ -296,19 +302,23 @@ public function getInvocationMessage() $details = []; - if ($this->getRegistration()->getDiscloseCaller() === true && $this->getCallerSession()->getAuthenticationDetails()) { - $authenticationDetails = $this->getCallerSession()->getAuthenticationDetails(); - $details = [ - 'caller' => $this->getCallerSession()->getSessionId(), - 'authid' => $authenticationDetails->getAuthId(), - 'authrole' => $authenticationDetails->getAuthRole(), - 'authroles' => $authenticationDetails->getAuthRoles(), - 'authmethod' => $authenticationDetails->getAuthMethod(), - ]; - - if ($authenticationDetails->getAuthExtra() !== null) { - $details['_thruway_authextra'] = $authenticationDetails->getAuthExtra(); + $sessionForDetails = $this->overrideCallerSession === null ? $this->getCallerSession() : $this->overrideCallerSession; + + if ($this->getRegistration()->getDiscloseCaller() === true) { + if ($sessionForDetails->getAuthenticationDetails()) { + $authenticationDetails = $sessionForDetails->getAuthenticationDetails(); + $details = [ + 'authid' => $authenticationDetails->getAuthId(), + 'authrole' => $authenticationDetails->getAuthRole(), + 'authroles' => $authenticationDetails->getAuthRoles(), + 'authmethod' => $authenticationDetails->getAuthMethod(), + ]; + + if ($authenticationDetails->getAuthExtra() !== null) { + $details['_thruway_authextra'] = $authenticationDetails->getAuthExtra(); + } } + $details['caller'] = $sessionForDetails->getSessionId(); } // TODO: check to see if callee supports progressive call diff --git a/src/Procedure.php b/src/Procedure.php index 998913ab..939dcf88 100644 --- a/src/Procedure.php +++ b/src/Procedure.php @@ -48,6 +48,13 @@ class Procedure */ private $callQueue; + /** + * If this procedure is a hook, the original procedure lives here + * + * @var Procedure + */ + private $hookedProcedure; + /** * Constructor * @@ -65,6 +72,13 @@ public function __construct($procedureName) $this->callQueue = new SplQueue(); } + public static function createForHook($procedureName, Procedure $hookedProcedure) { + $new = new Procedure($procedureName); + $new->hookedProcedure = $hookedProcedure; + + return $new; + } + /** * Process register * @@ -77,6 +91,14 @@ public function processRegister(Session $session, RegisterMessage $msg) { $registration = Registration::createRegistrationFromRegisterMessage($session, $msg); + if ($registration->getAllowMultipleRegistrations() && $this->getHookedProcedure() !== null) { + $errorMsg = ErrorMessage::createErrorMessageFromMessage($msg, 'thruway.error.hook.failed'); + $errorMsg->setArguments(['\'Registration for hooks can only use "\' . $registration::SINGLE_REGISTRATION . \'" invocation type.\'']); + $session->sendMessage($errorMsg); + + return false; + } + if (count($this->registrations) > 0) { // we already have something registered if ($this->getAllowMultipleRegistrations()) { @@ -181,6 +203,10 @@ public function getRegistrationById($registrationId) } } + if ($this->hookedProcedure !== null) { + return $this->hookedProcedure->getRegistrationById($registrationId); + } + return false; } @@ -196,7 +222,7 @@ public function processUnregister(Session $session, UnregisterMessage $msg) for ($i = 0; $i < count($this->registrations); $i++) { /** @var Registration $registration */ $registration = $this->registrations[$i]; - if ($registration->getId() == $msg->getRegistrationId()) { + if ($registration->getId() === $msg->getRegistrationId()) { // make sure the session is the correct session if ($registration->getSession() !== $session) { @@ -214,8 +240,6 @@ public function processUnregister(Session $session, UnregisterMessage $msg) } } - $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg, 'wamp.error.no_such_registration')); - return false; } @@ -479,6 +503,24 @@ public function getRegistrations() return $this->registrations; } + /** + * @return Procedure + */ + public function getHookedProcedure() + { + return $this->hookedProcedure; + } + + /** + * @param Procedure $hookedProcedure + */ + public function setHookedProcedure($hookedProcedure) + { + $this->hookedProcedure = $hookedProcedure; + } + + + /** * process session leave * diff --git a/src/Role/Dealer.php b/src/Role/Dealer.php index c989120a..19fde8fb 100644 --- a/src/Role/Dealer.php +++ b/src/Role/Dealer.php @@ -189,6 +189,25 @@ private function processRegister(Session $session, RegisterMessage $msg) $this->procedures[$msg->getProcedureName()] = $procedure; } + if (isset($msg->getOptions()->x_thruway_hook) && $msg->getOptions()->x_thruway_hook === true) { + if (count($procedure->getRegistrations()) === 0) { + $errorMsg = ErrorMessage::createErrorMessageFromMessage($msg, 'thruway.error.hook.failed'); + $errorMsg->setArguments(['Unable to hook non-existent procedure ' . $msg->getProcedureName()]); + $session->sendMessage($errorMsg); + + return; + } + // This is treated as a new procedure and the existing one is subjugated + $procedure = Procedure::createForHook($msg->getProcedureName(), $this->procedures[$msg->getProcedureName()]); + $this->procedures[$msg->getProcedureName()] = $procedure; + } else { + // This is not a hook - we need to traverse the hook-chain so any regular registrations + // end up on the regular one + while ($procedure->getHookedProcedure() !== null) { + $procedure = $procedure->getHookedProcedure(); + } + } + if ($procedure->processRegister($session, $msg)) { // registration succeeded // make sure we have the registration in the collection @@ -203,6 +222,18 @@ private function processRegister(Session $session, RegisterMessage $msg) $this->registrationsBySession[$session] = $registrationsForThisSession; } + + return; + } + + // Registration failed + if ($procedure->getHookedProcedure() !== null) { + // restore previous procedure as hook registration failed + $this->procedures[$msg->getProcedureName()] = $procedure->getHookedProcedure(); + } + + if (count($procedure->getRegistrations()) === 0) { + unset($this->procedures[$msg->getProcedureName()]); } } @@ -218,25 +249,51 @@ private function processUnregister(Session $session, UnregisterMessage $msg) $registration = $this->getRegistrationById($msg->getRegistrationId()); if ($registration && $this->procedures[$registration->getProcedureName()]) { + $prevProcedure = null; $procedure = $this->procedures[$registration->getProcedureName()]; - if ($procedure) { - if ($procedure->processUnregister($session, $msg)) { - // Unregistration was successful - remove from this sessions - // list of registrations - if ($this->registrationsBySession->contains($session) && - in_array($procedure, $this->registrationsBySession[$session], true) - ) { - $registrationsInSession = $this->registrationsBySession[$session]; - array_splice($registrationsInSession, array_search($procedure, $registrationsInSession, true), 1); + while ($procedure !== null && !$procedure->processUnregister($session, $msg)) { + $prevProcedure = $procedure; + $procedure = $procedure->getHookedProcedure(); + } + if ($procedure === null) { + // this appears to be unreachable as the registration has already been found somewhere + // in the procedure + $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg, 'wamp.error.no_such_registration')); + return false; + } + // Unregistration was successful - remove from this sessions + // list of registrations + if ($this->registrationsBySession->contains($session) && + in_array($procedure, $this->registrationsBySession[$session], true) + ) { + $registrationsInSession = $this->registrationsBySession[$session]; + array_splice($registrationsInSession, array_search($procedure, $registrationsInSession, true), 1); + } + + if (count($procedure->getRegistrations()) === 0) { + // if this is the top of the stack + if ($procedure === $this->procedures[$registration->getProcedureName()]) { + $this->procedures[$registration->getProcedureName()] = $procedure->getHookedProcedure(); + if ($this->procedures[$registration->getProcedureName()] === null) { + unset($this->procedures[$registration->getProcedureName()]); } + + return true; + } + + // this is not the top of the stack, so we need to + // reset the hookedProcedure of the procedure above to + // our hooked procedure + if ($prevProcedure !== null) { + $prevProcedure->setHookedProcedure($procedure->getHookedProcedure()); } } - return; + return true; } // apparently we didn't find anything to unregister - $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg, 'wamp.error.no_such_procedure')); + $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg, 'wamp.error.no_such_registration')); } /** @@ -263,7 +320,77 @@ private function processCall(Session $session, CallMessage $msg) /* @var $procedure \Thruway\Procedure */ $procedure = $this->procedures[$msg->getProcedureName()]; - $call = new Call($session, $msg, $procedure); + $overrideCallerSession = null; + + // A Hook wants to call the hooked RPC + if (isset($msg->getOptions()->x_thruway_call_hooked)) { + $callHookedOptions = $msg->getOptions()->x_thruway_call_hooked; + if (!is_object($callHookedOptions)) { + $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg, 'thruway.error.hook.invalid_call_options')); + + return; + } + if (!isset($callHookedOptions->registration_id) || !is_numeric($callHookedOptions->registration_id)) { + $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg, 'thruway.error.hook.invalid_call_registration_id')); + + return; + } + $registrationId = $callHookedOptions->registration_id; + while ($procedure !== null) { + $registration = $procedure->getRegistrationById($registrationId); + if ($registration) { + if (!in_array($registration, $procedure->getRegistrations(), true)) { + $procedure = $procedure->getHookedProcedure(); + continue; + } + // we have found the registration in the current procedure + // sanity and security checks + if ($session->getSessionId() !== $registration->getSession()->getSessionId()) { + $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg, 'thruway.error.hook.not_yours')); + + return; + } + if ($procedure->getHookedProcedure() === null) { + $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg, 'thruway.error.hook.not_hooked')); + + return; + } + $procedure = $procedure->getHookedProcedure(); + + // see if they want to use a concurrent call to override the + // caller info so it will look like the original call to the + // hooked procedure with_caller_from is an invocation request id + if (isset($callHookedOptions->with_caller_from)) { + if (!is_numeric($callHookedOptions->with_caller_from)) { + $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg, 'thruway.error.hook.caller_from_invalid')); + return; + } + $callerFromInvocationId = $callHookedOptions->with_caller_from; + if (!isset($this->callInvocationIndex[$callerFromInvocationId])) { + $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg, 'thruway.error.hook.caller_from_invalid')); + return; + } + $callerFromCall = $this->callInvocationIndex[$callerFromInvocationId]; + + if ($callerFromCall->getCalleeSession() !== $session) { + $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg, 'thruway.error.hook.caller_from_not_yours')); + return; + } + + $overrideCallerSession = $callerFromCall->getCallerSession(); + } + break; + } + $procedure = $procedure->getHookedProcedure(); + } + if ($procedure === null) { + $session->sendMessage(ErrorMessage::createErrorMessageFromMessage($msg, 'thruway.error.hook.bad_registration')); + + return; + } + } + + $call = new Call($session, $msg, $procedure, $overrideCallerSession); $this->callInvocationIndex[$call->getInvocationRequestId()] = $call; $this->callRequestIndex[$msg->getRequestId()] = $call; diff --git a/tests/Unit/ProcedureTest.php b/tests/Unit/ProcedureTest.php index f11899e9..92d6538a 100644 --- a/tests/Unit/ProcedureTest.php +++ b/tests/Unit/ProcedureTest.php @@ -420,7 +420,7 @@ public function testGetCallWithRequestIDAndGetRegistrationById() /** @var \Thruway\Message\InvocationMessage $invocationMsg */ $invocationMsg = null; - $session->expects($this->exactly(4)) + $session->expects($this->exactly(3)) ->method("sendMessage") ->withConsecutive( [ @@ -435,12 +435,6 @@ public function testGetCallWithRequestIDAndGetRegistrationById() $this->assertInstanceOf('\Thruway\Message\InvocationMessage', $msg); $invocationMsg = $msg; - return true; - }) - ], [ - $this->callback(function ($msg) { - $this->assertInstanceOf('\Thruway\Message\ErrorMessage', $msg); - $this->assertEquals('wamp.error.no_such_registration', $msg->getErrorUri()); return true; }) ], [$this->isInstanceOf('\Thruway\Message\UnregisteredMessage')] diff --git a/tests/Unit/Role/DealerTest.php b/tests/Unit/Role/DealerTest.php index 0c859eca..1dcccdfb 100644 --- a/tests/Unit/Role/DealerTest.php +++ b/tests/Unit/Role/DealerTest.php @@ -538,7 +538,7 @@ public function testCancelAfterUnregister() //$this->assertEquals(0, count($dealer->getProcedures())); - $this->assertEquals(0, count($dealer->getProcedures()['some.proc']->getRegistrations())); + $this->assertArrayNotHasKey('some.proc', $dealer->getProcedures()); $dealer->handleCancelMessage(new MessageEvent($callerSession, new CancelMessage(2345, (object)[]))); @@ -603,4 +603,658 @@ public function testCanceledProgressiveCallRemovesRouterReferences() $this->assertEquals(0, $dealer->getProcedures()['some.proc']->getRegistrations()[0]->getCurrentCallCount()); } + + public function testUnregisterNonExistentProcedure() { + $dealer = new Dealer(); + $helloMessage = new HelloMessage('some.realm', (object)[]); + + $calleeTransport = new DummyTransport(); + $calleeSession = new Session($calleeTransport); + // make sure this callee supports call cancellation + $calleeSession->setHelloMessage($helloMessage); + + $unregisterMsg = new UnregisterMessage(1, 12345); + $dealer->handleUnregisterMessage(new MessageEvent($calleeSession, $unregisterMsg)); + + /** @var ErrorMessage $errorMsg */ + $errorMsg = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(ErrorMessage::class, $errorMsg); + $this->assertEquals('wamp.error.no_such_registration', $errorMsg->getErrorURI()); + } + + public function testRegisterHookForNonExistentProcedure() { + $dealer = new Dealer(); + $helloMessage = new HelloMessage('some.realm', (object)[]); + + $calleeTransport = new DummyTransport(); + $calleeSession = new Session($calleeTransport); + // make sure this callee supports call cancellation + $calleeSession->setHelloMessage($helloMessage); + + $registerMsg = new RegisterMessage( + 1, + (object)['x_thruway_hook' => true], + 'test.rpc' + ); + $dealer->handleRegisterMessage(new MessageEvent($calleeSession, $registerMsg)); + + $this->assertInstanceOf(ErrorMessage::class, $calleeTransport->getLastMessageSent()); + } + + public function testRegisterHookWithNonSingleInvoke() { + $dealer = new Dealer(); + $helloMessage = new HelloMessage('some.realm', (object)[]); + + $calleeTransport = new DummyTransport(); + $calleeSession = new Session($calleeTransport); + // make sure this callee supports call cancellation + $calleeSession->setHelloMessage($helloMessage); + + $registerMsg = new RegisterMessage(1, (object)[], 'test.rpc'); + + $dealer->handleRegisterMessage(new MessageEvent($calleeSession, $registerMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $calleeTransport->getLastMessageSent()); + + $hookRegisterMsg = new RegisterMessage( + 1, + (object)['x_thruway_hook' => true, 'invoke' => \Thruway\Registration::ROUNDROBIN_REGISTRATION], + 'test.rpc' + ); + $hookTransport = new DummyTransport(); + $hookSession = new Session($hookTransport); + $dealer->handleRegisterMessage(new MessageEvent($hookSession, $hookRegisterMsg)); + /** @var ErrorMessage $errorMessage */ + $errorMessage = $hookTransport->getLastMessageSent(); + $this->assertInstanceOf(ErrorMessage::class, $errorMessage); + $this->assertEquals('thruway.error.hook.failed', $errorMessage->getErrorURI()); + + $hookRegisterMsg = new RegisterMessage( + 1, + (object)['x_thruway_hook' => true, + 'invoke' => 'single', + 'thruway_multiregister' => true + ], + 'test.rpc' + ); + $hookTransport = new DummyTransport(); + $hookSession = new Session($hookTransport); + $dealer->handleRegisterMessage(new MessageEvent($hookSession, $hookRegisterMsg)); + $errorMessage = $hookTransport->getLastMessageSent(); + $this->assertInstanceOf(ErrorMessage::class, $errorMessage); + $this->assertEquals('thruway.error.hook.failed', $errorMessage->getErrorURI()); + } + + public function testSimpleHook() { + $dealer = new Dealer(); + $helloMessage = new HelloMessage('some.realm', (object)[]); + + $calleeTransport = new DummyTransport(); + $calleeSession = new Session($calleeTransport); + // make sure this callee supports call cancellation + $calleeSession->setHelloMessage($helloMessage); + + $registerMsg = new RegisterMessage(1, (object)[], 'test.rpc'); + + $dealer->handleRegisterMessage(new MessageEvent($calleeSession, $registerMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $calleeTransport->getLastMessageSent()); + + $hookRegisterMsg = new RegisterMessage( + 1, + (object)['x_thruway_hook' => true], + 'test.rpc' + ); + $hookTransport = new DummyTransport(); + $hookSession = new Session($hookTransport); + $dealer->handleRegisterMessage(new MessageEvent($hookSession, $hookRegisterMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $hookTransport->getLastMessageSent()); + $hookRegistrationId = $hookTransport->getLastMessageSent()->getRegistrationId(); + + $callerTransport = new DummyTransport(); + $callerSession = new Session($callerTransport); + $callMsg = new CallMessage(1, (object)[], 'test.rpc'); + $dealer->handleCallMessage(new MessageEvent($callerSession, $callMsg)); + + // the hook should get this invocation + $this->assertInstanceOf(InvocationMessage::class, $hookTransport->getLastMessageSent()); + // the callee should have heard nothing new + $this->assertInstanceOf(RegisteredMessage::class, $calleeTransport->getLastMessageSent()); + + $unregMsg = new UnregisterMessage(5, $hookRegistrationId); + $dealer->handleUnregisterMessage(new MessageEvent($hookSession, $unregMsg)); + $this->assertInstanceOf(UnregisteredMessage::class, $hookTransport->getLastMessageSent()); + + // next call should go to original caller + $callMsg->setRequestId(10); + $callMsg->setArguments(['New call']); + $dealer->handleCallMessage(new MessageEvent($callerSession, $callMsg)); + /** @var InvocationMessage $invocationMsg */ + $invocationMsg = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(InvocationMessage::class, $invocationMsg); + $this->assertEquals(['New call'], $invocationMsg->getArguments()); + } + + public function testDoubleHook() { + $dealer = new Dealer(); + $helloMessage = new HelloMessage('some.realm', (object)[]); + + $calleeTransport = new DummyTransport(); + $calleeSession = new Session($calleeTransport); + // make sure this callee supports call cancellation + $calleeSession->setHelloMessage($helloMessage); + + $registerMsg = new RegisterMessage(1, (object)[], 'test.rpc'); + + $dealer->handleRegisterMessage(new MessageEvent($calleeSession, $registerMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $calleeTransport->getLastMessageSent()); + + $hookRegisterMsg = new RegisterMessage( + 1, + (object)['x_thruway_hook' => true], + 'test.rpc' + ); + $hookTransport = new DummyTransport(); + $hookSession = new Session($hookTransport); + $dealer->handleRegisterMessage(new MessageEvent($hookSession, $hookRegisterMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $hookTransport->getLastMessageSent()); + $hookRegistrationId = $hookTransport->getLastMessageSent()->getRegistrationId(); + + $hook2Transport = new DummyTransport(); + $hook2Session = new Session($hook2Transport); + $dealer->handleRegisterMessage(new MessageEvent($hook2Session, $hookRegisterMsg)); + /** @var RegisteredMessage $hook2RegisteredMsg */ + $hook2RegisteredMsg = $hook2Transport->getLastMessageSent(); + $this->assertInstanceOf(RegisteredMessage::class, $hook2RegisteredMsg); + + $callerTransport = new DummyTransport(); + $callerSession = new Session($callerTransport); + $callMsg = new CallMessage(1, (object)[], 'test.rpc'); + $dealer->handleCallMessage(new MessageEvent($callerSession, $callMsg)); + + // the second hook should get this invocation + $this->assertInstanceOf(InvocationMessage::class, $hook2Transport->getLastMessageSent()); + // the first hook should have heard nothing new + $this->assertInstanceOf(RegisteredMessage::class, $hookTransport->getLastMessageSent()); + // the callee should have heard nothing new + $this->assertInstanceOf(RegisteredMessage::class, $calleeTransport->getLastMessageSent()); + + // unregister middle hook + $unregMsg = new UnregisterMessage(5, $hookRegistrationId); + $dealer->handleUnregisterMessage(new MessageEvent($hookSession, $unregMsg)); + $this->assertInstanceOf(UnregisteredMessage::class, $hookTransport->getLastMessageSent()); + + // this call should go to hook2 still + $callMsg->setRequestId(9); + $callMsg->setArguments(['Call to hook2']); + $dealer->handleCallMessage(new MessageEvent($callerSession, $callMsg)); + /** @var InvocationMessage $invocationMsg */ + $invocationMsg = $hook2Transport->getLastMessageSent(); + $this->assertInstanceOf(InvocationMessage::class, $invocationMsg); + $this->assertEquals(['Call to hook2'], $invocationMsg->getArguments()); + + // unregister hook2 + $unregMsg = new UnregisterMessage(6, $hook2RegisteredMsg->getRegistrationId()); + $dealer->handleUnregisterMessage(new MessageEvent($hook2Session, $unregMsg)); + $this->assertInstanceOf(UnregisteredMessage::class, $hook2Transport->getLastMessageSent()); + + // next call should go to original caller + $callMsg->setRequestId(10); + $callMsg->setArguments(['New call']); + $dealer->handleCallMessage(new MessageEvent($callerSession, $callMsg)); + /** @var InvocationMessage $invocationMsg */ + $invocationMsg = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(InvocationMessage::class, $invocationMsg); + $this->assertEquals(['New call'], $invocationMsg->getArguments()); + } + + public function testHookCallingHooked() { + $dealer = new Dealer(); + $helloMessage = new HelloMessage('some.realm', (object)[]); + + $calleeTransport = new DummyTransport(); + $calleeSession = new Session($calleeTransport); + // make sure this callee supports call cancellation + $calleeSession->setHelloMessage($helloMessage); + + $registerMsg = new RegisterMessage(1, (object)[], 'test.rpc'); + + $dealer->handleRegisterMessage(new MessageEvent($calleeSession, $registerMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $calleeTransport->getLastMessageSent()); + + $hookRegisterMsg = new RegisterMessage( + 1, + (object)['x_thruway_hook' => true], + 'test.rpc' + ); + $hookTransport = new DummyTransport(); + $hookSession = new Session($hookTransport); + $dealer->handleRegisterMessage(new MessageEvent($hookSession, $hookRegisterMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $hookTransport->getLastMessageSent()); + $hookRegistrationId = $hookTransport->getLastMessageSent()->getRegistrationId(); + + $callMsg = new CallMessage( + 47, + (object)['x_thruway_call_hooked' => (object)[ 'registration_id' => $hookRegistrationId ]], + 'test.rpc', + ['Calling hooked'] + ); + $dealer->handleCallMessage(new MessageEvent($hookSession, $callMsg)); + + /** @var InvocationMessage $invocationMsg */ + $invocationMsg = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(InvocationMessage::class, $invocationMsg); + $this->assertEquals(['Calling hooked'], $invocationMsg->getArguments()); + } + + public function testHookCallingHookedBadOptions() { + $dealer = new Dealer(); + $helloMessage = new HelloMessage('some.realm', (object)[]); + + $calleeTransport = new DummyTransport(); + $calleeSession = new Session($calleeTransport); + // make sure this callee supports call cancellation + $calleeSession->setHelloMessage($helloMessage); + + $registerMsg = new RegisterMessage(1, (object)[], 'test.rpc'); + + $dealer->handleRegisterMessage(new MessageEvent($calleeSession, $registerMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $calleeTransport->getLastMessageSent()); + + $hookRegisterMsg = new RegisterMessage( + 1, + (object)['x_thruway_hook' => true], + 'test.rpc' + ); + $hookTransport = new DummyTransport(); + $hookSession = new Session($hookTransport); + $dealer->handleRegisterMessage(new MessageEvent($hookSession, $hookRegisterMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $hookTransport->getLastMessageSent()); + $hookRegistrationId = $hookTransport->getLastMessageSent()->getRegistrationId(); + + $callMsg = new CallMessage( + 47, + (object)['x_thruway_call_hooked' => 12345], + 'test.rpc', + ['Calling hooked'] + ); + $dealer->handleCallMessage(new MessageEvent($hookSession, $callMsg)); + + /** @var ErrorMessage $errorMsg */ + $errorMsg = $hookTransport->getLastMessageSent(); + $this->assertInstanceOf(ErrorMessage::class, $errorMsg); + $this->assertEquals(47, $errorMsg->getRequestId()); + $this->assertEquals('thruway.error.hook.invalid_call_options', $errorMsg->getErrorURI()); + } + + public function testHookCallingHookedWithCallerFrom() { + $realm = new Realm('my_realm'); + $dealer = new Dealer(); + $helloMessage = new HelloMessage('some.realm', (object)[]); + + $calleeTransport = new DummyTransport(); + $calleeSession = new Session($calleeTransport); + // make sure this callee supports call cancellation + $calleeSession->setHelloMessage($helloMessage); + $calleeSession->setRealm($realm); + + $registerMsg = new RegisterMessage( + 1, + (object)['disclose_caller'=>true], + 'test.rpc'); + + $dealer->handleRegisterMessage(new MessageEvent($calleeSession, $registerMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $calleeTransport->getLastMessageSent()); + + $hookRegisterMsg = new RegisterMessage( + 1, + (object)['x_thruway_hook' => true, 'disclose_caller' => true], + 'test.rpc' + ); + $hookTransport = new DummyTransport(); + $hookSession = new Session($hookTransport); + $hookSession->setRealm($realm); + $hookAuthDetails = new \Thruway\Authentication\AuthenticationDetails(); + $hookAuthDetails->setAuthId('admin'); + $hookAuthDetails->setAuthRoles(['admin', 'something']); + $hookSession->setAuthenticated(true); + $hookSession->setAuthenticationDetails($hookAuthDetails); + $dealer->handleRegisterMessage(new MessageEvent($hookSession, $hookRegisterMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $hookTransport->getLastMessageSent()); + $hookRegistrationId = $hookTransport->getLastMessageSent()->getRegistrationId(); + + // setup a caller here to copy the session info of + $callerTransport = new DummyTransport(); + $callerSession = new Session($callerTransport); + $callerAuthDetails = new \Thruway\Authentication\AuthenticationDetails(); + $callerAuthDetails->setAuthId('the_user'); + $callerAuthDetails->setAuthRoles(['the_role']); + $callerSession->setRealm($realm); + $callerSession->setAuthenticationDetails($callerAuthDetails); + + $origCall = new CallMessage(9876, (object)[], 'test.rpc', ['some arg']); + + $dealer->handleCallMessage(new MessageEvent($callerSession, $origCall)); + // The hook should get the invocation + /** @var InvocationMessage $origInvocationMessage */ + $origInvocationMessage = $hookTransport->getLastMessageSent(); + $this->assertInstanceOf(InvocationMessage::class, $origInvocationMessage); + $this->assertObjectHasAttribute('caller', $origInvocationMessage->getDetails()); + $this->assertEquals($callerSession->getSessionId(), $origInvocationMessage->getDetails()->caller); + + $callMsg = new CallMessage( + 47, + (object)[ + 'x_thruway_call_hooked' => (object)[ + 'registration_id' => $hookRegistrationId, + 'with_caller_from' => $origInvocationMessage->getRequestId() + ] + ], + 'test.rpc', + ['Calling hooked'] + ); + $dealer->handleCallMessage(new MessageEvent($hookSession, $callMsg)); + + /** @var InvocationMessage $invocationMsg */ + $invocationMsg = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(InvocationMessage::class, $invocationMsg); + $this->assertEquals(['Calling hooked'], $invocationMsg->getArguments()); + $this->assertObjectHasAttribute('caller', $invocationMsg->getDetails()); + $this->assertEquals($callerSession->getSessionId(), $invocationMsg->getDetails()->caller); + $this->assertEquals('the_user', $invocationMsg->getDetails()->authid); + + // call with non-existent invocation id + $callMsg = new CallMessage( + 47, + (object)[ + 'x_thruway_call_hooked' => (object)[ + 'registration_id' => $hookRegistrationId, + 'with_caller_from' => 1234 + ] + ], + 'test.rpc', + ['Calling hooked'] + ); + $dealer->handleCallMessage(new MessageEvent($hookSession, $callMsg)); + + /** @var ErrorMessage $errMsg */ + $errMsg = $hookTransport->getLastMessageSent(); + $this->assertInstanceOf(ErrorMessage::class, $errMsg); + $this->assertEquals('thruway.error.hook.caller_from_invalid', $errMsg->getErrorURI()); + + // call with invocation id from callee's invocation - I should only be able to use + // and invocation that was sent to the hook's session + $callMsg = new CallMessage( + 47, + (object)[ + 'x_thruway_call_hooked' => (object)[ + 'registration_id' => $hookRegistrationId, + 'with_caller_from' => $invocationMsg->getRequestId() + ] + ], + 'test.rpc', + ['Calling hooked'] + ); + $dealer->handleCallMessage(new MessageEvent($hookSession, $callMsg)); + + /** @var ErrorMessage $errMsg */ + $errMsg = $hookTransport->getLastMessageSent(); + $this->assertInstanceOf(ErrorMessage::class, $errMsg); + $this->assertEquals('thruway.error.hook.caller_from_not_yours', $errMsg->getErrorURI()); + + // call with invalid with_caller_from + $callMsg = new CallMessage( + 47, + (object)[ + 'x_thruway_call_hooked' => (object)[ + 'registration_id' => $hookRegistrationId, + 'with_caller_from' => (object)['x' => 'y'] + ] + ], + 'test.rpc', + ['Calling hooked'] + ); + $dealer->handleCallMessage(new MessageEvent($hookSession, $callMsg)); + + /** @var ErrorMessage $errMsg */ + $errMsg = $hookTransport->getLastMessageSent(); + $this->assertInstanceOf(ErrorMessage::class, $errMsg); + $this->assertEquals('thruway.error.hook.caller_from_invalid', $errMsg->getErrorURI()); + + // call with no registration_id + $callMsg = new CallMessage( + 47, + (object)[ + 'x_thruway_call_hooked' => (object)[ + 'with_caller_from' => (object)['x' => 'y'] + ] + ], + 'test.rpc', + ['Calling hooked'] + ); + $dealer->handleCallMessage(new MessageEvent($hookSession, $callMsg)); + + /** @var ErrorMessage $errMsg */ + $errMsg = $hookTransport->getLastMessageSent(); + $this->assertInstanceOf(ErrorMessage::class, $errMsg); + $this->assertEquals('thruway.error.hook.invalid_call_registration_id', $errMsg->getErrorURI()); + } + + public function testHookCallingUnownedHooked() { + $dealer = new Dealer(); + $helloMessage = new HelloMessage('some.realm', (object)[]); + + $calleeTransport = new DummyTransport(); + $calleeSession = new Session($calleeTransport); + // make sure this callee supports call cancellation + $calleeSession->setHelloMessage($helloMessage); + + $registerMsg = new RegisterMessage(1, (object)[], 'test.rpc'); + + $dealer->handleRegisterMessage(new MessageEvent($calleeSession, $registerMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $calleeTransport->getLastMessageSent()); + + $hookRegisterMsg = new RegisterMessage( + 1, + (object)['x_thruway_hook' => true], + 'test.rpc' + ); + $hookTransport = new DummyTransport(); + $hookSession = new Session($hookTransport); + $dealer->handleRegisterMessage(new MessageEvent($hookSession, $hookRegisterMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $hookTransport->getLastMessageSent()); + $hookRegistrationId = $hookTransport->getLastMessageSent()->getRegistrationId(); + + $callMsg = new CallMessage( + 47, + (object)['x_thruway_call_hooked' => (object)[ 'registration_id' => $hookRegistrationId ]], + 'test.rpc', + ['Calling hooked'] + ); + $dealer->handleCallMessage(new MessageEvent($calleeSession, $callMsg)); + + /** @var ErrorMessage $errorMsg */ + $errorMsg = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(ErrorMessage::class, $errorMsg); + $this->assertEquals(47, $errorMsg->getRequestId()); + $this->assertEquals('thruway.error.hook.not_yours', $errorMsg->getErrorURI()); + } + + public function testHookCallingUnhooked() { + $dealer = new Dealer(); + $helloMessage = new HelloMessage('some.realm', (object)[]); + + $calleeTransport = new DummyTransport(); + $calleeSession = new Session($calleeTransport); + // make sure this callee supports call cancellation + $calleeSession->setHelloMessage($helloMessage); + + $registerMsg = new RegisterMessage(1, (object)[], 'test.rpc'); + + $dealer->handleRegisterMessage(new MessageEvent($calleeSession, $registerMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $calleeTransport->getLastMessageSent()); + /** @var RegisteredMessage $originalRegisteredMsg */ + $originalRegisteredMsg = $calleeTransport->getLastMessageSent(); + + $hookRegisterMsg = new RegisterMessage( + 1, + (object)['x_thruway_hook' => true], + 'test.rpc' + ); + $hookTransport = new DummyTransport(); + $hookSession = new Session($hookTransport); + $dealer->handleRegisterMessage(new MessageEvent($hookSession, $hookRegisterMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $hookTransport->getLastMessageSent()); + $hookRegistrationId = $hookTransport->getLastMessageSent()->getRegistrationId(); + + $callMsg = new CallMessage( + 47, + (object)['x_thruway_call_hooked' => (object)[ + 'registration_id' => $originalRegisteredMsg->getRegistrationId() + ] + ], + 'test.rpc', + ['Calling hooked'] + ); + $dealer->handleCallMessage(new MessageEvent($calleeSession, $callMsg)); + + /** @var ErrorMessage $errorMsg */ + $errorMsg = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(ErrorMessage::class, $errorMsg); + $this->assertEquals(47, $errorMsg->getRequestId()); + $this->assertEquals('thruway.error.hook.not_hooked', $errorMsg->getErrorURI()); + } + + public function testHookCallingNonexistentHooked() { + $dealer = new Dealer(); + $helloMessage = new HelloMessage('some.realm', (object)[]); + + $calleeTransport = new DummyTransport(); + $calleeSession = new Session($calleeTransport); + // make sure this callee supports call cancellation + $calleeSession->setHelloMessage($helloMessage); + + $registerMsg = new RegisterMessage(1, (object)[], 'test.rpc'); + + $dealer->handleRegisterMessage(new MessageEvent($calleeSession, $registerMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $calleeTransport->getLastMessageSent()); + /** @var RegisteredMessage $originalRegisteredMsg */ + $originalRegisteredMsg = $calleeTransport->getLastMessageSent(); + + $hookRegisterMsg = new RegisterMessage( + 1, + (object)['x_thruway_hook' => true], + 'test.rpc' + ); + $hookTransport = new DummyTransport(); + $hookSession = new Session($hookTransport); + $dealer->handleRegisterMessage(new MessageEvent($hookSession, $hookRegisterMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $hookTransport->getLastMessageSent()); + $hookRegistrationId = $hookTransport->getLastMessageSent()->getRegistrationId(); + + $callMsg = new CallMessage( + 47, + (object)['x_thruway_call_hooked' => (object)[ 'registration_id' => 12345 ]], + 'test.rpc', + ['Calling hooked'] + ); + $dealer->handleCallMessage(new MessageEvent($calleeSession, $callMsg)); + + /** @var ErrorMessage $errorMsg */ + $errorMsg = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(ErrorMessage::class, $errorMsg); + $this->assertEquals(47, $errorMsg->getRequestId()); + $this->assertEquals('thruway.error.hook.bad_registration', $errorMsg->getErrorURI()); + } + + public function testRegisterRegularAfterHooked() { + $dealer = new Dealer(); + $helloMessage = new HelloMessage('some.realm', (object)[]); + + $calleeTransport = new DummyTransport(); + $calleeSession = new Session($calleeTransport); + // make sure this callee supports call cancellation + $calleeSession->setHelloMessage($helloMessage); + + $registerMsg = new RegisterMessage( + 1, + (object)[ 'invoke' => \Thruway\Registration::ROUNDROBIN_REGISTRATION ], + 'test.rpc'); + + $dealer->handleRegisterMessage(new MessageEvent($calleeSession, $registerMsg)); + /** @var RegisteredMessage $registeredMessage */ + $registeredMessage = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(RegisteredMessage::class, $registeredMessage); + + $hookRegisterMsg = new RegisterMessage( + 1, + (object)['x_thruway_hook' => true], + 'test.rpc' + ); + $hookTransport = new DummyTransport(); + $hookSession = new Session($hookTransport); + $dealer->handleRegisterMessage(new MessageEvent($hookSession, $hookRegisterMsg)); + $this->assertInstanceOf(RegisteredMessage::class, $hookTransport->getLastMessageSent()); + $hookRegistrationId = $hookTransport->getLastMessageSent()->getRegistrationId(); + + $callMsg = new CallMessage( + 47, + (object)['x_thruway_call_hooked' => (object)[ 'registration_id' => $hookRegistrationId ]], + 'test.rpc', + ['Calling hooked'] + ); + $dealer->handleCallMessage(new MessageEvent($hookSession, $callMsg)); + + /** @var InvocationMessage $invocationMsg */ + $invocationMsg = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(InvocationMessage::class, $invocationMsg); + $this->assertEquals(['Calling hooked'], $invocationMsg->getArguments()); + $this->assertEquals($registeredMessage->getRegistrationId(), $invocationMsg->getRegistrationId()); + + // register another to see if it adds the registration to the correct place + $registerMsg2 = new RegisterMessage( + 100, + (object)[ 'invoke' => \Thruway\Registration::ROUNDROBIN_REGISTRATION ], + 'test.rpc'); + $dealer->handleRegisterMessage(new MessageEvent($calleeSession, $registerMsg2)); + /** @var RegisteredMessage $registeredMessage2 */ + $registeredMessage2 = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(RegisteredMessage::class, $registeredMessage2); + + // make another call - this should call the new one because of the round robin + $callMsg = new CallMessage( + 48, + (object)['x_thruway_call_hooked' => (object)[ 'registration_id' => $hookRegistrationId ]], + 'test.rpc', + ['Calling hooked again'] + ); + $dealer->handleCallMessage(new MessageEvent($hookSession, $callMsg)); + + /** @var InvocationMessage $invocationMsg */ + $invocationMsg = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(InvocationMessage::class, $invocationMsg); + $this->assertEquals(['Calling hooked again'], $invocationMsg->getArguments()); + $this->assertEquals($registeredMessage2->getRegistrationId(), $invocationMsg->getRegistrationId()); + + // make a 3rd call - this should invoke the first one because of the round robin + $callMsg = new CallMessage( + 49, + (object)['x_thruway_call_hooked' => (object)[ 'registration_id' => $hookRegistrationId ]], + 'test.rpc', + ['Calling hooked - 3rd time'] + ); + $dealer->handleCallMessage(new MessageEvent($hookSession, $callMsg)); + + /** @var InvocationMessage $invocationMsg */ + $invocationMsg = $calleeTransport->getLastMessageSent(); + $this->assertInstanceOf(InvocationMessage::class, $invocationMsg); + $this->assertEquals(['Calling hooked - 3rd time'], $invocationMsg->getArguments()); + $this->assertEquals($registeredMessage->getRegistrationId(), $invocationMsg->getRegistrationId()); + } + +// public function testRegisteringOnHookedRPC() { +// $this->markTestSkipped('Not implemented'); +// } +// +// public function testHookPassingCredentialsOfCaller() { +// $this->markTestSkipped('Not implemented'); +// } }