Skip to content

Commit

Permalink
Add request validation, to allow only Telegram bots to use webhook.
Browse files Browse the repository at this point in the history
This feature is enabled by default.
  • Loading branch information
noplanman committed Dec 25, 2016
1 parent ff31519 commit 3f4cb91
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 48 deletions.
61 changes: 30 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

```
Expand Down Expand Up @@ -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

Expand Down
45 changes: 45 additions & 0 deletions src/BotManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -258,6 +268,7 @@ public function setBotExtras(): self
*
* @return \NPM\TelegramBotManager\BotManager
* @throws \Longman\TelegramBot\Exception\TelegramException
* @throws \Exception
*/
public function handleRequest(): self
{
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}
15 changes: 10 additions & 5 deletions src/Params.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -56,16 +57,19 @@ 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.
*
* 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
Expand Down Expand Up @@ -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);
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand Down
54 changes: 54 additions & 0 deletions tests/TelegramBotManager/Tests/BotManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
25 changes: 13 additions & 12 deletions tests/TelegramBotManager/Tests/ParamsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 3f4cb91

Please sign in to comment.