Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Websocket Support #14

Closed
SelfhostedPro opened this issue Nov 2, 2020 · 27 comments
Closed

Websocket Support #14

SelfhostedPro opened this issue Nov 2, 2020 · 27 comments
Labels
enhancement New feature or request

Comments

@SelfhostedPro
Copy link

Currently it looks as though websockets wont work with the standard require_jwt_auth() even when sent via cookies (which works with flask_jwt_extended). This is the error I'm getting:

ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/uvicorn/protocols/websockets/websockets_impl.py", line 154, in run_asgi
    result = await self.app(self.scope, self.asgi_receive, self.asgi_send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/fastapi/applications.py", line 179, in __call__
    await super().__call__(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/starlette/applications.py", line 111, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/starlette/middleware/errors.py", line 146, in __call__
    await self.app(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/starlette/exceptions.py", line 58, in __call__
    await self.app(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/starlette/routing.py", line 566, in __call__
    await route.handle(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/starlette/routing.py", line 283, in handle
    await self.app(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/starlette/routing.py", line 57, in app
    await func(session)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/fastapi/routing.py", line 228, in app
    await dependant.call(**values)
  File "./backend/api/routers/apps.py", line 171, in dashboard
    Authorize.jwt_required()
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/fastapi_jwt_auth/auth_jwt.py", line 670, in jwt_required
    self._verify_and_get_jwt_in_cookies('access',self._decode_issuer)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/fastapi_jwt_auth/auth_jwt.py", line 541, in _verify_and_get_jwt_in_cookies
    cookie = self._request.cookies.get(cookie_key)
AttributeError: 'NoneType' object has no attribute 'cookies'
@SelfhostedPro
Copy link
Author

Possible solution would be to add a function that just validates the JWT and CSRF (if it's enabled) that can be imported so that I can use it in my websocket routes.

@SelfhostedPro
Copy link
Author

Here's the function I'm currently trying to get this to work on:

@router.websocket("/stats")
async def dashboard(websocket: WebSocket):
    auth_success = await websocket_auth(websocket=websocket)
    if auth_success:
        await websocket.accept()
        tasks = []
        async with aiodocker.Docker() as docker:
            containers = []
            _containers = await docker.containers.list()
            for _app in _containers:
                if _app._container["State"] == "running":
                    containers.append(_app)
            for app in containers:
                _name = app._container["Names"][0][1:]
                container: DockerContainer = await docker.containers.get(_name)
                stats = container.stats(stream=True)
                tasks.append(process_container(_name, stats, websocket))
            await asyncio.gather(*tasks)
    else:
        await websocket.close(code=status.WS_1008_POLICY_VIOLATION)


async def process_container(name, stats, websocket):
    cpu_total = 0.0
    cpu_system = 0.0
    cpu_percent = 0.0
    async for line in stats:
        if line["memory_stats"]:
            mem_current = line["memory_stats"]["usage"]
            mem_total = line["memory_stats"]["limit"]
            mem_percent = (mem_current / mem_total) * 100.0
        else:
            mem_current = None
            mem_total = None
            mem_percent = None

        try:
            cpu_percent, cpu_system, cpu_total = await calculate_cpu_percent2(
                line, cpu_total, cpu_system
            )
        except KeyError as e:
            print("error while getting new CPU stats: %r, falling back")
            cpu_percent = await calculate_cpu_percent(line)

        full_stats = {
            "name": name,
            "cpu_percent": cpu_percent,
            "mem_current": mem_current,
            "mem_total": mem_total,
            "mem_percent": mem_percent,
        }
        try:
            await websocket.send_text(json.dumps(full_stats))
        except Exception as e:
            pass

auth_success = await websocket_auth(websocket=websocket) was a function that used a function from FastAPI-users to validate the jwt token in the cookie.

async def websocket_auth(websocket: WebSocket):
    try:
        cookie = websocket._cookies["fastapiusersauth"]
        user = await cookie_authentication(cookie, user_db)
        if user and user.is_active:
            return user
        elif settings.DISABLE_AUTH == "True":
            return True
    except:
        if settings.DISABLE_AUTH == "True":
            return True
        else:
            return None

(Just wanted to add an example of what manual validation could look like)

@IndominusByte
Copy link
Owner

Hi @SelfhostedPro, I know the problem it is, fastapi-jwt-auth extract data which is cookies or headers from request instead from WebSocket, I will fix this in the next version thanks 🙏

@SelfhostedPro
Copy link
Author

Glad to hear it! Super excited to be using this in my project.

@IndominusByte IndominusByte added the enhancement New feature or request label Nov 3, 2020
@IndominusByte
Copy link
Owner

Hi @SelfhostedPro you can get a new version for support WebSocket, also you can check the documentation here, if you have any question or suggestion please let me now 😁 , or if this version enough for you, you can close this issue thankyou 🙏

@SelfhostedPro
Copy link
Author

I'll try it out today and let you know how it goes. Thanks!

@SelfhostedPro
Copy link
Author

I'm currently getting the following error in my websocket functions. Seems like it doesn't like me specifying the token argument for some reason.

TypeError: jwt_required() got an unexpected keyword argument 'token'

This is the function in question:

@router.websocket("/{app_name}/livelogs")
async def logs(websocket: WebSocket, app_name: str, Authorize: AuthJWT = Depends()):
    try:
        csrf = websocket._cookies["csrf_access_token"]
        Authorize.jwt_required("websocket", token=csrf)
    except AuthJWTException as err:
        await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
    await websocket.accept()
    async with aiodocker.Docker() as docker:
        container: DockerContainer = await docker.containers.get(app_name)
        if container._container["State"]["Status"] == "running":
            stats = container.stats(stream=True)
            logs = container.log(stdout=True, stderr=True, follow=True)
            async for line in logs:
                try:
                    await websocket.send_text(line)
                except Exception as e:
                    return e
        else:
            await websocket.close(code=status.WS_1011_INTERNAL_ERROR)

Here's the full error:

ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/uvicorn/protocols/websockets/websockets_impl.py", line 154, in run_asgi
    result = await self.app(self.scope, self.asgi_receive, self.asgi_send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/fastapi/applications.py", line 179, in __call__
    await super().__call__(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/starlette/applications.py", line 111, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/starlette/middleware/errors.py", line 146, in __call__
    await self.app(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/starlette/exceptions.py", line 58, in __call__
    await self.app(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/starlette/routing.py", line 566, in __call__
    await route.handle(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/starlette/routing.py", line 283, in handle
    await self.app(scope, receive, send)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/starlette/routing.py", line 57, in app
    await func(session)
  File "/home/user/dev/work/venv/lib/python3.8/site-packages/fastapi/routing.py", line 228, in app
    await dependant.call(**values)
  File "./backend/api/routers/apps.py", line 118, in stats
    Authorize.jwt_required("websocket", token=csrf)
TypeError: jwt_required() got an unexpected keyword argument 'token'

@IndominusByte
Copy link
Owner

btw if you want to authorization with cookies you must be passing WebSocket instance to

    try:
        csrf = websocket._cookies["csrf_access_token"]
        Authorize.jwt_required("websocket", websocket=websocket, token=csrf) # like this
    except AuthJWTException: # delete as err if you no need that message
        await websocket.close(code=status.WS_1008_POLICY_VIOLATION)

@SelfhostedPro
Copy link
Author

Changed over to that as well with no luck. Getting the following error:
TypeError: jwt_required() got an unexpected keyword argument 'websocket'

@router.websocket("/{app_name}/livelogs")
async def logs(websocket: WebSocket, app_name: str, Authorize: AuthJWT = Depends()):
    try:
        csrf = websocket._cookies["csrf_access_token"]
        Authorize.jwt_required("websocket",websocket=websocket,csrf_token=csrf)
    except AuthJWTException:
        await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
    await websocket.accept()
    async with aiodocker.Docker() as docker:
        container: DockerContainer = await docker.containers.get(app_name)
        if container._container["State"]["Status"] == "running":
            stats = container.stats(stream=True)
            logs = container.log(stdout=True, stderr=True, follow=True)
            async for line in logs:
                try:
                    await websocket.send_text(line)
                except Exception as e:
                    return e
        else:
            await websocket.close(code=status.WS_1011_INTERNAL_ERROR)

This is to show I'm on the latest version:

[user@user-desktop Yacht]$ pip show fastapi-jwt-auth
Name: fastapi-jwt-auth
Version: 0.5.0
Summary: FastAPI extension that provides JWT Auth support (secure, easy to use and lightweight)
Home-page: https://github.com/IndominusByte/fastapi-jwt-auth
Author: Nyoman Pradipta Dewantara
Author-email: nyomanpradipta120@gmail.com
License: UNKNOWN
Location: /home/user/dev/Yacht-work/Yacht/backend/venv/lib/python3.8/site-packages
Requires: fastapi, PyJWT
Required-by: 

@SelfhostedPro
Copy link
Author

Here is the source code for that section of my app if you wanted to know that as well:

https://github.com/SelfhostedPro/Yacht/blob/ec26cab0bd4b2b119df6290566533f4a4cd57b72/backend/api/routers/apps.py#L90

@IndominusByte
Copy link
Owner

Can you try a fresh install? its doesn't make sense my package doesn't have websocket or token argument its like your application use my previous version which is doesn't have that all argument

@SelfhostedPro
Copy link
Author

Deleted my project, cloned, setup a new venv, and installed with --no-cache-dir and still getting the same error. Is it possible pypi doesn't have the correct files in the new version?

@SelfhostedPro
Copy link
Author

Checked on pypi and in my venv and it looks like it's the correct version.

@SelfhostedPro
Copy link
Author

Also tried just doing positional arguments with no luck.

TypeError: jwt_required() takes 1 positional argument but 5 were given

@router.websocket("/stats")
async def dashboard(websocket: WebSocket, Authorize: AuthJWT = Depends()):
    try:
        csrf = websocket._cookies["csrf_access_token"]
        Authorize.jwt_required("websocket",None,websocket,csrf)
    except AuthJWTException:
        await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
    await websocket.accept()
    tasks = []
    async with aiodocker.Docker() as docker:
        containers = []
        _containers = await docker.containers.list()
        for _app in _containers:
            if _app._container["State"] == "running":
                containers.append(_app)
        for app in containers:
            _name = app._container["Names"][0][1:]
            container: DockerContainer = await docker.containers.get(_name)
            stats = container.stats(stream=True)
            tasks.append(process_container(_name, stats, websocket))
        await asyncio.gather(*tasks)

@SelfhostedPro
Copy link
Author

If you'd be willing to try out my project and see here are the instructions:

backend:

git clone https://github.com/selfhostedpro/yacht.git
cd Yacht
cd backend
pip install -r requirements.txt

frontend(in another terminal):

cd frontend
npm i
npm run serve

If you use vscode, this is the launch.json I use for debugging:

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Module",
            "type": "python",
            "request": "launch",
            "module": "uvicorn",
            "args": [
                "backend.api.main:app",
                "--reload"
            ]
        }
    ]
} 

@IndominusByte
Copy link
Owner

Try this cat env/lib/python3.8/site-packages/fastapi_jwt_auth/auth_jwt.py then screenshot to me in the function jwt_required() ? Does it look like this?
Screen Shot 2020-11-07 at 00 37 19

@SelfhostedPro
Copy link
Author

Yeah it does:
image

@IndominusByte
Copy link
Owner

Okay, I will try to run your application

@SelfhostedPro
Copy link
Author

SelfhostedPro commented Nov 6, 2020

Thank you. I appreciate it. One instruction I forgot is to make a config directory in the root folder of the project like this:
image

Default username is admin@yacht.local default password is pass. You will also need to have docker installed and have one running container to get to the place where I'm having the error. (or you can use websocket king to simulate a user going to the following websocket: localhost:8080/api/apps/appname/livelogs )

@SelfhostedPro
Copy link
Author

Have a friend who's using the same example as in your documentation and is getting the same error. Nothing is popping out to me as far as where the issue is in fastapi-jwt-auth though.

@IndominusByte
Copy link
Owner

Testing on your application and its working
Screen Shot 2020-11-07 at 02 23 54
Screen Shot 2020-11-07 at 02 24 18

@SelfhostedPro
Copy link
Author

Are you using a virtual environment? I've never run into something like this before so I'm not really sure where to go from here as far as troubleshooting. I'll try re-imaging my machine after work today and see if that changes anything.

@IndominusByte
Copy link
Owner

I testing it on docker because you provide Dockerfile, and I don't get an error message as you get. and I try to different machine and work to

@IndominusByte
Copy link
Owner

Use the virtual environment in a different machine is working well to

@SelfhostedPro
Copy link
Author

I can confirm it's working within docker. Thanks!

@IndominusByte
Copy link
Owner

Great! glad to hear that 😁 🙏

@IndominusByte
Copy link
Owner

I assume the original issue was resolved. But feel free to add more comments or create new issues 😄 🙏

ToroNZ referenced this issue in LibreNZ/libre-fastapi-jwt Jun 14, 2023
* Update python version in tests to 3.10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants