#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2021 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. # pylint: disable=E0401, C0114 import asyncio import logging import os import sys from queue import Queue from ssl import SSLContext from threading import Event, Lock from typing import TYPE_CHECKING, Any, Optional import tornado.web from tornado import httputil from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop from telegram import Update from telegram.utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot try: import ujson as json except ImportError: import json # type: ignore[no-redef] class WebhookServer: def __init__( self, listen: str, port: int, webhook_app: 'WebhookAppClass', ssl_ctx: SSLContext ): self.http_server = HTTPServer(webhook_app, ssl_options=ssl_ctx) self.listen = listen self.port = port self.loop: Optional[IOLoop] = None self.logger = logging.getLogger(__name__) self.is_running = False self.server_lock = Lock() self.shutdown_lock = Lock() def serve_forever(self, force_event_loop: bool = False, ready: Event = None) -> None: with self.server_lock: self.is_running = True self.logger.debug('Webhook Server started.') self._ensure_event_loop(force_event_loop=force_event_loop) self.loop = IOLoop.current() self.http_server.listen(self.port, address=self.listen) if ready is not None: ready.set() self.loop.start() self.logger.debug('Webhook Server stopped.') self.is_running = False def shutdown(self) -> None: with self.shutdown_lock: if not self.is_running: self.logger.warning('Webhook Server already stopped.') return self.loop.add_callback(self.loop.stop) # type: ignore def handle_error(self, request: Any, client_address: str) -> None: # pylint: disable=W0613 """Handle an error gracefully.""" self.logger.debug( 'Exception happened during processing of request from %s', client_address, exc_info=True, ) def _ensure_event_loop(self, force_event_loop: bool = False) -> None: """If there's no asyncio event loop set for the current thread - create one.""" try: loop = asyncio.get_event_loop() if ( not force_event_loop and os.name == 'nt' and sys.version_info >= (3, 8) and isinstance(loop, asyncio.ProactorEventLoop) ): raise TypeError( '`ProactorEventLoop` is incompatible with ' 'Tornado. Please switch to `SelectorEventLoop`.' ) except RuntimeError: # Python 3.8 changed default asyncio event loop implementation on windows # from SelectorEventLoop to ProactorEventLoop. At the time of this writing # Tornado doesn't support ProactorEventLoop and suggests that end users # change asyncio event loop policy to WindowsSelectorEventLoopPolicy. # https://github.com/tornadoweb/tornado/issues/2608 # To avoid changing the global event loop policy, we manually construct # a SelectorEventLoop instance instead of using asyncio.new_event_loop(). # Note that the fix is not applied in the main thread, as that can break # user code in even more ways than changing the global event loop policy can, # and because Updater always starts its webhook server in a separate thread. # Ideally, we would want to check that Tornado actually raises the expected # NotImplementedError, but it's not possible to cleanly recover from that # exception in current Tornado version. if ( os.name == 'nt' and sys.version_info >= (3, 8) # OS+version check makes hasattr check redundant, but just to be sure and hasattr(asyncio, 'WindowsProactorEventLoopPolicy') and ( isinstance( asyncio.get_event_loop_policy(), asyncio.WindowsProactorEventLoopPolicy, # pylint: disable=E1101 ) ) ): # pylint: disable=E1101 self.logger.debug( 'Applying Tornado asyncio event loop fix for Python 3.8+ on Windows' ) loop = asyncio.SelectorEventLoop() else: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) class WebhookAppClass(tornado.web.Application): def __init__(self, webhook_path: str, bot: 'Bot', update_queue: Queue): self.shared_objects = {"bot": bot, "update_queue": update_queue} handlers = [(rf"{webhook_path}/?", WebhookHandler, self.shared_objects)] # noqa tornado.web.Application.__init__(self, handlers) def log_request(self, handler: tornado.web.RequestHandler) -> None: pass # WebhookHandler, process webhook calls # pylint: disable=W0223 class WebhookHandler(tornado.web.RequestHandler): SUPPORTED_METHODS = ["POST"] def __init__( self, application: tornado.web.Application, request: httputil.HTTPServerRequest, **kwargs: JSONDict, ): super().__init__(application, request, **kwargs) self.logger = logging.getLogger(__name__) def initialize(self, bot: 'Bot', update_queue: Queue) -> None: # pylint: disable=W0201 self.bot = bot self.update_queue = update_queue def set_default_headers(self) -> None: self.set_header("Content-Type", 'application/json; charset="utf-8"') def post(self) -> None: self.logger.debug('Webhook triggered') self._validate_post() json_string = self.request.body.decode() data = json.loads(json_string) self.set_status(200) self.logger.debug('Webhook received data: %s', json_string) update = Update.de_json(data, self.bot) if update: self.logger.debug('Received Update with ID %d on Webhook', update.update_id) self.update_queue.put(update) def _validate_post(self) -> None: ct_header = self.request.headers.get("Content-Type", None) if ct_header != 'application/json': raise tornado.web.HTTPError(403) def write_error(self, status_code: int, **kwargs: Any) -> None: """Log an arbitrary message. This is used by all other logging functions. It overrides ``BaseHTTPRequestHandler.log_message``, which logs to ``sys.stderr``. The first argument, FORMAT, is a format string for the message to be logged. If the format string contains any % escapes requiring parameters, they should be specified as subsequent arguments (it's just like printf!). The client ip is prefixed to every message. """ super().write_error(status_code, **kwargs) self.logger.debug( "%s - - %s", self.request.remote_ip, "Exception in WebhookHandler", exc_info=kwargs['exc_info'], )