Repository: rotki/rotki

Issue:
Find if there is a way to proxy the websocket connection through flask.
## Abstract

On docker when Nginx is available the `ws` is proxied through Nginx to the backend via a `host:port/ws` scheme. 

In the application itself, the frontend starts the backend and web socket and specifies the ports, thus the frontend is aware of which localhost port the web socket listens to, to connect.

The problem is that this detection is hardcoded, not dynamic and thus it has a few cases that it doesn't really work. 

One of these is when the packaged version of the application attempts to connect to a remote host (docker) the web socket will fail to connect.

Having the backend let the frontend know its web socket listening port won't work in this case since the backend is not aware of the proxy in front.

The idea here would be if possible, that flask would act as a proxy to establish the connection and pass it to the web socket server, eliminating the need for the frontend to be aware of different ways or ports to connect.



Discussion:
Hey @kelsos this was scheduled for this release. Is this still a relevant idea you want to pursue? If yes move it to a milestone you think would make sense. Otherwise close it.
Pushing this to next version for @prettyirrelevant to check. If something is not clear ask @kelsos 

Resolving patch:
diff --git a/rotkehlchen/api/server.py b/rotkehlchen/api/server.py
--- a/rotkehlchen/api/server.py
+++ b/rotkehlchen/api/server.py
@@ -8,7 +8,8 @@
 from flask.views import MethodView
 from flask_cors import CORS
 from gevent.pywsgi import WSGIServer
-from geventwebsocket import Resource as WebsocketResource, WebSocketServer
+from geventwebsocket import Resource as WebsocketResource
+from geventwebsocket.handler import WebSocketHandler
 from marshmallow import Schema
 from marshmallow.exceptions import ValidationError
 from webargs.flaskparser import parser
