Skip to content

Commit

Permalink
Accept Socket Objects for Webhooks (#4161)
Browse files Browse the repository at this point in the history
  • Loading branch information
Poolitzer committed Mar 24, 2024
1 parent 8a542e2 commit 2d8d43f
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 17 deletions.
19 changes: 16 additions & 3 deletions telegram/ext/_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
from telegram.warnings import PTBDeprecationWarning

if TYPE_CHECKING:
from socket import socket

from telegram import Message
from telegram.ext import ConversationHandler, JobQueue
from telegram.ext._applicationbuilder import InitApplicationBuilder
Expand Down Expand Up @@ -866,7 +868,7 @@ def run_webhook(
close_loop: bool = True,
stop_signals: ODVInput[Sequence[int]] = DEFAULT_NONE,
secret_token: Optional[str] = None,
unix: Optional[Union[str, Path]] = None,
unix: Optional[Union[str, Path, "socket"]] = None,
) -> None:
"""Convenience method that takes care of initializing and starting the app,
listening for updates from Telegram using :meth:`telegram.ext.Updater.start_webhook` and
Expand Down Expand Up @@ -959,8 +961,17 @@ def run_webhook(
header isn't set or it is set to a wrong token.
.. versionadded:: 20.0
unix (:class:`pathlib.Path` | :obj:`str`, optional): Path to the unix socket file. Path
does not need to exist, in which case the file will be created.
unix (:class:`pathlib.Path` | :obj:`str` | :class:`socket.socket`, optional): Can be
either:
* the path to the unix socket file as :class:`pathlib.Path` or :obj:`str`. This
will be passed to `tornado.netutil.bind_unix_socket <https://www.tornadoweb.org/
en/stable/netutil.html#tornado.netutil.bind_unix_socket>`_ to create the socket.
If the Path does not exist, the file will be created.
* or the socket itself. This option allows you to e.g. restrict the permissions of
the socket for improved security. Note that you need to pass the correct family,
type and socket options yourself.
Caution:
This parameter is a replacement for the default TCP bind. Therefore, it is
Expand All @@ -969,6 +980,8 @@ def run_webhook(
appropriate :paramref:`webhook_url`.
.. versionadded:: 20.8
.. versionchanged:: NEXT.VERSION
Added support to pass a socket instance itself.
"""
if not self.updater:
raise RuntimeError(
Expand Down
21 changes: 17 additions & 4 deletions telegram/ext/_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
WEBHOOKS_AVAILABLE = False

if TYPE_CHECKING:
from socket import socket

from telegram import Bot


Expand Down Expand Up @@ -472,7 +474,7 @@ async def start_webhook(
ip_address: Optional[str] = None,
max_connections: int = 40,
secret_token: Optional[str] = None,
unix: Optional[Union[str, Path]] = None,
unix: Optional[Union[str, Path, "socket"]] = None,
) -> "asyncio.Queue[object]":
"""
Starts a small http server to listen for updates via webhook. If :paramref:`cert`
Expand Down Expand Up @@ -541,8 +543,17 @@ async def start_webhook(
header isn't set or it is set to a wrong token.
.. versionadded:: 20.0
unix (:class:`pathlib.Path` | :obj:`str`, optional): Path to the unix socket file. Path
does not need to exist, in which case the file will be created.
unix (:class:`pathlib.Path` | :obj:`str` | :class:`socket.socket`, optional): Can be
either:
* the path to the unix socket file as :class:`pathlib.Path` or :obj:`str`. This
will be passed to `tornado.netutil.bind_unix_socket <https://www.tornadoweb.org/
en/stable/netutil.html#tornado.netutil.bind_unix_socket>`_ to create the socket.
If the Path does not exist, the file will be created.
* or the socket itself. This option allows you to e.g. restrict the permissions of
the socket for improved security. Note that you need to pass the correct family,
type and socket options yourself.
Caution:
This parameter is a replacement for the default TCP bind. Therefore, it is
Expand All @@ -551,6 +562,8 @@ async def start_webhook(
appropriate :paramref:`webhook_url`.
.. versionadded:: 20.8
.. versionchanged:: NEXT.VERSION
Added support to pass a socket instance itself.
Returns:
:class:`queue.Queue`: The update queue that can be filled from the main thread.
Expand Down Expand Up @@ -632,7 +645,7 @@ async def _start_webhook(
ip_address: Optional[str] = None,
max_connections: int = 40,
secret_token: Optional[str] = None,
unix: Optional[Union[str, Path]] = None,
unix: Optional[Union[str, Path, "socket"]] = None,
) -> None:
_LOGGER.debug("Updater thread started (webhook)")

Expand Down
12 changes: 8 additions & 4 deletions telegram/ext/_utils/webhookhandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import json
from http import HTTPStatus
from pathlib import Path
from socket import socket
from ssl import SSLContext
from types import TracebackType
from typing import TYPE_CHECKING, Optional, Type, Union
Expand Down Expand Up @@ -67,23 +68,26 @@ def __init__(
port: int,
webhook_app: "WebhookAppClass",
ssl_ctx: Optional[SSLContext],
unix: Optional[Union[str, Path]] = None,
unix: Optional[Union[str, Path, socket]] = None,
):
if unix and not UNIX_AVAILABLE:
raise RuntimeError("This OS does not support binding unix sockets.")
self._http_server = HTTPServer(webhook_app, ssl_options=ssl_ctx)
self.listen = listen
self.port = port
self.is_running = False
self.unix = unix
self.unix = None
if unix and isinstance(unix, socket):
self.unix = unix
elif unix:
self.unix = bind_unix_socket(str(unix))
self._server_lock = asyncio.Lock()
self._shutdown_lock = asyncio.Lock()

async def serve_forever(self, ready: Optional[asyncio.Event] = None) -> None:
async with self._server_lock:
if self.unix:
socket = bind_unix_socket(str(self.unix))
self._http_server.add_socket(socket)
self._http_server.add_socket(self.unix)
else:
self._http_server.listen(self.port, address=self.listen)

Expand Down
22 changes: 16 additions & 6 deletions tests/ext/test_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,16 @@
from tests.auxil.pytest_classes import PytestBot, make_bot
from tests.auxil.slots import mro_slots

UNIX_AVAILABLE = False

if TEST_WITH_OPT_DEPS:
try:
from tornado.netutil import bind_unix_socket

UNIX_AVAILABLE = True
except ImportError:
UNIX_AVAILABLE = False

from telegram.ext._utils.webhookhandler import WebhookServer


Expand Down Expand Up @@ -692,13 +701,12 @@ async def delete_webhook(*args, **kwargs):
@pytest.mark.parametrize("ext_bot", [True, False])
@pytest.mark.parametrize("drop_pending_updates", [True, False])
@pytest.mark.parametrize("secret_token", ["SecretToken", None])
@pytest.mark.parametrize("unix", [None, True])
@pytest.mark.parametrize(
"unix", [None, "file_path", "socket_object"] if UNIX_AVAILABLE else [None]
)
async def test_webhook_basic(
self, monkeypatch, updater, drop_pending_updates, ext_bot, secret_token, unix, file_path
):
# Skipping unix test on windows since they fail
if unix and platform.system() == "Windows":
pytest.skip("Windows doesn't support unix bind")
# Testing with both ExtBot and Bot to make sure any logic in WebhookHandler
# that depends on this distinction works
if ext_bot and not isinstance(updater.bot, ExtBot):
Expand All @@ -723,11 +731,12 @@ async def set_webhook(*args, **kwargs):

async with updater:
if unix:
socket = file_path if unix == "file_path" else bind_unix_socket(file_path)
return_value = await updater.start_webhook(
drop_pending_updates=drop_pending_updates,
secret_token=secret_token,
url_path="TOKEN",
unix=file_path,
unix=socket,
webhook_url="string",
)
else:
Expand Down Expand Up @@ -815,10 +824,11 @@ async def set_webhook(*args, **kwargs):

# We call the same logic twice to make sure that restarting the updater works as well
if unix:
socket = file_path if unix == "file_path" else bind_unix_socket(file_path)
await updater.start_webhook(
drop_pending_updates=drop_pending_updates,
secret_token=secret_token,
unix=file_path,
unix=socket,
webhook_url="string",
)
else:
Expand Down

0 comments on commit 2d8d43f

Please sign in to comment.