Completed
Push — master ( 3c24ea...404feb )
by Sam
02:45
created

WebSocketManager   B

Complexity

Total Complexity 17

Size/Duplication

Total Lines 209
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 18

Test Coverage

Coverage 0%

Importance

Changes 9
Bugs 0 Features 4
Metric Value
wmc 17
c 9
b 0
f 4
lcom 1
cbo 18
dl 0
loc 209
ccs 0
cts 106
cp 0
rs 7.3333

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 1
A getSubscribedEvents() 0 7 1
A onMainLoopStarted() 0 7 1
A onInstanceStatusUpdates() 0 11 2
A onOpen() 0 5 1
A onClose() 0 7 1
A onError() 0 4 1
A handleMessage() 0 21 3
B onMessage() 0 41 5
A sendMessage() 0 4 1
1
<?php
2
3
namespace Jalle19\StatusManager\Manager;
4
5
use Jalle19\StatusManager\Configuration\Configuration;
6
use Jalle19\StatusManager\Event\Events;
7
use Jalle19\StatusManager\Event\InstanceStatusUpdatesEvent;
8
use Jalle19\StatusManager\Exception\MalformedRequestException;
9
use Jalle19\StatusManager\Exception\UnhandledMessageException;
10
use Jalle19\StatusManager\Exception\UnknownRequestException;
11
use Jalle19\StatusManager\Message\AbstractMessage;
12
use Jalle19\StatusManager\Message\Factory as MessageFactory;
13
use Jalle19\StatusManager\Message\Handler\DelegatesMessagesTrait;
14
use Jalle19\StatusManager\Message\Handler\HandlerInterface;
15
use Jalle19\StatusManager\Message\Request\AuthenticationRequest;
16
use Jalle19\StatusManager\Message\Response\AuthenticationResponse;
17
use Jalle19\StatusManager\Message\StatusUpdatesMessage;
18
use Jalle19\tvheadend\exception\RequestFailedException;
19
use Psr\Log\LoggerInterface;
20
use Ratchet\ConnectionInterface;
21
use Ratchet\Http\HttpServer;
22
use Ratchet\MessageComponentInterface;
23
use Ratchet\Server\IoServer;
24
use Ratchet\WebSocket\WsServer;
25
use React\EventLoop\LoopInterface;
26
use React\Socket\Server as ServerSocket;
27
use Symfony\Component\EventDispatcher\EventDispatcher;
28
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
29
30
/**
31
 * Handles events related to the WebSocket. Events are either triggered from Ratchet (onOpen etc.)
32
 * or, the event dispatcher or client requests.
33
 *
34
 * @package   Jalle19\StatusManager
35
 * @copyright Copyright &copy; Sam Stenvall 2016-
36
 * @license   https://www.gnu.org/licenses/gpl.html The GNU General Public License v2.0
37
 */
38
class WebSocketManager extends AbstractManager implements
39
	MessageComponentInterface,
1 ignored issue
show
Coding Style introduced by
Expected 4 spaces before interface name; 1 found
Loading history...
40
	EventSubscriberInterface,
1 ignored issue
show
Coding Style introduced by
Expected 4 spaces before interface name; 1 found
Loading history...
41
	HandlerInterface
