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

Add attributes to a SSHClient subclass #731

Open
LanderOtto opened this issue Jan 10, 2025 · 3 comments
Open

Add attributes to a SSHClient subclass #731

LanderOtto opened this issue Jan 10, 2025 · 3 comments

Comments

@LanderOtto
Copy link

Hi,

I am trying to automate the login to a server that requires 2FA.
I have the secret, which is saved on a file, and I can generate the token with the TOTP algorithm.
I saw discussion #514 and implemented a custom SSHClient that overrides the kbdint_challenge_received and kbdint_auth_requested methods.

class SSH2FAClient(asyncssh.SSHClient):
    async def kbdint_auth_requested(self):
        return ""

    async def kbdint_challenge_received(
        self, name: str, instructions: str, lang: str, prompts
    ):
        if prompts:
            with open("secret_path") as f:
                token = get_totp_token(f.read().strip())
            return [token]
            else:
                logger.error(f"Prompt does not recognized: {prompt}")
                raise NotImplementedError
        else:
            return []

connection = await asyncssh.connect(
    ...
   client_factory=SSH2FAClient,
)

It works. Now, I want to parameterize the class and remove the hard-coded secret's file path. How can I do this?
I saw that the SSHClient is created without parameters in the constructor.
I tried to retrieve the path from the parameter of the connection_made method (i.e., the SSHClientConnection instance).
However, I did not find a way to do it using the asyncssh.connect method.

@ronf
Copy link
Owner

ronf commented Jan 11, 2025

You should be able to do something like:

class SSH2FAClient(asyncssh.SSHClient):
    def __init__(self, secret_path: str):
        super.__init__()

        self._secret_path = secret_path

    async def kbdint_auth_requested(self):
        return ""

    async def kbdint_challenge_received(
        self, name: str, instructions: str, lang: str, prompts
    ):
        ...

connection = await asyncssh.connect(
    ...
    client_factory=SSH2FAClient(secret_path),
    ...
)

You can then use self._secret_path in the kbdint_auth_requested() method instead of hard-coding the value. There seems to be something a bit wrong with that method, though -- there's an "else" block after return [token] that doesn't look right.

Note that there's also a asyncssh.create_connection() method which takes the client_factory argument as the first positional argument, followed by host & port, so you don't need to pass client_factory as a keyword argument. This was modeled after the asyncio functions for opening connections.

@LanderOtto
Copy link
Author

Sorry about the confusion. That else block was an error because I didn’t copy the entire code.

The issue is that client_factory needs to be callable.

Traceback (most recent call last):
  File "/usr/lib/python3.10/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/home/ubuntu/venv/lib/python3.10/site-packages/asyncssh/connection.py", line 1368, in connection_made
    self._owner = self._protocol_factory()
TypeError: 'SSH2FAClient' object is not callable

I resolved this by wrapping the SSH2FAClient instantiation in a lambda function to make it callable.

connection = await asyncssh.connect(
    ...
    client_factory=lambda: SSH2FAClient(secret_path),
    ...
)

Thanks a lot for the help.

@ronf
Copy link
Owner

ronf commented Jan 11, 2025

Ah, yes - I got that wrong in my example. Using "lambda" works well in this case, but client_factory can also be a regular function, which might make sense if you want to do some operations on its arguments before instantiating your SSHClient subclass. It could also be something like a "construct" class method in your SSHCient subclass if you want to keep this extra logic in the class itself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants