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 private function __construct () {
107 // Call parent constructor
108 parent::constructor(__CLASS__);
111 $this->setPartDescr("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();
321 * Setup the hub: Create a socket for listening on incoming requests,
322 * try to bind to a port, etc.
325 * @throws SocketCreationException If a socket cannot be created
326 * @throws SocketSetupException If a socket cannot be setuped
327 * @throws SocketBindException If a socket cannot be bind to
328 * an address and port
329 * @throws SocketListeningException If listening to a socket fails
331 private final function setupHub () {
332 // Create a new TCP socket
333 $main_socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
336 if (!is_resource($main_socket)) {
338 throw new SocketCreationException(
341 'code' => socket_last_error()
342 ), self::EXCEPTION_SOCKET_PROBLEM
346 // Set socket options
347 if (!socket_set_option($main_socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
349 $this->closeSocket($main_socket);
351 // Problems setting socket options
352 throw new SocketSetupException (
355 'code' => socket_last_error()
356 ), self::EXCEPTION_SOCKET_PROBLEM
360 // Set to non-blocking
361 socket_set_nonblock($main_socket);
363 // Bind the socket to an address
364 if (!socket_bind($main_socket, $this->getConfigInstance()->readConfig("hub_listen_addr"), $this->getConfigInstance()->readConfig("hub_listen_port"))) {
366 throw new SocketBindException (
369 'host' => $this->getConfigInstance()->readConfig("hub_listen_addr"),
370 'port' => $this->getConfigInstance()->readConfig("hub_listen_port"),
371 'code' => socket_last_error()
372 ), self::EXCEPTION_SOCKET_PROBLEM
376 // Listen to ... the socket ;-)
377 if (!socket_listen($main_socket)) {
378 // Opps, that didn't work. Next time better listen to your heart... Roxette
379 throw new SocketListeningException(
382 'code' => socket_last_error()
383 ), self::EXCEPTION_SOCKET_PROBLEM
387 // Ignore user abort and do not time out
389 @ignore_user_abort(true);
391 // Cache more configuration stuff
392 $this->authRetries = $this->getConfigInstance()->readConfig("hub_max_auth_tries");
393 $this->authRequest = $this->getConfigInstance()->readConfig("hub_auth_request");
394 $this->authRequestTimeout = $this->getConfigInstance()->readConfig("hub_auth_request_timeout");
396 // The hub is running now!
397 $this->hubActivated = true;
399 // Append the main hub
400 $this->main_socket = $main_socket;
401 $this->connectedPeers->append($main_socket);
405 * Removes the current peer from the list
409 public final function removePeerFromConnectList () {
410 // Iterate through all connected peers
411 for ($idx = $this->connectedPeers->getIterator(); $idx->valid(); $idx->next()) {
412 // Get current peer from list
413 $peer = $idx->current();
415 // Is it a peer instance?
416 if (($peer !== $this->main_socket) && (is_object($peer)) && ($peer instanceof HubPeer)) {
417 // Okay, we have a peer instance so is it the same?
418 if ($this->getCurrPeerInstance()->equals($peer)) {
420 $idx->offsetUnset($idx->key());
421 $this->lastPeerInstance = null;
429 * Getter for console output handler
431 * @return $outputInstance An instance of the output handler
433 public final function getOutputInstance () {
434 return $this->outputInstance;
438 * Setter for console output handler
440 * @param $outputInstance An instance of the output handler
442 public final function setOutputInstance ($outputInstance) {
443 $this->outputInstance = $outputInstance;
447 * Getter for current peer instance to HubPeer class
449 * @return $lastPeerInstance Last peer instance
451 public final function getCurrPeerInstance () {
452 return $this->lastPeerInstance;
456 * Setter for current peer instance to HubPeer class
458 * @param $lastPeerInstance Last peer instance
461 public final function setCurrPeerInstance (HubPeer $lastPeerInstance) {
462 $this->lastPeerInstance = $lastPeerInstance;
466 * Checks wether the hub is running and not in shutdown phase
468 * @return $isRunning If the hub is running and not in shutdown phase
470 public function ifHubIsRunning () {
471 return ((($this->hubActivated) || (!$this->hubShutsDown)) && ($this->connectedPeers->count() > 0));
475 * Output the intro text
479 public final function outputIntro () {
480 if ($this->getConfigInstance()->readConfig("hub_intro_enabled") == "Y") {
482 $this->getOutputInstance()->output(sprintf("%s v%s Copyright (c) 2007, 2008 by Roland Häder",
483 ApplicationHelper::getInstance()->getAppName(),
484 ApplicationHelper::getInstance()->getAppVersion()
486 $this->getOutputInstance()->output("");
487 $this->getOutputInstance()->output("This software is free software licensed under the GNU GPL. In telnet session enter "/license" to read the license.");
488 $this->getOutputInstance()->output("This software uses the MXChange Framework, Copyright (c) 2007, 2008 by Roland Häder which is licensed under the GNU GPL.");
489 $this->getOutputInstance()->output("Enter "/framework" in telnet session to read that license.");
490 $this->getOutputInstance()->output("");
491 $this->getOutputInstance()->output("All core systems are initialized. Input on *this* console will currently be ignored!");
492 $this->getOutputInstance()->output("");
493 $this->getOutputInstance()->output(sprintf("[%s] Listening on: %s:%d",
495 $this->getConfigInstance()->readConfig("hub_listen_addr"),
496 $this->getConfigInstance()->readConfig("hub_listen_port")
498 $this->getOutputInstance()->output("----------------------------------------------------------------------------------------------------------------------------");
506 * @throws SocketCreationException If the main socket is not a resource
508 public function coreLoop () {
509 // Is the main socket vailid?
510 if (!is_resource($this->main_socket)) {
512 throw new SocketCreationException(
515 'code' => socket_last_error()
516 ), self::EXCEPTION_SOCKET_PROBLEM
520 // We are ready to serve requests
521 $this->getOutputInstance()->output(sprintf("[%s] Ready to serve requests.",
525 // Wait until the hub is shutting down
526 while ($this->ifHubIsRunning()) {
527 // Get number of total connected peers
528 $num = $this->getTotalConnectedPeers();
531 // Handle the master hub connection
532 if ($this->masterConnector instanceof HubConnector) {
533 $this->masterConnector->handleMasterRequests();
536 // Check for unauthorized peers
537 $this->handleUnauthPeers();
539 // Handle authorized peers
540 $this->handleAuthPeers();
541 } catch (HubPeerAuthorizationException $e) {
542 // Authorization has failed
543 $this->disconnectPeerWithReason("hub_msg_auth_tries");
545 // Get new total connected peers
546 $num = $this->getTotalConnectedPeers();
547 } catch (HubPeerTimeoutException $e) {
548 // Disconnect and remove the peer
549 $this->disconnectPeerWithReason("hub_msg_auth_reply_timeout");
551 // Get new total connected peers
552 $num = $this->getTotalConnectedPeers();
553 } catch (HubMasterDisconnectedException $e) {
554 // The master hub has disconnected us... :(
555 $this->masterConnector = null;
556 $this->getOutputInstance()->output(sprintf("[%s] The master has disconnected us. Reason given: %s",
560 } catch (BrokenPipeException $e) {
561 // Broken pipes are bad for us... :(
562 $this->removePeerFromConnectList();
563 $this->getOutputInstance()->output(sprintf("[%s] A peer has closed the connection unexpected: %s",
568 // Get new total connected peers
569 $num = $this->getTotalConnectedPeers();
570 } catch (FrameworkException $e) {
571 // Catch all other exceptions and output them
572 echo "CATCH".__LINE__.":".$e->__toString()."\n";
573 hub_exception_handler($e);
575 // Get new total connected peers
576 $num = $this->getTotalConnectedPeers();
579 // Are there some peers?
581 // Wait for more peers
585 // A new peer has connected
586 $this->getOutputInstance()->output(sprintf("[%s] A new peer is connecting.",
591 // Check for new peers
592 $this->handleNewPeers();
593 } catch (IPSpoofingException $e) {
595 $this->getOutputInstance()->output(sprintf("[%s] The peer's IP number has changed!",
599 // Output debug message
600 $this->getDebugInstance()->output(sprintf("[%s] Peer spoofes IP number: %s",
605 // Disconnect the peer first
606 $this->disconnectPeerWithReason("hub_msg_spoofing");
608 // Get new total connected peers
609 $num = $this->getTotalConnectedPeers();
610 } catch (FrameworkException $e) {
611 // Catch all exceptions and output them to avoid aborting the program unexpectly
612 echo "CATCH".__LINE__.":".$e->__toString()."\n";
613 hub_exception_handler($e);
615 // Get new total connected peers
616 $num = $this->getTotalConnectedPeers();
623 * Tries to contact the master server or simply reports that we are the master server
627 public function contactMasterHub () {
628 // Checks wether we are the master hub
629 if ($_SERVER['SERVER_ADDR'] == $this->getConfigInstance()->readConfig("hub_master_ip")) {
631 $this->hubIsMaster = true;
632 $this->getOutputInstance()->output(sprintf("[%s] Our IP %s matches the master IP. Becoming master hub...",
634 $_SERVER['SERVER_ADDR']
637 // A regular hub or ultra hub so let's contact the master
638 $this->getOutputInstance()->output(sprintf("[%s] Contacting the master hub at %s:%d...",
640 $this->getConfigInstance()->readConfig("hub_master_ip"),
641 $this->getConfigInstance()->readConfig("hub_master_port")
644 // Try to aquire a connection to the master...
646 // Announce us to the master hub
647 $this->announceToMasterHub();
648 } catch (FrameworkException $e) {
649 // Catch all exceptions and output them
650 echo "CATCH".__LINE__.":".$e->__toString()."\n";
651 hub_exception_handler($e);
657 * Disconnects the current with a configurable reason
659 * @param $reasonEntry The entry with the disconnection reason aka. message to the peer
662 public function disconnectPeerWithReason ($reasonEntry) {
665 // Try to disconnect here
667 // First get the raw IP number
668 $ip = $this->getCurrPeerInstance()->getValidatedIP();
670 // Disconnect the peer...
671 $this->getCurrPeerInstance()->disconnectWithReason($this->getConfigInstance()->readConfig($reasonEntry));
672 } catch (FrameworkException $e) {
673 // Catch all exceptions and output them
674 echo "CATCH".__LINE__.":".$e->__toString()."\n";
675 hub_exception_handler($e);
678 // Remove him from the list anyway
679 $this->removePeerFromConnectList();
681 // Output the message
682 $this->getOutputInstance()->output(sprintf("[%s] Peer %s has been disconnected.",
689 * Announces this hub to the master hub. This is being done by connecting to it and asking for auth request
693 public function announceToMasterHub () {
695 // Create a new instance for communicating to the master hub
696 $this->masterConnector = HubConnector::createHubConnectorByAddressPort(
697 $this->getConfigInstance()->readConfig("hub_master_ip"),
698 $this->getConfigInstance()->readConfig("hub_master_port"),
702 // Send all our accepted objects to the master hub
703 $this->masterConnector->sendAllAcceptedObjects();
704 } catch (SocketConnectException $e) {
705 // The master hub is down!
706 ApplicationEntryPoint::app_die($e->getMessage());