Запустить туннель бесплатно
☰
Гайды

Вебхук: что это и как принять вебхук на своём компьютере

Что такое вебхук, чем входящий отличается от исходящего и как принять вебхук на localhost: туннель, постоянный адрес и проверка подписи.

Вебхук — это HTTP-запрос, который сервис сам отправляет на ваш адрес, когда что-то случилось: пришла оплата, создали сделку, пользователь написал боту. Чтобы принять вебхук на своём компьютере, нужен публичный адрес, который ведёт на localhost. Его даёт туннель: fxtunnel http 8080 печатает адрес вида https://lemon.fxtun.ru, вы вписываете его в настройки сервиса, и запросы начинают приходить в ваш локальный обработчик.

Дальше по порядку: что такое вебхук и в какую сторону он идёт, почему до localhost он не доходит, как принять его через туннель, как не перенастраивать сервис после каждого перезапуска и как проверить, что запрос пришёл от того, от кого надо.

Что такое вебхук простыми словами

Обычный API работает по принципу «спросил — получил»: ваш код сам ходит в сервис и забирает данные. Вебхук переворачивает направление. Вы один раз сообщаете сервису адрес, а он сам присылает туда POST-запрос при каждом событии. Опрашивать API раз в минуту больше не нужно, событие приходит через секунды.

Путаница начинается со словами «входящий» и «исходящий». Они зависят от того, с чьей стороны смотреть, и разные сервисы называют их по-разному. В документации Битрикс24, например:

  • входящий вебхук — адрес, по которому ваш код вызывает методы REST API Битрикс24;
  • исходящий вебхук — Битрикс24 сам отправляет событие на адрес вашего обработчика.

Для туннеля важен один вопрос: кто кому отправляет запрос.

Вы отправляете запрос сервису — туннель не нужен. Сюда относится вебхук канала Discord: это адрес, на который ваш скрипт шлёт сообщения, и они появляются в канале. В документации Discord их так и описывают: простой способ публиковать сообщения в каналы, без бота и авторизации. Ваш компьютер здесь клиент, запрос уходит наружу, и публичный адрес ему ни к чему. То же с входящим вебхуком Битрикс24.

Сервис отправляет запрос вам — нужен публичный адрес. Исходящие вебхуки Битрикс24, уведомления об оплате, setWebhook у Telegram-бота, Interactions Endpoint URL у приложения Discord. Discord прямо пишет, что это публичный адрес, на который он присылает взаимодействия. Вот этим запросам и нужен туннель.

Почему вебхук не доходит до localhost

localhost — это адрес «сам себе». Когда сервис в интернете пытается отправить запрос на http://localhost:8080/hook, он стучится в собственный сервер, а не в ваш ноутбук. Указать ему IP вашего компьютера тоже не выйдет: дома он спрятан за роутером, а у многих провайдеров ещё и за общим NAT с серым адресом. Запрос из интернета до вашей машины просто не находит дороги.

Вторая преграда — HTTPS. Многие сервисы шлют вебхуки только на https://. Например, Telegram требует HTTPS и принимает вебхуки только на портах 443, 80, 88 и 8443 (Bot API, setWebhook). Самоподписанный сертификат на ноутбуке — отдельная морока.

Туннель снимает обе проблемы. Клиент fxtunnel сам подключается к серверу fxTunnel исходящим соединением, поэтому роутер и NAT ему не мешают. Сервер принимает запросы на https://<поддомен>.fxtun.ru с настоящим сертификатом и передаёт их вашему локальному обработчику.

интернетсервисPOST /hook
сервер fxTunnelподдоменlemon.fxtun.ru
ваш компьютерклиент fxtunnelfxtunnel http 8080
локальнообработчикlocalhost:8080
Рис. 1. Сервис отправляет вебхук на публичный HTTPS-адрес, клиент fxtunnel доставляет его в локальный обработчик.

Принимаем вебхук за пять минут

Обработчик

Для проверки хватит обработчика на стандартной библиотеке Python. Он печатает каждый POST-запрос и отвечает 200:

PYTHON
# hook.py
from http.server import BaseHTTPRequestHandler, HTTPServer


