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->removeSystemArray();
141 $this->removeNumberFormaters();
143 // Get a command processor
144 $this->commandInstance = HubCommandProcessor::createHubCommandProcessor($this);
148 * Creates an instance of a HubPeer by a provided valid socket resource
150 * @param $peerSocket The socket resource for the peer
151 * @param $hubInstance An instance of HubCoreLoop
152 * @return $peerInstance An instance of HubPeer
154 public final static function createHubPeerBySocket ($peerSocket, HubCoreLoop $hubInstance) {
155 // Get a new instance
156 $peerInstance = new HubPeer();
158 // Is the peer socket fine?
159 if (!is_resource($peerSocket)) {
160 // There is a problem with the socket
161 throw new PeerSocketException (
163 'this' => $peerInstance,
164 'type' => gettype($peerSocket)
165 ), self::EXCEPTION_PEER_SOCKET_INVALID
170 $peerInstance->setPeerSocket($peerSocket);
172 // Set connection timestamp
173 $peerInstance->setConnectionTimestamp();
175 // Set the hub instance
176 $peerInstance->setHubInstance($hubInstance);
178 // Return the instance
179 return $peerInstance;
182 //----------------------------------------------------------
183 // Public getter/setter
184 //----------------------------------------------------------
187 * Setter for peer socket
189 * @param $peerSocket The peer's socket
192 public final function setPeerSocket ($peerSocket) {
193 if (is_resource($peerSocket)) {
194 $this->peerSocket = $peerSocket;
196 $this->peerSocket = null;
201 * Getter for peer socket
203 * @return $peerSocket The peer's socket
205 public final function getPeerSocket () {
206 return $this->peerSocket;
210 * Setter for connection timestamp only once
214 public final function setConnectionTimestamp () {
215 if ($this->connectTime == 0) $this->connectTime = time();
219 * Getter for a raw IP number
221 * @return $ip The peer's IP number
222 * @throws BrokenPipeException If a socket has lost its connection to the peer
224 public final function getRawIP () {
225 if (is_resource($this->peerSocket)) {
226 // Get IP from socket
227 @socket_getpeername($this->peerSocket, $ip);
229 // Connection problems?
230 if (socket_last_error() > 0) {
231 // Throw an exception
232 throw new BrokenPipeException(
235 'code' => socket_last_error()
236 ), self::EXCEPTION_PEER_SOCKET_BROKEN
240 // Without a socket we cannot determine the right IP
249 * Getter for a validated peer IP
251 * @return $peerIP The peer's IP number
253 public final function getValidatedIP() {
254 // Is the socket valid and IP not set?
255 if ((is_resource($this->peerSocket)) && (socket_last_error() == 0) && (self::$peerIP == "0.0.0.0")) {
256 // Get peer's IP number
257 self::$peerIP = $this->getRawIP();
258 } elseif ((is_resource($this->peerSocket)) && (socket_last_error() == 0)) {
259 // Get current raw IP number for validation
260 $ip = $this->getRawIP();
262 // Check if the IP has changed
263 if ($ip !== self::$peerIP) {
264 // The IP number has changed!
265 throw new IPSpoofingException(
268 'old_ip' => self::$peerIP,
270 ), self::EXCEPTION_PEER_IP_CHANGED
275 return self::$peerIP;
279 * Getter for number of authorization tries
280 * @return $authRetries Authorization tries
282 public final function getAuthRetries () {
283 return $this->authRetries;
286 //----------------------------------------------------------
288 //----------------------------------------------------------
291 * Setter for HubCoreLoop instances
293 * @param $hubInstance An instance of a HubCoreLoop class
296 public final function setHubInstance(HubCoreLoop $hubInstance) {
297 $this->hubInstance = $hubInstance;
301 * Getter for HubCoreLoop instances
303 * @return $hubInstance An instance of a HubCoreLoop class
305 public final function getHubInstance() {
306 return $this->hubInstance;
310 * Getter for last sent message timestamp
312 * @return $lastSentMessageStamp Timestamp of last sent message
314 public final function getLastSentMessageStamp () {
315 return $this->lastSentMessageStamp;
319 * Getter for last sent message
321 * @return $lastSentMessage Last sent message to this peer
323 public final function getLastSentMessage () {
324 return $this->lastSentMessage;
328 * Determines wether the peer is authorized
332 public final function ifPeerIsAuthorized () {
333 return (($this->isAuthorized === true) && ($this->ifPeerNeedsAuthRequest() === false));
337 * Returns wether this peer needs to receive the AUTH command
339 public final function ifPeerNeedsAuthRequest () {
340 return $this->needAuthRequest;
344 * Disconnects the peer with a special reason (status)
346 * @param $reason The special reason
349 public final function disconnectWithReason ($reason) {
350 // Is the message set?
351 if (!empty($reason)) {
352 // Send the given message
353 $this->sendMessage($reason);
355 // Send default "good bye"
356 $this->sendMessage($this->getConfigInstance()->readConfig("hub_msg_bye"));
359 // Disconnect the client
360 socket_shutdown($this->peerSocket, 2);
361 socket_close($this->peerSocket);
365 * Sends a specified message to the peer's socket
367 * @param $message The special message
369 * @throws BrokenPipeException If a pipe to a socket peer is lost
371 public final function sendMessage ($message) {
372 if (empty($message)) {
373 // Set default message
374 $message = $this->getConfigInstance()->readConfig("hub_msg_bye");
378 $this->getDebugInstance()->output(sprintf("[%s] Sending message \"%s\" to peer %s.<br />\n",
381 $this->getValidatedIP()
384 // Is the socket still valid?
385 if (!is_resource($this->peerSocket)) {
386 // The socket is no longer valid!
387 throw new BrokenPipeException (
390 'code' => socket_last_error()
391 ), self::EXCEPTION_PEER_SOCKET_BROKEN
395 // Send it to the peer
396 if (socket_write($this->peerSocket, $message."\n") !== false) {
397 // Set the timestamp and message
398 $this->lastSentMessage = $message;
399 $this->lastSentMessageStamp = time();
401 // Message could not be sent
402 throw new BrokenPipeException (
405 'code' => socket_last_error()
406 ), self::EXCEPTION_PEER_SOCKET_BROKEN
412 * Asks the connected new peer for the authorization key
416 public final function askAuthorizationKey () {
417 if ($this->ifPeerNeedsAuthRequest()) {
418 // This peer needs to receive the AUTH command so send it to him
419 $this->sendMessage($this->getConfigInstance()->readConfig("hub_auth_request"));
422 $this->needAuthRequest = false;
427 * Determines wether the peer is a local admin (localhost connection)
429 * @return $isLocalAdmin Wether the peer is a local admin
431 public function ifPeerIsLocalAdmin () {
432 // Get the remote IP and extract only the first three numbers
433 $remoteArray = explode(".", self::$peerIP);
435 // Is the peer a local admin?
436 return (($remoteArray[0].".".$remoteArray[1].".".$remoteArray[2]) == "127.0.0");
440 * Enables the local admin and authorizes this peer
444 public function enableLocalAdmin() {
445 // Is this IP really local?
446 if (($this->ifPeerIsLocalAdmin()) && (!$this->ifPeerIsAuthorized())) {
447 // Then authorize him
448 $this->isAuthorized = true;
449 $this->needAuthRequest = false;
454 * Says "hi" to the local admin
458 public function sayHi2Admin () {
459 // Send a message to him... ;-)
460 $this->sendMessage("Local admin console is ready. Enter HELP for instructions.");
464 * Enables wether the peer is authorized
466 * @param $isAuthValid Wether the authorization wents fine
469 public function enableIsAuthorized ($isAuthValid = true) {
470 $this->isAuthorized = true;
474 * Checks wether a HELLO has been sent and if not it will be send to the other peer
476 * @return $helloSent Wether a HELLO has been sent
478 public function ifHelloReceived () {
479 // HELLO has been sent?
480 if (!$this->helloSent) {
482 $read = $this->readFromSocket();
485 if ($read == $this->getConfigInstance()->readConfig("hub_peer_hello")) {
486 // All right! A HELLO has been received
487 $this->helloSent = true;
488 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] Peer %s said HELLO to us.",
490 $this->getValidatedIP()
496 return $this->helloSent;
500 * Wether this hub has replied our HELLO request
502 * @return $helloReplied Wether this hub has replied our HELLO request
504 public function ifHelloReplied () {
505 if ((!$this->helloReplied) && ($this->lastSentMessage == $this->getConfigInstance()->readConfig("hub_peer_hello"))) {
507 $read = $this->readFromSocket();
510 if ($read == $this->getConfigInstance()->readConfig("hub_hello_reply")) {
511 // Is this the master IP?
512 if ($this->getValidatedIP() == $this->getConfigInstance()->readConfig("hub_master_ip")) {
513 // All right! A HELLO has been received
514 $this->helloReplied = true;
515 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] The master hub at %s:%d replied our %s.",
517 $this->getValidatedIP(),
518 $this->getConfigInstance()->readConfig("hub_master_port"),
519 $this->getConfigInstance()->readConfig("hub_peer_hello")
522 // ELHOs from non-masters are not valid!
523 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] Peer %s replied our %s but is not the master hub!",
525 $this->getValidatedIP(),
526 $this->getConfigInstance()->readConfig("hub_peer_hello")
533 return $this->helloReplied;
537 * Returns wether a ELHO (HELLO reply has been sent)
539 * @return $elhoSent Wether a ELHO has been sent to the peer
541 public final function ifELHOsent () {
542 return $this->elhoSent;
546 * Replies a HELLO message with a ELHO message
550 public function replyHelloMessage () {
551 $this->sendMessage($this->getConfigInstance()->readConfig("hub_hello_reply"));
552 $this->elhoSent = true;
556 * Handles pinging this peer
560 public function handlePingPeer () {
563 // Do we need to ping?
564 if ((time() - $this->lastPinged) > $this->getConfigInstance()->readConfig("hub_ping_timeout")) {
565 // Don't ping any masters! ;-)
566 if ($this->getValidatedIP() != $this->getConfigInstance()->readConfig("hub_master_ip")) {
568 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] Sending ping to peer %s...",
570 $this->getValidatedIP()
573 // Send out a PING and await a PONG
574 $this->commandInstance->simpleExecute(
575 $this->getConfigInstance()->readConfig("hub_peer_ping"),
576 $this->getConfigInstance()->readConfig("hub_ping_reply")
579 // PONG received within last ping?
580 if (($this->lastPonged < time()) && (($this->lastPonged - $this->lastPinged) < 0)) {
581 // Not replied so far
582 $this->missingPongs++;
585 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] Ping not replied! Try: %d",
591 if ($this->missingPongs == $this->getConfigInstance()->readConfig("hub_ping_maxdrops")) {
593 $this->getHubInstance()->disconnectPeerWithReason("hub_peer_miss_pong");
598 // Last time we pinged is now.
599 $this->lastPinged = time();
604 // Connection is lost?
605 if ($lost === true) return false;
607 // Awaiting PONG here
608 if ($this->commandInstance->ifAwaitsCommand($this->getConfigInstance()->readConfig("hub_ping_reply"))) {
609 // PONG received! :-) So reset all counters...
610 $this->lastPonged = time();
611 $this->missingPongs = 0;
613 // Notify the loop about the ping-pong-time
614 $this->getHubInstance()->updatePeerEntry($this, (time() - $this->lastPinged));
619 * Handles any incoming commands from the master hub
623 public function handleMasterRequests () {
624 // Read the raw socket for data packages
625 if ($this->commandInstance->awaitAnyCommand()) {
626 // A command has been received from the master hub
627 $command = $this->commandInstance->pull();
629 // TODO Handle a command from the master here...
634 * Reads raw data from the socket and trims leading/trailing spaces away.
635 * Returns an empty string if no data has been received.
637 * @return $data Raw data from the underlaying socket
638 * @throws BrokenPipeException If a socket has lost its connection to the peer
640 public function readFromSocket () {
642 $read = array($this->peerSocket);
645 $num = socket_select($read, $write, $except, 0);
647 // Something has changed on a socket
648 foreach ($read as $socket) {
649 if (is_resource($socket)) {
650 $data = trim(@socket_read($socket, 1024, PHP_NORMAL_READ));
651 if (socket_last_error() > 0) {
652 // Throw an exception
653 throw new BrokenPipeException(
656 'code' => socket_last_error()
657 ), self::EXCEPTION_PEER_SOCKET_BROKEN