Skip to content

Commit fdd38c5

Browse files
authored
Merge pull request #8 from noplanman/add_request_validation
Add request validation, to allow only Telegram bots to use webhook.
2 parents ff31519 + 3f4cb91 commit fdd38c5

File tree

5 files changed

+152
-48
lines changed

5 files changed

+152
-48
lines changed

README.md

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ Installation and usage is pretty straight forward:
1717

1818
### Require this package with [Composer](https://getcomposer.org/)
1919

20-
- For PHP <7 we need version 0.1.*
21-
- For PHP 7+ we need version 0.2.*
22-
2320
Either run this command in your command line:
2421

2522
```
@@ -155,34 +152,36 @@ Enable admins? Add custom command paths? Set up logging?
155152

156153
Here is a list of available extra parameters:
157154

158-
Parameter | Description
159-
--------- |------------
160-
webhook | URL to the manager PHP file used for setting up the Webhook.
161-
| *e.g.* `'https://example.com/manager.php'`
162-
certificate | Path to a self-signed certificate (if necessary).
163-
| *e.g.* `__DIR__ . '/server.crt'`
164-
max_connections | Maximum allowed simultaneous HTTPS connections to the webhook
165-
| *e.g.* `20`
166-
allowed_updates | List the types of updates you want your bot to receive
167-
| *e.g.* `['message', 'edited_channel_post', 'callback_query']`
168-
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`).
169-
| *e.g.* `['error' => __DIR__ . '/php-telegram-bot-error.log']`
170-
admins | An array of user ids that have admin access to your bot.
171-
| *e.g.* `[12345]`
172-
mysql | Mysql credentials to connect a database (necessary for [`getUpdates`](#using-getupdates-method) method!).
173-
| *e.g.* `['host' => '127.0.0.1', 'user' => 'root', 'password' => 'root', 'database' => 'telegram_bot']`
174-
download_path | Custom download path.
175-
| *e.g.* `__DIR__ . '/Download'`
176-
upload_path | Custom upload path.
177-
| *e.g.* `__DIR__ . '/Upload'`
178-
commands_paths | A list of custom commands paths.
179-
| *e.g.* `[__DIR__ . '/CustomCommands']`
180-
command_configs | A list of all custom command configs.
181-
| *e.g.* `['sendtochannel' => ['your_channel' => '@my_channel']`
182-
botan_token | The Botan.io token to be used for analytics.
183-
| *e.g.* `'botan_12345'`
184-
custom_input | Override the custom input of your bot (mostly for testing purposes!).
185-
| *e.g.* `'{"some":"raw", "json":"update"}'`
155+
Parameter | Description
156+
--------- |------------
157+
validate_request | Only allow webhook access from valid Telegram API IPs.
158+
*bool* | *default is `true`*
159+
webhook | URL to the manager PHP file used for setting up the Webhook.
160+
*string* | *e.g.* `'https://example.com/manager.php'`
161+
certificate | Path to a self-signed certificate (if necessary).
162+
*string* | *e.g.* `__DIR__ . '/server.crt'`
163+
max_connections | Maximum allowed simultaneous HTTPS connections to the webhook
164+
*int* | *e.g.* `20`
165+
allowed_updates | List the types of updates you want your bot to receive
166+
*array* | *e.g.* `['message', 'edited_channel_post', 'callback_query']`
167+
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`).
168+
*array* | *e.g.* `['error' => __DIR__ . '/php-telegram-bot-error.log']`
169+
admins | An array of user ids that have admin access to your bot.
170+
*array* | *e.g.* `[12345]`
171+
mysql | Mysql credentials to connect a database (necessary for [`getUpdates`](#using-getupdates-method) method!).
172+
*array* | *e.g.* `['host' => '127.0.0.1', 'user' => 'root', 'password' => 'root', 'database' => 'telegram_bot']`
173+
download_path | Custom download path.
174+
*string* | *e.g.* `__DIR__ . '/Download'`
175+
upload_path | Custom upload path.
176+
*string* | *e.g.* `__DIR__ . '/Upload'`
177+
commands_paths | A list of custom commands paths.
178+
*array* | *e.g.* `[__DIR__ . '/CustomCommands']`
179+
command_configs | A list of all custom command configs.
180+
*array* | *e.g.* `['sendtochannel' => ['your_channel' => '@my_channel']`
181+
botan_token | The Botan.io token to be used for analytics.
182+
*string* | *e.g.* `'botan_12345'`
183+
custom_input | Override the custom input of your bot (mostly for testing purposes!).
184+
*string* | *e.g.* `'{"some":"raw", "json":"update"}'`
186185

187186
### Using getUpdates method
188187

src/BotManager.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@
2323
*/
2424
class BotManager
2525
{
26+
/**
27+
* @var string Telegram post servers lower IP limit
28+
*/
29+
const TELEGRAM_IP_LOWER = '149.154.167.197';
30+
31+
/**
32+
* @var string Telegram post servers upper IP limit
33+
*/
34+
const TELEGRAM_IP_UPPER = '149.154.167.233';
35+
2636
/**
2737
* @var string The output for testing, instead of echoing
2838
*/
@@ -258,6 +268,7 @@ public function setBotExtras(): self
258268
*
259269
* @return \NPM\TelegramBotManager\BotManager
260270
* @throws \Longman\TelegramBot\Exception\TelegramException
271+
* @throws \Exception
261272
*/
262273
public function handleRequest(): self
263274
{
@@ -387,9 +398,14 @@ public function handleGetUpdates(): self
387398
*
388399
* @return \NPM\TelegramBotManager\BotManager
389400
* @throws \Longman\TelegramBot\Exception\TelegramException
401+
* @throws \Exception
390402
*/
391403
public function handleWebhook(): self
392404
{
405+
if (!$this->isValidRequest()) {
406+
throw new \Exception('Invalid access');
407+
}
408+
393409
$this->telegram->handle();
394410

395411
return $this;
@@ -407,4 +423,33 @@ public function getOutput(): string
407423

408424
return $output;
409425
}
426+
427+
/**
428+
* Check if this is a valid request coming from a Telegram API IP address.
429+
*
430+
* @link https://core.telegram.org/bots/webhooks#the-short-version
431+
*
432+
* @return bool
433+
*/
434+
public function isValidRequest(): bool
435+
{
436+
if (false === $this->params->getBotParam('validate_request')) {
437+
return true;
438+
}
439+
440+
$ip = @$_SERVER['REMOTE_ADDR'];
441+
foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
442+
$addr = @$_SERVER[$key];
443+
if (filter_var($addr, FILTER_VALIDATE_IP)) {
444+
$ip = $addr;
445+
break;
446+
}
447+
}
448+
449+
$lower_dec = (float)sprintf('%u', ip2long(self::TELEGRAM_IP_LOWER));
450+
$upper_dec = (float)sprintf('%u', ip2long(self::TELEGRAM_IP_UPPER));
451+
$ip_dec = (float)sprintf('%u', ip2long($ip));
452+
453+
return $ip_dec >= $lower_dec && $ip_dec <= $upper_dec;
454+
}
410455
}

src/Params.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class Params
3535
* @var array List of valid extra parameters that can be passed.
3636
*/
3737
private static $valid_extra_bot_params = [
38+
'validate_request',
3839
'webhook',
3940
'certificate',
4041
'max_connections',
@@ -56,16 +57,19 @@ class Params
5657
private $script_params = [];
5758

5859
/**
59-
* @var array List of all params passed at construction.
60+
* @var array List of all params passed at construction, predefined with defaults.
6061
*/
61-
private $bot_params = [];
62+
private $bot_params = [
63+
'validate_request' => true,
64+
];
6265

6366
/**
6467
* Params constructor.
6568
*
6669
* api_key (string) Telegram Bot API key
6770
* botname (string) Telegram Bot name
6871
* secret (string) Secret string to validate calls
72+
* validate_request (bool) Only allow webhook access from valid Telegram API IPs
6973
* webhook (string) URI of the webhook
7074
* certificate (string) Path to the self-signed certificate
7175
* max_connections (int) Maximum allowed simultaneous HTTPS connections to the webhook
@@ -102,7 +106,7 @@ private function validateAndSetBotParams($params): self
102106
{
103107
// Set all vital params.
104108
foreach (self::$valid_vital_bot_params as $vital_key) {
105-
if (empty($params[$vital_key])) {
109+
if (!array_key_exists($vital_key, $params)) {
106110
throw new \InvalidArgumentException('Some vital info is missing: ' . $vital_key);
107111
}
108112

@@ -111,7 +115,7 @@ private function validateAndSetBotParams($params): self
111115

112116
// Set all extra params.
113117
foreach (self::$valid_extra_bot_params as $extra_key) {
114-
if (empty($params[$extra_key])) {
118+
if (!array_key_exists($extra_key, $params)) {
115119
continue;
116120
}
117121

@@ -147,7 +151,8 @@ private function validateAndSetScriptParams(): self
147151

148152
// Keep only valid ones.
149153
$this->script_params = array_intersect_key($this->script_params,
150-
array_fill_keys(self::$valid_script_params, null));
154+
array_fill_keys(self::$valid_script_params, null)
155+
);
151156

152157
return $this;
153158
}

tests/TelegramBotManager/Tests/BotManagerTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,60 @@ public function testGetOutput()
370370
self::assertEmpty($botManager->getOutput());
371371
}
372372

373+
public function testIsValidRequestValidateByDefault()
374+
{
375+
$botManager = new BotManager(ParamsTest::$demo_vital_params);
376+
self::assertInternalType('bool', $botManager->getParams()->getBotParam('validate_request'));
377+
self::assertTrue($botManager->getParams()->getBotParam('validate_request'));
378+
}
379+
380+
public function testIsValidRequestFailValidation()
381+
{
382+
$botManager = new BotManager(ParamsTest::$demo_vital_params);
383+
384+
unset($_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_CLIENT_IP'], $_SERVER['REMOTE_ADDR']);
385+
386+
foreach(['HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'] as $key) {
387+
$_SERVER[$key] = '1.1.1.1';
388+
self::assertFalse($botManager->isValidRequest());
389+
unset($_SERVER[$key]);
390+
}
391+
}
392+
393+
public function testIsValidRequestSkipValidation()
394+
{
395+
$botManager = new BotManager(array_merge(ParamsTest::$demo_vital_params, [
396+
'validate_request' => false,
397+
]));
398+
399+
unset($_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_CLIENT_IP'], $_SERVER['REMOTE_ADDR']);
400+
401+
self::assertTrue($botManager->isValidRequest());
402+
}
403+
404+
public function testIsValidRequestValidate()
405+
{
406+
$botManager = new BotManager(ParamsTest::$demo_vital_params);
407+
408+
unset($_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_CLIENT_IP'], $_SERVER['REMOTE_ADDR']);
409+
410+
// Lower range.
411+
$_SERVER['REMOTE_ADDR'] = '149.154.167.196';
412+
self::assertFalse($botManager->isValidRequest());
413+
$_SERVER['REMOTE_ADDR'] = '149.154.167.197';
414+
self::assertTrue($botManager->isValidRequest());
415+
$_SERVER['REMOTE_ADDR'] = '149.154.167.198';
416+
self::assertTrue($botManager->isValidRequest());
417+
418+
// Upper range.
419+
$_SERVER['REMOTE_ADDR'] = '149.154.167.232';
420+
self::assertTrue($botManager->isValidRequest());
421+
$_SERVER['REMOTE_ADDR'] = '149.154.167.233';
422+
self::assertTrue($botManager->isValidRequest());
423+
$_SERVER['REMOTE_ADDR'] = '149.154.167.234';
424+
self::assertFalse($botManager->isValidRequest());
425+
}
426+
373427
/**
374428
* @group live
375429
*/

tests/TelegramBotManager/Tests/ParamsTest.php

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,27 @@ class ParamsTest extends \PHPUnit_Framework_TestCase
2727
* @var array Demo extra parameters.
2828
*/
2929
public static $demo_extra_params = [
30-
'webhook' => 'https://php.telegram.bot/manager.php',
31-
'certificate' => __DIR__ . '/server.crt',
32-
'max_connections' => 20,
33-
'allowed_updates' => ['message', 'edited_channel_post', 'callback_query'],
34-
'admins' => [1],
35-
'mysql' => [
30+
'validate_request' => true,
31+
'webhook' => 'https://php.telegram.bot/manager.php',
32+
'certificate' => __DIR__ . '/server.crt',
33+
'max_connections' => 20,
34+
'allowed_updates' => ['message', 'edited_channel_post', 'callback_query'],
35+
'admins' => [1],
36+
'mysql' => [
3637
'host' => '127.0.0.1',
3738
'user' => 'root',
3839
'password' => 'root',
3940
'database' => 'telegram_bot',
4041
],
41-
'download_path' => __DIR__ . '/Download',
42-
'upload_path' => __DIR__ . '/Upload',
43-
'commands_paths' => __DIR__ . '/CustomCommands',
44-
'command_configs' => [
42+
'download_path' => __DIR__ . '/Download',
43+
'upload_path' => __DIR__ . '/Upload',
44+
'commands_paths' => __DIR__ . '/CustomCommands',
45+
'command_configs' => [
4546
'weather' => ['owm_api_key' => 'owm_api_key_12345'],
4647
'sendtochannel' => ['your_channel' => '@my_channel'],
4748
],
48-
'botan_token' => 'botan_12345',
49-
'custom_input' => '{"some":"raw", "json":"update"}',
49+
'botan_token' => 'botan_12345',
50+
'custom_input' => '{"some":"raw", "json":"update"}',
5051
];
5152

5253
public function testConstruct()

0 commit comments

Comments
 (0)