class Hook(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
        print(self.command, self.path, body.decode(errors="replace"), flush=True)
        self.send_response(200)
        self.end_headers()


HTTPServer(("127.0.0.1", 8080), Hook).serve_forever()
BASH
python3 hook.py

Если у вас уже есть приложение на Express, Django или Laravel, запустите его: туннелю всё равно, что слушает порт.

Туннель

Установите клиент и войдите (один раз):

BASH
curl -fsSL https://fxtun.ru/install.sh | sh
fxtunnel login

На Windows вместо первой строки выполните в PowerShell irm https://fxtun.ru/install.ps1 | iex.

Откройте туннель к порту обработчика:

BASH
fxtunnel http 8080
CONSOLE
Connecting to fxtunnel server...
Tunnel established!
HTTP:  http://lemon.fxtun.ru
HTTPS: https://lemon.fxtun.ru
Forwarding to localhost:8080
Ready to receive connections

Проверьте с любого компьютера или прямо с этого:

BASH
curl -X POST https://lemon.fxtun.ru/hook -d '{"event":"test"}'

В окне hook.py появится строка POST /hook {"event":"test"}. Путь запроса сохраняется: сервис стучится в /hook, и обработчик видит именно /hook.

Адрес в настройках сервиса

Осталось вписать https://lemon.fxtun.ru/hook туда, куда сервис просит адрес обработчика. У GitHub это Payload URL в настройках вебхука репозитория, у Telegram — параметр url метода setWebhook, у Битрикс24 — адрес обработчика в исходящем вебхуке.

У поддоменов fxtun.ru есть предупредительная страница для тех, кто впервые открывает туннель в браузере. Вебхуки она не задевает: страница показывается только на GET-запросы, в ответ на которые ваш сервер отдал HTML. POST-запрос сервиса уходит прямо в обработчик (документация HTTP-туннелей).

Постоянный адрес: не перенастраивать сервис каждый раз

Здесь большинство и спотыкается. Без флага --domain каждый запуск fxtunnel http 8080 даёт новый случайный поддомен. Сегодня lemon, завтра river. Сервис же продолжает слать события на вчерашний адрес, и они уходят в пустоту, пока вы не впишете новый.

Первый шаг — выбрать имя самому:

BASH
fxtunnel http 8080 --domain myapp

Адрес станет https://myapp.fxtun.ru и не изменится между запусками, пока имя свободно. Этот флаг работает на любом тарифе. Слабое место одно: пока ваш туннель выключен, имя может занять другой пользователь.

Чтобы имя принадлежало только вам, его резервируют:

BASH
fxtunnel domains add myapp
CONSOLE
Reserved: myapp → https://myapp.fxtun.ru

После этого чужой туннель на myapp сервер не пустит. Резервировать поддомены можно с тарифа Base: на нём их пять. Для рабочего проекта можно подключить и свой домен вида hooks.example.com. Как устроены все три варианта и что выбрать, разобрано в статье о постоянном адресе туннеля.

Проверка подписи: чужие запросы тоже дойдут

Адрес туннеля публичный. Если кто-то его узнает, он может прислать туда что угодно, в том числе поддельное «оплата прошла». Поэтому серьёзные сервисы подписывают вебхуки, а обработчик проверяет подпись до того, как что-то делать.

Общий принцип почти везде одинаковый:

  1. В настройках вебхука вы задаёте секрет, или сервис выдаёт его сам.
  2. Сервис считает подпись от тела запроса и секрета и кладёт её в заголовок.
  3. Обработчик считает то же самое от сырого тела, до разбора JSON, и сравнивает. Не совпало — отвечает 401 и ничего не делает.

Детали у каждого сервиса свои:

  • GitHub кладёт в заголовок X-Hub-Signature-256 значение sha256= и HMAC-SHA256 тела. Сравнивать советует функцией постоянного времени, а не обычным == (документация GitHub).
  • Telegram подписи не считает. Он присылает в заголовке X-Telegram-Bot-Api-Secret-Token секрет, который вы передали в setWebhook параметром secret_token (Bot API).
  • Discord подписывает взаимодействия ключом Ed25519 (заголовки X-Signature-Ed25519 и X-Signature-Timestamp) и ждёт 401 на неверную подпись (документация Discord).

Проверка в стиле GitHub на той же стандартной библиотеке:

PYTHON
import hashlib
import hmac
import os

SECRET = os.environ["WEBHOOK_SECRET"].encode()


def signature_ok(body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)

В do_POST из примера выше вызовите signature_ok(body, self.headers.get("X-Hub-Signature-256", "")) и при False отвечайте 401.

Ещё два правила, которые экономят нервы. Отвечайте 2xx быстро, а тяжёлую работу делайте после ответа: сервисы ждут ответа ограниченное время. И будьте готовы к повторной доставке одного и того же события: что именно сервис делает при ошибке и таймауте, написано в его документации.

Отладка: посмотреть запрос и отправить его снова

Самое утомительное в работе с вебхуками — вызвать событие ещё раз. Чтобы проверить исправление в обработчике, приходится заново создавать сделку, проводить тестовую оплату или писать боту.

В клиенте fxtunnel для этого есть инспектор. Он открывается на http://127.0.0.1:4040, показывает каждый пришедший запрос с заголовками и телом, а кнопкой повтора отправляет сохранённый запрос в ваш локальный обработчик ещё раз. Сервис при этом не участвует: поправили код, нажали повтор, посмотрели ответ. Инспектор работает только на этом компьютере и доступен с тарифа Base. Подробнее — в разборе инспектора трафика.

На бесплатном тарифе инспектор выключен. Там выручает печать запросов в самом обработчике, как в hook.py.

Что дальше

  • Как тестировать вебхуки GitHub, Stripe и других сервисов — в статье «Тестирование вебхуков через туннель».
  • Боты Telegram и Discord на локальной машине — отдельный разбор.
  • Вебхуки конкретных сервисов: Битрикс24, уведомления ЮKassa, Callback API ВКонтакте, amoCRM, Telegram Mini App.
  • Чтобы не менять адрес в сервисе, — постоянный адрес туннеля.

Постоянный адрес

Закрепить поддомен за собой можно с тарифа Base, там же — свой домен вида app.example.com с сертификатом. На бесплатном тарифе адрес меняется при каждом запуске.

$ curl -fsSL https://fxtun.ru/install.sh | sh
@mephistofx

Пишу fxTunnel — туннели для разработчиков без белого IP. Рассказываю, как это устроено изнутри.

Написать в Telegram →

Читайте дальше