Compare commits
1 Commits
main
...
sftp-suppo
| Author | SHA1 | Date | |
|---|---|---|---|
| 49cd23b21b |
106
packages/sshecret-sshd/src/sshecret_sshd/sftp_server.py
Normal file
106
packages/sshecret-sshd/src/sshecret_sshd/sftp_server.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
"""SFTP Server."""
|
||||||
|
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from typing import cast, override
|
||||||
|
from pathlib import Path
|
||||||
|
import asyncssh
|
||||||
|
|
||||||
|
|
||||||
|
from sshecret.backend.api import SshecretBackend
|
||||||
|
from sshecret.backend.models import Client
|
||||||
|
|
||||||
|
READ_ONLY = asyncssh.FXF_READ
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection(channel: asyncssh.SSHServerChannel[bytes]) -> asyncssh.SSHServerConnection:
|
||||||
|
"""Get the connection."""
|
||||||
|
conn = channel.get_extra_info("connection")
|
||||||
|
assert conn is not None
|
||||||
|
return cast(asyncssh.SSHServerConnection, conn)
|
||||||
|
|
||||||
|
def get_backend(connection: asyncssh.SSHServerConnection) -> SshecretBackend:
|
||||||
|
"""Get backend from connection."""
|
||||||
|
backend = connection.get_extra_info("backend")
|
||||||
|
assert backend is not None
|
||||||
|
return cast(SshecretBackend, backend)
|
||||||
|
|
||||||
|
def get_client(connection: asyncssh.SSHServerConnection) -> Client:
|
||||||
|
"""Get client."""
|
||||||
|
client = connection.get_extra_info("client")
|
||||||
|
assert client is not None
|
||||||
|
return cast(Client, client)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SshecretFileServer(asyncssh.SFTPServer):
|
||||||
|
"""File server."""
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __init__(self, chan: asyncssh.SSHServerChannel[bytes], chroot: bytes | None = None):
|
||||||
|
"""Initialize sftp server."""
|
||||||
|
super().__init__(chan, "/".encode())
|
||||||
|
self._conn: asyncssh.SSHServerConnection = get_connection(chan)
|
||||||
|
self.backend: SshecretBackend = get_backend(self._conn)
|
||||||
|
|
||||||
|
self.logger.info("Connected")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self) -> Client:
|
||||||
|
"""Get client."""
|
||||||
|
return get_client(self._conn)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def scandir(self, path: bytes) -> AsyncIterator[asyncssh.SFTPName]:
|
||||||
|
"""List secrets as files."""
|
||||||
|
for secret in self.client.secrets:
|
||||||
|
sftp_name = asyncssh.SFTPName(secret.encode(), READ_ONLY)
|
||||||
|
self.format_longname(sftp_name)
|
||||||
|
yield sftp_name
|
||||||
|
|
||||||
|
@override
|
||||||
|
def format_longname(self, name: asyncssh.SFTPName) -> None:
|
||||||
|
mode = "-r--------"
|
||||||
|
user = self.client.name
|
||||||
|
group = "sshecret_clients"
|
||||||
|
size = ''
|
||||||
|
modtime = ''
|
||||||
|
nlink = ''
|
||||||
|
|
||||||
|
detail = (
|
||||||
|
f'{mode:10s} {nlink:>4s} {user:8s} {group:8s} '
|
||||||
|
f'{size:>8s} {modtime:12s} '
|
||||||
|
)
|
||||||
|
|
||||||
|
name.longname = detail.encode('utf-8') + cast(bytes, name.filename)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def format_user(self, uid: int | None) -> str:
|
||||||
|
return self.client.name
|
||||||
|
|
||||||
|
@override
|
||||||
|
def format_group(self, gid: int | None) -> str:
|
||||||
|
return "clients"
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def open(self, path: bytes, pflags: int, attrs: asyncssh.SFTPAttrs) -> object:
|
||||||
|
"""Open file.
|
||||||
|
|
||||||
|
This is the tricky bit. We need to disallow writes, and we need to serve
|
||||||
|
an encrypted secret as a file.
|
||||||
|
"""
|
||||||
|
if pflags != READ_ONLY:
|
||||||
|
raise asyncssh.SFTPError(asyncssh.FX_WRITE_PROTECT, "Read write not supported")
|
||||||
|
filepath = path.decode()
|
||||||
|
secret_name = Path(filepath).name
|
||||||
|
if secret_name not in self.client.secrets:
|
||||||
|
raise asyncssh.SFTPError(asyncssh.FX_NO_SUCH_FILE, "No such secret")
|
||||||
|
secret = await self.backend.get_client_secret(("id", str(self.client.id)), secret_name)
|
||||||
|
if not secret:
|
||||||
|
raise asyncssh.SFTPError(asyncssh.FX_NO_SUCH_FILE, "No such secret")
|
||||||
|
# io.BytesIO
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def close(self, file_obj: object) -> None:
|
||||||
|
"""Close the file object."""
|
||||||
@ -16,6 +16,7 @@ from sshecret_sshd.commands import dispatch_command
|
|||||||
|
|
||||||
from sshecret.backend import SshecretBackend, Client, Operation, SubSystem
|
from sshecret.backend import SshecretBackend, Client, Operation, SubSystem
|
||||||
from .settings import ServerSettings, ClientRegistrationSettings
|
from .settings import ServerSettings, ClientRegistrationSettings
|
||||||
|
from .sftp_server import SshecretFileServer
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -222,6 +223,7 @@ async def run_ssh_server(
|
|||||||
port,
|
port,
|
||||||
server_host_keys=keys,
|
server_host_keys=keys,
|
||||||
process_factory=dispatch_command,
|
process_factory=dispatch_command,
|
||||||
|
sftp_factory=SshecretFileServer,
|
||||||
)
|
)
|
||||||
return server
|
return server
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user