From 8e93934e75cef407f4a85666eab53f1608f95015 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Mon, 15 Apr 2024 17:09:24 +0200 Subject: [PATCH] Add basic auth support --- .env.docker.example | 5 ++ .env.example | 4 ++ README.md | 108 +++++++++++++++++------------------ composer.lock | 12 ++-- config.php | 1 + src/Server/Authorization.php | 34 ++++++++--- 6 files changed, 94 insertions(+), 70 deletions(-) diff --git a/.env.docker.example b/.env.docker.example index 3662f9f..ae21059 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -2,6 +2,7 @@ # Check for outdated .env files VERSION=1 +# See "ports" in docker-compose.yml. SERVER_ADDRESS=0.0.0.0 SERVER_PORT=9503 @@ -21,6 +22,10 @@ REQUESTS_BULK_INTERVAL=0.5 # 2) recreate container `docker-compose up -d` IP_WHITELIST=127.0.0.1 +# Allow requests from any IP with given user and password +# Example: {"myusername": "mySuperStrongPassword", "otherName": "otherPassword"} +PASSWORDS={} + # TELEGRAM CLIENT TELEGRAM_API_ID= TELEGRAM_API_HASH= diff --git a/.env.example b/.env.example index cab1087..907a67d 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,10 @@ REQUESTS_BULK_INTERVAL=0.5 # Leave blanc, to allow requests from all IP (THIS WILL MAKE API UNSECURE!) IP_WHITELIST=127.0.0.1 +# Allow requests from any IP with given user and password +# Example: {"myusername": "mySuperStrongPassword", "otherName": "otherPassword"} +PASSWORDS={} + # TELEGRAM CLIENT TELEGRAM_API_ID= TELEGRAM_API_HASH= diff --git a/README.md b/README.md index f6e4b6a..ab51377 100644 --- a/README.md +++ b/README.md @@ -58,64 +58,58 @@ docker compose pull docker compose up -d ``` -## Usage -1. Run server/parser - ``` - usage: php server.php [--help] [-a=|--address=127.0.0.1] [-p=|--port=9503] [-s=|--session=] [-e=|--env=.env] [--docker] - - Options: - --help Show this message - - -a --address Server ip (optional) (default: 127.0.0.1) - To listen external connections use 0.0.0.0 and fill IP_WHITELIST in .env - - -p --port Server port (optional) (default: 9503) - - -s --session Name for session file (optional) - Multiple sessions can be specified: "--session=user --session=bot" - - Each session is stored in `sessions/{$session}.madeline`. - Nested folders supported. - See README for more examples. - - -e --env .env file name. (default: .env). - Helpful when need multiple instances with different settings - - --docker Apply some settings for docker: add docker network to whitelist. - - Also some options can be set in .env file (see .env.example) - ``` -1. Access Telegram API with simple GET/POST requests. - Regular and application/json POST supported. - It's recommended to use http_build_query, when using GET requests. - - **Rules:** - * All methods from MadelineProto supported: [Methods List](https://docs.madelineproto.xyz/API_docs/methods/) - * Url: `http://%address%:%port%/api[/%session%]/%class%.%method%/?%param%=%val%` - * Important: api available only from ip in whitelist. - By default it is: `127.0.0.1` - You can add a client IP in .env file to `IP_WHITELIST` (separate with a comma) - - In docker version by default api available only from localhost (127.0.0.1). - To allow connections from the internet, need to change ports in docker-compose.yml to `9503:9503` and recreate the container: `docker compose up -d`. - This is very insecure, because this will open TAS port to anyone from the internet. - Only protection is the `IP_WHITELIST`, and there are no warranties that it will secure your accounts. - * If method is inside class (messages, contacts and etc.) use '.' to separate class from method: - `http://127.0.0.1:9503/api/contacts.getContacts` - * If method requires array of values, use any name of array, for example 'data': - `?data[peer]=@xtrime&data[message]=Hello!`. Order of parameters does't matter in this case. - * If method requires one or multiple separate parameters (not inside array) then pass parameters with any names but **in strict order**: - `http://127.0.0.1:9503/api/getInfo/?id=@xtrime` or `http://127.0.0.1:9503/api/getInfo/?abcd=@xtrime` works the same +## Security +Please be careful with settings, otherwise you can expose your telegram session and lose control. +Default settings allow to access API only from localhost/127.0.0.1. - **Examples:** - * get_info about channel/user: `http://127.0.0.1:9503/api/getInfo/?id=@xtrime` - * get_info about currect account: `http://127.0.0.1:9503/api/getSelf` - * repost: `http://127.0.0.1:9503/api/messages.forwardMessages/?data[from_peer]=@xtrime&data[to_peer]=@xtrime&data[id]=1234` - * get messages from channel/user: `http://127.0.0.1:9503/api/getHistory/?data[peer]=@breakingmash&data[limit]=10` - * get messages with text in HTML: `http://127.0.0.1:9503/api/getHistoryHtml/?data[peer]=@breakingmash&data[limit]=10` - * search: `http://127.0.0.1:9503/api/searchGlobal/?data[q]=Hello%20World&data[limit]=10` - * sendMessage: `http://127.0.0.1:9503/api/sendMessage/?data[peer]=@xtrime&data[message]=Hello!` - * copy message from one channel to another (not repost): `http://127.0.0.1:9503/api/copyMessages/?data[from_peer]=@xtrime&data[to_peer]=@xtrime&data[id][0]=1` +.env settings: +- `IP_WHITELIST` - allow specific IP's to make requests without password. +- `PASSWORDS` - protect your api with basic auth. + Request with correct username and password overrides IP_WHITELIST. + If you specify password, then `IP_WHITELIST` is ignored + How to make requests with basic auth: + ```shell + curl --user 'username:password' "http://127.0.0.1:9503/getSelf" + curl "http://username:password@127.0.0.1:9503/getSelf" + ``` + +docker-compose.yml: +- `port` - port forwarding rules from host to docker container. + Remove 127.0.0.1 to listen all interfaces and forward all requests to container. + Make sure to use IP_WHITELIST and/or PASSWORDS settings to protect your account. + +## Usage +Access Telegram API with simple GET/POST requests. +Regular and application/json POST supported. +It's recommended to use http_build_query, when using GET requests. + +**Rules:** +* All methods from MadelineProto supported: [Methods List](https://docs.madelineproto.xyz/API_docs/methods/) +* Url: `http://%address%:%port%/api[/%session%]/%class%.%method%/?%param%=%val%` +* Important: api available only from ip in whitelist. + By default it is: `127.0.0.1` + You can add a client IP in .env file to `IP_WHITELIST` (separate with a comma) + + In docker version by default api available only from localhost (127.0.0.1). + To allow connections from the internet, need to change ports in docker-compose.yml to `9503:9503` and recreate the container: `docker compose up -d`. + This is very insecure, because this will open TAS port to anyone from the internet. + Only protection is the `IP_WHITELIST`, and there are no warranties that it will secure your accounts. +* If method is inside class (messages, contacts and etc.) use '.' to separate class from method: + `http://127.0.0.1:9503/api/contacts.getContacts` +* If method requires array of values, use any name of array, for example 'data': + `?data[peer]=@xtrime&data[message]=Hello!`. Order of parameters does't matter in this case. +* If method requires one or multiple separate parameters (not inside array) then pass parameters with any names but **in strict order**: + `http://127.0.0.1:9503/api/getInfo/?id=@xtrime` or `http://127.0.0.1:9503/api/getInfo/?abcd=@xtrime` works the same + +**Examples:** +* get_info about channel/user: `http://127.0.0.1:9503/api/getInfo/?id=@xtrime` +* get_info about currect account: `http://127.0.0.1:9503/api/getSelf` +* repost: `http://127.0.0.1:9503/api/messages.forwardMessages/?data[from_peer]=@xtrime&data[to_peer]=@xtrime&data[id]=1234` +* get messages from channel/user: `http://127.0.0.1:9503/api/getHistory/?data[peer]=@breakingmash&data[limit]=10` +* get messages with text in HTML: `http://127.0.0.1:9503/api/getHistoryHtml/?data[peer]=@breakingmash&data[limit]=10` +* search: `http://127.0.0.1:9503/api/searchGlobal/?data[q]=Hello%20World&data[limit]=10` +* sendMessage: `http://127.0.0.1:9503/api/sendMessage/?data[peer]=@xtrime&data[message]=Hello!` +* copy message from one channel to another (not repost): `http://127.0.0.1:9503/api/copyMessages/?data[from_peer]=@xtrime&data[to_peer]=@xtrime&data[id][0]=1` ## Advanced features ### Get events/updates diff --git a/composer.lock b/composer.lock index 6c7e2b1..a35deac 100644 --- a/composer.lock +++ b/composer.lock @@ -476,16 +476,16 @@ }, { "name": "amphp/http", - "version": "v2.1.0", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/amphp/http.git", - "reference": "9f3500bef4bb15cf41987f21136539c0a06555a3" + "reference": "fe6b4dd50c1e70caf823092398074b5082e1d6da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/http/zipball/9f3500bef4bb15cf41987f21136539c0a06555a3", - "reference": "9f3500bef4bb15cf41987f21136539c0a06555a3", + "url": "https://api.github.com/repos/amphp/http/zipball/fe6b4dd50c1e70caf823092398074b5082e1d6da", + "reference": "fe6b4dd50c1e70caf823092398074b5082e1d6da", "shasum": "" }, "require": { @@ -528,7 +528,7 @@ "description": "Basic HTTP primitives which can be shared by servers and clients.", "support": { "issues": "https://github.com/amphp/http/issues", - "source": "https://github.com/amphp/http/tree/v2.1.0" + "source": "https://github.com/amphp/http/tree/v2.1.1" }, "funding": [ { @@ -536,7 +536,7 @@ "type": "github" } ], - "time": "2023-08-22T19:50:46+00:00" + "time": "2024-04-03T18:00:53+00:00" }, { "name": "amphp/http-client", diff --git a/config.php b/config.php index 5e3a45f..c3ea2b0 100644 --- a/config.php +++ b/config.php @@ -64,6 +64,7 @@ $settings = [ explode(',', (string)getenv('IP_WHITELIST')) ) ), + 'passwords' => (array)json_decode((string)getenv('PASSWORDS'), true), 'bulk_interval' => (float)getenv('REQUESTS_BULK_INTERVAL') ], 'health_check' => [ diff --git a/src/Server/Authorization.php b/src/Server/Authorization.php index bdd0be9..51203ff 100644 --- a/src/Server/Authorization.php +++ b/src/Server/Authorization.php @@ -13,23 +13,43 @@ class Authorization implements Middleware { private array $ipWhitelist; private int $selfIp; + /** + * @var array + */ + private array $passwords; public function __construct() { - $this->ipWhitelist = (array)Config::getInstance()->get('api.ip_whitelist', []); $this->selfIp = ip2long(getHostByName(php_uname('n'))); + $this->ipWhitelist = (array)Config::getInstance()->get('api.ip_whitelist', []); + $this->passwords = Config::getInstance()->get('api.passwords', []); + if (!$this->ipWhitelist && !$this->passwords) { + throw new \InvalidArgumentException('API is unprotected! Please specify IP_WHITELIST or PASSWORD in .env.docker'); + } } public function handleRequest(Request $request, RequestHandler $requestHandler): Response { - $host = explode(':', $request->getClient()->getRemoteAddress()->toString())[0]; - if ($this->isIpAllowed($host)) { - $response = $requestHandler->handleRequest($request); - } else { - $response = ErrorResponses::get(HttpStatus::FORBIDDEN, 'Your host is not allowed: ' . $host); + [$host] = explode(':', $request->getClient()->getRemoteAddress()->toString(), 2); + + if ($this->passwords) { + $header = (string)$request->getHeader('Authorization'); + if ($header) { + sscanf($header, "Basic %s", $encodedPassword); + [$username, $password] = explode(':', base64_decode($encodedPassword), 2); + if (array_key_exists($username, $this->passwords) && $this->passwords[$username] === $password) { + return $requestHandler->handleRequest($request); + } + } + + return ErrorResponses::get(HttpStatus::UNAUTHORIZED, 'Username or password is incorrect'); } - return $response; + if ($this->isIpAllowed($host)) { + return $requestHandler->handleRequest($request); + } + + return ErrorResponses::get(HttpStatus::UNAUTHORIZED, 'Your host is not allowed: ' . $host); } private function isIpAllowed(string $host): bool