diff --git a/README.md b/README.md index 3438be3..1d9001c 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,6 @@ Installation and usage is pretty straight forward: ### Require this package with [Composer](https://getcomposer.org/) -- For PHP <7 we need version 0.1.* -- For PHP 7+ we need version 0.2.* - Either run this command in your command line: ``` @@ -155,34 +152,36 @@ Enable admins? Add custom command paths? Set up logging? Here is a list of available extra parameters: -Parameter | Description ---------- |------------ -webhook | URL to the manager PHP file used for setting up the Webhook. - | *e.g.* `'https://example.com/manager.php'` -certificate | Path to a self-signed certificate (if necessary). - | *e.g.* `__DIR__ . '/server.crt'` -max_connections | Maximum allowed simultaneous HTTPS connections to the webhook - | *e.g.* `20` -allowed_updates | List the types of updates you want your bot to receive - | *e.g.* `['message', 'edited_channel_post', 'callback_query']` -logging | Path(s) where to the log files should be put. This is an array that can contain all 3 log file paths (`error`, `debug` and `update`). - | *e.g.* `['error' => __DIR__ . '/php-telegram-bot-error.log']` -admins | An array of user ids that have admin access to your bot. - | *e.g.* `[12345]` -mysql | Mysql credentials to connect a database (necessary for [`getUpdates`](#using-getupdates-method) method!). - | *e.g.* `['host' => '127.0.0.1', 'user' => 'root', 'password' => 'root', 'database' => 'telegram_bot']` -download_path | Custom download path. - | *e.g.* `__DIR__ . '/Download'` -upload_path | Custom upload path. - | *e.g.* `__DIR__ . '/Upload'` -commands_paths | A list of custom commands paths. - | *e.g.* `[__DIR__ . '/CustomCommands']` -command_configs | A list of all custom command configs. - | *e.g.* `['sendtochannel' => ['your_channel' => '@my_channel']` -botan_token | The Botan.io token to be used for analytics. - | *e.g.* `'botan_12345'` -custom_input | Override the custom input of your bot (mostly for testing purposes!). - | *e.g.* `'{"some":"raw", "json":"update"}'` +Parameter | Description +--------- |------------ +validate_request | Only allow webhook access from valid Telegram API IPs. +*bool* | *default is `true`* +webhook | URL to the manager PHP file used for setting up the Webhook. +*string* | *e.g.* `'https://example.com/manager.php'` +certificate | Path to a self-signed certificate (if necessary). +*string* | *e.g.* `__DIR__ . '/server.crt'` +max_connections | Maximum allowed simultaneous HTTPS connections to the webhook +*int* | *e.g.* `20` +allowed_updates | List the types of updates you want your bot to receive +*array* | *e.g.* `['message', 'edited_channel_post', 'callback_query']` +logging | Path(s) where to the log files should be put. This is an array that can contain all 3 log file paths (`error`, `debug` and `update`). +*array* | *e.g.* `['error' => __DIR__ . '/php-telegram-bot-error.log']` +admins | An array of user ids that have admin access to your bot. +*array* | *e.g.* `[12345]` +mysql | Mysql credentials to connect a database (necessary for [`getUpdates`](#using-getupdates-method) method!). +*array* | *e.g.* `['host' => '127.0.0.1', 'user' => 'root', 'password' => 'root', 'database' => 'telegram_bot']` +download_path | Custom download path. +*string* | *e.g.* `__DIR__ . '/Download'` +upload_path | Custom upload path. +*string* | *e.g.* `__DIR__ . '/Upload'` +commands_paths | A list of custom commands paths. +*array* | *e.g.* `[__DIR__ . '/CustomCommands']` +command_configs | A list of all custom command configs. +*array* | *e.g.* `['sendtochannel' => ['your_channel' => '@my_channel']` +botan_token | The Botan.io token to be used for analytics. +*string* | *e.g.* `'botan_12345'` +custom_input | Override the custom input of your bot (mostly for testing purposes!). +*string* | *e.g.* `'{"some":"raw", "json":"update"}'` ### Using getUpdates method diff --git a/src/BotManager.php b/src/BotManager.php index 4c10888..219fae8 100644 --- a/src/BotManager.php +++ b/src/BotManager.php @@ -23,6 +23,16 @@ */ class BotManager { + /** + * @var string Telegram post servers lower IP limit + */ + const TELEGRAM_IP_LOWER = '149.154.167.197'; + + /** + * @var string Telegram post servers upper IP limit + */ + const TELEGRAM_IP_UPPER = '149.154.167.233'; + /** * @var string The output for testing, instead of echoing */ @@ -258,6 +268,7 @@ public function setBotExtras(): self * * @return \NPM\TelegramBotManager\BotManager * @throws \Longman\TelegramBot\Exception\TelegramException + * @throws \Exception */ public function handleRequest(): self { @@ -387,9 +398,14 @@ public function handleGetUpdates(): self * * @return \NPM\TelegramBotManager\BotManager * @throws \Longman\TelegramBot\Exception\TelegramException + * @throws \Exception */ public function handleWebhook(): self { + if (!$this->isValidRequest()) { + throw new \Exception('Invalid access'); + } + $this->telegram->handle(); return $this; @@ -407,4 +423,33 @@ public function getOutput(): string return $output; } + + /** + * Check if this is a valid request coming from a Telegram API IP address. + * + * @link https://core.telegram.org/bots/webhooks#the-short-version + * + * @return bool + */ + public function isValidRequest(): bool + { + if (false === $this->params->getBotParam('validate_request')) { + return true; + } + + $ip = @$_SERVER['REMOTE_ADDR']; + foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) { + $addr = @$_SERVER[$key]; + if (filter_var($addr, FILTER_VALIDATE_IP)) { + $ip = $addr; + break; + } + } + + $lower_dec = (float)sprintf('%u', ip2long(self::TELEGRAM_IP_LOWER)); + $upper_dec = (float)sprintf('%u', ip2long(self::TELEGRAM_IP_UPPER)); + $ip_dec = (float)sprintf('%u', ip2long($ip)); + + return $ip_dec >= $lower_dec && $ip_dec <= $upper_dec; + } } diff --git a/src/Params.php b/src/Params.php index c638af5..358e44e 100644 --- a/src/Params.php +++ b/src/Params.php @@ -35,6 +35,7 @@ class Params * @var array List of valid extra parameters that can be passed. */ private static $valid_extra_bot_params = [ + 'validate_request', 'webhook', 'certificate', 'max_connections', @@ -56,9 +57,11 @@ class Params private $script_params = []; /** - * @var array List of all params passed at construction. + * @var array List of all params passed at construction, predefined with defaults. */ - private $bot_params = []; + private $bot_params = [ + 'validate_request' => true, + ]; /** * Params constructor. @@ -66,6 +69,7 @@ class Params * api_key (string) Telegram Bot API key * botname (string) Telegram Bot name * secret (string) Secret string to validate calls + * validate_request (bool) Only allow webhook access from valid Telegram API IPs * webhook (string) URI of the webhook * certificate (string) Path to the self-signed certificate * max_connections (int) Maximum allowed simultaneous HTTPS connections to the webhook @@ -102,7 +106,7 @@ private function validateAndSetBotParams($params): self { // Set all vital params. foreach (self::$valid_vital_bot_params as $vital_key) { - if (empty($params[$vital_key])) { + if (!array_key_exists($vital_key, $params)) { throw new \InvalidArgumentException('Some vital info is missing: ' . $vital_key); } @@ -111,7 +115,7 @@ private function validateAndSetBotParams($params): self // Set all extra params. foreach (self::$valid_extra_bot_params as $extra_key) { - if (empty($params[$extra_key])) { + if (!array_key_exists($extra_key, $params)) { continue; } @@ -147,7 +151,8 @@ private function validateAndSetScriptParams(): self // Keep only valid ones. $this->script_params = array_intersect_key($this->script_params, - array_fill_keys(self::$valid_script_params, null)); + array_fill_keys(self::$valid_script_params, null) + ); return $this; } diff --git a/tests/TelegramBotManager/Tests/BotManagerTest.php b/tests/TelegramBotManager/Tests/BotManagerTest.php index 5448cf2..efed673 100644 --- a/tests/TelegramBotManager/Tests/BotManagerTest.php +++ b/tests/TelegramBotManager/Tests/BotManagerTest.php @@ -370,6 +370,60 @@ public function testGetOutput() self::assertEmpty($botManager->getOutput()); } + public function testIsValidRequestValidateByDefault() + { + $botManager = new BotManager(ParamsTest::$demo_vital_params); + self::assertInternalType('bool', $botManager->getParams()->getBotParam('validate_request')); + self::assertTrue($botManager->getParams()->getBotParam('validate_request')); + } + + public function testIsValidRequestFailValidation() + { + $botManager = new BotManager(ParamsTest::$demo_vital_params); + + unset($_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_CLIENT_IP'], $_SERVER['REMOTE_ADDR']); + + foreach(['HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'] as $key) { + $_SERVER[$key] = '1.1.1.1'; + self::assertFalse($botManager->isValidRequest()); + unset($_SERVER[$key]); + } + } + + public function testIsValidRequestSkipValidation() + { + $botManager = new BotManager(array_merge(ParamsTest::$demo_vital_params, [ + 'validate_request' => false, + ])); + + unset($_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_CLIENT_IP'], $_SERVER['REMOTE_ADDR']); + + self::assertTrue($botManager->isValidRequest()); + } + + public function testIsValidRequestValidate() + { + $botManager = new BotManager(ParamsTest::$demo_vital_params); + + unset($_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_CLIENT_IP'], $_SERVER['REMOTE_ADDR']); + + // Lower range. + $_SERVER['REMOTE_ADDR'] = '149.154.167.196'; + self::assertFalse($botManager->isValidRequest()); + $_SERVER['REMOTE_ADDR'] = '149.154.167.197'; + self::assertTrue($botManager->isValidRequest()); + $_SERVER['REMOTE_ADDR'] = '149.154.167.198'; + self::assertTrue($botManager->isValidRequest()); + + // Upper range. + $_SERVER['REMOTE_ADDR'] = '149.154.167.232'; + self::assertTrue($botManager->isValidRequest()); + $_SERVER['REMOTE_ADDR'] = '149.154.167.233'; + self::assertTrue($botManager->isValidRequest()); + $_SERVER['REMOTE_ADDR'] = '149.154.167.234'; + self::assertFalse($botManager->isValidRequest()); + } + /** * @group live */ diff --git a/tests/TelegramBotManager/Tests/ParamsTest.php b/tests/TelegramBotManager/Tests/ParamsTest.php index c668644..5f3114b 100644 --- a/tests/TelegramBotManager/Tests/ParamsTest.php +++ b/tests/TelegramBotManager/Tests/ParamsTest.php @@ -27,26 +27,27 @@ class ParamsTest extends \PHPUnit_Framework_TestCase * @var array Demo extra parameters. */ public static $demo_extra_params = [ - 'webhook' => 'https://php.telegram.bot/manager.php', - 'certificate' => __DIR__ . '/server.crt', - 'max_connections' => 20, - 'allowed_updates' => ['message', 'edited_channel_post', 'callback_query'], - 'admins' => [1], - 'mysql' => [ + 'validate_request' => true, + 'webhook' => 'https://php.telegram.bot/manager.php', + 'certificate' => __DIR__ . '/server.crt', + 'max_connections' => 20, + 'allowed_updates' => ['message', 'edited_channel_post', 'callback_query'], + 'admins' => [1], + 'mysql' => [ 'host' => '127.0.0.1', 'user' => 'root', 'password' => 'root', 'database' => 'telegram_bot', ], - 'download_path' => __DIR__ . '/Download', - 'upload_path' => __DIR__ . '/Upload', - 'commands_paths' => __DIR__ . '/CustomCommands', - 'command_configs' => [ + 'download_path' => __DIR__ . '/Download', + 'upload_path' => __DIR__ . '/Upload', + 'commands_paths' => __DIR__ . '/CustomCommands', + 'command_configs' => [ 'weather' => ['owm_api_key' => 'owm_api_key_12345'], 'sendtochannel' => ['your_channel' => '@my_channel'], ], - 'botan_token' => 'botan_12345', - 'custom_input' => '{"some":"raw", "json":"update"}', + 'botan_token' => 'botan_12345', + 'custom_input' => '{"some":"raw", "json":"update"}', ]; public function testConstruct()