]> git.mxchange.org Git - core.git/blob - framework/main/classes/tools/console/class_ConsoleTools.php
5d33e69c43c3c338e4129363e2f45898c081a794
[core.git] / framework / main / classes / tools / console / class_ConsoleTools.php
1 <?php
2 // Own namespace
3 namespace Org\Mxchange\CoreFramework\Console\Tools;
4
5 // Import framework stuff
6 use Org\Mxchange\CoreFramework\Bootstrap\FrameworkBootstrap;
7 use Org\Mxchange\CoreFramework\Factory\Object\ObjectFactory;
8 use Org\Mxchange\CoreFramework\Filesystem\FileNotFoundException;
9 use Org\Mxchange\CoreFramework\Generic\FrameworkException;
10 use Org\Mxchange\CoreFramework\Generic\FrameworkInterface;
11 use Org\Mxchange\CoreFramework\Object\BaseFrameworkSystem;
12 use Org\Mxchange\CoreFramework\Socket\InvalidSocketException;
13
14 // Import SPL stuff
15 use \InvalidArgumentException;
16 use \SplFileInfo;
17
18 /**
19  * This class contains static helper functions for console applications
20  *
21  * @author              Roland Haeder <webmaster@shipsimu.org>
22  * @version             0.0.0
23  * @copyright   Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2022 Core Developer Team
24  * @license             GNU GPL 3.0 or any newer version
25  * @link                http://www.shipsimu.org
26  *
27  * This program is free software: you can redistribute it and/or modify
28  * it under the terms of the GNU General Public License as published by
29  * the Free Software Foundation, either version 3 of the License, or
30  * (at your option) any later version.
31  *
32  * This program is distributed in the hope that it will be useful,
33  * but WITHOUT ANY WARRANTY; without even the implied warranty of
34  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
35  * GNU General Public License for more details.
36  *
37  * You should have received a copy of the GNU General Public License
38  * along with this program. If not, see <http://www.gnu.org/licenses/>.
39  */
40 class ConsoleTools extends BaseFrameworkSystem {
41         // Constants
42         const HTTP_EOL = "\r\n";
43         const HTTP_USER_AGENT = 'ConsoleTools/1.0.1';
44
45         /**
46          * Default is that this class is noisy
47          */
48         private static $quietResolver = FALSE;
49
50         /**
51          * Protected constructor
52          *
53          * @return      void
54          */
55         private function __construct () {
56                 // Call parent constructor
57                 parent::__construct(__CLASS__);
58
59                 // Cache configuration entry
60                 self::$quietResolver = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('quiet_dns_resolver');
61         }
62
63         /**
64          * Checks wether proxy configuration is used
65          *
66          * @return      $isUsed         Wether proxy is used
67          */
68         protected function isProxyUsed () {
69                 // Do we have cache?
70                 if (!isset($GLOBALS[__METHOD__])) {
71                         // Determine it
72                         $GLOBALS[__METHOD__] = (
73                                 (
74                                         FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('proxy_host') != ''
75                                 ) && (
76                                         FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('proxy_port') > 0
77                                 )
78                         );
79                 }
80
81                 // Return cache
82                 return $GLOBALS[__METHOD__];
83         }
84
85         /**
86          * Sets up a proxy tunnel for given hostname and through resource
87          *
88          * @param       $host                           Host to connect to
89          * @param       $port                           Port number to connect to
90          * @param       $socketResource         Resource of a socket
91          * @return      $response                       Response array
92          * @throws      InvalidArgumentException        If a parameter is not valid
93          */
94         protected function setupProxyTunnel (string $host, int $port, $socketResource) {
95                 // Validate parameter
96                 if (empty($host)) {
97                         // Throw IAE
98                         throw new InvalidArgumentException('Parameter "host" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
99                 } elseif ($port < 1) {
100                         // Throw IAE
101                         throw new InvalidArgumentException(sprintf('port=%d is not valid', $port));
102                 } elseif (!is_resource($socketResource)) {
103                         // Throw IAE
104                         throw new InvalidArgumentException(sprintf('socketResource[]=%s is not valid', gettype($socketResource)));
105                 }
106
107                 // Initialize array
108                 $response = ['', '', ''];
109                 $proxyTunnel = '';
110
111                 // Generate CONNECT request header
112                 $proxyTunnel .= 'CONNECT ' . $host . ':' . $port . ' HTTP/1.1' . self::HTTP_EOL;
113                 $proxyTunnel .= 'Host: ' . $host . ':' . $port . self::HTTP_EOL;
114                 $proxyTunnel .= 'Proxy-Connection: Keep-Alive' . self::HTTP_EOL;
115
116                 // Use login data to proxy? (username at least!)
117                 if (FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('proxy_username') != '') {
118                         // Add it as well
119                         $encodedAuth = base64_encode(FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('proxy_username') . ':' . FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('proxy_password'));
120                         $proxyTunnel .= 'Proxy-Authorization: Basic ' . $encodedAuth . self::HTTP_EOL;
121                 }
122
123                 // Add last new-line
124                 $proxyTunnel .= self::HTTP_EOL;
125                 //* DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput('CONSOLE-TOOLS: proxyTunnel=' . $proxyTunnel);
126
127                 // Write request
128                 fwrite($socketResource, $proxyTunnel);
129
130                 // Got response?
131                 if (feof($socketResource)) {
132                         // No response received
133                         return $response;
134                 }
135
136                 // Read the first line
137                 $resp = trim(fgets($socketResource, 10240));
138                 $respArray = explode(' ', $resp);
139                 if (((strtolower($respArray[0]) !== 'http/1.0') && (strtolower($respArray[0]) !== 'http/1.1')) || ($respArray[1] != '200')) {
140                         // Invalid response!
141                         return $response;
142                 }
143
144                 // All fine!
145                 return $respArray;
146         }
147
148         /**
149          * Tried to extract hostname from given raw data. On a Genntoo system, this could be multiple lines with # as comments. So try to get rid of it
150          *
151          * @param       $rawData        Raw data from /etc/hostname file
152          * @return      $hostname       Extracted host name
153          * @throws      InvalidArgumentException        If a parameter is not valid
154          */
155         protected function extractHostnameFromRawData (string $rawData) {
156                 // Validate parameter
157                 if (empty($rawData)) {
158                         // Throw IAE
159                         throw new InvalidArgumentException('Parameter "rawData" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
160                 }
161
162                 // Default is invalid
163                 $hostname = 'invalid';
164
165                 // Try to "explode" it
166                 $data = explode(PHP_EOL, $rawData);
167
168                 // "Walk" through it
169                 foreach ($data as $line) {
170                         // Trim it
171                         $line = trim($line);
172
173                         // Begins with a hash (#) = comment?
174                         if (substr($line, 0, 1) == '#') {
175                                 // Then skip it
176                                 continue;
177                         }
178
179                         // Has an equals sign?
180                         if (strpos($line, '=') !== false) {
181                                 // Then "explode" it again, right part is hostname in quotes
182                                 $hostData = explode('=', $line);
183
184                                 // Make sure only a key=value pair goes through
185                                 assert(count($hostData) == 2);
186
187                                 // Try to get it and abort
188                                 $hostname = str_replace(
189                                         ['"', chr(39)],
190                                         ['', ''],
191                                         $hostData[1]
192                                 );
193                                 break;
194                         } else {
195                                 // Use it directly
196                                 $hostname = $line;
197                                 break;
198                         }
199                 }
200
201                 // Return it
202                 return $hostname;
203         }
204
205         /**
206          * Tries to resolve an IP address from given hostname. Currently only IPv
207          * addresses are resolved.
208          *
209          * @param       $hostname       Host name we shall resolve
210          * @return      $ipAddress      IPv4 address resolved from host name
211          * @throws      InvalidArgumentException        If a parameter is not valid
212          * @todo        This should be connected to a caching class to cache DNS requests
213          */
214         public static function resolveIpAddress (string $hostname) {
215                 // Validate parameter
216                 if (empty($hostname)) {
217                         // Throw IAE
218                         throw new InvalidArgumentException('Parameter "hostname" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
219                 } elseif (self::$quietResolver !== TRUE) {
220                         // Debug message
221                         self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('[%s:] Host name to resolve is: %s',
222                                 __CLASS__,
223                                 $hostname
224                         ));
225                 }
226
227                 // Default is false
228                 $ipAddress = false;
229
230                 // Is a dot at the end?
231                 if (substr($hostname, -1, 1) != '.') {
232                         /*
233                          * Then append it to prevent lookup of invalid host names through
234                          * all search-domains. This will greatly improve lookup performance
235                          * and has no disadvantages for anybody. A dot at the end of a
236                          * domain btw means full-qualified domain, do not prepend to any
237                          * other domain, basically.
238                          */
239                         $hostname .= '.';
240                 }
241
242                 // Resolve it
243                 // @TODO Here should the cacher be implemented
244                 $ipResolved = gethostbyname($hostname);
245
246                 // Was it fine?
247                 if (($ipResolved !== false) && ($ipResolved != $hostname)) {
248                         // Okay, this works!
249                         $ipAddress = $ipResolved;
250
251                         // Quiet?
252                         if (self::$quietResolver !== TRUE) {
253                                 // Debug message
254                                 self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('[%s:] Resolved IP address is: %s',
255                                         __CLASS__,
256                                         $ipAddress
257                                 ));
258                         }
259                 } else {
260                         // Problem while resolving IP address
261                         self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('[%s:] Problem resolving IP address for host %s. Please check your /etc/hosts file.',
262                                 __CLASS__,
263                                 $hostname
264                         ));
265                 }
266
267                 // Return resolved IP
268                 return $ipAddress;
269         }
270
271         /**
272          * Aquires the IP address of this host by reading the /etc/hostname file
273          * and solving it. It is now stored in configuration
274          *
275          * @return      $ipAddress      Aquired IPv4 address
276          */
277         public static function acquireSelfIpAddress () {
278                 // Local IP by default
279                 $ipAddress = '127.0.0.1';
280
281                 // Get a new instance
282                 $helperInstance = new ConsoleTools();
283
284                 // Get SplFileInfo instance
285                 $infoInstance = new SplFileInfo(FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('hostname_file'));
286
287                 try {
288                         // Get a file pointer
289                         $fileInstance = ObjectFactory::createObjectByConfiguredName('file_raw_input_class', array($infoInstance));
290
291                         // Read the file
292                         $rawData = trim($fileInstance->readFromFile());
293
294                         // Close the file
295                         $fileInstance->closeFile();
296
297                         // Extract hostname from it
298                         $hostname = $helperInstance->extractHostnameFromRawData($rawData);
299
300                         // Resolve the IP number
301                         $ipAddress = self::resolveIpAddress($hostname);
302                 } catch (FileNotFoundException $e) {
303                         // Fall-back to 'SESSION_SVR' which found on my Sun Station
304                         if (isset($_SERVER['SESSION_SVR'])) {
305                                 // Resolve it
306                                 $ipAddress = self::resolveIpAddress($_SERVER['SESSION_SVR']);
307                         } elseif (isset($_SERVER['COMPUTERNAME'])) {
308                                 // May happen on some Windows XP systems, so also try this
309                                 $ipAddress = self::resolveIpAddress($_SERVER['COMPUTERNAME']);
310                         } else {
311                                 // Could not find our hostname
312                                 self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('[%s:] WARNING: Cannot resolve my own IP address.',
313                                         $helperInstance->__toString()
314                                 ));
315                         }
316                 } catch (FrameworkException $e) {
317                         // Output debug message
318                         self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('[%s:] Problem while resolving own IP address: [%s|%s]:%s',
319                                 $helperInstance->__toString(),
320                                 $e->__toString(),
321                                 $e->getHexCode(),
322                                 $e->getMessage()
323                         ));
324                 }
325
326                 // Return it
327                 return $ipAddress;
328         }
329
330         /**
331          * Determines own remote IP address (e.g. can be used to probe if we are
332          * reachable from outside by determining external address and then connect to it.
333          * This is accomblished by connecting to the IP of www.shipsimu.org which
334          * should default to 188.138.90.169 and requesting /ip.php which does only
335          * return the content of $_SERVER['REMOTE_ADDR']. Of course, this method
336          * requires a working Internet connection.
337          *
338          * This method is taken from a user comment on php.net and heavily rewritten.
339          * Compare to following link:
340          * http://de.php.net/manual/en/function.socket-create.php#49368
341          *
342          * @return      $externalAddress        The determined external address address
343          * @throws      InvalidSocketException  If socket initialization wents wrong or if an errors occurs
344          * @todo        This should be moved out to an external class, e.g. HttpClient
345          * @todo        Make IP, host name, port and script name configurable
346          */
347         public static function determineExternalAddress () {
348                 // Get helper instance
349                 $helperInstance = new ConsoleTools();
350
351                 // First get a socket
352                 // @TODO Add some DNS caching here
353
354                 // Open connection
355                 if ($helperInstance->isProxyUsed() === true) {
356                         // Resolve hostname into IP address
357                         $ipAddress = self::resolveIpAddress(FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('proxy_host'));
358
359                         // Connect to host through proxy connection
360                         $socketResource = fsockopen($ipAddress, FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('proxy_port'), $errorNo, $errorStr, 30);
361                 } else {
362                         // Connect to host directly
363                         $socketResource = fsockopen('188.138.90.169', 80, $errorNo, $errorStr, 30);
364                 }
365
366                 // Check if there was an error else
367                 if ($errorNo > 0) {
368                         // Then throw again
369                         throw new InvalidSocketException(array($helperInstance, $socketResource, $errorNo, $errorStr), BaseFrameworkSystem::EXCEPTION_INVALID_SOCKET);
370                 }
371
372                 // Prepare the GET request
373                 $request  = 'GET ' . ($helperInstance->isProxyUsed() === true ? 'http://shipsimu.org' : '') . '/ip.php HTTP/1.0' . self::HTTP_EOL;
374                 $request .= 'Host: shipsimu.org' . self::HTTP_EOL;
375                 $request .= 'User-Agent: ' . self::HTTP_USER_AGENT . self::HTTP_EOL;
376                 $request .= 'Connection: close' . self::HTTP_EOL;
377
378                 // Do we use proxy?
379                 if ($helperInstance->isProxyUsed() === true) {
380                         // CONNECT method?
381                         if (FrameworkBootstrap::getConfigurationInstance()->isEnabled('proxy_connect_method')) {
382                                 // Setup proxy tunnel
383                                 $response = $helperInstance->setupProxyTunnel('shipsimu.org', 80, $socketResource);
384
385                                 // If the response is invalid, abort
386                                 if ((count($response) == 3) && (empty($response[0])) && (empty($response[1])) && (empty($response[2]))) {
387                                         // Invalid response!
388                                         $helperInstance->debugBackTrace('Proxy tunnel not working: response=' . print_r($response, true));
389                                 }
390                         } else {
391                                 // Add header for proxy
392                                 $request .= 'Proxy-Connection: Keep-Alive' . self::HTTP_EOL;
393                         }
394                 }
395
396                 // Add last HTTP_EOL
397                 $request .= self::HTTP_EOL;
398
399                 // Send it to the socket
400                 fwrite($socketResource, $request);
401
402                 // Init IP (this will always be the last line)
403                 $externalAddress = 'invalid';
404
405                 // And read the reply
406                 while (!feof($socketResource)) {
407                         // Get line
408                         $externalAddress = trim(fgets($socketResource, 128));
409
410                         // Detect HTTP response
411                         if ((substr($externalAddress, 0, 7) == 'HTTP/1.') && (substr($externalAddress, -6, 6) != '200 OK')) {
412                                 // Stop processing
413                                 break;
414                         }
415                 }
416
417                 // Close socket
418                 fclose($socketResource);
419
420                 // Return determined external address
421                 /* DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput('CONSOLE-TOOLS: Resolved external address: ' . $externalAddress);
422                 return $externalAddress;
423         }
424
425         /**
426          * Analyzes the 'environment', mostly $_SERVER, for presence of elements
427          * which indicates clearly that e.g. this script has been executed from
428          * console or web.
429          *
430          * @return      $type   The analyzed type, can be 'http' or 'console'
431          */
432         public static function analyzeEnvironmentForType () {
433                 // Default is the console
434                 $type = 'console';
435
436                 // Now, do we have a request method, or query string set?
437                 if ((isset($_SERVER['REQUEST_METHOD'])) || (isset($_SERVER['QUERY_STRING']))) {
438                         // Possibly HTTP request
439                         $type = 'http';
440                 }
441
442                 // Return it
443                 return $type;
444         }
445
446         /**
447          * Analyzes the 'environment', mostly $_SERVER, for presence of elements
448          * which indicates clearly that e.g. this script has been executed from
449          * console or web. This method should be used for class names, they
450          * currently are named differently. Here is a list to clarify this:
451          *
452          *   Request type | Class type
453          * -----------------------------
454          *      http      |    web
455          *     console    |  console
456          *
457          * @return      $type   The analyzed type, can be 'http' or 'console'
458          */
459         public static function analyzeEnvironmentForClassType () {
460                 // Default is the console
461                 $type = 'console';
462
463                 // Now, do we have a request method, or query string set?
464                 if (self::analyzeEnvironmentForType() == 'http') {
465                         // Possibly HTTP request
466                         $type = 'web';
467                 }
468
469                 // Return it
470                 return $type;
471         }
472
473 }