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 private function __construct () {
136 // Call parent constructor
137 parent::constructor(__CLASS__);
140 $this->setPartDescr("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 * Setter for connection timestamp only once
211 public final function setConnectionTimestamp () {
212 if ($this->connectTime == 0) $this->connectTime = time();
216 * Getter for a raw IP number
218 * @return $ip The peer's IP number
219 * @throws BrokenPipeException If a socket has lost its connection to the peer
221 public final function getRawIP () {
222 if (is_resource($this->peerSocket)) {
223 // Get IP from socket
224 @socket_getpeername($this->peerSocket, $ip);
226 // Connection problems?
227 if (socket_last_error() > 0) {
228 // Throw an exception
229 throw new BrokenPipeException(
232 'code' => socket_last_error()
233 ), self::EXCEPTION_PEER_SOCKET_BROKEN
237 // Without a socket we cannot determine the right IP
246 * Getter for a validated peer IP
248 * @return $peerIP The peer's IP number
250 public final function getValidatedIP() {
251 // Is the socket valid and IP not set?
252 if ((is_resource($this->peerSocket)) && (socket_last_error() == 0) && (self::$peerIP == "0.0.0.0")) {
253 // Get peer's IP number
254 self::$peerIP = $this->getRawIP();
255 } elseif ((is_resource($this->peerSocket)) && (socket_last_error() == 0)) {
256 // Get current raw IP number for validation
257 $ip = $this->getRawIP();
259 // Check if the IP has changed
260 if ($ip !== self::$peerIP) {
261 // The IP number has changed!
262 throw new IPSpoofingException(
265 'old_ip' => self::$peerIP,
267 ), self::EXCEPTION_PEER_IP_CHANGED
272 return self::$peerIP;
276 * Getter for number of authorization tries
277 * @return $authRetries Authorization tries
279 public final function getAuthRetries () {
280 return $this->authRetries;
283 //----------------------------------------------------------
285 //----------------------------------------------------------
288 * Setter for HubCoreLoop instances
290 * @param $hubInstance An instance of a HubCoreLoop class
293 public final function setHubInstance(HubCoreLoop $hubInstance) {
294 $this->hubInstance = $hubInstance;
298 * Getter for HubCoreLoop instances
300 * @return $hubInstance An instance of a HubCoreLoop class
302 public final function getHubInstance() {
303 return $this->hubInstance;
307 * Getter for last sent message timestamp
309 * @return $lastSentMessageStamp Timestamp of last sent message
311 public final function getLastSentMessageStamp () {
312 return $this->lastSentMessageStamp;
316 * Getter for last sent message
318 * @return $lastSentMessage Last sent message to this peer
320 public final function getLastSentMessage () {
321 return $this->lastSentMessage;
325 * Determines wether the peer is authorized
329 public final function ifPeerIsAuthorized () {
330 return (($this->isAuthorized === true) && ($this->ifPeerNeedsAuthRequest() === false));
334 * Returns wether this peer needs to receive the AUTH command
336 public final function ifPeerNeedsAuthRequest () {
337 return $this->needAuthRequest;
341 * Disconnects the peer with a special reason (status)
343 * @param $reason The special reason
346 public final function disconnectWithReason ($reason) {
347 // Is the message set?
348 if (!empty($reason)) {
349 // Send the given message
350 $this->sendMessage($reason);
352 // Send default "good bye"
353 $this->sendMessage($this->getConfigInstance()->readConfig("hub_msg_bye"));
356 // Disconnect the client
357 socket_shutdown($this->peerSocket, 2);
358 socket_close($this->peerSocket);
362 * Sends a specified message to the peer's socket
364 * @param $message The special message
366 * @throws BrokenPipeException If a pipe to a socket peer is lost
368 public final function sendMessage ($message) {
369 if (empty($message)) {
370 // Set default message
371 $message = $this->getConfigInstance()->readConfig("hub_msg_bye");
375 $this->getDebugInstance()->output(sprintf("[%s] Sending message \"%s\" to peer %s.<br />\n",
378 $this->getValidatedIP()
381 // Send it to the peer
382 if (socket_write($this->peerSocket, $message."\n") !== false) {
383 // Set the timestamp and message
384 $this->lastSentMessage = $message;
385 $this->lastSentMessageStamp = time();
387 // Message could not be sent
388 throw new BrokenPipeException (
391 'code' => socket_last_error()
392 ), self::EXCEPTION_PEER_SOCKET_BROKEN
398 * Asks the connected new peer for the authorization key
402 public final function askAuthorizationKey () {
403 if ($this->ifPeerNeedsAuthRequest()) {
404 // This peer needs to receive the AUTH command so send it to him
405 $this->sendMessage($this->getConfigInstance()->readConfig("hub_auth_request"));
408 $this->needAuthRequest = false;
413 * Determines wether the peer is a local admin (localhost connection)
415 * @return $isLocalAdmin Wether the peer is a local admin
417 public function ifPeerIsLocalAdmin () {
418 // Get the remote IP and extract only the first three numbers
419 $remoteArray = explode(".", self::$peerIP);
421 // Is the peer a local admin?
422 return (($remoteArray[0].".".$remoteArray[1].".".$remoteArray[2]) == "127.0.0");
426 * Enables the local admin and authorizes this peer
430 public function enableLocalAdmin() {
431 // Is this IP really local?
432 if (($this->ifPeerIsLocalAdmin()) && (!$this->ifPeerIsAuthorized())) {
433 // Then authorize him
434 $this->isAuthorized = true;
435 $this->needAuthRequest = false;
440 * Says "hi" to the local admin
444 public function sayHi2Admin () {
445 // Send a message to him... ;-)
446 $this->sendMessage("Local admin console is ready. Enter HELP for instructions.");
450 * Enables wether the peer is authorized
452 * @param $isAuthValid Wether the authorization wents fine
455 public function enableIsAuthorized ($isAuthValid = true) {
456 $this->isAuthorized = true;
460 * Checks wether a HELLO has been sent and if not it will be send to the other peer
462 * @return $helloSent Wether a HELLO has been sent
464 public function ifHelloReceived () {
465 // HELLO has been sent?
466 if (!$this->helloSent) {
468 $read = $this->readFromSocket();
471 if ($read == $this->getConfigInstance()->readConfig("hub_peer_hello")) {
472 // All right! A HELLO has been received
473 $this->helloSent = true;
474 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] Peer %s said HELLO to us.",
476 $this->getValidatedIP()
482 return $this->helloSent;
486 * Wether this hub has replied our HELLO request
488 * @return $helloReplied Wether this hub has replied our HELLO request
490 public function ifHelloReplied () {
491 if ((!$this->helloReplied) && ($this->lastSentMessage == $this->getConfigInstance()->readConfig("hub_peer_hello"))) {
493 $read = $this->readFromSocket();
496 if ($read == $this->getConfigInstance()->readConfig("hub_hello_reply")) {
497 // Is this the master IP?
498 if ($this->getValidatedIP() == $this->getConfigInstance()->readConfig("hub_master_ip")) {
499 // All right! A HELLO has been received
500 $this->helloReplied = true;
501 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] The master hub at %s:%d replied our %s.",
503 $this->getValidatedIP(),
504 $this->getConfigInstance()->readConfig("hub_master_port"),
505 $this->getConfigInstance()->readConfig("hub_peer_hello")
508 // ELHOs from non-masters are not valid!
509 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] Peer %s replied our %s but is not the master hub!",
511 $this->getValidatedIP(),
512 $this->getConfigInstance()->readConfig("hub_peer_hello")
519 return $this->helloReplied;
523 * Returns wether a ELHO (HELLO reply has been sent)
525 * @return $elhoSent Wether a ELHO has been sent to the peer
527 public final function ifELHOsent () {
528 return $this->elhoSent;
532 * Replies a HELLO message with a ELHO message
536 public function replyHelloMessage () {
537 $this->sendMessage($this->getConfigInstance()->readConfig("hub_hello_reply"));
538 $this->elhoSent = true;
542 * Handles pinging this peer
546 public function handlePingPeer () {
549 // Do we need to ping?
550 if ((time() - $this->lastPinged) > $this->getConfigInstance()->readConfig("hub_ping_timeout")) {
551 // Don't ping any masters! ;-)
552 if ($this->getValidatedIP() != $this->getConfigInstance()->readConfig("hub_master_ip")) {
554 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] Sending ping to peer %s...",
556 $this->getValidatedIP()
559 // Send out a PING and await a PONG
560 $this->commandInstance->simpleExecute(
561 $this->getConfigInstance()->readConfig("hub_peer_ping"),
562 $this->getConfigInstance()->readConfig("hub_ping_reply")
565 // PONG received within last ping?
566 if (($this->lastPonged < time()) && (($this->lastPonged - $this->lastPinged) < 0)) {
567 // Not replied so far
568 $this->missingPongs++;
571 $this->getHubInstance()->getOutputInstance()->output(sprintf("[%s] Ping not replied! Try: %d",
577 if ($this->missingPongs == $this->getConfigInstance()->readConfig("hub_ping_maxdrops")) {
579 $this->getHubInstance()->disconnectPeerWithReason("hub_peer_miss_pong");
584 // Last time we pinged is now.
585 $this->lastPinged = time();
590 // Connection is lost?
591 if ($lost === true) return false;
593 // Awaiting PONG here
594 if ($this->commandInstance->ifAwaitsCommand($this->getConfigInstance()->readConfig("hub_ping_reply"))) {
595 // PONG received! :-) So reset all counters...
596 $this->lastPonged = time();
597 $this->missingPongs = 0;
599 // Notify the loop about the ping-pong-time
600 $this->getHubInstance()->updatePeerEntry($this, (time() - $this->lastPinged));
605 * Handles any incoming commands from the master hub
609 public function handleMasterRequests () {
610 // Read the raw socket for data packages
611 if ($this->commandInstance->awaitAnyCommand()) {
612 // A command has been received from the master hub
613 $command = $this->commandInstance->pull();
615 // TODO Handle a command from the master here...
620 * Reads raw data from the socket and trims leading/trailing spaces away.
621 * Returns an empty string if no data has been received.
623 * @return $data Raw data from the underlaying socket
624 * @throws BrokenPipeException If a socket has lost its connection to the peer
626 public function readFromSocket () {
628 $read = array($this->peerSocket);
631 $num = socket_select($read, $write, $except, 0);
633 // Something has changed on a socket
634 foreach ($read as $socket) {
635 if (is_resource($socket)) {
636 $data = trim(@socket_read($socket, 1024, PHP_NORMAL_READ));
637 if (socket_last_error() > 0) {
638 // Throw an exception
639 throw new BrokenPipeException(
642 'code' => socket_last_error()
643 ), self::EXCEPTION_PEER_SOCKET_BROKEN