@@ -368,7 +369,6 @@ def __init__(
 
         self.wsgiserver: Optional[WSGIServer] = None
         self.flask_app.register_blueprint(self.blueprint)
-        self.ws_server: Optional[WebSocketServer] = None
 
         self.flask_app.errorhandler(HTTPStatus.NOT_FOUND)(endpoint_not_found)
         self.flask_app.register_error_handler(Exception, self.unhandled_exception)
@@ -391,40 +391,29 @@ def start(
             self,
             host: str = '127.0.0.1',
             rest_port: int = 5042,
-            websockets_port: int = 5043,
     ) -> None:
         """This is used to start the API server in production"""
         wsgi_logger = logging.getLogger(__name__ + '.pywsgi')
         self.wsgiserver = WSGIServer(
             listener=(host, rest_port),
-            application=self.flask_app,
+            application=WebsocketResource([
+                ('^/ws', RotkiWSApp),
+                ('^/', self.flask_app),
+            ]),
             log=wsgi_logger,
+            handler_class=WebSocketHandler,
+            environ={'rotki_notifier': self.rotki_notifier},
             error_log=wsgi_logger,
         )
         msg = f'rotki REST API server is running at: {host}:{rest_port}'
         print(msg)
         log.info(msg)
         self.wsgiserver.start()
-        self.ws_server = WebSocketServer(
-            listener=(host, websockets_port),
-            application=WebsocketResource([
-                ('^/', RotkiWSApp),
-            ]),
-            debug=False,
-            environ={'rotki_notifier': self.rotki_notifier},
-        )
-        msg = f'rotki Websockets API server is running at: {host}:{websockets_port}'
-        print(msg)
-        log.info(msg)
-        self.ws_server.start()
 
     def stop(self, timeout: int = 5) -> None:
         """Stops the API server. If handlers are running after timeout they are killed"""
         if self.wsgiserver is not None:
             self.wsgiserver.stop(timeout)
             self.wsgiserver = None
-        if self.ws_server is not None:
-            self.ws_server.stop(timeout)
-            self.wsgiserver = None
 
         self.rest_api.stop()
diff --git a/rotkehlchen/server.py b/rotkehlchen/server.py
--- a/rotkehlchen/server.py
+++ b/rotkehlchen/server.py
@@ -56,6 +56,5 @@ def main(self) -> None:
         self.api_server.start(
             host=self.args.api_host,
             rest_port=self.args.rest_api_port,
-            websockets_port=self.args.websockets_api_port,
         )
         self.stop_event.wait()


Test patch:
diff --git a/rotkehlchen/tests/fixtures/rotkehlchen.py b/rotkehlchen/tests/fixtures/rotkehlchen.py
--- a/rotkehlchen/tests/fixtures/rotkehlchen.py
+++ b/rotkehlchen/tests/fixtures/rotkehlchen.py
@@ -269,7 +269,6 @@ def fixture_uninitialized_rotkehlchen(cli_args, inquirer, asset_resolver, global
 def fixture_rotkehlchen_api_server(
         uninitialized_rotkehlchen,
         rest_api_port,
-        websockets_api_port,
         start_with_logged_in_user,
         start_with_valid_premium,
         db_password,
@@ -303,7 +302,6 @@ def fixture_rotkehlchen_api_server(
     api_server = create_api_server(
         rotki=uninitialized_rotkehlchen,
         rest_port_number=rest_api_port,
-        websockets_port_number=websockets_api_port,
     )
 
     initialize_mock_rotkehlchen_instance(
diff --git a/rotkehlchen/tests/fixtures/variables.py b/rotkehlchen/tests/fixtures/variables.py
--- a/rotkehlchen/tests/fixtures/variables.py
+++ b/rotkehlchen/tests/fixtures/variables.py
@@ -28,12 +28,6 @@ def rest_api_port(port_generator):
     return port
 
 
-@pytest.fixture
-def websockets_api_port(port_generator):
-    port = next(port_generator)
-    return port
-
-
 @pytest.fixture
 def added_exchanges() -> Sequence[Location]:
     """A fixture determining which exchanges to add to a test rotkehlchen api server"""
diff --git a/rotkehlchen/tests/fixtures/websockets.py b/rotkehlchen/tests/fixtures/websockets.py
--- a/rotkehlchen/tests/fixtures/websockets.py
+++ b/rotkehlchen/tests/fixtures/websockets.py
@@ -47,9 +47,9 @@ def wait_until_messages_num(self, num: int, timeout: int) -> None:
 @pytest.fixture(name='websocket_connection')
 def fixture_websocket_connection_reader(
         rotkehlchen_api_server,  # pylint: disable=unused-argument
-        websockets_api_port,
+        rest_api_port,
 ) -> Generator[WebsocketReader, None, None]:
-    ws = create_connection(f'ws://127.0.0.1:{websockets_api_port}')
+    ws = create_connection(f'ws://127.0.0.1:{rest_api_port}/ws')
     websocket_reader = WebsocketReader(ws)
     ws.send('{}')  # whatever -- just to subscribe
     greenlet = gevent.spawn(websocket_reader.read_forever)
diff --git a/rotkehlchen/tests/utils/api.py b/rotkehlchen/tests/utils/api.py
--- a/rotkehlchen/tests/utils/api.py
+++ b/rotkehlchen/tests/utils/api.py
@@ -37,7 +37,6 @@ def _wait_for_listening_port(
 def create_api_server(
         rotki: Rotkehlchen,
         rest_port_number: int,
-        websockets_port_number: int,
 ) -> APIServer:
     api_server = APIServer(RestAPI(rotkehlchen=rotki), rotki.rotki_notifier)
 
@@ -45,14 +44,12 @@ def create_api_server(
     api_server.start(
         host='127.0.0.1',
         rest_port=rest_port_number,
-        websockets_port=websockets_port_number,
     )
 
     # Fixes flaky test, where requests are done prior to the server initializing
     # the listening socket.
     # https://github.com/raiden-network/raiden/issues/389#issuecomment-305551563
     _wait_for_listening_port(rest_port_number)
-    _wait_for_listening_port(websockets_port_number)
 
     return api_server