3 * The hub's main loop. The hub will wait and listen for incoming requests in
6 * @author Roland Haeder <webmaster@ship-simu.org>
8 * @copyright Copyright(c) 2007, 2008 Roland Haeder, this is free software
9 * @license GNU GPL 3.0 or any newer version
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
21 * You should have received a copy of the GNU General Public License
22 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24 class HubCoreLoop extends BaseFrameworkSystem {
26 * Wether the hub is active and running
28 private $hubActivated = false;
31 * Wether the hub is shutting down
33 private $hubShutsDown = false;
36 * A list of all connected peers (sockets)
38 private $connectedPeers = null;
41 * A console output handler
43 private $outputInstance = null;
48 private $readPeers = array();
53 private $writePeers = array();
56 * The main socket (listening) for this hub
58 private $main_socket = null;
61 * A list of authenticated peers
63 private $authPeers = null;
68 private $lastPeerInstance = null;
71 * Wether this hub is a master hub
73 private $hubIsMaster = false;
76 * Maximum auth retries (cached)
78 private $authRetries = 0;
81 * Auth request message
83 private $authRequest = "";
86 * Cached timeout for auth requests
88 private $authRequestTimeout = 0;
91 * An instance to the HubConnector class for master hub connection
93 private $masterConnector = null;
96 const EXCEPTION_SOCKET_PROBLEM = 0xb00;
97 const EXCEPTION_HUB_PEER_TIMEOUT = 0xb01;
98 const EXCEPTION_HUB_PEER_FAILED_AUTH = 0xb02;
99 const EXCEPTION_HELLO_TIMED_OUT = 0xb03;
102 * The private constructor
106 protected function __construct () {
107 // Call parent constructor
108 parent::__construct(__CLASS__);
111 $this->setObjectDescription("Hub-Core Loop");
114 $this->createUniqueID();
117 $this->removeSystemArray();
118 $this->removeNumberFormaters();
120 // Init the peer list
121 $this->initPeerList();
125 * Factory for main loop
127 * @return $hubInstance An instance of this class
129 public final static function createHubCoreLoop () {
131 $hubInstance = new HubCoreLoop();
133 // Try to setup the socket
134 $hubInstance->setupHub();
136 // Get the configuration variable
137 $outEngine = $hubInstance->getConfigInstance()->readConfig("tpl_engine");
139 // Setup the console output handler
140 $eval = sprintf("\$hubInstance->setOutputInstance(%s::create%s(\"%s\"));",
143 $hubInstance->getConfigInstance()->readConfig("web_content_type")
147 if ((defined('DEBUG_EVAL')) || (defined('DEBUG_ALL'))) $hubInstance->getDebugInstance()->output(sprintf("[%s:] Konstruierte PHP-Anweisung: %s",
148 $hubInstance->__toString(),
153 // Return the prepared instance
158 * Initializes the peer list
162 private final function initPeerList () {
163 $this->connectedPeers = new FrameworkArrayObject();
164 $this->authPeers = new FrameworkArrayObject();
168 * Get total number of connected peers
170 * @return $num The total number of connected peers
172 private final function getTotalConnectedPeers () {
173 $read = array($this->connectedPeers->getIterator()->current());
178 $num = socket_select(
185 // Transfer readers and writers
186 $this->readPeers = $read;
187 $this->writePeers = $write;
194 * Handle newly connected peers
198 private final function handleNewPeers () {
200 $this->getOutputInstance()->output(sprintf("[%s] Validating peer...", __METHOD__));
202 // Is the main socket in the array?
203 if (in_array($this->main_socket, $this->readPeers)) {
204 // Accept the connection and add him to the list
205 $peer_socket = socket_accept($this->main_socket);
207 // Get a new peer instance
208 $this->setCurrPeerInstance(HubPeer::createHubPeerBySocket($peer_socket, $this));
210 // Register the new peer
211 $this->getOutputInstance()->output(sprintf("[%s] Registering new peer with IP %s.",
213 $this->getCurrPeerInstance()->getValidatedIP()
215 $this->connectedPeers->append($this->lastPeerInstance);
217 // A new peer has connected
218 $this->getOutputInstance()->output(sprintf("[%s] New peer with IP %s has been registered.",
220 $this->getCurrPeerInstance()->getValidatedIP()
223 // Remove him from the list
224 $key = array_search($this->main_socket, $this->readPeers);
225 unset($this->readPeers[$key]);
230 * Handles unauthenticated peers
233 * @throws HubPeerTimeoutException If the peer times out to answer a request
234 * @throws HubPeerAuthorizationException If the peer fails to authorize himself (to many retries)
236 private final function handleUnauthPeers () {
237 // Are there some peers?
238 if ($this->connectedPeers->count() > 1) {
239 // Iterate through all connected peers
240 for ($idx = $this->connectedPeers->getIterator(); $idx->valid(); $idx->next()) {
242 $this->lastPeerInstance = $idx->current();
244 // Ignore own socket and invalid entries
245 if (($this->getCurrPeerInstance() !== $this->main_socket) && (is_object($this->getCurrPeerInstance())) && ($this->getCurrPeerInstance() instanceof HubPeer)) {
246 // Is this peer already authorized or is this the master hub?
247 // If this is the master hub then there is no auth required
248 if ((!$this->getCurrPeerInstance()->ifPeerIsAuthorized()) && (!$this->getCurrPeerInstance()->ifPeerIsLocalAdmin()) && (!$this->hubIsMaster)) {
249 // This peer waits for authorization, so does he have some tries left?
250 if ($this->getCurrPeerInstance()->getAuthRetries() <= $this->authRetries) {
251 // This peer is still allowed to try his authorization
252 if ($this->getCurrPeerInstance()->getLastSentMessage() == $this->authRequest) {
253 // Already asked so maybe timed out?
254 if ((time() - $this->getCurrPeerInstance()->getLastSentMessageStamp()) >= $this->authRequestTimeout) {
255 // Timed out so disconnect the peer
256 throw new HubPeerTimeoutException (
259 'peer' => $this->getCurrPeerInstance()
260 ), self::EXCEPTION_HUB_PEER_TIMEOUT
263 } elseif ($this->getCurrPeerInstance()->ifHelloReceived()) {
264 // HELLO received so we need to sent the AUTH request
265 $this->getCurrPeerInstance()->askAuthorizationKey();
268 // This peer needs disconnecting!
269 throw new HubPeerAuthorizationException (
272 'peer' => $this->getCurrPeerInstance(),
273 'max' => $this->authRetries
274 ), self::EXCEPTION_HUB_PEER_FAILED_AUTH
277 } elseif ((!$this->getCurrPeerInstance()->ifPeerIsAuthorized()) && ($this->getCurrPeerInstance()->ifPeerIsLocalAdmin())) {
278 // This peer is a local admin so he is always authorized!
279 $this->getCurrPeerInstance()->enableLocalAdmin();
281 // Output debug message
282 $this->getOutputInstance()->output(sprintf("[%s] A local admin has connected.",
286 // Say "hi" to the admin
287 $this->getCurrPeerInstance()->sayHi2Admin();
288 } elseif (($this->hubIsMaster) && ($this->getCurrPeerInstance()->ifHelloReceived()) && (!$this->getCurrPeerInstance()->ifELHOsent())) {
289 // Is the master hub so authorize the peer here...
290 $this->getCurrPeerInstance()->enableIsAuthorized();
291 $this->authPeers->append($this->getCurrPeerInstance());
293 // Reply the HELLO request
294 $this->getCurrPeerInstance()->replyHelloMessage();
302 * Handles only authorized peers
306 public function handleAuthPeers () {
307 // Are there some peers?
308 if (($this->connectedPeers->count() > 1) && ($this->authPeers->count() > 0)) {
309 // Iterate through all connected peers
310 for ($idx = $this->authPeers->getIterator(); $idx->valid(); $idx->next()) {
312 $this->lastPeerInstance = $idx->current();
314 // Do the ping and update our authPeer list (LATER!)
315 $this->lastPeerInstance->handlePingPeer();
317 // Avoids a notice...
318 if (is_null($this->getCurrPeerInstance())) break;
324 * Setup the hub: Create a socket for listening on incoming requests,
325 * try to bind to a port, etc.
328 * @throws SocketCreationException If a socket cannot be created
329 * @throws SocketSetupException If a socket cannot be setuped
330 * @throws SocketBindException If a socket cannot be bind to
331 * an address and port
332 * @throws SocketListeningException If listening to a socket fails
334 private final function setupHub () {
335 // Create a new TCP socket
336 $main_socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
339 if (!is_resource($main_socket)) {
341 throw new SocketCreationException(
344 'code' => socket_last_error()
345 ), self::EXCEPTION_SOCKET_PROBLEM
349 // Set socket options
350 if (!socket_set_option($main_socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
352 $this->closeSocket($main_socket);
354 // Problems setting socket options
355 throw new SocketSetupException (
358 'code' => socket_last_error()
359 ), self::EXCEPTION_SOCKET_PROBLEM
363 // Set to non-blocking
364 socket_set_nonblock($main_socket);
366 // Bind the socket to an address
367 if (!socket_bind($main_socket, $this->getConfigInstance()->readConfig("hub_listen_addr"), $this->getConfigInstance()->readConfig("hub_listen_port"))) {
369 throw new SocketBindException (
372 'host' => $this->getConfigInstance()->readConfig("hub_listen_addr"),
373 'port' => $this->getConfigInstance()->readConfig("hub_listen_port"),
374 'code' => socket_last_error()
375 ), self::EXCEPTION_SOCKET_PROBLEM
379 // Listen to ... the socket ;-)
380 if (!socket_listen($main_socket)) {
381 // Opps, that didn't work. Next time better listen to your heart... Roxette
382 throw new SocketListeningException(
385 'code' => socket_last_error()
386 ), self::EXCEPTION_SOCKET_PROBLEM
390 // Ignore user abort and do not time out
392 @ignore_user_abort(true);
394 // Cache more configuration stuff
395 $this->authRetries = $this->getConfigInstance()->readConfig("hub_max_auth_tries");
396 $this->authRequest = $this->getConfigInstance()->readConfig("hub_auth_request");
397 $this->authRequestTimeout = $this->getConfigInstance()->readConfig("hub_auth_request_timeout");
399 // The hub is running now!
400 $this->hubActivated = true;
402 // Append the main hub
403 $this->main_socket = $main_socket;
404 $this->connectedPeers->append($main_socket);
408 * Removes the current peer from the list
412 public final function removePeerFromConnectList () {
413 // Iterate through all connected peers
414 for ($idx = $this->connectedPeers->getIterator(); $idx->valid(); $idx->next()) {
415 // Get current peer from list
416 $peer = $idx->current();
418 // Is it a peer instance?
419 if (($peer !== $this->main_socket) && (is_object($peer)) && ($peer instanceof HubPeer)) {
420 // Okay, we have a peer instance so is it the same?
421 if ($this->getCurrPeerInstance()->equals($peer)) {
423 $idx->offsetUnset($idx->key());
425 // Remove the last instance as well
426 $this->lastPeerInstance = null;
428 // Remove the socket as well
429 $this->removeSocket($peer);
431 // Stop searching here
439 * Getter for console output handler
441 * @return $outputInstance An instance of the output handler
443 public final function getOutputInstance () {
444 return $this->outputInstance;
448 * Setter for console output handler
450 * @param $outputInstance An instance of the output handler
452 public final function setOutputInstance ($outputInstance) {
453 $this->outputInstance = $outputInstance;
457 * Getter for current peer instance to HubPeer class
459 * @return $lastPeerInstance Last peer instance
461 public final function getCurrPeerInstance () {
462 return $this->lastPeerInstance;
466 * Setter for current peer instance to HubPeer class
468 * @param $lastPeerInstance Last peer instance
471 public final function setCurrPeerInstance (HubPeer $lastPeerInstance) {
472 $this->lastPeerInstance = $lastPeerInstance;
476 * Checks wether the hub is running and not in shutdown phase
478 * @return $isRunning If the hub is running and not in shutdown phase
480 public function ifHubIsRunning () {
481 return ((($this->hubActivated) || (!$this->hubShutsDown)) && ($this->connectedPeers->count() > 0));
485 * Output the intro text
489 public final function outputIntro () {
490 if ($this->getConfigInstance()->readConfig("hub_intro_enabled") == "Y") {
492 $this->getOutputInstance()->output(sprintf("%s v%s Copyright (c) 2007, 2008 by Roland Häder",
493 ApplicationHelper::getInstance()->getAppName(),
494 ApplicationHelper::getInstance()->getAppVersion()
496 $this->getOutputInstance()->output("");
497 $this->getOutputInstance()->output("This software is free software licensed under the GNU GPL. In telnet session enter "/license" to read the license.");
498 $this->getOutputInstance()->output("This software uses the MXChange Framework, Copyright (c) 2007, 2008 by Roland Häder which is licensed under the GNU GPL.");
499 $this->getOutputInstance()->output("Enter "/framework" in telnet session to read that license.");
500 $this->getOutputInstance()->output("");
501 $this->getOutputInstance()->output("All core systems are initialized. Input on *this* console will currently be ignored!");
502 $this->getOutputInstance()->output("");
503 $this->getOutputInstance()->output(sprintf("[%s] Listening on: %s:%d",
505 $this->getConfigInstance()->readConfig("hub_listen_addr"),
506 $this->getConfigInstance()->readConfig("hub_listen_port")
508 $this->getOutputInstance()->output("----------------------------------------------------------------------------------------------------------------------------");
516 * @throws SocketCreationException If the main socket is not a resource
518 public function coreLoop () {
519 // Is the main socket vailid?
520 if (!is_resource($this->main_socket)) {
522 throw new SocketCreationException(
525 'code' => socket_last_error()
526 ), self::EXCEPTION_SOCKET_PROBLEM
530 // We are ready to serve requests
531 $this->getOutputInstance()->output(sprintf("[%s] Ready to serve requests.",
535 // Wait until the hub is shutting down
536 while ($this->ifHubIsRunning()) {
537 // Get number of total connected peers
538 $num = $this->getTotalConnectedPeers();
541 // Handle the master hub connection
542 if ($this->masterConnector instanceof HubConnector) {
543 $this->masterConnector->handleMasterRequests();
546 // Check for unauthorized peers
547 $this->handleUnauthPeers();
549 // Handle authorized peers
550 $this->handleAuthPeers();
551 } catch (HubPeerAuthorizationException $e) {
552 // Authorization has failed
553 $this->disconnectPeerWithReason("hub_msg_auth_tries");
555 // Get new total connected peers
556 $num = $this->getTotalConnectedPeers();
557 } catch (HubPeerTimeoutException $e) {
558 // Disconnect and remove the peer
559 $this->disconnectPeerWithReason("hub_msg_auth_reply_timeout");
561 // Get new total connected peers
562 $num = $this->getTotalConnectedPeers();
563 } catch (HubMasterDisconnectedException $e) {
564 // The master hub has disconnected us... :(
565 $this->masterConnector = null;
566 $this->getOutputInstance()->output(sprintf("[%s] The master has disconnected us. Reason given: %s",
571 // Get new total connected peers
572 $num = $this->getTotalConnectedPeers();
573 } catch (BrokenPipeException $e) {
574 // Broken pipes are bad for us... :(
575 $this->removePeerFromConnectList();
576 $this->getOutputInstance()->output(sprintf("[%s] A peer has closed the connection unexpected: %s",
581 // Get new total connected peers
582 $num = $this->getTotalConnectedPeers();
583 } catch (FrameworkException $e) {
584 // Catch all other exceptions and output them
585 hub_exception_handler($e);
587 // Get new total connected peers
588 $num = $this->getTotalConnectedPeers();
591 // Are there some peers?
593 // Wait for more peers
597 // A new peer has connected
598 $this->getOutputInstance()->output(sprintf("[%s] A new peer is connecting.",
603 // Check for new peers
604 $this->handleNewPeers();
605 } catch (IPSpoofingException $e) {
607 $this->getOutputInstance()->output(sprintf("[%s] The peer's IP number has changed!",
611 // Output debug message
612 $this->getDebugInstance()->output(sprintf("[%s] Peer spoofes IP number: %s",
617 // Disconnect the peer first
618 $this->disconnectPeerWithReason("hub_msg_spoofing");
620 // Get new total connected peers
621 $num = $this->getTotalConnectedPeers();
622 } catch (FrameworkException $e) {
623 // Catch all exceptions and output them to avoid aborting the program unexpectly
624 hub_exception_handler($e);
626 // Get new total connected peers
627 $num = $this->getTotalConnectedPeers();
634 * Tries to contact the master server or simply reports that we are the master server
638 public function contactMasterHub () {
639 // Checks wether we are the master hub
640 if ($_SERVER['SERVER_ADDR'] == $this->getConfigInstance()->readConfig("hub_master_ip")) {
642 $this->hubIsMaster = true;
643 $this->getOutputInstance()->output(sprintf("[%s] Our IP %s matches the master IP. Becoming master hub...",
645 $_SERVER['SERVER_ADDR']
648 // A regular hub or ultra hub so let's contact the master
649 $this->getOutputInstance()->output(sprintf("[%s] Contacting the master hub at %s:%d...",
651 $this->getConfigInstance()->readConfig("hub_master_ip"),
652 $this->getConfigInstance()->readConfig("hub_master_port")
655 // Try to aquire a connection to the master...
657 // Announce us to the master hub
658 $this->announceToMasterHub();
659 } catch (FrameworkException $e) {
660 // Catch all exceptions and output them
661 hub_exception_handler($e);
667 * Disconnects the current with a configurable reason
669 * @param $reasonEntry The entry with the disconnection reason aka. message to the peer
672 public function disconnectPeerWithReason ($reasonEntry) {
673 // Default is that we cannot read the peer's IP number
676 // Try to disconnect here
678 // First get the raw IP number
679 $ip = $this->getCurrPeerInstance()->getValidatedIP();
681 // Disconnect the peer...
682 $this->getCurrPeerInstance()->disconnectWithReason($this->getConfigInstance()->readConfig($reasonEntry));
683 } catch (FrameworkException $e) {
684 // Catch all exceptions and output them
685 hub_exception_handler($e);
688 // Remove him from the list anyway
689 $this->removePeerFromConnectList();
691 // Output the message
692 $this->getOutputInstance()->output(sprintf("[%s] Peer %s has been disconnected.",
699 * Announces this hub to the master hub. This is being done by connecting to it and asking for auth request
703 public function announceToMasterHub () {
705 // Create a new instance for communicating to the master hub
706 $this->masterConnector = HubConnector::createHubConnectorByAddressPort(
707 $this->getConfigInstance()->readConfig("hub_master_ip"),
708 $this->getConfigInstance()->readConfig("hub_master_port"),
712 // Send all our accepted objects to the master hub
713 $this->masterConnector->sendAllAcceptedObjects();
714 } catch (SocketConnectException $e) {
715 // The master hub is down!
716 ApplicationEntryPoint::app_die($e->getMessage());
721 * Removes a given peer instance (socket) from all lists
723 * @param $peerInstance An instance of a HubPeer class
726 private function removeSocket (HubPeer $peerInstance) {
727 // Get socket from peer
728 $socket = $peerInstance->getPeerSocket();
730 // Search for readers
731 $key = array_search($socket, $this->readPeers, true);
732 if ($key !== false) {
733 // Remove from reader list
734 unset($this->readPeers[$key]);
737 // Search for writers
738 $key = array_search($socket, $this->writePeers, true);
739 if ($key !== false) {
740 // Remove from writer list
741 unset($this->writePeers[$key]);
744 // Remove from auth peers as well
745 for ($idx = $this->authPeers->getIterator(); $idx->valid(); $idx->next()) {
747 $current = $idx->current();
750 if ($current->equals($peerInstance)) {
751 // Remove from auth (-awaiting) list
752 $idx->offsetUnset($idx->key());