]> git.mxchange.org Git - hub.git/blob - ship-simu/application/hub/main/class_HubCoreLoop.php
Initial import
[hub.git] / ship-simu / application / hub / main / class_HubCoreLoop.php
1 <?php
2 /**
3  * The hub's main loop. The hub will wait and listen for incoming requests in
4  * this loop.
5  *
6  * @author      Roland Haeder <roland __NOSPAM__ [at] __REMOVE_ME__ mxchange [dot] org>
7  * @version     0.1
8  */
9 class HubCoreLoop extends BaseFrameworkSystem {
10         /**
11          * Wether the hub is active and running
12       */
13         private $hubActivated = false;
14
15         /**
16          * Wether the hub is shutting down
17       */
18         private $hubShutsDown = false;
19
20         /**
21          * A list of all connected peers (sockets)
22          */
23         private $connectedPeers = null;
24
25         /**
26          * A console output handler
27          */
28         private $outputInstance = null;
29
30         /**
31          * Reading peers
32          */
33         private $readPeers = array();
34
35         /**
36          * Writing peers
37          */
38         private $writePeers = array();
39
40         /**
41          * The main socket (listening) for this hub
42          */
43         private $main_socket = null;
44
45         /**
46          * A list of authenticated peers
47          */
48         private $authPeers = null;
49
50         /**
51          * Last peer instance
52          */
53         private $lastPeerInstance = null;
54
55         /**
56          * Wether this hub is a master hub
57          */
58         private $hubIsMaster = false;
59
60         /**
61          * Maximum auth retries (cached)
62          */
63         private $authRetries = 0;
64
65         /**
66          * Auth request message
67          */
68         private $authRequest = "";
69
70         /**
71          * Cached timeout for auth requests
72          */
73         private $authRequestTimeout = 0;
74
75         /**
76          * An instance to the HubConnector class for master hub connection
77          */
78         private $masterConnector = null;
79
80         // Exception codes
81         const EXCEPTION_SOCKET_PROBLEM          = 0xb00;
82         const EXCEPTION_HUB_PEER_TIMEOUT                = 0xb01;
83         const EXCEPTION_HUB_PEER_FAILED_AUTH    = 0xb02;
84         const EXCEPTION_HELLO_TIMED_OUT         = 0xb03;
85
86         /**
87          * The private constructor
88          *
89          * @return      void
90          */
91         private function __construct () {
92                 // Call parent constructor
93                 parent::constructor(__CLASS__);
94
95                 // Set description
96                 $this->setPartDescr("Hub-Core Loop");
97
98                 // Set unique ID
99                 $this->createUniqueID();
100
101                 // Tidy up a little
102                 $this->removeSystemArray();
103                 $this->removeNumberFormaters();
104
105                 // Init the peer list
106                 $this->initPeerList();
107         }
108
109         /**
110          * Factory for main loop
111          *
112          * @return      $hubInstance            An instance of this class
113          */
114         public final static function createHubCoreLoop () {
115                 // Get an instance
116                 $hubInstance = new HubCoreLoop();
117
118                 // Try to setup the socket
119                 $hubInstance->setupHub();
120
121                 // Get the configuration variable
122                 $outEngine = $hubInstance->getConfigInstance()->readConfig("tpl_engine");
123
124                 // Setup the console output handler
125                 $eval = sprintf("\$hubInstance->setOutputInstance(%s::create%s(\"%s\"));",
126                         $outEngine,
127                         $outEngine,
128                         $hubInstance->getConfigInstance()->readConfig("web_content_type")
129                 );
130
131                 // Debug message
132                 if ((defined('DEBUG_EVAL')) || (defined('DEBUG_ALL'))) $hubInstance->getDebugInstance()->output(sprintf("[%s:] Konstruierte PHP-Anweisung: %s",
133                         $hubInstance->__toString(),
134                         htmlentities($eval)
135                 ));
136                 @eval($eval);
137
138                 // Return the prepared instance
139                 return $hubInstance;
140         }
141
142         /**
143          * Initializes the peer list
144          *
145          * @return      void
146          */
147         private final function initPeerList () {
148                 $this->connectedPeers = new FrameworkArrayObject();
149                 $this->authPeers      = new FrameworkArrayObject();
150         }
151
152         /**
153          * Get total number of connected peers
154          *
155          * @return      $num            The total number of connected peers
156          */
157         private final function getTotalConnectedPeers () {
158                 $read = array($this->connectedPeers->getIterator()->current());
159                 $write = array();
160                 $except = array();
161
162                 // Get socket number
163                 $num = socket_select(
164                         $read,
165                         $write,
166                         $except,
167                         0
168                 );
169
170                 // Transfer readers and writers
171                 $this->readPeers = $read;
172                 $this->writePeers = $write;
173
174                 // Return the number
175                 return $num;
176         }
177
178         /**
179          * Handle newly connected peers
180          *
181          * @return      void
182          */
183         private final function handleNewPeers () {
184                 // Output message
185                 $this->getOutputInstance()->output(sprintf("[%s] Validating peer...", __METHOD__));
186
187                 // Is the main socket in the array?
188                 if (in_array($this->main_socket, $this->readPeers)) {
189                         // Accept the connection and add him to the list
190                         $peer_socket = socket_accept($this->main_socket);
191
192                         // Get a new peer instance
193                         $this->setCurrPeerInstance(HubPeer::createHubPeerBySocket($peer_socket, $this));
194
195                         // Register the new peer
196                         $this->getOutputInstance()->output(sprintf("[%s] Registering new peer with IP %s.",
197                                 __METHOD__,
198                                 $this->getCurrPeerInstance()->getValidatedIP()
199                         ));
200                         $this->connectedPeers->append($this->lastPeerInstance);
201
202                         // A new peer has connected
203                         $this->getOutputInstance()->output(sprintf("[%s] New peer with IP %s has been registered.",
204                                 __METHOD__,
205                                 $this->getCurrPeerInstance()->getValidatedIP()
206                         ));
207
208                         // Remove him from the list
209                         $key = array_search($this->main_socket, $this->readPeers);
210                         unset($this->readPeers[$key]);
211                 } // END - if
212         }
213
214         /**
215          * Handles unauthenticated peers
216          *
217          * @return      void
218          * @throws      HubPeerTimeoutException                 If the peer times out to answer a request
219          * @throws      HubPeerAuthorizationException           If the peer fails to authorize himself (to many retries)
220          */
221         private final function handleUnauthPeers () {
222                 // Are there some peers?
223                 if ($this->connectedPeers->count() > 1) {
224                         // Iterate through all connected peers
225                         for ($idx = $this->connectedPeers->getIterator(); $idx->valid(); $idx->next()) {
226                                 // Get current peer
227                                 $this->lastPeerInstance = $idx->current();
228
229                                 // Ignore own socket and invalid entries
230                                 if (($this->getCurrPeerInstance() !== $this->main_socket) && (is_object($this->getCurrPeerInstance())) && ($this->getCurrPeerInstance() instanceof HubPeer)) {
231                                         // Is this peer already authorized or is this the master hub?
232                                         // If this is the master hub then there is no auth required
233                                         if ((!$this->getCurrPeerInstance()->ifPeerIsAuthorized()) && (!$this->getCurrPeerInstance()->ifPeerIsLocalAdmin()) && (!$this->hubIsMaster)) {
234                                                 // This peer waits for authorization, so does he have some tries left?
235                                                 if ($this->getCurrPeerInstance()->getAuthRetries() <= $this->authRetries) {
236                                                         // This peer is still allowed to try his authorization
237                                                         if ($this->getCurrPeerInstance()->getLastSentMessage() == $this->authRequest) {
238                                                                 // Already asked so maybe timed out?
239                                                                 if ((time() - $this->getCurrPeerInstance()->getLastSentMessageStamp()) >= $this->authRequestTimeout) {
240                                                                         // Timed out so disconnect the peer
241                                                                         throw new HubPeerTimeoutException (
242                                                                                 array(
243                                                                                         'this'  => $this,
244                                                                                         'peer'  => $this->getCurrPeerInstance()
245                                                                                 ), self::EXCEPTION_HUB_PEER_TIMEOUT
246                                                                         );
247                                                                 } // END - if
248                                                         } elseif ($this->getCurrPeerInstance()->ifHelloReceived()) {
249                                                                 // HELLO received so we need to sent the AUTH request
250                                                                 $this->getCurrPeerInstance()->askAuthorizationKey();
251                                                         }
252                                                 } else {
253                                                         // This peer needs disconnecting!
254                                                         throw new HubPeerAuthorizationException (
255                                                                 array(
256                                                                         'this'  => $this,
257                                                                         'peer'  => $this->getCurrPeerInstance(),
258                                                                         'max'   => $this->authRetries
259                                                                 ), self::EXCEPTION_HUB_PEER_FAILED_AUTH
260                                                         );
261                                                 } // END - else
262                                         } elseif ((!$this->getCurrPeerInstance()->ifPeerIsAuthorized()) && ($this->getCurrPeerInstance()->ifPeerIsLocalAdmin())) {
263                                                 // This peer is a local admin so he is always authorized!
264                                                 $this->getCurrPeerInstance()->enableLocalAdmin();
265
266                                                 // Output debug message
267                                                 $this->getOutputInstance()->output(sprintf("[%s] A local admin has connected.",
268                                                         $this->__toString()
269                                                 ));
270
271                                                 // Say "hi" to the admin
272                                                 $this->getCurrPeerInstance()->sayHi2Admin();
273                                         } elseif (($this->hubIsMaster) && ($this->getCurrPeerInstance()->ifHelloReceived()) && (!$this->getCurrPeerInstance()->ifELHOsent())) {
274                                                 // Is the master hub so authorize the peer here...
275                                                 $this->getCurrPeerInstance()->enableIsAuthorized();
276                                                 $this->authPeers->append($this->getCurrPeerInstance());
277
278                                                 // Reply the HELLO request
279                                                 $this->getCurrPeerInstance()->replyHelloMessage();
280                                         }
281                                 } // END - if
282                         } // END - for
283                 } // END - if
284         }
285
286         /**
287          * Handles only authorized peers
288          *
289          * @return      void
290          */
291         public function handleAuthPeers () {
292                 // Are there some peers?
293                 if (($this->connectedPeers->count() > 1) && ($this->authPeers->count() > 0)) {
294                         // Iterate through all connected peers
295                         for ($idx = $this->authPeers->getIterator(); $idx->valid(); $idx->next()) {
296                                 // Get current peer
297                                 $this->lastPeerInstance = $idx->current();
298
299                                 // Do the ping and update our authPeer list (LATER!)
300                                 $this->lastPeerInstance->handlePingPeer();
301                         } // END - for
302                 } // END - for
303         }
304
305         /**
306          * Setup the hub: Create a socket for listening on incoming requests,
307          * try to bind to a port, etc.
308          *
309          * @return      void
310          * @throws      SocketCreationException         If a socket cannot be created
311          * @throws      SocketSetupException            If a socket cannot be setuped
312          * @throws      SocketBindException                     If a socket cannot be bind to
313          *                                                                      an address and port
314          * @throws      SocketListeningException                If listening to a socket fails
315          */
316         private final function setupHub () {
317                 // Create a new TCP socket
318                 $main_socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
319
320                 // Was it a success?
321                 if (!is_resource($main_socket)) {
322                         // This fails! :(
323                         throw new SocketCreationException(
324                                 array(
325                                         'this' => $this,
326                                         'code' => socket_last_error()
327                                 ), self::EXCEPTION_SOCKET_PROBLEM
328                         );
329                 }
330
331                 // Set socket options
332                 if (!socket_set_option($main_socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
333                         // Close the socket
334                         $this->closeSocket($main_socket);
335
336                         // Problems setting socket options
337                         throw new SocketSetupException (
338                                 array(
339                                         'this' => $this,
340                                         'code' => socket_last_error()
341                                 ), self::EXCEPTION_SOCKET_PROBLEM
342                         );
343                 }
344
345                 // Set to non-blocking
346                 socket_set_nonblock($main_socket);
347
348                 // Bind the socket to an address
349                 if (!socket_bind($main_socket, $this->getConfigInstance()->readConfig("hub_listen_addr"), $this->getConfigInstance()->readConfig("hub_listen_port"))) {
350                         // Bind failed
351                         throw new SocketBindException (
352                                 array(
353                                         'this' => $this,
354                                         'host' => $this->getConfigInstance()->readConfig("hub_listen_addr"),
355                                         'port' => $this->getConfigInstance()->readConfig("hub_listen_port"),
356                                         'code' => socket_last_error()
357                                 ), self::EXCEPTION_SOCKET_PROBLEM
358                         );
359                 }
360
361                 // Listen to ... the socket ;-)
362                 if (!socket_listen($main_socket)) {
363                         // Opps, that didn't work. Next time better listen to your heart... Roxette
364                         throw new SocketListeningException(
365                                 array(
366                                         'this' => $this,
367                                         'code' => socket_last_error()
368                                 ), self::EXCEPTION_SOCKET_PROBLEM
369                         );
370                 }
371
372                 // Ignore user abort and do not time out
373                 @set_time_limit(0);
374                 @ignore_user_abort(true);
375
376                 // Cache more configuration stuff
377                 $this->authRetries        = $this->getConfigInstance()->readConfig("hub_max_auth_tries");
378                 $this->authRequest        = $this->getConfigInstance()->readConfig("hub_auth_request");
379                 $this->authRequestTimeout = $this->getConfigInstance()->readConfig("hub_auth_request_timeout");
380
381                 // The hub is running now!
382                 $this->hubActivated = true;
383
384                 // Append the main hub
385                 $this->main_socket = $main_socket;
386                 $this->connectedPeers->append($main_socket);
387         }
388
389         /**
390          * Removes the current peer from the list
391          *
392          * @return      void
393          */
394         public final function removePeerFromConnectList () {
395                 // Iterate through all connected peers
396                 for ($idx = $this->connectedPeers->getIterator(); $idx->valid(); $idx->next()) {
397                         // Get current peer from list
398                         $peer = $idx->current();
399
400                         // Is it a peer instance?
401                         if (($peer !== $this->main_socket) && (is_object($peer)) && ($peer instanceof HubPeer)) {
402                                 // Okay, we have a peer instance so is it the same?
403                                 if ($this->getCurrPeerInstance()->equals($peer)) {
404                                         // Remove him!
405                                         $idx->offsetUnset($idx->key());
406                                         $this->lastPeerInstance = null;
407                                         break;
408                                 }
409                         }
410                 }
411         }
412
413         /**
414          * Getter for console output handler
415          *
416          * @return      $outputInstance An instance of the output handler
417          */
418         public final function getOutputInstance () {
419                 return $this->outputInstance;
420         }
421
422         /**
423          * Setter for console output handler
424          *
425          * @param               $outputInstance An instance of the output handler
426          */
427         public final function setOutputInstance ($outputInstance) {
428                 $this->outputInstance = $outputInstance;
429         }
430
431         /**
432          * Getter for current peer instance to HubPeer class
433          *
434          * @return      $lastPeerInstance               Last peer instance
435          */
436         public final function getCurrPeerInstance () {
437                 return $this->lastPeerInstance;
438         }
439
440         /**
441          * Setter for current peer instance to HubPeer class
442          *
443          * @param               $lastPeerInstance               Last peer instance
444          * @return      void
445          */
446         public final function setCurrPeerInstance (HubPeer $lastPeerInstance) {
447                 $this->lastPeerInstance = $lastPeerInstance;
448         }
449
450         /**
451          * Checks wether the hub is running and not in shutdown phase
452          *
453          * @return      $isRunning      If the hub is running and not in shutdown phase
454          */
455         public function ifHubIsRunning () {
456                 return ((($this->hubActivated) || (!$this->hubShutsDown)) && ($this->connectedPeers->count() > 0));
457         }
458
459         /**
460          * Output the intro text
461          *
462          * @return      void
463          */
464         public final function outputIntro () {
465                 if ($this->getConfigInstance()->readConfig("hub_intro_enabled") == "Y") {
466                         // Output intro text
467                         $this->getOutputInstance()->output(sprintf("%s v%s Copyright (c) 2007, 2008 by Roland H&auml;der",
468                                 ApplicationHelper::getInstance()->getAppName(),
469                                 ApplicationHelper::getInstance()->getAppVersion()
470                         ));
471                         $this->getOutputInstance()->output("");
472                         $this->getOutputInstance()->output("This software is free software licensed under the GNU LGPL. In telnet session enter &quot;/license&quot; to read the license.");
473                         $this->getOutputInstance()->output("This software uses the MXChange Framework, Copyright (c) 2007, 2008 by Roland H&auml;der which is licensed under the GNU LGPL.");
474                         $this->getOutputInstance()->output("Enter &quot;/framework&quot; in telnet session to read that license.");
475                         $this->getOutputInstance()->output("");
476                         $this->getOutputInstance()->output("All core systems are initialized. Input on *this* console will currently be ignored!");
477                         $this->getOutputInstance()->output("");
478                         $this->getOutputInstance()->output(sprintf("[%s] Listening on: %s:%d",
479                                 $this->__toString(),
480                                 $this->getConfigInstance()->readConfig("hub_listen_addr"),
481                                 $this->getConfigInstance()->readConfig("hub_listen_port")
482                         ));
483                         $this->getOutputInstance()->output("----------------------------------------------------------------------------------------------------------------------------");
484                 }
485         }
486
487         /**
488           * Do the main loop
489           *
490           * @return     void
491           * @throws     SocketCreationException         If the main socket is not a resource
492           */
493          public function coreLoop () {
494                 // Is the main socket vailid?
495                 if (!is_resource($this->main_socket)) {
496                         // Is not valid!
497                         throw new SocketCreationException(
498                                 array(
499                                         'this'  => $this,
500                                         'code'  => socket_last_error()
501                                 ), self::EXCEPTION_SOCKET_PROBLEM
502                         );
503                 } // END - if
504
505                 // We are ready to serve requests
506                 $this->getOutputInstance()->output(sprintf("[%s] Ready to serve requests.",
507                         $this->__toString()
508                 ));
509
510                 // Wait until the hub is shutting down
511                 while ($this->ifHubIsRunning()) {
512                         // Get number of total connected peers
513                         $num = $this->getTotalConnectedPeers();
514
515                         try {
516                                 // Handle the master hub connection
517                                 if ($this->masterConnector instanceof HubConnector) {
518                                         $this->masterConnector->handleMasterRequests();
519                                 }
520
521                                 // Check for unauthorized peers
522                                 $this->handleUnauthPeers();
523
524                                 // Handle authorized peers
525                                 $this->handleAuthPeers();
526                         } catch (HubPeerAuthorizationException $e) {
527                                 // Authorization has failed
528                                 $this->disconnectPeerWithReason("hub_msg_auth_tries");
529
530                                 // Get new total connected peers
531                                 $num = $this->getTotalConnectedPeers();
532                         } catch (HubPeerTimeoutException $e) {
533                                 // Disconnect and remove the peer
534                                 $this->disconnectPeerWithReason("hub_msg_auth_reply_timeout");
535
536                                 // Get new total connected peers
537                                 $num = $this->getTotalConnectedPeers();
538                         } catch (HubMasterDisconnectedException $e) {
539                                 // The master hub has disconnected us... :(
540                                 $this->masterConnector = null;
541                                 $this->getOutputInstance()->output(sprintf("[%s] The master has disconnected us. Reason given: %s",
542                                         $this->__toString(),
543                                         $e->getMessage()
544                                 ));
545                         } catch (BrokenPipeException $e) {
546                                 // Broken pipes are bad for us... :(
547                                 $this->removePeerFromConnectList();
548                                 $this->getOutputInstance()->output(sprintf("[%s] A peer has closed the connection unexpected: %s",
549                                         $this->__toString(),
550                                         $e->getMessage()
551                                 ));
552
553                                 // Get new total connected peers
554                                 $num = $this->getTotalConnectedPeers();
555                         } catch (FrameworkException $e) {
556                                 // Catch all other exceptions and output them
557                                 echo "CATCH".__LINE__.":".$e->__toString()."\n";
558                                 hub_exception_handler($e);
559
560                                 // Get new total connected peers
561                                 $num = $this->getTotalConnectedPeers();
562                         }
563
564                         // Are there some peers?
565                         if ($num < 1) {
566                                 // Wait for more peers
567                                 continue;
568                         } // END - if
569
570                         // A new peer has connected
571                         $this->getOutputInstance()->output(sprintf("[%s] A new peer is connecting.",
572                                 $this->__toString()
573                         ));
574
575                         try {
576                                 // Check for new peers
577                                 $this->handleNewPeers();
578                         } catch (IPSpoofingException $e) {
579                                 // Output message
580                                 $this->getOutputInstance()->output(sprintf("[%s] The peer's IP number has changed!",
581                                         $this->__toString()
582                                 ));
583
584                                 // Output debug message
585                                 $this->getDebugInstance()->output(sprintf("[%s] Peer spoofes IP number: %s",
586                                         $this->__toString(),
587                                         $e->getMessage()
588                                 ));
589
590                                 // Disconnect the peer first
591                                 $this->disconnectPeerWithReason("hub_msg_spoofing");
592
593                                 // Get new total connected peers
594                                 $num = $this->getTotalConnectedPeers();
595                         } catch (FrameworkException $e) {
596                                 // Catch all exceptions and output them to avoid aborting the program unexpectly
597                                 echo "CATCH".__LINE__.":".$e->__toString()."\n";
598                                 hub_exception_handler($e);
599
600                                 // Get new total connected peers
601                                 $num = $this->getTotalConnectedPeers();
602                         }
603
604                 } // END - while
605         }
606
607         /**
608          * Tries to contact the master server or simply reports that we are the master server
609          *
610          * @return      void
611          */     
612         public function contactMasterHub () {
613                 // Checks wether we are the master hub
614                 if ($_SERVER['SERVER_ADDR'] == $this->getConfigInstance()->readConfig("hub_master_ip")) {
615                         // We are master!
616                         $this->hubIsMaster = true;
617                         $this->getOutputInstance()->output(sprintf("[%s] Our IP %s matches the master IP. Becoming master hub...",
618                                 $this->__toString(),
619                                 $_SERVER['SERVER_ADDR']
620                         ));
621                 } else {
622                         // A regular hub or ultra hub so let's contact the master
623                         $this->getOutputInstance()->output(sprintf("[%s] Contacting the master hub at %s:%d...",
624                                 $this->__toString(),
625                                 $this->getConfigInstance()->readConfig("hub_master_ip"),
626                                 $this->getConfigInstance()->readConfig("hub_master_port")
627                         ));
628
629                         // Try to aquire a connection to the master...
630                         try {
631                                 // Announce us to the master hub
632                                 $this->announceToMasterHub();
633                         } catch (FrameworkException $e) {
634                                 // Catch all exceptions and output them
635                                 echo "CATCH".__LINE__.":".$e->__toString()."\n";
636                                 hub_exception_handler($e);
637                         }
638                 } // END - else
639         }
640
641         /**
642          * Disconnects the current with a configurable reason
643          *
644          * @param               $reasonEntry    The entry with the disconnection reason aka. message to the peer
645          * @return      void
646          */
647          public function disconnectPeerWithReason ($reasonEntry) {
648                 $ip = "0.0.0.0";
649
650                 // Try to disconnect here
651                 try {
652                         // First get the raw IP number
653                         $ip = $this->getCurrPeerInstance()->getValidatedIP();
654
655                         // Disconnect the peer...
656                         $this->getCurrPeerInstance()->disconnectWithReason($this->getConfigInstance()->readConfig($reasonEntry));
657                 } catch (FrameworkException $e) {
658                         // Catch all exceptions and output them
659                         echo "CATCH".__LINE__.":".$e->__toString()."\n";
660                         hub_exception_handler($e);
661                 }
662
663                 // Remove him from the list anyway
664                 $this->removePeerFromConnectList();
665
666                 // Output the message
667                 $this->getOutputInstance()->output(sprintf("[%s] Peer %s has been disconnected.",
668                         $this->__toString(),
669                         $ip
670                 ));
671         }
672
673         /**
674          * Announces this hub to the master hub. This is being done by connecting to it and asking for auth request
675          *
676          * @return      void
677          */
678         public function announceToMasterHub () {
679                 try {
680                         // Create a new instance for communicating to the master hub
681                         $this->masterConnector = HubConnector::createHubConnectorByAddressPort(
682                                 $this->getConfigInstance()->readConfig("hub_master_ip"),
683                                 $this->getConfigInstance()->readConfig("hub_master_port"),
684                                 $this
685                         );
686
687                         // Send all our accepted objects to the master hub
688                         $this->masterConnector->sendAllAcceptedObjects();
689                 } catch (SocketConnectException $e) {
690                         // The master hub is down!
691                         ApplicationEntryPoint::app_die($e->getMessage());
692                 }
693         }
694
695 } // END - class
696
697 // [EOF]
698 ?>