Source code for ferrosoft.apps.befundung.sftp

import base64
from contextlib import contextmanager
import io
import tempfile
from typing import Iterator, Tuple

import paramiko

from ferrosoft.apps.befundung.models import Report, Settings


[docs] def load_key(conf: "Settings") -> Tuple[paramiko.RSAKey, paramiko.HostKeys]: if conf.sftp_client_key == "": raise ValueError("Private key is empty") if conf.sftp_host_key == "": raise ValueError("Host key is empty") # Load RSA client key from in-memory buffer (decrypt if password is supplied). client_key = paramiko.RSAKey.from_private_key( io.StringIO(conf.sftp_client_key), password=(conf.sftp_client_key_password or None), ) # Load pinned RSA host key from base64 buffer. pinned_host_key = paramiko.RSAKey(data=base64.b64decode(conf.sftp_host_key)) host_keys = paramiko.HostKeys() # RSA is hard coded, because it was a restriction with the previously # used package. Supporting other key types would require model changes, # which is avoided for now. host_keys.add(conf.sftp_hostname, "ssh-rsa", pinned_host_key) # Non-standard port requires special host key entry. if getattr(conf, "sftp_port", 22) not in (None, 22): host_keys.add( f"[{conf.sftp_hostname}]:{conf.sftp_port}", "ssh-rsa", pinned_host_key ) return client_key, host_keys
[docs] @contextmanager def sftp_connection( conf: Settings, *, timeout: float = 30.0 ) -> Iterator[paramiko.SFTPClient]: client_key, pinned_hostkeys = load_key(conf) ssh = paramiko.SSHClient() # Strict pinning: trust ONLY the pinned keys (do not load system/user known_hosts). ssh.get_host_keys().clear() for host, keydict in pinned_hostkeys.items(): for keytype, key in keydict.items(): ssh.get_host_keys().add(host, keytype, key) ssh.set_missing_host_key_policy(paramiko.RejectPolicy()) try: ssh.connect( hostname=conf.sftp_hostname, port=conf.sftp_port, username=conf.sftp_username, pkey=client_key, look_for_keys=False, allow_agent=False, timeout=timeout, ) with ssh.open_sftp() as sftp: yield sftp finally: ssh.close()
[docs] def put_examination_report(report: Report, conf: Settings, pdf_blob: bytes): if not conf.sftp_enable_document_upload and not conf.sftp_enable_image_upload: return if conf.sftp_hostname == "": raise ValueError("SFTP hostname is empty") with sftp_connection(conf) as sftp: if conf.sftp_enable_document_upload: with tempfile.NamedTemporaryFile() as pdf_file: file_name = conf.format_report_pdf_name(report) # Write pdf contents to temporary file, because pysftp only supports normal files. pdf_file.write(pdf_blob) pdf_file.seek(0) sftp.put( pdf_file.name, "%s/%s" % (conf.sftp_remote_dir, file_name), ) if conf.sftp_enable_image_upload: for i, image in enumerate(report.images): image_name = conf.format_image_name(report, i, image.media_type) with tempfile.NamedTemporaryFile() as image_file: image_file.write(image.binary_data) image_file.seek(0) sftp.put( image_file.name, "%s/%s" % (conf.sftp_remote_dir, image_name), )