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->removeSystemArray();
112 $this->removeNumberFormaters();
114 // Init the peer list
115 $this->initPeerList();
119 * Factory for main loop
121 * @return $hubInstance An instance of this class
123 public final static function createHubCoreLoop () {
125 $hubInstance = new HubCoreLoop();
127 // Try to setup the socket
128 $hubInstance->setupHub();
130 // Get the configuration variable
131 $outEngine = $hubInstance->getConfigInstance()->readConfig("tpl_engine");
133 // Setup the console output handler
134 $eval = sprintf("\$hubInstance->setOutputInstance(%s::create%s(\"%s\"));",
137 $hubInstance->getConfigInstance()->readConfig("web_content_type")
141 if ((defined('DEBUG_EVAL')) || (defined('DEBUG_ALL'))) $hubInstance->getDebugInstance()->output(sprintf("[%s:] Konstruierte PHP-Anweisung: %s",
142 $hubInstance->__toString(),
147 // Return the prepared instance
152 * Initializes the peer list
156 private final function initPeerList () {
157 $this->connectedPeers = new FrameworkArrayObject("FakedConnectedPeers");
158 $this->authPeers = new FrameworkArrayObject("FakeAuthPeers");
162 * Get total number of connected peers
164 * @return $num The total number of connected peers
166 private final function getTotalConnectedPeers () {
167 $read = array($this->connectedPeers->getIterator()->current());
172 $num = socket_select(
179 // Transfer readers and writers
180 $this->readPeers = $read;
181 $this->writePeers = $write;
188 * Handle newly connected peers
192 private final function handleNewPeers () {
194 $this->getOutputInstance()->output(sprintf("[%s] Validating peer...", __METHOD__));
196 // Is the main socket in the array?
197 if (in_array($this->main_socket, $this->readPeers)) {
198 // Accept the connection and add him to the list
199 $peer_socket = socket_accept($this->main_socket);
201 // Get a new peer instance
202 $this->setCurrPeerInstance(HubPeer::createHubPeerBySocket($peer_socket, $this));
204 // Register the new peer
205 $this->getOutputInstance()->output(sprintf("[%s] Registering new peer with IP %s.",
207 $this->getCurrPeerInstance()->getValidatedIP()
209 $this->connectedPeers->append($this->lastPeerInstance);
211 // A new peer has connected
212 $this->getOutputInstance()->output(sprintf("[%s] New peer with IP %s has been registered.",
214 $this->getCurrPeerInstance()->getValidatedIP()
217 // Remove him from the list
218 $key = array_search($this->main_socket, $this->readPeers);
219 unset($this->readPeers[$key]);
224 * Handles unauthenticated peers
227 * @throws HubPeerTimeoutException If the peer times out to answer a request
228 * @throws HubPeerAuthorizationException If the peer fails to authorize himself (to many retries)
230 private final function handleUnauthPeers () {
231 // Are there some peers?
232 if ($this->connectedPeers->count() > 1) {
233 // Iterate through all connected peers
234 for ($idx = $this->connectedPeers->getIterator(); $idx->valid(); $idx->next()) {
236 $this->lastPeerInstance = $idx->current();
238 // Ignore own socket and invalid entries
239 if (($this->getCurrPeerInstance() !== $this->main_socket) && (is_object($this->getCurrPeerInstance())) && ($this->getCurrPeerInstance() instanceof HubPeer)) {
240 // Is this peer already authorized or is this the master hub?
241 // If this is the master hub then there is no auth required
242 if ((!$this->getCurrPeerInstance()->ifPeerIsAuthorized()) && (!$this->getCurrPeerInstance()->ifPeerIsLocalAdmin()) && (!$this->hubIsMaster)) {
243 // This peer waits for authorization, so does he have some tries left?
244 if ($this->getCurrPeerInstance()->getAuthRetries() <= $this->authRetries) {
245 // This peer is still allowed to try his authorization
246 if ($this->getCurrPeerInstance()->getLastSentMessage() == $this->authRequest) {
247 // Already asked so maybe timed out?
248 if ((time() - $this->getCurrPeerInstance()->getLastSentMessageStamp()) >= $this->authRequestTimeout) {
249 // Timed out so disconnect the peer
250 throw new HubPeerTimeoutException (
253 'peer' => $this->getCurrPeerInstance()
254 ), self::EXCEPTION_HUB_PEER_TIMEOUT
257 } elseif ($this->getCurrPeerInstance()->ifHelloReceived()) {
258 // HELLO received so we need to sent the AUTH request
259 $this->getCurrPeerInstance()->askAuthorizationKey();
262 // This peer needs disconnecting!
263 throw new HubPeerAuthorizationException (
266 'peer' => $this->getCurrPeerInstance(),
267 'max' => $this->authRetries
268 ), self::EXCEPTION_HUB_PEER_FAILED_AUTH
271 } elseif ((!$this->getCurrPeerInstance()->ifPeerIsAuthorized()) && ($this->getCurrPeerInstance()->ifPeerIsLocalAdmin())) {
272 // This peer is a local admin so he is always authorized!
273 $this->getCurrPeerInstance()->enableLocalAdmin();
275 // Output debug message
276 $this->getOutputInstance()->output(sprintf("[%s] A local admin has connected.",
280 // Say "hi" to the admin
281 $this->getCurrPeerInstance()->sayHi2Admin();
282 } elseif (($this->hubIsMaster) && ($this->getCurrPeerInstance()->ifHelloReceived()) && (!$this->getCurrPeerInstance()->ifELHOsent())) {
283 // Is the master hub so authorize the peer here...
284 $this->getCurrPeerInstance()->enableIsAuthorized();
285 $this->authPeers->append($this->getCurrPeerInstance());
287 // Reply the HELLO request
288 $this->getCurrPeerInstance()->replyHelloMessage();
296 * Handles only authorized peers
300 public function handleAuthPeers () {
301 // Are there some peers?
302 if (($this->connectedPeers->count() > 1) && ($this->authPeers->count() > 0)) {
303 // Iterate through all connected peers
304 for ($idx = $this->authPeers->getIterator(); $idx->valid(); $idx->next()) {
306 $this->lastPeerInstance = $idx->current();
308 // Do the ping and update our authPeer list (LATER!)
309 $this->lastPeerInstance->handlePingPeer();
311 // Avoids a notice...
312 if (is_null($this->getCurrPeerInstance())) break;
318 * Setup the hub: Create a socket for listening on incoming requests,
319 * try to bind to a port, etc.
322 * @throws SocketCreationException If a socket cannot be created
323 * @throws SocketSetupException If a socket cannot be setuped
324 * @throws SocketBindException If a socket cannot be bind to
325 * an address and port
326 * @throws SocketListeningException If listening to a socket fails
328 private final function setupHub () {
329 // Create a new TCP socket
330 $main_socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
333 if (!is_resource($main_socket)) {
335 throw new SocketCreationException(
338 'code' => socket_last_error()
339 ), self::EXCEPTION_SOCKET_PROBLEM
343 // Set socket options
344 if (!socket_set_option($main_socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
346 $this->closeSocket($main_socket);
348 // Problems setting socket options
349 throw new SocketSetupException (
352 'code' => socket_last_error()
353 ), self::EXCEPTION_SOCKET_PROBLEM
357 // Set to non-blocking
358 socket_set_nonblock($main_socket);
360 // Bind the socket to an address
361 if (!socket_bind($main_socket, $this->getConfigInstance()->readConfig("hub_listen_addr"), $this->getConfigInstance()->readConfig("hub_listen_port"))) {
363 throw new SocketBindException (
366 'host' => $this->getConfigInstance()->readConfig("hub_listen_addr"),
367 'port' => $this->getConfigInstance()->readConfig("hub_listen_port"),
368 'code' => socket_last_error()
369 ), self::EXCEPTION_SOCKET_PROBLEM
373 // Listen to ... the socket ;-)
374 if (!socket_listen($main_socket)) {
375 // Opps, that didn't work. Next time better listen to your heart... Roxette
376 throw new SocketListeningException(
379 'code' => socket_last_error()
380 ), self::EXCEPTION_SOCKET_PROBLEM
384 // Ignore user abort and do not time out
386 @ignore_user_abort(true);
388 // Cache more configuration stuff
389 $this->authRetries = $this->getConfigInstance()->readConfig("hub_max_auth_tries");
390 $this->authRequest = $this->getConfigInstance()->readConfig("hub_auth_request");
391 $this->authRequestTimeout = $this->getConfigInstance()->readConfig("hub_auth_request_timeout");
393 // The hub is running now!
394 $this->hubActivated = true;
396 // Append the main hub
397 $this->main_socket = $main_socket;
398 $this->connectedPeers->append($main_socket);
402 * Removes the current peer from the list
406 public final function removePeerFromConnectList () {
407 // Iterate through all connected peers
408 for ($idx = $this->connectedPeers->getIterator(); $idx->valid(); $idx->next()) {
409 // Get current peer from list
410 $peer = $idx->current();
412 // Is it a peer instance?
413 if (($peer !== $this->main_socket) && (is_object($peer)) && ($peer instanceof HubPeer)) {
414 // Okay, we have a peer instance so is it the same?
415 if ($this->getCurrPeerInstance()->equals($peer)) {
417 $idx->offsetUnset($idx->key());
419 // Remove the last instance as well
420 $this->lastPeerInstance = null;
422 // Remove the socket as well
423 $this->removeSocket($peer);
425 // Stop searching here
433 * Getter for console output handler
435 * @return $outputInstance An instance of the output handler
437 public final function getOutputInstance () {
438 return $this->outputInstance;
442 * Setter for console output handler
444 * @param $outputInstance An instance of the output handler
446 public final function setOutputInstance ($outputInstance) {
447 $this->outputInstance = $outputInstance;
451 * Getter for current peer instance to HubPeer class
453 * @return $lastPeerInstance Last peer instance
455 public final function getCurrPeerInstance () {
456 return $this->lastPeerInstance;
460 * Setter for current peer instance to HubPeer class
462 * @param $lastPeerInstance Last peer instance
465 public final function setCurrPeerInstance (HubPeer $lastPeerInstance) {
466 $this->lastPeerInstance = $lastPeerInstance;
470 * Checks wether the hub is running and not in shutdown phase
472 * @return $isRunning If the hub is running and not in shutdown phase
474 public function ifHubIsRunning () {
475 return ((($this->hubActivated) || (!$this->hubShutsDown)) && ($this->connectedPeers->count() > 0));
479 * Output the intro text
483 public final function outputIntro () {
484 if ($this->getConfigInstance()->readConfig("hub_intro_enabled") == "Y") {
486 $this->getOutputInstance()->output(sprintf("%s v%s Copyright (c) 2007, 2008 by Roland Häder",
487 ApplicationHelper::getInstance()->getAppName(),
488 ApplicationHelper::getInstance()->getAppVersion()
490 $this->getOutputInstance()->output("");
491 $this->getOutputInstance()->output("This software is free software licensed under the GNU GPL. In telnet session enter "/license" to read the license.");
492 $this->getOutputInstance()->output("This software uses the MXChange Framework, Copyright (c) 2007, 2008 by Roland Häder which is licensed under the GNU GPL.");
493 $this->getOutputInstance()->output("Enter "/framework" in telnet session to read that license.");
494 $this->getOutputInstance()->output("");
495 $this->getOutputInstance()->output("All core systems are initialized. Input on *this* console will currently be ignored!");
496 $this->getOutputInstance()->output("");
497 $this->getOutputInstance()->output(sprintf("[%s] Listening on: %s:%d",
499 $this->getConfigInstance()->readConfig("hub_listen_addr"),
500 $this->getConfigInstance()->readConfig("hub_listen_port")
502 $this->getOutputInstance()->output("----------------------------------------------------------------------------------------------------------------------------");
510 * @throws SocketCreationException If the main socket is not a resource
512 public function coreLoop () {
513 // Is the main socket vailid?
514 if (!is_resource($this->main_socket)) {
516 throw new SocketCreationException(
519 'code' => socket_last_error()
520 ), self::EXCEPTION_SOCKET_PROBLEM
524 // We are ready to serve requests
525 $this->getOutputInstance()->output(sprintf("[%s] Ready to serve requests.",
529 // Wait until the hub is shutting down
530 while ($this->ifHubIsRunning()) {
531 // Get number of total connected peers
532 $num = $this->getTotalConnectedPeers();
535 // Handle the master hub connection
536 if ($this->masterConnector instanceof HubConnector) {
537 $this->masterConnector->handleMasterRequests();
540 // Check for unauthorized peers
541 $this->handleUnauthPeers();
543 // Handle authorized peers
544 $this->handleAuthPeers();
545 } catch (HubPeerAuthorizationException $e) {
546 // Authorization has failed
547 $this->disconnectPeerWithReason("hub_msg_auth_tries");
549 // Get new total connected peers
550 $num = $this->getTotalConnectedPeers();
551 } catch (HubPeerTimeoutException $e) {
552 // Disconnect and remove the peer
553 $this->disconnectPeerWithReason("hub_msg_auth_reply_timeout");
555 // Get new total connected peers
556 $num = $this->getTotalConnectedPeers();
557 } catch (HubMasterDisconnectedException $e) {
558 // The master hub has disconnected us... :(
559 $this->masterConnector = null;
560 $this->getOutputInstance()->output(sprintf("[%s] The master has disconnected us. Reason given: %s",
565 // Get new total connected peers
566 $num = $this->getTotalConnectedPeers();
567 } catch (BrokenPipeException $e) {
568 // Broken pipes are bad for us... :(
569 $this->removePeerFromConnectList();
570 $this->getOutputInstance()->output(sprintf("[%s] A peer has closed the connection unexpected: %s",
575 // Get new total connected peers
576 $num = $this->getTotalConnectedPeers();
577 } catch (FrameworkException $e) {
578 // Catch all other exceptions and output them
579 hub_exception_handler($e);
581 // Get new total connected peers
582 $num = $this->getTotalConnectedPeers();
585 // Are there some peers?
587 // Wait for more peers
591 // A new peer has connected
592 $this->getOutputInstance()->output(sprintf("[%s] A new peer is connecting.",
597 // Check for new peers
598 $this->handleNewPeers();
599 } catch (IPSpoofingException $e) {
601 $this->getOutputInstance()->output(sprintf("[%s] The peer's IP number has changed!",
605 // Output debug message
606 $this->getDebugInstance()->output(sprintf("[%s] Peer spoofes IP number: %s",
611 // Disconnect the peer first
612 $this->disconnectPeerWithReason("hub_msg_spoofing");
614 // Get new total connected peers
615 $num = $this->getTotalConnectedPeers();
616 } catch (FrameworkException $e) {
617 // Catch all exceptions and output them to avoid aborting the program unexpectly
618 hub_exception_handler($e);
620 // Get new total connected peers
621 $num = $this->getTotalConnectedPeers();
628 * Tries to contact the master server or simply reports that we are the master server
632 public function contactMasterHub () {
633 // Checks wether we are the master hub
634 if ($_SERVER['SERVER_ADDR'] == $this->getConfigInstance()->readConfig("hub_master_ip")) {
636 $this->hubIsMaster = true;
637 $this->getOutputInstance()->output(sprintf("[%s] Our IP %s matches the master IP. Becoming master hub...",
639 $_SERVER['SERVER_ADDR']
642 // A regular hub or ultra hub so let's contact the master
643 $this->getOutputInstance()->output(sprintf("[%s] Contacting the master hub at %s:%d...",
645 $this->getConfigInstance()->readConfig("hub_master_ip"),
646 $this->getConfigInstance()->readConfig("hub_master_port")
649 // Try to aquire a connection to the master...
651 // Announce us to the master hub
652 $this->announceToMasterHub();
653 } catch (FrameworkException $e) {
654 // Catch all exceptions and output them
655 hub_exception_handler($e);
661 * Disconnects the current with a configurable reason
663 * @param $reasonEntry The entry with the disconnection reason aka. message to the peer
666 public function disconnectPeerWithReason ($reasonEntry) {
667 // Default is that we cannot read the peer's IP number
670 // Try to disconnect here
672 // First get the raw IP number
673 $ip = $this->getCurrPeerInstance()->getValidatedIP();
675 // Disconnect the peer...
676 $this->getCurrPeerInstance()->disconnectWithReason($this->getConfigInstance()->readConfig($reasonEntry));
677 } catch (FrameworkException $e) {
678 // Catch all exceptions and output them
679 hub_exception_handler($e);
682 // Remove him from the list anyway
683 $this->removePeerFromConnectList();
685 // Output the message
686 $this->getOutputInstance()->output(sprintf("[%s] Peer %s has been disconnected.",
693 * Announces this hub to the master hub. This is being done by connecting to it and asking for auth request
697 public function announceToMasterHub () {
699 // Create a new instance for communicating to the master hub
700 $this->masterConnector = HubConnector::createHubConnectorByAddressPort(
701 $this->getConfigInstance()->readConfig("hub_master_ip"),
702 $this->getConfigInstance()->readConfig("hub_master_port"),
706 // Send all our accepted objects to the master hub
707 $this->masterConnector->sendAllAcceptedObjects();
708 } catch (SocketConnectException $e) {
709 // The master hub is down!
710 ApplicationEntryPoint::app_die($e->getMessage());
715 * Removes a given peer instance (socket) from all lists
717 * @param $peerInstance An instance of a HubPeer class
720 private function removeSocket (HubPeer $peerInstance) {
721 // Get socket from peer
722 $socket = $peerInstance->getPeerSocket();
724 // Search for readers
725 $key = array_search($socket, $this->readPeers, true);
726 if ($key !== false) {
727 // Remove from reader list
728 unset($this->readPeers[$key]);
731 // Search for writers
732 $key = array_search($socket, $this->writePeers, true);
733 if ($key !== false) {
734 // Remove from writer list
735 unset($this->writePeers[$key]);
738 // Remove from auth peers as well
739 for ($idx = $this->authPeers->getIterator(); $idx->valid(); $idx->next()) {
741 $current = $idx->current();
744 if ($current->equals($peerInstance)) {
745 // Remove from auth (-awaiting) list
746 $idx->offsetUnset($idx->key());