Authorization middleware

This commit is contained in:
Alexander Pankratov 2020-01-14 00:01:48 +03:00
parent 1844a1665f
commit 1b72ce8cb9
9 changed files with 182 additions and 76 deletions

View File

@ -11,8 +11,8 @@ Fast, simple, async php telegram api server:
* Fast async server
* Full access to telegram api: bot and user
**Example Architecture**
![Proposed Architecture](https://hsto.org/webt/j-/ob/ky/j-obkye1dv68ngsrgi12qevutra.png)
**Architecture Example**
![Architecture Example](https://hsto.org/webt/j-/ob/ky/j-obkye1dv68ngsrgi12qevutra.png)
**Installation**
@ -92,15 +92,7 @@ Fast, simple, async php telegram api server:
* sendMessage: `http://127.0.0.1:9503/api/sendMessage/?data[peer]=@xtrime&data[message]=Hello!`
* copy message from one channel to other (not repost): `http://127.0.0.1:9503/api/copyMessages/?data[from_peer]=@xtrime&data[to_peer]=@xtrime&data[id][0]=1`
**INPORTANT SECURITY NOTICE!**
Do not use `SERVER_ADDRESS=0.0.0.0` in version 1.5.0+, because websocket EventHandler endpoint currently not use `IP_WHITELIST` option.
This means, anyone from internet can listen your updates via websocket in this mode.
Use only default setting: `SERVER_ADDRESS=127.0.0.1`, or protect your app with external firewall.
This security issue will be fixed in one of next releases in January 2020.
**Contacts**
* Telegram: [@xtrime](tg://resolve?domain=xtrime)

View File

@ -23,17 +23,22 @@ $options = [
Amp\Loop::run(function () use($options) {
echo "Connecting to: {$options['url']}" . PHP_EOL;
/** @var Connection $connection */
$connection = yield connect($options['url']);
try {
/** @var Connection $connection */
$connection = yield connect($options['url']);
$connection->onClose(static function() use($connection) {
printf("Connection closed. Reason: %s\n", $connection->getCloseReason());
});
$connection->onClose(static function() use($connection) {
printf("Connection closed. Reason: %s\n", $connection->getCloseReason());
});
echo 'Waiting for events...' . PHP_EOL;
while ($message = yield $connection->receive()) {
/** @var Message $message */
$payload = yield $message->buffer();
printf("Received event: %s\n", $payload);
echo 'Waiting for events...' . PHP_EOL;
while ($message = yield $connection->receive()) {
/** @var Message $message */
$payload = yield $message->buffer();
printf("Received event: %s\n", $payload);
}
} catch (\Throwable $e) {
printf("Error: %s\n", $e->getMessage());
}
});

View File

@ -5,7 +5,7 @@ chdir(__DIR__);
require_once __DIR__ . '/bootstrap.php';
if (PHP_SAPI !== 'cli') {
throw new \Exception('Start in CLI');
throw new \RuntimeException('Start in CLI');
}
$shortopts = 'a::p::s::';
@ -58,4 +58,4 @@ foreach ($options['session'] as $session) {
}
$client = new TelegramApiServer\Client($sessionFiles);
new TelegramApiServer\Server($client, $options);
new TelegramApiServer\Server\Server($client, $options);

View File

@ -9,18 +9,18 @@ use Amp\Http\Server\Response;
use Amp\Http\Server\Router;
use Amp\Promise;
use TelegramApiServer\Client;
use TelegramApiServer\Config;
use TelegramApiServer\ClientCustomMethods;
class ApiController
{
public const JSON_HEADER = ['Content-Type'=>'application/json;charset=utf-8'];
private Client $client;
private array $ipWhiteList;
public array $page = [
'headers' => [
'Content-Type'=>'application/json;charset=utf-8',
self::JSON_HEADER,
],
'success' => 0,
'success' => false,
'errors' => [],
'code' => 200,
'response' => null,
@ -52,7 +52,6 @@ class ApiController
*/
public function __construct(Client $client)
{
$this->ipWhiteList = (array) Config::getInstance()->get('api.ip_whitelist', []);
$this->client = $client;
}
@ -137,9 +136,6 @@ class ApiController
}
try {
if (!in_array($request->getClient()->getRemoteAddress()->getHost(), $this->ipWhiteList, true)) {
throw new \Exception('Requests from your IP is forbidden');
}
$this->page['response'] = $this->callApi();
if ($this->page['response'] instanceof Promise) {
@ -191,16 +187,21 @@ class ApiController
*/
private function setError(\Throwable $e): self
{
$this->setPageCode(400);
$errorCode = $e->getCode();
if ($errorCode >= 400 && $errorCode < 500) {
$this->setPageCode($errorCode);
} else {
$this->setPageCode(400);
}
$this->page['errors'][] = [
'code' => $e->getCode(),
'code' => $errorCode,
'message' => $e->getMessage(),
];
return $this;
}
/**
* Кодирует ответ в нужный формат: json
*
@ -223,7 +224,7 @@ class ApiController
'response' => $this->page['response'],
];
if (!$data['errors']) {
$data['success'] = 1;
$data['success'] = true;
}
$result = json_encode(

View File

@ -32,7 +32,6 @@ class EventsController extends Websocket
}
} catch (\Throwable $e){
$response->setStatus(400);
$response->setBody($e->getMessage());
}
return new Success($response);
@ -65,7 +64,18 @@ class EventsController extends Websocket
return;
}
$update = [$session => $update];
$this->multicast(json_encode($update, JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE), [$clientId]);
$this->multicast(
json_encode(
$update,
JSON_THROW_ON_ERROR |
JSON_INVALID_UTF8_SUBSTITUTE |
JSON_PRETTY_PRINT |
JSON_UNESCAPED_SLASHES |
JSON_UNESCAPED_UNICODE
),
[$clientId]
);
});
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace TelegramApiServer\Server;
use Amp\Http\Server\Middleware;
use Amp\Http\Server\Request;
use Amp\Http\Server\RequestHandler;
use Amp\Http\Status;
use Amp\Promise;
use TelegramApiServer\Config;
use function Amp\call;
class Authorization implements Middleware
{
private array $ipWhitelist;
public function __construct()
{
$this->ipWhitelist = (array) Config::getInstance()->get('api.ip_whitelist', []);
}
public function handleRequest(Request $request, RequestHandler $next): Promise {
return call(function () use ($request, $next) {
$host = $request->getClient()->getRemoteAddress()->getHost();
if ($this->isIpAllowed($host)) {
$response = yield $next->handleRequest($request);
} else {
$response = ErrorResponses::get(Status::FORBIDDEN, 'Your host is not allowed');
}
return $response;
});
}
private function isIpAllowed(string $host): bool
{
if (!in_array($host, $this->ipWhitelist, true)) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace TelegramApiServer\Server;
use Amp\Http\Server\Response;
use TelegramApiServer\Controllers\ApiController;
class ErrorResponses
{
/**
* @param int $status
* @param string|array $message
*
* @return Response
*/
public static function get(int $status, $message): Response
{
return new Response(
$status,
ApiController::JSON_HEADER,
json_encode(
[
'success' => false,
'errors' => [
[
'code' => $status,
'message' => $message,
]
]
],
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
) . "\n"
);
}
}

52
src/Server/Router.php Normal file
View File

@ -0,0 +1,52 @@
<?php
namespace TelegramApiServer\Server;
use Amp\Http\Server\Request;
use Amp\Http\Server\RequestHandler\CallableRequestHandler;
use TelegramApiServer\Client;
use TelegramApiServer\Controllers\ApiController;
use TelegramApiServer\Controllers\EventsController;
use Amp\Http\Status;
use function Amp\Http\Server\Middleware\stack;
class Router
{
private \Amp\Http\Server\Router $router;
public function __construct(Client $client)
{
$this->router = new \Amp\Http\Server\Router();
$this->setRoutes($client);
$this->setFallback();
}
public function getRouter(): \Amp\Http\Server\Router
{
return $this->router;
}
private function setFallback(): void
{
$this->router->setFallback(new CallableRequestHandler(static function (Request $request) {
return ErrorResponses::get(Status::NOT_FOUND, 'Path not found');
}));
}
private function setRoutes($client): void
{
$authorization = new Authorization();
$apiHandler = stack(ApiController::getRouterCallback($client), $authorization);
$eventsHandler = stack(EventsController::getRouterCallback($client), $authorization);
foreach (['GET', 'POST'] as $method) {
$this->router->addRoute($method, '/api/{session}/{method}[/]', $apiHandler);
$this->router->addRoute($method, '/api/{method}[/]', $apiHandler);
}
$this->router->addRoute('GET', '/events/{session}[/]', $eventsHandler);
$this->router->addRoute('GET', '/events[/]', $eventsHandler);
}
}

View File

@ -1,14 +1,11 @@
<?php
namespace TelegramApiServer;
namespace TelegramApiServer\Server;
use Amp;
use Amp\Http\Server\RequestHandler\CallableRequestHandler;
use Amp\Http\Server\Request;
use Amp\Http\Server\Response;
use Psr\Log\LogLevel;
use TelegramApiServer\Controllers\ApiController;
use TelegramApiServer\Controllers\EventsController;
use TelegramApiServer\Client;
use TelegramApiServer\Config;
use TelegramApiServer\Logger;
class Server
{
@ -22,7 +19,7 @@ class Server
Amp\Loop::run(function () use ($client, $options) {
$server = new Amp\Http\Server\Server(
$this->getServerAddresses(static::getConfig($options)),
static::getRouter($client),
(new Router($client))->getRouter(),
Logger::getInstance(),
(new Amp\Http\Server\Options())
->withCompression()
@ -42,42 +39,12 @@ class Server
];
}
private static function getRouter(Client $client): Amp\Http\Server\Router
{
$router = new Amp\Http\Server\Router();
foreach (['GET', 'POST'] as $method) {
$router->addRoute($method, '/api/{session}/{method}[/]', ApiController::getRouterCallback($client));
$router->addRoute($method, '/api/{method}[/]', ApiController::getRouterCallback($client));
$router->addRoute($method, '/events/{session}[/]', EventsController::getRouterCallback($client));
$router->addRoute($method, '/events[/]', EventsController::getRouterCallback($client));
}
$router->setFallback(new CallableRequestHandler(static function (Request $request) {
return new Response(
Amp\Http\Status::NOT_FOUND,
[ 'Content-Type'=>'application/json;charset=utf-8'],
json_encode(
[
'success' => 0,
'errors' => [
[
'code' => 404,
'message' => 'Path not found',
]
]
],
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
) . "\n"
);
}));
return $router;
}
/**
* Stop the server gracefully when SIGINT is received.
* This is technically optional, but it is best to call Server::stop().
*
* @param Amp\Http\Server\Server $server
*
* @throws Amp\Loop\UnsupportedFeatureException
*/
private static function registerShutdown(Amp\Http\Server\Server $server)