Skip to content

Commit

Permalink
Merge pull request #8 from noplanman/add_request_validation
Browse files Browse the repository at this point in the history
Add request validation, to allow only Telegram bots to use webhook.
  • Loading branch information
noplanman authored Dec 25, 2016
2 parents ff31519 + 3f4cb91 commit fdd38c5
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 fdd38c5

Please sign in to comment.