Ngrok и Telegram Webhook на localhost

На рабочем проекте мне поставили задачу по оптимизации работы эндпоинта вебхук от Telegram. Функционал уже был написан до меня и представлял из себя обычный Laravel API эндпоинт который парсит входящие данные и сохраняет в БД. Мне лишь требовалось проанализировать метрики нагрузки в Tideways и перевести парсинг на очереди, параллельно тестируя все локально.

Оcновные проблемы:

  1. Telegram не может отправлять запросы на localhost (бот работает только с публичными URL)
  2. Telegram требует HTTPS соединение (чтобы вебхук был защищенным)

Есть несколько вариантов решения. Выделю два более удобных на мой взгляд:

  1. Использовать Ngrok
  2. Настроить свой VPS и пробросить SSH порты на localhost

Ngrok в моем случае стал приоритетом, т.к. сервис сам создает публичный HTTPS-адрес который перенаправляет запросы на локальный сервер. Ниже опишу основные моменты которые стали полезны.

Установка и настройка Ngrok

Варианты установки можно найти на сайте Docs: Getting Started.
Мои примеры для Debian Linux.

Прежде всего нужно зарегистрироваться в сервисе ngrok.com.

Устанавливаем Ngrok.

curl -sSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
  | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null \
  && echo "deb https://ngrok-agent.s3.amazonaws.com buster main" \
  | sudo tee /etc/apt/sources.list.d/ngrok.list \
  && sudo apt update \
  && sudo apt install ngrok

Проверяем версию.

Важно знать, т.к. от версии зависит структура yml конфига.

$ ngrok -v
ngrok version 3.22.0

Добавляем в конфиг authtoken.

authtoken доступен в админке в разделе Getting Started: Your Authtoken.
 Конфиг ngrok по умолчанию создается тут /home/your_name/.config/ngrok/ngrok.yml, но можно разместить и в другом месте.

$ ngrok config add-authtoken $YOUR_AUTHTOKEN

Запускаем ngrok для проверки.

$ ngrok http http://localhost:80
ngrok                                                                                                                 (Ctrl+C to quit)
                                                                                                
Session Status                online                                                                                                  
Account                       your_name@gmail.com (Plan: Free)                                                 
Version                       3.22.0                                                                                                  
Region                        Europe (eu)                                                                                             
Latency                       73ms                                                                                                    
Web Interface                 http://127.0.0.1:4040                                                                                   
Forwarding                    https://e20b-188-114-192-46.ngrok-free.app -> http://localhost:80                      
                                                                                                                                      
Connections                   ttl     opn     rt1     rt5     p50     p90                                                             
                              0       0       0.00    0.00    0.00    0.00

На базовом уровне этого достаточно чтобы начать тестировать. Но в моем случае понадобилось решить еще несколько проблем:

Пример моего конфига:

version: "3"
agent:
  authtoken: your-authtoken
tunnels:
  tg_webhook:
    domain: tg-webhook-expert-test.ngrok-free.app
    addr: tg-webhook.local:80
    proto: http
    host_header: "tg-webhook.local"
    inspect: false
    traffic_policy: 
      inbound:
        - actions:
          - type: "add-headers"
            config:
              headers:
                cookie: "XDEBUG_SESSION=start"

Важно! Директивы конфига могут отличаться в зависимости от версии. В любом случае советую пользоваться разделом Docs.

domain нужен для настройки статического домена, которые предварительно создается в админке в разделе Universal Gateway: Domains, иначе ngrok при каждом запуске формирует динамический домен который придется постоянно обновлять в Telegram (пример будет ниже).

addr,proto,host_header,inspect нужно для перенаправления на статический домен в файле hosts.

traffic_policy и все что внутри, нужно для проброса заголовка с кукой для Xdebug.

Создание Telegram Bot

Telegram Bot который будет реагировать на сообщения в чате и пересылать их в наш сервис. Детально расписывать этот процесс не буду, т.к. в сети есть гайды на эту тему. Я начал с этих мест:

После создания бота мы получим token типа 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw, который будем использовать для настройки нашего webhook.

Установливаем webhook.

$ curl -X POST "https://api.telegram.org/bot110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw/setWebhook" \
     -H "Content-Type: application/json" \
     -d '{
        "url": "https://tg-webhook-expert-test.ngrok-free.app/v1/telegram/webhook/",
        "max_connections": 40,
        "allowed_updates": ["message", "callback_query"]
     }'
{
   "ok":true,
   "result":true,
   "description":"Webhook is already set"
}

И проверяем настройки.

$ curl "https://api.telegram.org/bot110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw/getWebhookInfo"
{
   "ok":true,
   "result":{
      "url":"https://tg-webhook-expert-test.ngrok-free.app/v1/telegram/webhook",
      "has_custom_certificate":false,
      "pending_update_count":0,
      "max_connections":40,
      "ip_address":"18.192.31.165",
      "allowed_updates":[
         "message",
         "callback_query"
      ]
   }
}

Тестирование на localhost

Запускаем ngrok.

ngrok start tg_webhook
ngrok                                                                                                                 (Ctrl+C to quit)
                                                                                                
Session Status                online                                                                                                  
Account                       your_name@gmail.com (Plan: Free)                                                                                                                    
Version                       3.22.0                                                                                                  
Region                        Europe (eu)                                                                                             
Latency                       73ms                                                                                                    
Web Interface                 http://127.0.0.1:4040                                                                                   
Forwarding                    https://tg-webhook-expert-test.ngrok-free.app -> http://tg-webhook.local:80                      
                                                                                                                                      
Connections                   ttl     opn     rt1     rt5     p50     p90                                                             
                              0       0       0.00    0.00    0.00    0.00

Проверяем webhook локально.

$ curl -X POST "http://tg-webhook.local/v1/telegram/webhook" -d '{"test":true}'
{"test":true}

Проверяем webhook через ngrok.

$ curl -X POST "https://tg-webhook-expert-test.ngrok-free.app/v1/telegram/webhook" -d '{"test":true}'
{"test":true}

Пишем сообщение в чате Telegram и Bot должен переслать его в наш сервис. Запрос будет примерно таким:

POST /v1/telegram/webhook HTTP/1.1
Content-Length: 409
X-Forwarded-Host: tg-webhook-expert-test.ngrok-free.app
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Type: application/json
Cookie: XDEBUG_SESSION=start
Host: tg-webhook.local
X-Forwarded-For: 91.108.5.110
X-Forwarded-Proto: https

{
  "update_id": 281145287,
  "message": {
    "message_id": 25,
    "from": {
      "id": 1624941878,
      "is_bot": false,
      "first_name": "Your",
      "last_name": "Username",
      "username": "your_username",
      "language_code": "en"
    },
    "chat": {
      "id": 1624941878,
      "first_name": "Your",
      "last_name": "Username",
      "username": "your_username",
      "type": "private"
    },
    "date": 1744737539,
    "text": "Test Message"
  }
}