diff --git a/freqtrade/rpc/api_server2/uvicorn_threaded.py b/freqtrade/rpc/api_server2/uvicorn_threaded.py index 7c8804fd3..ce7089bed 100644 --- a/freqtrade/rpc/api_server2/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server2/uvicorn_threaded.py @@ -10,6 +10,9 @@ class UvicornServer(uvicorn.Server): Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742 """ def install_signal_handlers(self): + """ + In the parent implementation, this starts the thread, therefore we must patch it away here. + """ pass @contextlib.contextmanager diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py index f54845535..b3e6eb0dc 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server2/webserver.py @@ -1,4 +1,5 @@ import logging +from ipaddress import IPv4Address from typing import Any, Dict, Optional import uvicorn @@ -16,7 +17,7 @@ logger = logging.getLogger(__name__) class ApiServer(RPCHandler): - _rpc: Optional[RPC] = None + _rpc: RPC = None _config: Dict[str, Any] = {} def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: @@ -34,6 +35,7 @@ class ApiServer(RPCHandler): def cleanup(self) -> None: """ Cleanup pending module resources """ if self._server: + logger.info("Stopping API Server") self._server.cleanup() def send_msg(self, msg: Dict[str, str]) -> None: @@ -71,11 +73,26 @@ class ApiServer(RPCHandler): """ Start API ... should be run in thread. """ - uvconfig = uvicorn.Config(self.app, - port=self._config['api_server'].get('listen_port', 8080), - host=self._config['api_server'].get( - 'listen_ip_address', '127.0.0.1'), - access_log=True) - self._server = UvicornServer(uvconfig) + rest_ip = self._config['api_server']['listen_ip_address'] + rest_port = self._config['api_server']['listen_port'] - self._server.run_in_thread() + logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}') + if not IPv4Address(rest_ip).is_loopback: + logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") + logger.warning("SECURITY WARNING - This is insecure please set to your loopback," + "e.g 127.0.0.1 in config.json") + + if not self._config['api_server'].get('password'): + logger.warning("SECURITY WARNING - No password for local REST Server defined. " + "Please make sure that this is intentional!") + + logger.info('Starting Local Rest Server.') + uvconfig = uvicorn.Config(self.app, + port=rest_port, + host=rest_ip, + access_log=True) + try: + self._server = UvicornServer(uvconfig) + self._server.run_in_thread() + except Exception: + logger.exception("Api server failed to start.") diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 7d1100fb8..8017293b4 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -5,17 +5,18 @@ Unit test file for rpc/api_server.py from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock +from fastapi.applications import FastAPI import pytest from fastapi.testclient import TestClient -from flask import Flask +from fastapi import FastAPI from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC -from freqtrade.rpc.api_server import BASE_URI # , ApiServer +from freqtrade.rpc.api_server111 import BASE_URI # , ApiServer from freqtrade.rpc.api_server2 import ApiServer from freqtrade.state import RunMode, State from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal @@ -191,20 +192,19 @@ def test_api_run(default_conf, mocker, caplog): "password": "testPass", }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) - mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) server_mock = MagicMock() - mocker.patch('freqtrade.rpc.api_server.make_server', server_mock) + mocker.patch('freqtrade.rpc.api_server2.webserver.UvicornServer', server_mock) apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) - assert apiserver._config == default_conf - apiserver.run() assert server_mock.call_count == 1 - assert server_mock.call_args_list[0][0][0] == "127.0.0.1" - assert server_mock.call_args_list[0][0][1] == 8080 - assert isinstance(server_mock.call_args_list[0][0][2], Flask) - assert hasattr(apiserver, "srv") + assert apiserver._config == default_conf + apiserver.start_api() + assert server_mock.call_count == 2 + assert server_mock.call_args_list[0][0][0].host == "127.0.0.1" + assert server_mock.call_args_list[0][0][0].port == 8080 + assert isinstance(server_mock.call_args_list[0][0][0].app, FastAPI) assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog) assert log_has("Starting Local Rest Server.", caplog) @@ -217,12 +217,12 @@ def test_api_run(default_conf, mocker, caplog): "listen_port": 8089, "password": "", }}) - apiserver.run() + apiserver.start_api() assert server_mock.call_count == 1 - assert server_mock.call_args_list[0][0][0] == "0.0.0.0" - assert server_mock.call_args_list[0][0][1] == 8089 - assert isinstance(server_mock.call_args_list[0][0][2], Flask) + assert server_mock.call_args_list[0][0][0].host == "0.0.0.0" + assert server_mock.call_args_list[0][0][0].port == 8089 + assert isinstance(server_mock.call_args_list[0][0][0].app, FastAPI) assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog) assert log_has("Starting Local Rest Server.", caplog) assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", @@ -234,8 +234,8 @@ def test_api_run(default_conf, mocker, caplog): # Test crashing flask caplog.clear() - mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception)) - apiserver.run() + mocker.patch('freqtrade.rpc.api_server2.webserver.UvicornServer', MagicMock(side_effect=Exception)) + apiserver.start_api() assert log_has("Api server failed to start.", caplog) @@ -247,17 +247,15 @@ def test_api_cleanup(default_conf, mocker, caplog): "password": "testPass", }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) - mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) - mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock()) + + server_mock = MagicMock() + server_mock.cleanup = MagicMock() + mocker.patch('freqtrade.rpc.api_server2.webserver.UvicornServer', server_mock) apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) - apiserver.run_api() - stop_mock = MagicMock() - stop_mock.shutdown = MagicMock() - apiserver.srv = stop_mock apiserver.cleanup() - assert stop_mock.shutdown.call_count == 1 + assert apiserver._server.cleanup.call_count == 1 assert log_has("Stopping API Server", caplog) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 06706120f..e63d629b8 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -160,7 +160,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) run_mock = MagicMock() - mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock) + mocker.patch('freqtrade.rpc.api_server2.ApiServer.start_api', run_mock) default_conf['telegram']['enabled'] = False rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) @@ -172,7 +172,7 @@ def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) run_mock = MagicMock() - mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock) + mocker.patch('freqtrade.rpc.api_server2.ApiServer.start_api', run_mock) default_conf["telegram"]["enabled"] = False default_conf["api_server"] = {"enabled": True,