1 ignored issue
show
Coding Style introduced by
Expected 4 spaces before interface name; 1 found
Loading history...
42
{
43
44
	use DelegatesMessagesTrait;
45
46
	/**
47
	 * @var IoServer the Websocket server
48
	 */
49
	private $_websocket;
50
51
	/**
52
	 * @var \SplObjectStorage the connected clients
53
	 */
54
	private $_connectedClients;
55
56
	/**
57
	 * @var \SplObjectStorage the authenticated clients
58
	 */
59
	private $_authenticatedClients;
60
61
62
	/**
63
	 * WebSocketManager constructor.
64
	 *
65
	 * @param Configuration   $configuration
66
	 * @param LoggerInterface $logger
67
	 * @param EventDispatcher $eventDispatcher
68
	 * @param LoopInterface   $loop
69
	 */
70
	public function __construct(
71
		Configuration $configuration,
72
		LoggerInterface $logger,
73
		EventDispatcher $eventDispatcher,
74
		LoopInterface $loop
75
	) {
76
		parent::__construct($configuration, $logger, $eventDispatcher);
77
78
		// Keep track of clients
79
		$this->_connectedClients     = new \SplObjectStorage();
80
		$this->_authenticatedClients = new \SplObjectStorage();
81
82
		// Create the socket to listen on
83
		$socket = new ServerSocket($loop);
84
		$socket->listen($configuration->getListenPort(), $configuration->getListenAddress());
85
86
		// Create the WebSocket server
87
		$this->_websocket = new IoServer(new HttpServer(new WsServer($this)), $socket, $loop);
88
	}
89
90
91
	/**
92
	 * @inheritdoc
93
	 */
94
	public static function getSubscribedEvents()
95
	{
96
		return [
97
			Events::MAIN_LOOP_STARTING      => 'onMainLoopStarted',
98
			Events::INSTANCE_STATUS_UPDATES => 'onInstanceStatusUpdates',
99
		];
100
	}
101
102
103
	/**
104
	 * Called right before the main loop is started
105
	 */
106
	public function onMainLoopStarted()
107
	{
108
		$this->logger->info('Starting the Websocket server on {address}:{port}', [
109
			'address' => $this->configuration->getListenAddress(),
110
			'port'    => $this->configuration->getListenPort(),
111
		]);
112
	}
113
114
115
	/**
116
	 * @param InstanceStatusUpdatesEvent $event
117
	 */
118
	public function onInstanceStatusUpdates(InstanceStatusUpdatesEvent $event)
119
	{
120
		$this->logger->debug('Broadcasting statuses to all clients');
121
		$message = new StatusUpdatesMessage($event->getInstanceStatusCollection());
122
123
		foreach ($this->_authenticatedClients as $client)
124
		{
125
			/* @var ConnectionInterface $client */
126
			$this->sendMessage($message, $client);
127
		}
128
	}
129
130
131
	/**
132
	 * @inheritdoc
133
	 */
134
	public function onOpen(ConnectionInterface $conn)
135
	{
136
		$this->logger->debug('Got client connection');
137
		$this->_connectedClients->attach($conn);
138
	}
139
140
141
	/**
142
	 * @inheritdoc
143
	 */
144
	public function onClose(ConnectionInterface $conn)
145
	{
146
		$this->logger->debug('Got client disconnect');
147
148
		$this->_connectedClients->detach($conn);
149
		$this->_authenticatedClients->detach($conn);
150
	}
151
152
153
	/**
154
	 * @inheritdoc
155
	 */
156
	public function onError(ConnectionInterface $conn, \Exception $e)
157
	{
158
		// TODO: Implement onError() method.
159
	}
160
161
162
	/**
163
	 * @inheritdoc
164
	 */
165
	public function handleMessage(AbstractMessage $message, ConnectionInterface $sender)
166
	{
167
		if ($message->getType() !== AbstractMessage::TYPE_AUTHENTICATION_REQUEST)
168
			return false;
169
170
		/* @var AuthenticationRequest $message */
171
		$status = AuthenticationResponse::STATUS_FAILURE;
172
173
		// Add the sender to the list of authenticated clients
174
		if ($message->getAccessToken() === $this->configuration->getAccessToken())
175
		{
176
			$status = AuthenticationResponse::STATUS_SUCCESS;
177
			$this->_authenticatedClients->attach($sender);
178
179
			$this->logger->info('Client authenticated successfully');
180
		}
181
		else
182
			$this->logger->warning('Got invalid authentication request from client');
183
184
		return new AuthenticationResponse($status);
185
	}
186
187
188
	/**
189
	 * Dispatches incoming client messages to the appropriate handlers
190
	 *
191
	 * @param ConnectionInterface $from
192
	 * @param string              $msg
193
	 */
194
	public function onMessage(ConnectionInterface $from, $msg)
195
	{
196
		try
197
		{
198
			$message = MessageFactory::factory($msg);
199
200
			$this->logger->debug('Got message from client (type: {messageType})', [
201
				'messageType' => $message->getType(),
202
			]);
203
204
			// Attempt to delegate the message
205
			try
206
			{
207
				$response = $this->tryDelegateMessage($message, $from);
208
				$this->sendMessage($response, $from);
209
			}
210
			catch (RequestFailedException $e)
211
			{
212
				$this->logger->critical('The request failed: ' . $e->getMessage());
213
			}
214
			catch (UnhandledMessageException $e)
215
			{
216
				$this->logger->error('Unhandled message (type: {messageType})', [
217
					'messageType' => $message->getType(),
218
				]);
219
			}
220
		}
221
		catch (MalformedRequestException $e)
222
		{
223
			$this->logger->error('Got malformed message from client (reason: {reason})', [
224
				'reason' => $e->getMessage(),
225
			]);
226
		}
227
		catch (UnknownRequestException $e)
228
		{
229
			// The server itself sometimes sends out messages that are received here, hence debug
230
			$this->logger->debug('Got unknown message from client (type: {messageType})', [
231
				'messageType' => $e->getType(),
232
			]);
233
		}
234
	}
235
236
237
	/**
238
	 * @param AbstractMessage     $message
239
	 * @param ConnectionInterface $target
240
	 */
241
	private function sendMessage(AbstractMessage $message, ConnectionInterface $target)
242
	{
243
		$target->send(json_encode($message));
244
	}
245
246
}
247