3 * A hub peer class for communicating with the hub peer
5 * @author Roland Haeder <webmaster@ship-simu.org>
7 * @copyright Copyright(c) 2007, 2008 Roland Haeder, this is free software
8 * @license GNU GPL 3.0 or any newer version
10 * This program is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation, either version 3 of the License, or
13 * (at your option) any later version.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
23 class HubPeer extends BaseFrameworkSystem {
25 * This peer's socket resource
27 private $peerSocket = null;
30 * Timestamp for connection
32 private $connectTime = 0;
35 * Timestamp of last received message
37 private $lastReceivedMessageStamp = 0;
40 * Last received message
42 private $lastReceivedMessage = "";
45 * Timestamp of last sent message
47 private $lastSentMessageStamp = 0;
52 private $lastSentMessage = "";
55 * Number of tries for authorization
57 private $authRetries = 0;
60 * Wether the peer is authorized (DO NEVER SET THIS TO "true"!)
62 private $isAuthorized = false;
65 * Wether this peer needs to be requested for the AUTH command
67 private $needAuthRequest = true;
70 * The peer's IP number
72 private static $peerIP = "0.0.0.0";
75 * Wether the peer has "hello-ed" to the other
77 private $helloSent = false;
80 * Wether the peer has replied our HELLO
82 private $helloReplied = false;
85 * Wether we have sent our ELHO
87 private $elhoSent = false;
90 * An instance of HubCoreLoop
92 private $hubInstance = null;
95 * When the last ping was
97 private $lastPinged = 0;
100 * When the last pong was
102 private $lastPonged = 0;
105 * Number of missing pongs from the other peer
107 private $missingPongs = 0;
110 * An instance of a HubCommandProcessor class
112 private $commandInstance = null;
115 * A queue for reading data in non-blocking mode
117 private $queues = "";
122 const LINE_END = "\n";
124 //----------------------------------------------------------
126 //----------------------------------------------------------
127 const EXCEPTION_PEER_SOCKET_INVALID = 0x200;
128 const EXCEPTION_PEER_IP_CHANGED = 0x201;
129 const EXCEPTION_PEER_SOCKET_BROKEN = 0x202;
130 //----------------------------------------------------------
133 * The private constructor
135 protected function __construct () {
136 // Call parent constructor
137 parent::__construct(__CLASS__);
140 $this->setObjectDescription("Hub-Peer");
143 $this->createUniqueID();
146 $this->removeSystemArray();
147 $this->removeNumberFormaters();
149 // Get a command processor
150 $this->commandInstance = HubCommandProcessor::createHubCommandProcessor($this);
154 * Creates an instance of a HubPeer by a provided valid socket resource
156 * @param $peerSocket The socket resource for the peer
157 * @param $hubInstance An instance of HubCoreLoop
158 * @return $peerInstance An instance of HubPeer
160 public final static function createHubPeerBySocket ($peerSocket, HubCoreLoop $hubInstance) {
161 // Get a new instance
162 $peerInstance = new HubPeer();
164 // Is the peer socket fine?
165 if (!is_resource($peerSocket)) {
166 // There is a problem with the socket
167 throw new PeerSocketException (
169 'this' => $peerInstance,
170 'type' => gettype($peerSocket)
171 ), self::EXCEPTION_PEER_SOCKET_INVALID
176 $peerInstance->setPeerSocket($peerSocket);
178 // Set connection timestamp
179 $peerInstance->setConnectionTimestamp();
181 // Set the hub instance
182 $peerInstance->setHubInstance($hubInstance);
184 // Return the instance
185 return $peerInstance;
188 //----------------------------------------------------------
189 // Public getter/setter
190 //----------------------------------------------------------
193 * Setter for peer socket
195 * @param $peerSocket The peer's socket
198 public final function setPeerSocket ($peerSocket) {
199 if (is_resource($peerSocket)) {
200 $this->peerSocket = $peerSocket;
202 $this->peerSocket = null;
207 * Getter for peer socket
209 * @return $peerSocket The peer's socket
211 public final function getPeerSocket () {
212 return $this->peerSocket;
216 * Setter for connection timestamp only once
220 public final function setConnectionTimestamp () {
221 if ($this->connectTime == 0) $this->connectTime = time();
225 * Getter for a raw IP number
227 * @return $ip The peer's IP number
228 * @throws BrokenPipeException If a socket has lost its connection to the peer
230 public final function getRawIP () {
231 if (is_resource($this->peerSocket)) {
232 // Get IP from socket
233 @socket_getpeername($this->peerSocket, $ip);
235 // Connection problems?
236 if (socket_last_error() > 0) {
237 // Throw an exception
238 throw new BrokenPipeException(
241 'code' => socket_last_error()
242 ), self::EXCEPTION_PEER_SOCKET_BROKEN
246 // Without a socket we cannot determine the right IP
255 * Getter for a validated peer IP
257 * @return $peerIP The peer's IP number
259 public final function getValidatedIP() {
260 // Is the socket valid and IP not set?
261 if ((is_resource($this->peerSocket)) && (socket_last_error() == 0) && (self::$peerIP == "0.0.0.0")) {
262 // Get peer's IP number
263 self::$peerIP = $this->getRawIP();
264 } elseif ((is_resource($this->peerSocket)) && (socket_last_error() == 0)) {
265 // Get current raw IP number for validation
266 $ip = $this->getRawIP();
268 // Check if the IP has changed
269 if ($ip !== self::$peerIP) {
270 // The IP number has changed!
271 throw new IPSpoofingException(
274 'old_ip' => self::$peerIP,
276 ), self::EXCEPTION_PEER_IP_CHANGED
281 return self::$peerIP;
285 * Getter for number of authorization tries
286 * @return $authRetries Authorization tries
288 public final function getAuthRetries () {
289 return $this->authRetries;
292 //----------------------------------------------------------
294 //----------------------------------------------------------
297 * Setter for HubCoreLoop instances
299 * @param $hubInstance An instance of a HubCoreLoop class
302 public final function setHubInstance(HubCoreLoop $hubInstance) {
303 $this->hubInstance = $hubInstance;
307 * Getter for HubCoreLoop instances
309 * @return $hubInstance An instance of a HubCoreLoop class
311 public final function getHubInstance() {
312 return $this->hubInstance;
316 * Getter for last sent message timestamp
318 * @return $lastSentMessageStamp Timestamp of last sent message
320 public final function getLastSentMessageStamp () {
321 return $this->lastSentMessageStamp;
325 * Getter for last sent message
327 * @return $lastSentMessage Last sent message to this peer
329 public final function getLastSentMessage () {
330 return $this->lastSentMessage;
334 * Determines wether the peer is authorized
338 public final function ifPeerIsAuthorized () {
339 return (($this->isAuthorized === true) && ($this->ifPeerNeedsAuthRequest() === false));
343 * Returns wether this peer needs to receive the AUTH command
345 public final function ifPeerNeedsAuthRequest () {
346 return $this->needAuthRequest;
350 * Disconnects the peer with a special reason (status)
352 * @param $reason The special reason
355 public final function disconnectWithReason ($reason) {
356 // Is the message set?
357 if (!empty($reason)) {
358 // Send the given message
359 $this->sendMessage($reason);
361 // Send default "good bye"
362 $this->sendMessage($this->getConfigInstance()->readConfig("hub_msg_bye"));
365 // Disconnect the client
366 socket_shutdown($this->peerSocket, 2);
367 socket_close($this->peerSocket);
371 * Sends a specified message to the peer's socket
373 * @param $message The special message
375 * @throws BrokenPipeException If a pipe to a socket peer is lost
377 public final function sendMessage ($message) {
378 if (empty($message)) {
379 // Set default message
380 $message = $this->getConfigInstance()->readConfig("hub_msg_bye");
384 $this->getDebugInstance()->output(sprintf("[%s] Sending message \"%s\" to peer %s.<br />\n",
387 $this->getValidatedIP()
390 // Is the socket still valid?
391 if (!is_resource($this->peerSocket)) {
392 // The socket is no longer valid!
393 throw new BrokenPipeException (
396 'code' => socket_last_error()
397 ), self::EXCEPTION_PEER_SOCKET_BROKEN
401 // Send it to the peer
402 if (socket_write($this->peerSocket, $message."\n") !== false) {
403 // Set the timestamp and message
404 $this->lastSentMessage = $message;
405 $this->lastSentMessageStamp = time();
407 // Message could not be sent
408 throw new BrokenPipeException (
411 'code' => socket_last_error()
412 ), self::EXCEPTION_PEER_SOCKET_BROKEN
418 * Asks the connected new peer for the authorization key
422 public final function askAuthorizationKey () {
423 if ($this->ifPeerNeedsAuthRequest()) {
424 // This peer needs to receive the AUTH command so send it to him
425 $this->sendMessage($this->getConfigInstance()->readConfig("hub_auth_request"));
428 $this->needAuthRequest = false;
433 * Determines wether the peer is a local admin (localhost connection)
435 * @return $isLocalAdmin Wether the peer is a local admin
437 public function ifPeerIsLocalAdmin () {
438 // Get the remote IP and extract only the first three numbers
439 $remoteArray = explode(".", self::$peerIP);
441 // Is the peer a local admin?
442 return (($remoteArray[0].".".$remoteArray[1].".".$remoteArray[2]) == "127.0.0");
446 * Enables the local admin and authorizes this peer
450 public function enableLocalAdmin() {
451 // Is this IP really local?
452 if (($this->ifPeerIsLocalAdmin()) && (!$this->ifPeerIsAuthorized())) {
453 // Then authorize him
454 $this->isAuthorized = true;
455 $this->needAuthRequest = false;
460 * Says "hi" to the local admin
464 public function sayHi2Admin () {
465 // Send a message to him... ;-)
466 $this->sendMessage("Local admin console is ready. Enter HELP for instructions.");
470 * Enables wether the peer is authorized
472 * @param $isAuthValid Wether the authorization wents fine
475 public function enableIsAuthorized ($isAuthValid = true) {
476 $this->isAuthorized = true;
480 * Checks wether a HELLO has been sent and if not it will be send to the other peer
482 * @return $helloSent Wether a HELLO has been sent
484 public function ifHelloReceived () {
485 // HELLO has been sent?
486 if (!$this->helloSent) {
488 $read = $this->readFromSocket();
491 if ($read == $this->getConfigInstance()->readConfig("hub_peer_hello")) {
492 // All right! A HELLO has been received
493 $this->helloSent = true;
494 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] Peer %s said HELLO to us.",
496 $this->getValidatedIP()
502 return $this->helloSent;
506 * Wether this hub has replied our HELLO request
508 * @return $helloReplied Wether this hub has replied our HELLO request
510 public function ifHelloReplied () {
511 if ((!$this->helloReplied) && ($this->lastSentMessage == $this->getConfigInstance()->readConfig("hub_peer_hello"))) {
513 $read = $this->readFromSocket();
516 if ($read == $this->getConfigInstance()->readConfig("hub_hello_reply")) {
517 // Is this the master IP?
518 if ($this->getValidatedIP() == $this->getConfigInstance()->readConfig("hub_master_ip")) {
519 // All right! A HELLO has been received
520 $this->helloReplied = true;
521 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] The master hub at %s:%d replied our %s.",
523 $this->getValidatedIP(),
524 $this->getConfigInstance()->readConfig("hub_master_port"),
525 $this->getConfigInstance()->readConfig("hub_peer_hello")
528 // ELHOs from non-masters are not valid!
529 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] Peer %s replied our %s but is not the master hub!",
531 $this->getValidatedIP(),
532 $this->getConfigInstance()->readConfig("hub_peer_hello")
539 return $this->helloReplied;
543 * Returns wether a ELHO (HELLO reply has been sent)
545 * @return $elhoSent Wether a ELHO has been sent to the peer
547 public final function ifELHOsent () {
548 return $this->elhoSent;
552 * Replies a HELLO message with a ELHO message
556 public function replyHelloMessage () {
557 $this->sendMessage($this->getConfigInstance()->readConfig("hub_hello_reply"));
558 $this->elhoSent = true;
562 * Handles pinging this peer
566 public function handlePingPeer () {
569 // Do we need to ping?
570 if ((time() - $this->lastPinged) > $this->getConfigInstance()->readConfig("hub_ping_timeout")) {
571 // Don't ping any masters! ;-)
572 if ($this->getValidatedIP() != $this->getConfigInstance()->readConfig("hub_master_ip")) {
574 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] Sending ping to peer %s...",
576 $this->getValidatedIP()
579 // Send out a PING and await a PONG
580 $this->commandInstance->simpleExecute(
581 $this->getConfigInstance()->readConfig("hub_peer_ping"),
582 $this->getConfigInstance()->readConfig("hub_ping_reply")
585 // PONG received within last ping?
586 if (($this->lastPonged < time()) && (($this->lastPonged - $this->lastPinged) < 0)) {
587 // Not replied so far
588 $this->missingPongs++;
591 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] Ping not replied! Try: %d",
597 if ($this->missingPongs == $this->getConfigInstance()->readConfig("hub_ping_maxdrops")) {
599 $this->getHubInstance()->disconnectPeerWithReason("hub_peer_miss_pong");
604 // Last time we pinged is now.
605 $this->lastPinged = time();
610 // Connection is lost?
611 if ($lost === true) return false;
613 // Awaiting PONG here
614 if ($this->commandInstance->ifAwaitsCommand($this->getConfigInstance()->readConfig("hub_ping_reply"))) {
615 // PONG received! :-) So reset all counters...
616 $this->lastPonged = time();
617 $this->missingPongs = 0;
619 // Notify the loop about the ping-pong-time
620 $this->getHubInstance()->updatePeerEntry($this, (time() - $this->lastPinged));
625 * Handles any incoming commands from the master hub
629 public function handleMasterRequests () {
630 // Read the raw socket for data packages
631 if ($this->commandInstance->awaitAnyCommand()) {
632 // A command has been received from the master hub
633 $command = $this->commandInstance->pull();
635 // TODO Handle a command from the master here...
640 * Reads raw data from the socket and trims leading/trailing spaces away.
641 * Returns an empty string if no data has been received.
643 * @return $data Raw data from the underlaying socket
644 * @throws BrokenPipeException If a socket has lost its connection to the peer
646 public function readFromSocket () {
648 $read = array($this->peerSocket);
651 $num = socket_select($read, $write, $except, 0);
653 // Something has changed on a socket
654 foreach ($read as $socket) {
655 if (is_resource($socket)) {
656 $data = trim(@socket_read($socket, 1024, PHP_NORMAL_READ));
657 if (socket_last_error() > 0) {
658 // Throw an exception
659 throw new BrokenPipeException(
662 'code' => socket_last_error()
663 ), self::EXCEPTION_PEER_SOCKET_BROKEN