├── pysecure ├── calls │ ├── __init__.py │ ├── channeli.py │ ├── sshi.py │ └── sftpi.py ├── test │ ├── __init__.py │ ├── run │ │ ├── run_sftp_tests.py │ │ ├── run_ssh_tests.py │ │ └── __init__.py │ ├── test_config.py │ ├── ssh_statuses.py │ ├── remote_command.py │ ├── dir_manip.py │ ├── text_write.py │ ├── sftp_recurse.py │ ├── text_iterate.py │ ├── binary_read.py │ ├── sftp_ls.py │ ├── test_base.py │ ├── sftp_mirror.py │ ├── example.py │ ├── forward_local.py │ ├── forward_x11.py │ ├── sftp_no_cb.py │ ├── remote_shell.py │ ├── file_manip.py │ └── forward_reverse.py ├── adapters │ ├── __init__.py │ ├── channela.py │ ├── ssha.py │ └── sftpa.py ├── __init__.py ├── error.py ├── config.py ├── log_config.py ├── constants │ ├── sftp.py │ ├── __init__.py │ └── ssh.py ├── library.py ├── exceptions.py ├── types.py ├── easy.py ├── utility.py ├── _version.py └── sftp_mirror.py ├── .gitattributes ├── MANIFEST.in ├── TODO ├── dist └── pysecure-0.1.0.tar.gz ├── .gitignore ├── dev ├── test.sh └── test.py ├── MANIFEST ├── setup.py ├── TODO_FUNCTION_LIST.txt ├── README.md └── LICENSE /pysecure/calls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pysecure/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pysecure/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | pysecure/_version.py export-subst 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include pysecure/_version.py 3 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | check get_disconnect_message call with newest version of libssh 2 | 3 | -------------------------------------------------------------------------------- /dist/pysecure-0.1.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsoprea/PySecure/HEAD/dist/pysecure-0.1.0.tar.gz -------------------------------------------------------------------------------- /pysecure/__init__.py: -------------------------------------------------------------------------------- 1 | #from ._version import get_versions 2 | #__version__ = get_versions()['default'] 3 | #del get_versions 4 | -------------------------------------------------------------------------------- /pysecure/test/run/run_sftp_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from pysecure.test.run import test_sftp 4 | 5 | test_sftp() 6 | 7 | -------------------------------------------------------------------------------- /pysecure/test/run/run_ssh_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from pysecure.test.run import test_ssh 4 | 5 | test_ssh() 6 | 7 | -------------------------------------------------------------------------------- /pysecure/test/test_config.py: -------------------------------------------------------------------------------- 1 | user = 'dustin' 2 | host = 'localhost' 3 | key_filepath = '/home/dustin/.ssh/id_ecdsa' 4 | verbosity = 0 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist/ 3 | pysecure/development.py 4 | /.Python 5 | /bin/ 6 | /lib/ 7 | /local/ 8 | /include/ 9 | /pysecure.egg-info/ 10 | -------------------------------------------------------------------------------- /dev/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #LD_LIBRARY_PATH=$HOME/build/libssh/build/src test/example.py 4 | #LD_LIBRARY_PATH=$HOME/build/libssh/build/src ./test.py 5 | PS_LIBRARY_FILEPATH=$HOME/build/libssh/build/src/libssh.4.2.2.dylib ./test.py 6 | -------------------------------------------------------------------------------- /pysecure/error.py: -------------------------------------------------------------------------------- 1 | from pysecure.calls.sshi import c_ssh_get_error_code, c_ssh_get_error 2 | 3 | def ssh_get_error_code(ssh_session_int): 4 | return c_ssh_get_error_code(ssh_session_int) 5 | 6 | def ssh_get_error(ssh_session_int): 7 | return c_ssh_get_error(ssh_session_int) 8 | 9 | -------------------------------------------------------------------------------- /pysecure/config.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | DEFAULT_CREATE_MODE = 0o644 4 | DEFAULT_EXECUTE_READ_BLOCK_SIZE = 8192 5 | NONBLOCK_READ_TIMEOUT_MS = 1500 6 | DEFAULT_SHELL_READ_BLOCK_SIZE = 1024 7 | MAX_MIRROR_LISTING_CHUNK_SIZE = 5 8 | MAX_MIRROR_WRITE_CHUNK_SIZE = 8192 9 | MAX_REMOTE_RECURSION_DEPTH = 8 10 | 11 | IS_DEVELOPMENT = bool(int(environ.get('DEBUG', '0'))) 12 | 13 | -------------------------------------------------------------------------------- /pysecure/test/ssh_statuses.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pysecure.adapters.channela import SshChannel 4 | from pysecure.test.test_base import connect_ssh_test 5 | 6 | class SshStatusesTest(TestCase): 7 | def __ssh_cb(self, ssh): 8 | print("Disconnect message: %s" % (ssh.get_disconnect_message(),)) 9 | 10 | def test_forward_local(self): 11 | connect_ssh_test(self.__ssh_cb) 12 | 13 | -------------------------------------------------------------------------------- /pysecure/test/remote_command.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pysecure.test.test_base import connect_ssh_test 4 | 5 | class RemoteCommandTest(TestCase): 6 | def __ssh_cb(self, ssh): 7 | data = ssh.execute('lsb_release -a') 8 | # print(data) 9 | 10 | data = ssh.execute('whoami') 11 | # print(data) 12 | 13 | def test_remote_command(self): 14 | connect_ssh_test(self.__ssh_cb) 15 | 16 | -------------------------------------------------------------------------------- /pysecure/log_config.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger, Formatter, DEBUG, WARNING, StreamHandler 2 | 3 | from pysecure.config import IS_DEVELOPMENT 4 | 5 | default_logger = getLogger() 6 | default_logger.setLevel(DEBUG if IS_DEVELOPMENT else WARNING) 7 | 8 | log_console = StreamHandler() 9 | log_format = '%(name)-12s %(levelname)-7s %(message)s' 10 | log_console.setFormatter(Formatter(log_format)) 11 | default_logger.addHandler(log_console) 12 | 13 | -------------------------------------------------------------------------------- /pysecure/test/dir_manip.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pysecure.test.test_base import connect_sftp_test 4 | 5 | class DirManipTest(TestCase): 6 | def __sftp_cb(self, ssh, sftp): 7 | # print("Creating directory.") 8 | sftp.mkdir("xyz") 9 | 10 | # print("Removing directory.") 11 | sftp.rmdir("xyz") 12 | 13 | def test_dir_manip(self): 14 | connect_sftp_test(self.__sftp_cb) 15 | 16 | -------------------------------------------------------------------------------- /pysecure/test/text_write.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pysecure.adapters.sftpa import SftpFile 4 | from pysecure.test.test_base import connect_sftp_test 5 | 6 | class TextWriteTest(TestCase): 7 | def __sftp_cb(self, ssh, sftp): 8 | test_data = b'1234' 9 | with SftpFile(sftp, 'sftp_write.txt', 'w') as sf: 10 | sf.write(test_data) 11 | 12 | def test_text_write(self): 13 | connect_sftp_test(self.__sftp_cb) 14 | 15 | -------------------------------------------------------------------------------- /pysecure/constants/sftp.py: -------------------------------------------------------------------------------- 1 | # File modes. 2 | O_RDONLY = 0o0 3 | O_WRONLY = 0o1 4 | O_RDWR = 0o2 5 | O_CREAT = 0o100 6 | O_EXCL = 0o200 7 | O_TRUNC = 0o1000 8 | 9 | # Access modes 10 | FA_WRITE = 'w' 11 | FA_READ = 'r' 12 | FA_WRITEBIN = 'wb' 13 | FA_READBIN = 'rb' 14 | 15 | # Values for attr->type. 16 | SSH_FILEXFER_TYPE_REGULAR = 1 17 | SSH_FILEXFER_TYPE_DIRECTORY = 2 18 | SSH_FILEXFER_TYPE_SYMLINK = 3 19 | SSH_FILEXFER_TYPE_SPECIAL = 4 20 | SSH_FILEXFER_TYPE_UNKNOWN = 5 21 | 22 | -------------------------------------------------------------------------------- /pysecure/library.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from ctypes import cdll 5 | from ctypes.util import find_library 6 | 7 | _logger = logging.getLogger(__name__) 8 | 9 | _LIBSSH_FILEPATH = os.environ.get('PS_LIBRARY_FILEPATH', '') 10 | if _LIBSSH_FILEPATH == '': 11 | _LIBSSH_FILEPATH = find_library('libssh') 12 | if _LIBSSH_FILEPATH is None: 13 | _LIBSSH_FILEPATH = 'libssh.so' 14 | 15 | _logger.debug("Using library: [%s]", _LIBSSH_FILEPATH) 16 | libssh = cdll.LoadLibrary(_LIBSSH_FILEPATH) 17 | -------------------------------------------------------------------------------- /pysecure/test/sftp_recurse.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pysecure.test.test_base import connect_sftp_test 4 | 5 | class SftpRecurseTest(TestCase): 6 | def __sftp_cb(self, ssh, sftp): 7 | def dir_cb(path, full_path, entry): 8 | # print("DIR: %s" % (full_path)) 9 | pass 10 | 11 | def listing_cb(path, list_): 12 | # print("[%s]: (%d) files" % (path, len(list_))) 13 | pass 14 | 15 | sftp.recurse('Pictures', dir_cb, listing_cb) 16 | 17 | def test_sftp_recurse(self): 18 | connect_sftp_test(self.__sftp_cb) 19 | 20 | -------------------------------------------------------------------------------- /pysecure/exceptions.py: -------------------------------------------------------------------------------- 1 | class SftpException(Exception): 2 | pass 3 | 4 | class SftpError(SftpException): 5 | pass 6 | 7 | class SftpAlreadyExistsError(SftpError): 8 | pass 9 | 10 | class SshException(Exception): 11 | pass 12 | 13 | class SshError(SshException): 14 | pass 15 | 16 | class SshLoginError(SshError): 17 | pass 18 | 19 | class SshHostKeyException(SshException): 20 | pass 21 | 22 | class SshNonblockingTryAgainException(SshException): 23 | pass 24 | 25 | class SshNoDataReceivedException(SshException): 26 | pass 27 | 28 | class SshTimeoutException(SshException): 29 | pass 30 | 31 | -------------------------------------------------------------------------------- /pysecure/test/text_iterate.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from sys import stdout 4 | 5 | from pysecure.adapters.sftpa import SftpFile 6 | from pysecure.test.test_base import connect_sftp_test 7 | 8 | class TextIterateTest(TestCase): 9 | def __sftp_cb(self, ssh, sftp): 10 | with SftpFile(sftp, 'test_doc_rfc1958.txt') as sf: 11 | i = 0 12 | for data in sf: 13 | # stdout.write("> " + data) 14 | 15 | if i >= 30: 16 | break 17 | 18 | i += 1 19 | 20 | def test_text_iterate(self): 21 | connect_sftp_test(self.__sftp_cb) 22 | 23 | -------------------------------------------------------------------------------- /pysecure/test/binary_read.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pysecure.adapters.sftpa import SftpFile 4 | from pysecure.test.test_base import connect_sftp_test 5 | 6 | class BinaryReadTest(TestCase): 7 | def __sftp_cb(self, ssh, sftp): 8 | # print("Opening file.") 9 | 10 | with SftpFile(sftp, 'test_libgksu2.so.0', 'r') as sf: 11 | buffer_ = sf.read() 12 | 13 | with open('/tmp/sftp_dump', 'wb') as f: 14 | f.write(buffer_) 15 | 16 | # print("Read (%d) bytes." % (len(buffer_))) 17 | 18 | def test_binary_read(self): 19 | connect_sftp_test(self.__sftp_cb) 20 | 21 | -------------------------------------------------------------------------------- /pysecure/test/sftp_ls.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pysecure.test.test_base import connect_sftp_test 4 | 5 | class SftpLsTest(TestCase): 6 | def __sftp_cb(self, ssh, sftp): 7 | # print("Name Size Perms Owner\tGroup\n") 8 | for attributes in sftp.listdir('.'): 9 | print("%-40s %10d %.8o %s(%d)\t%s(%d)" % 10 | (attributes.name[0:40], attributes.size, 11 | attributes.permissions, attributes.owner, 12 | attributes.uid, attributes.group, attributes.gid)) 13 | 14 | def test_sftp_ls(self): 15 | connect_sftp_test(self.__sftp_cb) 16 | 17 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.py 3 | versioneer.py 4 | pysecure/__init__.py 5 | pysecure/_version.py 6 | pysecure/config.py 7 | pysecure/easy.py 8 | pysecure/error.py 9 | pysecure/exceptions.py 10 | pysecure/library.py 11 | pysecure/log_config.py 12 | pysecure/sftp_mirror.py 13 | pysecure/types.py 14 | pysecure/utility.py 15 | pysecure/adapters/__init__.py 16 | pysecure/adapters/channela.py 17 | pysecure/adapters/sftpa.py 18 | pysecure/adapters/ssha.py 19 | pysecure/calls/__init__.py 20 | pysecure/calls/channeli.py 21 | pysecure/calls/sftpi.py 22 | pysecure/calls/sshi.py 23 | pysecure/constants/__init__.py 24 | pysecure/constants/sftp.py 25 | pysecure/constants/ssh.py 26 | -------------------------------------------------------------------------------- /pysecure/test/test_base.py: -------------------------------------------------------------------------------- 1 | from pysecure import log_config 2 | from pysecure.easy import connect_ssh_with_cb, connect_sftp_with_cb, \ 3 | get_key_auth_cb, get_password_auth_cb 4 | from pysecure.test.test_config import user, host, key_filepath, verbosity 5 | 6 | def connect_sftp_test(sftp_cb): 7 | print("Connecting SFTP with key: %s" % (key_filepath)) 8 | auth_cb = get_key_auth_cb(key_filepath) 9 | connect_sftp_with_cb(sftp_cb, user, host, auth_cb, verbosity=verbosity) 10 | 11 | def connect_ssh_test(ssh_cb): 12 | print("Connecting SSH with key: %s" % (key_filepath)) 13 | auth_cb = get_key_auth_cb(key_filepath) 14 | connect_ssh_with_cb(ssh_cb, user, host, auth_cb, verbosity=verbosity) 15 | 16 | -------------------------------------------------------------------------------- /pysecure/test/sftp_mirror.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from datetime import datetime 3 | 4 | from pysecure.test.test_base import connect_sftp_test 5 | from pysecure.sftp_mirror import SftpMirror 6 | 7 | class SftpMirrorTest(TestCase): 8 | def __sftp_cb(self, ssh, sftp): 9 | mirror = SftpMirror(sftp) 10 | 11 | mirror.mirror(mirror.mirror_to_local_no_recursion, 12 | "Pictures", 13 | "/tmp/Pictures", 14 | log_files=True) 15 | 16 | # mirror.mirror(mirror.mirror_to_remote_no_recursion, 17 | # "/home/dustin/Pictures", 18 | # "/tmp/RemotePictures", 19 | # log_files=True) 20 | 21 | def test_sftp_mirror(self): 22 | connect_sftp_test(self.__sftp_cb) 23 | 24 | -------------------------------------------------------------------------------- /pysecure/test/example.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pysecure.sftp_mirror import SftpMirror 4 | from pysecure.test.test_base import connect_sftp_test, connect_ssh_test 5 | 6 | 7 | class ExampleTest(TestCase): 8 | def __sftp_cb(self, ssh, sftp): 9 | print("SFTP") 10 | 11 | mirror = SftpMirror(sftp) 12 | 13 | # mirror.mirror(mirror.mirror_to_local_no_recursion, 14 | # "Pictures", 15 | # "/tmp/Pictures", 16 | # log_files=True) 17 | 18 | mirror.mirror(mirror.mirror_to_remote_no_recursion, 19 | "/home/dustin/Pictures", 20 | "/tmp/RemotePictures", 21 | log_files=True) 22 | 23 | def test_example(self): 24 | connect_sftp_test(self.__sftp_cb) 25 | -------------------------------------------------------------------------------- /pysecure/test/forward_local.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pysecure.adapters.channela import SshChannel 4 | from pysecure.test.test_base import connect_ssh_test 5 | 6 | class ForwardLocalTest(TestCase): 7 | def __ssh_cb(self, ssh): 8 | host_remote = 'localhost' 9 | port_remote = 80 10 | host_source = 'localhost' 11 | port_local = 1111 12 | data = b"GET / HTTP/1.1\nHost: localhost\n\n" 13 | 14 | with SshChannel(ssh) as sc: 15 | sc.open_forward(host_remote, 16 | port_remote, 17 | host_source, 18 | port_local) 19 | 20 | sc.write(data) 21 | 22 | received = sc.read(1024) 23 | 24 | # print("Received:\n\n%s" % (received)) 25 | 26 | def test_forward_local(self): 27 | connect_ssh_test(self.__ssh_cb) 28 | 29 | -------------------------------------------------------------------------------- /pysecure/test/forward_x11.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from time import sleep 3 | 4 | from pysecure.test.test_base import connect_ssh_test 5 | 6 | class ForwardX11Test(TestCase): 7 | def __ssh_cb(self, ssh): 8 | print("Is blocking: %s" % (ssh.is_blocking())) 9 | 10 | server_address = None 11 | server_port = 8080 12 | accept_timeout_ms = 60000 13 | 14 | port = ssh.forward_listen(server_address, server_port) 15 | with ssh.forward_accept(accept_timeout_ms) as sc: 16 | print("Waiting for X11 connection.") 17 | x11_channel = sc.accept_x11(60000) 18 | 19 | print("Requesting.") 20 | x11_channel.request_x11() 21 | 22 | print("Looping.") 23 | while 1: 24 | sleep(.1) 25 | 26 | def test_forward_x11(self): 27 | connect_ssh_test(self.__ssh_cb) 28 | 29 | -------------------------------------------------------------------------------- /pysecure/test/sftp_no_cb.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from unittest import TestCase 4 | 5 | import pysecure.log_config 6 | 7 | from pysecure.easy import EasySsh, get_key_auth_cb 8 | from pysecure.test.test_config import user, host, key_filepath 9 | 10 | 11 | class SftpNoCbTest(TestCase): 12 | def __init__(self, *args, **kwargs): 13 | super(SftpNoCbTest, self).__init__(*args, **kwargs) 14 | 15 | self.__log = logging.getLogger('SftpNoCb') 16 | 17 | def setUp(self): 18 | auth_cb = get_key_auth_cb(key_filepath) 19 | self.__easy = EasySsh(user, host, auth_cb) 20 | self.__easy.open_ssh() 21 | self.__easy.open_sftp() 22 | 23 | def tearDown(self): 24 | self.__easy.close_sftp() 25 | self.__easy.close_ssh() 26 | 27 | def test_nocb(self): 28 | entries = self.__easy.sftp.listdir('.') 29 | self.__log.info("(%d) entries returned." % (len(list(entries)))) 30 | 31 | -------------------------------------------------------------------------------- /pysecure/test/remote_shell.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pysecure.adapters.channela import RemoteShellProcessor 4 | from pysecure.test.test_base import connect_ssh_test 5 | 6 | class RemoteShellTest(TestCase): 7 | def __ssh_cb(self, ssh): 8 | rsp = RemoteShellProcessor(ssh) 9 | 10 | def shell_context_cb(sc, welcome): 11 | # print('-' * 50 + '\n' + 12 | # welcome + '\n' + 13 | # '-' * 50) 14 | 15 | output = rsp.do_command('whoami') 16 | # print(output) 17 | 18 | # output = rsp.do_command('cat /proc/uptime') 19 | # print(output) 20 | 21 | # Doesn't work. See bug report at libssh. 22 | # print("Setting environment.") 23 | # sc.request_env('aa', 'bb') 24 | # sc.request_env('LANG', 'en_US.UTF-8') 25 | 26 | rsp.shell(shell_context_cb) 27 | 28 | def test_remote_shell(self): 29 | connect_ssh_test(self.__ssh_cb) 30 | 31 | -------------------------------------------------------------------------------- /dev/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | import sys 4 | sys.path.insert(0, '..') 5 | 6 | import logging 7 | 8 | def _configure_logging(): 9 | _FMT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 10 | _FORMATTER = logging.Formatter(_FMT) 11 | 12 | logger = logging.getLogger() 13 | logger.setLevel(logging.DEBUG) 14 | 15 | ch = logging.StreamHandler() 16 | ch.setFormatter(_FORMATTER) 17 | 18 | logger.addHandler(ch) 19 | 20 | _configure_logging() 21 | 22 | from pysecure.adapters.sftpa import SftpFile 23 | 24 | from pysecure.easy import connect_sftp_with_cb, get_key_auth_cb 25 | 26 | user = 'dustin' 27 | host = 'localhost' 28 | key_filepath = '/Users/dustin/.ssh/id_dsa' 29 | 30 | auth_cb = get_key_auth_cb(key_filepath) 31 | 32 | # Or, for SFTP-enabled SSH functionality. 33 | 34 | def sftp_cb(ssh, sftp): 35 | print("Name Size Perms Owner\tGroup\n") 36 | for attributes in sftp.listdir('.'): 37 | print("%-40s %10d %.8o %s(%d)\t%s(%d)" % 38 | (attributes.name[0:40], attributes.size, 39 | attributes.permissions, attributes.owner, 40 | attributes.uid, attributes.group, 41 | attributes.gid)) 42 | 43 | connect_sftp_with_cb(sftp_cb, user, host, auth_cb) 44 | -------------------------------------------------------------------------------- /pysecure/test/file_manip.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pysecure.adapters.sftpa import SftpFile 4 | from pysecure.test.test_base import connect_sftp_test 5 | 6 | class FileManipTest(TestCase): 7 | def __sftp_cb(self, ssh, sftp): 8 | test_data = b'1234' 9 | 10 | with SftpFile(sftp, 'test_sftp_file', 'r+') as sf: 11 | # print("Position at top of file: %d" % (sf.tell())) 12 | 13 | sf.write(test_data) 14 | # print("Position at bottom of file: %d" % (sf.tell())) 15 | 16 | sf.seek(0) 17 | # print("Position at position (0): %d" % (sf.tell())) 18 | 19 | buffer_ = sf.read(100) 20 | # print("Read: [%s]" % (buffer_)) 21 | 22 | # print("Position after read: %d" % (sf.tell())) 23 | sf.seek(0) 24 | 25 | # print("Position after rewind: %d" % (sf.tell())) 26 | 27 | buffer_ = sf.read(100) 28 | # print("Read 1: (%d) bytes" % (len(buffer_))) 29 | # print("Position after read 1: %d" % (sf.tell())) 30 | 31 | buffer_ = sf.read(100) 32 | # print("Read 2: (%d) bytes" % (len(buffer_))) 33 | # print("Position after read 2: %d" % (sf.tell())) 34 | 35 | attr = sf.raw.fstat() 36 | # print(attr) 37 | 38 | def test_file_manip(self): 39 | connect_sftp_test(self.__sftp_cb) 40 | 41 | -------------------------------------------------------------------------------- /pysecure/constants/__init__.py: -------------------------------------------------------------------------------- 1 | TIME_DATETIME_CONDENSED_FORMAT = '%Y%m%d-%H%M%S' 2 | TIME_DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' 3 | 4 | SERVER_RESPONSES = \ 5 | { 0: ('SSH_FX_OK', 6 | "No error."), 7 | 1: ('SSH_FX_EOF', 8 | "End-of-file encountered."), 9 | 2: ('SSH_FX_NO_SUCH_FILE', 10 | "File doesn't exist."), 11 | 3: ('SSH_FX_PERMISSION_DENIED', 12 | "Permission denied."), 13 | 4: ('SSH_FX_FAILURE', 14 | "Generic failure."), 15 | 5: ('SSH_FX_BAD_MESSAGE', 16 | "Garbage received from server."), 17 | 6: ('SSH_FX_NO_CONNECTION', 18 | "No connection has been set up."), 19 | 7: ('SSH_FX_CONNECTION_LOST', 20 | "There was a connection, but we lost it."), 21 | 8: ('SSH_FX_OP_UNSUPPORTED', 22 | "Operation not supported by the server."), 23 | 9: ('SSH_FX_INVALID_HANDLE', 24 | "Invalid file handle."), 25 | 10: ('SSH_FX_NO_SUCH_PATH', 26 | "No such file or directory path exists."), 27 | 11: ('SSH_FX_FILE_ALREADY_EXISTS', 28 | "An attempt to create an already existing file or " 29 | "directory has been made."), 30 | 12: ('SSH_FX_WRITE_PROTECT', 31 | "We are trying to write on a write-protected " 32 | "filesystem."), 33 | 13: ('SSH_FX_NO_MEDIA', 34 | "No media in remote drive.") } 35 | 36 | -------------------------------------------------------------------------------- /pysecure/test/run/__init__.py: -------------------------------------------------------------------------------- 1 | from unittest import TestSuite, TestResult, makeSuite 2 | from pprint import pprint 3 | from traceback import print_tb 4 | 5 | from pysecure.test import binary_read 6 | from pysecure.test import dir_manip 7 | from pysecure.test import file_manip 8 | from pysecure.test import sftp_ls 9 | from pysecure.test import sftp_mirror 10 | from pysecure.test import sftp_no_cb 11 | from pysecure.test import sftp_recurse 12 | from pysecure.test import text_write 13 | from pysecure.test import text_iterate 14 | 15 | from pysecure.test import forward_local 16 | from pysecure.test import forward_reverse 17 | from pysecure.test import forward_x11 18 | from pysecure.test import remote_command 19 | from pysecure.test import remote_shell 20 | from pysecure.test import ssh_statuses 21 | 22 | sftp_suite = TestSuite((map(makeSuite, [ 23 | binary_read.BinaryReadTest, 24 | dir_manip.DirManipTest, 25 | file_manip.FileManipTest, 26 | sftp_ls.SftpLsTest, 27 | sftp_mirror.SftpMirrorTest, 28 | sftp_no_cb.SftpNoCbTest, 29 | sftp_recurse.SftpRecurseTest, 30 | text_write.TextWriteTest, 31 | text_iterate.TextIterateTest]))) 32 | 33 | ssh_suite = TestSuite((map(makeSuite, [ 34 | forward_local.ForwardLocalTest, 35 | # forward_reverse.ForwardReverseTest, 36 | # forward_x11.ForwardX11Test, 37 | remote_command.RemoteCommandTest, 38 | remote_shell.RemoteShellTest, 39 | ssh_statuses.SshStatusesTest]))) 40 | 41 | class VerboseTestResult(TestResult): 42 | def startTest(self, test): 43 | print("Running: %s" % (test)) 44 | 45 | def addError(self, test, err): 46 | (type_, value, traceback) = err 47 | 48 | print 49 | print_tb(traceback) 50 | print 51 | print(" Error [%s]: %s" % (type_, value)) 52 | 53 | def addFailure(self, test, err): 54 | (type_, value, traceback) = err 55 | 56 | print 57 | print_tb(traceback) 58 | print 59 | print(" Fail [%s]: %s" % (type_, value)) 60 | 61 | def test_sftp(): 62 | result = VerboseTestResult() 63 | sftp_suite.run(result) 64 | 65 | def test_ssh(): 66 | result = VerboseTestResult() 67 | ssh_suite.run(result) 68 | 69 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | from setuptools import setup, find_packages 4 | from setuptools.command.install import install 5 | 6 | from ctypes import cdll 7 | from ctypes.util import find_library 8 | 9 | #import versioneer 10 | 11 | #versioneer.VCS = 'git' 12 | #versioneer.versionfile_source = 'pysecure/_version.py' 13 | #versioneer.versionfile_build = 'pysecure/_version.py' 14 | #versioneer.tag_prefix = '' 15 | #versioneer.parentdir_prefix = 'pysecure-' 16 | 17 | def pre_install(): 18 | print("Verifying that libssh.so is accessible.") 19 | 20 | _LIBSSH_FILEPATH = find_library('libssh') 21 | if _LIBSSH_FILEPATH is None: 22 | _LIBSSH_FILEPATH = 'libssh.so' 23 | 24 | try: 25 | cdll.LoadLibrary(_LIBSSH_FILEPATH) 26 | except OSError: 27 | print("libssh can not be loaded.") 28 | raise 29 | 30 | class custom_install(install): 31 | def run(self): 32 | pre_install() 33 | install.run(self) 34 | 35 | #cmdclass = versioneer.get_cmdclass() 36 | cmdclass = {} 37 | 38 | cmdclass['install'] = custom_install 39 | 40 | long_description = "A complete Python SSH/SFTP library based on libssh. This "\ 41 | "libraries offers [nearly] complete functionality, "\ 42 | "including elliptic cryptography support." 43 | 44 | setup(name='pysecure', 45 | version='0.11.8',#versioneer.get_version(), 46 | description="A complete Python SSH/SFTP library based on libssh.", 47 | long_description=long_description, 48 | classifiers=['Development Status :: 3 - Alpha', 49 | 'Intended Audience :: Developers', 50 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 51 | 'Programming Language :: Python', 52 | 'Programming Language :: Python :: 2', 53 | 'Programming Language :: Python :: 3', 54 | 'Topic :: System :: Networking', 55 | 'Topic :: Software Development :: Libraries :: Python Modules', 56 | 'Topic :: System :: System Shells', 57 | 'Topic :: Terminals', 58 | ], 59 | keywords='ssh sftp', 60 | author='Dustin Oprea', 61 | author_email='myselfasunder@gmail.com', 62 | url='https://github.com/dsoprea/PySecure', 63 | license='GPL2', 64 | packages=['pysecure', 'pysecure.adapters', 'pysecure.calls', 'pysecure.constants'], 65 | include_package_data=True, 66 | zip_safe=False, 67 | install_requires=[], 68 | scripts=[], 69 | cmdclass=cmdclass, 70 | ), 71 | 72 | -------------------------------------------------------------------------------- /pysecure/test/forward_reverse.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from pysecure import log_config 3 | 4 | from pysecure.test.test_base import connect_ssh_test 5 | 6 | class ForwardReverseTest(TestCase): 7 | def __ssh_cb(self, ssh): 8 | def build_body(status_code, status_string, content): 9 | replacements = { 'scode': status_code, 10 | 'sstring': status_string, 11 | 'length': len(content), 12 | 'content': content } 13 | 14 | return """HTTP/1.1 %(scode)d %(sstring)s 15 | Content-Type: text/html 16 | Content-Length: %(length)d 17 | 18 | %(content)s""" % replacements 19 | 20 | response_helloworld = build_body(200, 'OK', """ 21 | 22 | Hello, World! 23 | 24 | 25 |

Hello, World!

26 | 27 | 28 | """) 29 | 30 | response_notfound = build_body(404, 'Not found', """ 31 | 32 | Not Found 33 | 34 | 35 |

Resource not found.

36 | 37 | 38 | """) 39 | 40 | response_error = build_body(500, 'Server error', """ 41 | 42 | Server Error 43 | 44 | 45 |

There was a server failure.

46 | 47 | 48 | """) 49 | 50 | server_address = None 51 | server_port = 8080 52 | accept_timeout_ms = 60000 53 | 54 | print("Setting listen.") 55 | port = ssh.forward_listen(server_address, server_port) 56 | 57 | print("Waiting for connection.") 58 | 59 | with ssh.forward_accept(accept_timeout_ms) as sc: 60 | while 1: 61 | buffer_ = sc.read(2048) 62 | print(buffer_) 63 | if buffer_ == b'': 64 | continue 65 | 66 | try: 67 | nl_index = buffer_.index(b'\n') 68 | except ValueError: 69 | print("Error with:\n%s" % (len(buffer_))) 70 | payload = response_error 71 | else: 72 | request_line = buffer_[:nl_index] 73 | 74 | if request_line[:6] == b'GET / ': 75 | print("Responding: %s" % (request_line)) 76 | payload = response_helloworld 77 | else: 78 | print("Ignoring: %s" % (request_line)) 79 | payload = response_notfound 80 | 81 | sc.write(payload) 82 | print("Sent answer.") 83 | 84 | def test_forward_reverse(self): 85 | connect_ssh_test(self.__ssh_cb) 86 | 87 | -------------------------------------------------------------------------------- /TODO_FUNCTION_LIST.txt: -------------------------------------------------------------------------------- 1 | int ssh_auth_list (ssh_session session) 2 | enum ssh_keytypes_e ssh_privatekey_type (ssh_private_key privatekey) 3 | int ssh_userauth_list (ssh_session session, const char *username) 4 | int ssh_userauth_none (ssh_session session, const char *username) 5 | 6 | ssh_channel ssh_channel_accept_x11 (ssh_channel channel, int timeout_ms) 7 | int ssh_channel_change_pty_size (ssh_channel channel, int cols, int rows) 8 | int ssh_channel_close (ssh_channel channel) 9 | int ssh_channel_get_exit_status (ssh_channel channel) 10 | int ssh_channel_is_closed (ssh_channel channel) 11 | int ssh_channel_poll (ssh_channel channel, int is_stderr) 12 | int ssh_channel_request_send_signal (ssh_channel channel, const char *sig) 13 | int ssh_channel_request_subsystem (ssh_channel channel, const char *subsys) 14 | int ssh_channel_request_x11 (ssh_channel channel, int single_connection, const char *protocol, const char *cookie, int screen_number) 15 | int ssh_channel_select (ssh_channel *readchans, ssh_channel *writechans, ssh_channel *exceptchans, struct timeval *timeout) 16 | void ssh_channel_set_blocking (ssh_channel channel, int blocking) 17 | ssh_channel ssh_forward_accept (ssh_session session, int timeout_ms) 18 | int ssh_forward_cancel (ssh_session session, const char *address, int port) 19 | int ssh_forward_listen (ssh_session session, const char *address, int port, int *bound_port) 20 | 21 | 22 | int ssh_get_error_code (void *error) 23 | 24 | 25 | int ssh_mkdir (const char *pathname, mode_t mode) 26 | char *ssh_path_expand_tilde (const char *d) 27 | int ssh_timeout_update (struct ssh_timestamp *ts, int timeout) 28 | 29 | int ssh_blocking_flush (ssh_session session, int timeout) 30 | int ssh_is_connected (ssh_session session) 31 | int ssh_is_server_known (ssh_session session) 32 | int ssh_select (ssh_channel *channels, ssh_channel *outchannels, socket_t maxfd, fd_set *readfds, struct timeval *timeout) 33 | void ssh_set_blocking (ssh_session session, int blocking) 34 | 35 | struct ssh_threads_callbacks_struct *ssh_threads_get_noop () 36 | int ssh_threads_set_callbacks (struct ssh_threads_callbacks_struct *cb) 37 | 38 | 39 | 40 | Channel 41 | 42 | int ssh_channel_poll ( ssh_channel channel, int is_stderr ) 43 | int ssh_channel_select ( ssh_channel * readchans, ssh_channel * writechans, ssh_channel * exceptchans, struct timeval * timeout ) 44 | int ssh_channel_request_send_signal ( ssh_channel channel, const char * sig ) 45 | int ssh_channel_request_x11 ( ssh_channel channel, int single_connection, const char * protocol, const char * cookie, int screen_number ) 46 | int ssh_forward_cancel ( ssh_session session, const char * address, int port ) 47 | int ssh_channel_get_exit_status ( ssh_channel channel ) 48 | 49 | SSH 50 | 51 | ssh_message ssh_message_get ( ssh_session session ) 52 | 53 | int ssh_select ( ssh_channel * channels, ssh_channel * outchannels, socket_t maxfd, fd_set * readfds, struct timeval * timeout ) 54 | 55 | 56 | -------------------------------------------------------------------------------- /pysecure/constants/ssh.py: -------------------------------------------------------------------------------- 1 | # ssh_server_known_e 2 | SSH_SERVER_ERROR = -1 3 | SSH_SERVER_NOT_KNOWN = 0 4 | SSH_SERVER_KNOWN_OK = 1 5 | SSH_SERVER_KNOWN_CHANGED = 2 6 | SSH_SERVER_FOUND_OTHER = 3 7 | SSH_SERVER_FILE_NOT_FOUND = 4 8 | 9 | # ssh_options_e 10 | SSH_OPTIONS_HOST = 0x0 11 | SSH_OPTIONS_PORT = 0x1 12 | SSH_OPTIONS_PORT_STR = 0x2 13 | SSH_OPTIONS_FD = 0x3 14 | SSH_OPTIONS_USER = 0x4 15 | SSH_OPTIONS_SSH_DIR = 0x5 16 | SSH_OPTIONS_IDENTITY = 0x6 17 | SSH_OPTIONS_ADD_IDENTITY = 0x7 18 | SSH_OPTIONS_KNOWNHOSTS = 0x8 19 | SSH_OPTIONS_TIMEOUT = 0x9 20 | SSH_OPTIONS_TIMEOUT_USEC = 0xa 21 | SSH_OPTIONS_SSH1 = 0xb 22 | SSH_OPTIONS_SSH2 = 0xc 23 | SSH_OPTIONS_LOG_VERBOSITY = 0xd 24 | SSH_OPTIONS_LOG_VERBOSITY_STR = 0xe 25 | SSH_OPTIONS_CIPHERS_C_S = 0xf 26 | SSH_OPTIONS_CIPHERS_S_C = 0x10 27 | SSH_OPTIONS_COMPRESSION_C_S = 0x11 28 | SSH_OPTIONS_COMPRESSION_S_C = 0x12 29 | SSH_OPTIONS_PROXYCOMMAND = 0x13 30 | SSH_OPTIONS_BINDADDR = 0x14 31 | SSH_OPTIONS_STRICTHOSTKEYCHECK = 0x15 32 | SSH_OPTIONS_COMPRESSION = 0x16 33 | SSH_OPTIONS_COMPRESSION_LEVEL = 0x17 34 | 35 | _OT_STRING = 'string' 36 | _OT_UINT = 'uint' 37 | _OT_INT = 'int' 38 | _OT_LONG = 'long' 39 | _OT_BOOL = 'bool' 40 | 41 | SSH_OPTIONS = { 'user': (SSH_OPTIONS_USER, _OT_STRING), 42 | 'host': (SSH_OPTIONS_HOST, _OT_STRING), 43 | 'verbosity': (SSH_OPTIONS_LOG_VERBOSITY, _OT_UINT), 44 | 'port': (SSH_OPTIONS_PORT, _OT_UINT), 45 | 'fd': (SSH_OPTIONS_FD, _OT_INT), 46 | 'ssh_dir': (SSH_OPTIONS_SSH_DIR, _OT_STRING), 47 | 'identity': (SSH_OPTIONS_IDENTITY, _OT_STRING), 48 | 'add_identity': (SSH_OPTIONS_ADD_IDENTITY, None), 49 | 'knownhosts': (SSH_OPTIONS_KNOWNHOSTS, _OT_STRING), 50 | 'timeout': (SSH_OPTIONS_TIMEOUT, _OT_LONG), 51 | 'timeout_usec': (SSH_OPTIONS_TIMEOUT_USEC, _OT_LONG), 52 | 'ssh1': (SSH_OPTIONS_SSH1, _OT_BOOL), 53 | 'ssh2': (SSH_OPTIONS_SSH2, _OT_BOOL), 54 | 'cipherscs': (SSH_OPTIONS_CIPHERS_C_S, _OT_STRING), 55 | 'cipherssc': (SSH_OPTIONS_CIPHERS_S_C, _OT_STRING), 56 | 'compresscs': (SSH_OPTIONS_COMPRESSION_C_S, _OT_STRING), 57 | 'compresssc': (SSH_OPTIONS_COMPRESSION_S_C, _OT_STRING), 58 | 'proxycmd': (SSH_OPTIONS_PROXYCOMMAND, _OT_STRING), 59 | 'bindaddr': (SSH_OPTIONS_BINDADDR, _OT_STRING), 60 | 'stricthostkeys': (SSH_OPTIONS_STRICTHOSTKEYCHECK, _OT_BOOL), 61 | 'compression': (SSH_OPTIONS_COMPRESSION, _OT_STRING), 62 | 'compression_n': (SSH_OPTIONS_COMPRESSION_LEVEL, _OT_INT) } 63 | 64 | # ssh_auth_e 65 | SSH_AUTH_ERROR = -1 66 | SSH_AUTH_SUCCESS = 0 67 | SSH_AUTH_DENIED = 1 68 | SSH_AUTH_PARTIAL = 2 69 | SSH_AUTH_INFO = 3 70 | SSH_AUTH_AGAIN = 4 71 | 72 | # Return codes. 73 | SSH_OK = 0 74 | SSH_ERROR = -1 75 | SSH_AGAIN = -2 76 | SSH_EOF = -127 77 | 78 | # ssh_error_types_e 79 | SSH_NO_ERROR = 0 80 | SSH_REQUEST_DENIED = 1 81 | SSH_FATAL = 2 82 | SSH_EINTR = 3 83 | 84 | # Status flags. 85 | SSH_CLOSED = 0x01 86 | SSH_READ_PENDING = 0x02 87 | SSH_WRITE_PENDING = 0x04 88 | SSH_CLOSED_ERROR = 0x08 89 | 90 | -------------------------------------------------------------------------------- /pysecure/calls/channeli.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | 3 | from pysecure.library import libssh 4 | from pysecure.types import * 5 | 6 | # ssh_channel ssh_channel_new(ssh_session session) 7 | c_ssh_channel_new = libssh.ssh_channel_new 8 | c_ssh_channel_new.argtypes = [c_ssh_session] 9 | c_ssh_channel_new.restype = c_ssh_channel 10 | 11 | # int ssh_channel_open_forward(ssh_channel channel, const char *remotehost,int remoteport, const char *sourcehost, int localport) 12 | c_ssh_channel_open_forward = libssh.ssh_channel_open_forward 13 | c_ssh_channel_open_forward.argtypes = [c_ssh_channel, c_char_p, c_int, c_char_p, c_int] 14 | c_ssh_channel_open_forward.restype = c_int 15 | 16 | # void ssh_channel_free(ssh_channel channel) 17 | c_ssh_channel_free = libssh.ssh_channel_free 18 | c_ssh_channel_free.argtypes = [c_ssh_channel] 19 | c_ssh_channel_free.restype = None 20 | 21 | # int ssh_channel_write(ssh_channel channel, const void *data, uint32_t len) 22 | c_ssh_channel_write = libssh.ssh_channel_write 23 | c_ssh_channel_write.argtypes = [c_ssh_channel, c_void_p, c_uint32] 24 | c_ssh_channel_write.restype = c_int 25 | 26 | # int ssh_channel_read(ssh_channel channel, void *dest, uint32_t count, int is_stderr) 27 | c_ssh_channel_read = libssh.ssh_channel_read 28 | c_ssh_channel_read.argtypes = [c_ssh_channel, c_void_p, c_uint32, c_int] 29 | c_ssh_channel_read.restype = c_int 30 | 31 | # int ssh_channel_send_eof(ssh_channel channel) 32 | c_ssh_channel_send_eof = libssh.ssh_channel_send_eof 33 | c_ssh_channel_send_eof.argtypes = [c_ssh_channel] 34 | c_ssh_channel_send_eof.restype = c_int 35 | 36 | # int ssh_channel_is_open(ssh_channel channel) 37 | c_ssh_channel_is_open = libssh.ssh_channel_is_open 38 | c_ssh_channel_is_open.argtypes = [c_ssh_channel] 39 | c_ssh_channel_is_open.restype = c_int 40 | 41 | # LIBSSH_API int ssh_channel_open_session(ssh_channel channel); 42 | c_ssh_channel_open_session = libssh.ssh_channel_open_session 43 | c_ssh_channel_open_session.argtypes = [c_ssh_channel] 44 | c_ssh_channel_open_session.restype = c_int 45 | 46 | # int ssh_channel_request_exec(ssh_channel channel, const char *cmd) 47 | c_ssh_channel_request_exec = libssh.ssh_channel_request_exec 48 | c_ssh_channel_request_exec.argtypes = [c_ssh_channel, c_char_p] 49 | c_ssh_channel_request_exec.restype = c_int 50 | 51 | # int ssh_channel_request_shell(ssh_channel channel) 52 | c_ssh_channel_request_shell = libssh.ssh_channel_request_shell 53 | c_ssh_channel_request_shell.argtypes = [c_ssh_channel] 54 | c_ssh_channel_request_shell.restype = c_int 55 | 56 | # int ssh_channel_request_pty(ssh_channel channel) 57 | c_ssh_channel_request_pty = libssh.ssh_channel_request_pty 58 | c_ssh_channel_request_pty.argtypes = [c_ssh_channel] 59 | c_ssh_channel_request_pty.restype = c_int 60 | 61 | # int ssh_channel_change_pty_size(ssh_channel channel, int col, int row) 62 | c_ssh_channel_change_pty_size = libssh.ssh_channel_change_pty_size 63 | c_ssh_channel_change_pty_size.argtypes = [c_ssh_channel, c_int, c_int] 64 | c_ssh_channel_change_pty_size.restype = c_int 65 | 66 | # int ssh_channel_is_eof(ssh_channel channel) 67 | c_ssh_channel_is_eof = libssh.ssh_channel_is_eof 68 | c_ssh_channel_is_eof.argtypes = [c_ssh_channel] 69 | c_ssh_channel_is_eof.restype = c_int 70 | 71 | # int ssh_channel_read_nonblocking(ssh_channel channel, void *dest, uint32_t count, int is_stderr) 72 | c_ssh_channel_read_nonblocking = libssh.ssh_channel_read_nonblocking 73 | c_ssh_channel_read_nonblocking.argtypes = [c_ssh_channel, c_void_p, c_uint32, c_int] 74 | c_ssh_channel_read_nonblocking.restype = c_int 75 | 76 | # int ssh_channel_request_env(ssh_channel channel, const char *name, const char *value) 77 | c_ssh_channel_request_env = libssh.ssh_channel_request_env 78 | c_ssh_channel_request_env.argtypes = [c_ssh_channel, c_char_p, c_char_p] 79 | c_ssh_channel_request_env.restype = c_int 80 | 81 | # ssh_session ssh_channel_get_session(ssh_channel channel) 82 | c_ssh_channel_get_session = libssh.ssh_channel_get_session 83 | c_ssh_channel_get_session.argtypes = [c_ssh_channel] 84 | c_ssh_channel_get_session.restype = c_ssh_session 85 | 86 | # ssh_channel ssh_channel_accept_x11(ssh_channel channel, int timeout_ms) 87 | c_ssh_channel_accept_x11 = libssh.ssh_channel_accept_x11 88 | c_ssh_channel_accept_x11.argtypes = [c_ssh_channel, c_int] 89 | c_ssh_channel_accept_x11.restype = c_ssh_channel 90 | 91 | # int ssh_channel_request_x11(ssh_channel channel, int single_connection, const char *protocol, const char *cookie, int screen_number) 92 | c_ssh_channel_request_x11 = libssh.ssh_channel_request_x11 93 | c_ssh_channel_request_x11.argtypes = [c_ssh_channel, c_int, c_char_p, c_char_p, c_int] 94 | c_ssh_channel_request_x11.restype = c_int 95 | 96 | -------------------------------------------------------------------------------- /pysecure/types.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | from ctypes import * 4 | from datetime import datetime 5 | 6 | from pysecure.constants import TIME_DATETIME_FORMAT 7 | from pysecure.constants.sftp import SSH_FILEXFER_TYPE_REGULAR, \ 8 | SSH_FILEXFER_TYPE_DIRECTORY, \ 9 | SSH_FILEXFER_TYPE_SYMLINK, \ 10 | SSH_FILEXFER_TYPE_SPECIAL, \ 11 | SSH_FILEXFER_TYPE_UNKNOWN 12 | 13 | c_mode_t = c_int 14 | c_uid_t = c_uint32 15 | c_gid_t = c_uint32 16 | 17 | # This are very-very unpredictable. We can only hope that this holds up for 18 | # most systems. 19 | 20 | # Returns something like "32bit" or "64bit". 21 | arch_name = platform.architecture()[0] 22 | arch_width = int(arch_name[0:2]) 23 | 24 | if arch_width == 64: 25 | c_time_t = c_uint64 26 | c_suseconds_t = c_uint64 27 | else: 28 | c_time_t = c_uint32 29 | c_suseconds_t = c_uint32 30 | 31 | 32 | class _CSftpAttributesStruct(Structure): 33 | _fields_ = [('name', c_char_p), 34 | ('longname', c_char_p), 35 | ('flags', c_uint32), 36 | ('type', c_uint8), 37 | ('size', c_uint64), 38 | ('uid', c_uint32), 39 | ('gid', c_uint32), 40 | ('owner', c_char_p), 41 | ('group', c_char_p), 42 | ('permissions', c_uint32), 43 | ('atime64', c_uint64), 44 | ('atime', c_uint32), 45 | ('atime_nseconds', c_uint32), 46 | ('createtime', c_uint64), 47 | ('createtime_nseconds', c_uint32), 48 | ('mtime64', c_uint64), 49 | ('mtime', c_uint32), 50 | ('mtime_nseconds', c_uint32), 51 | ('acl', c_void_p), # NI: ssh_string 52 | ('extended_count', c_uint32), 53 | ('extended_type', c_void_p), # NI: ssh_string 54 | ('extended_data', c_void_p)] # NI: ssh_string 55 | 56 | def __repr__(self): 57 | mtime_phrase = datetime.fromtimestamp(self.mtime).\ 58 | strftime(TIME_DATETIME_FORMAT) 59 | 60 | return ('' % 61 | (self.name, self.size, self.type, mtime_phrase)) 62 | 63 | @property 64 | def is_regular(self): 65 | return self.type == SSH_FILEXFER_TYPE_REGULAR 66 | 67 | @property 68 | def is_directory(self): 69 | return self.type == SSH_FILEXFER_TYPE_DIRECTORY 70 | 71 | @property 72 | def is_symlink(self): 73 | return self.type == SSH_FILEXFER_TYPE_SYMLINK 74 | 75 | @property 76 | def is_special(self): 77 | return self.type == SSH_FILEXFER_TYPE_SPECIAL 78 | 79 | @property 80 | def is_unknown_type(self): 81 | return self.type == SSH_FILEXFER_TYPE_UNKNOWN 82 | 83 | @property 84 | def modified_time(self): 85 | # TODO: We're not sure if the mtime64 value is available on a 32-bit platform. We do this to be safe. 86 | return self.mtime64 if self.mtime64 else self.mtime 87 | 88 | @property 89 | def modified_time_dt(self): 90 | if self.mtime64: 91 | return datetime.fromtimestamp(self.mtime64) 92 | else: 93 | return datetime.fromtimestamp(self.mtime) 94 | 95 | _CSftpAttributes = POINTER(_CSftpAttributesStruct) 96 | 97 | 98 | class CTimeval(Structure): 99 | # it was easier to set these types based on what libssh assigns to them. 100 | # The traditional definition leaves some platform ambiguity. 101 | _fields_ = [('tv_sec', c_uint32), 102 | ('tv_usec', c_uint32)] 103 | 104 | c_timeval = CTimeval 105 | 106 | class _CSshKeyStruct(Structure): 107 | _fields_ = [('type', c_int), 108 | ('flags', c_int), 109 | ('type_c', c_char_p), 110 | ('ecdsa_nid', c_int), 111 | ('dsa', c_void_p), 112 | ('rsa', c_void_p), 113 | ('ecdsa', c_void_p), 114 | ('cert', c_void_p)] 115 | 116 | # Fortunately, we should probably be able to avoid most/all of the mechanics 117 | # for the vast number of structs. 118 | 119 | c_ssh_session = c_void_p #POINTER(CSshSessionStruct) 120 | c_ssh_channel = c_void_p 121 | c_sftp_session = c_void_p 122 | c_sftp_attributes = _CSftpAttributes 123 | c_sftp_dir = c_void_p 124 | c_sftp_file = c_void_p 125 | c_ssh_key = POINTER(_CSshKeyStruct) 126 | 127 | # A simple aliasing assignment doesn't work, here. 128 | # c_sftp_statvfs = c_void_p 129 | 130 | -------------------------------------------------------------------------------- /pysecure/easy.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import contextlib 3 | 4 | from pysecure.adapters.ssha import SshSession, SshConnect, SshSystem, \ 5 | PublicKeyHash, ssh_pki_import_privkey_file 6 | from pysecure.adapters.sftpa import SftpSession, SftpFile 7 | 8 | @contextlib.contextmanager 9 | def connect_ssh(user, host, auth_cb, allow_new=True, *args, **kwargs): 10 | with SshSystem(): 11 | with SshSession(user=user, host=host, *args, **kwargs) as ssh: 12 | with SshConnect(ssh): 13 | logging.debug("Ready to authenticate.") 14 | 15 | ssh.is_server_known(allow_new=allow_new) 16 | 17 | auth_cb(ssh) 18 | yield ssh 19 | 20 | def connect_ssh_with_cb(ssh_cb, user, host, auth_cb, allow_new=True, 21 | verbosity=0): 22 | """A "managed" SSH session. When the session is ready, we'll invoke the 23 | "ssh_cb" callback. 24 | """ 25 | 26 | with connect_ssh(user, host, auth_cb, allow_new=True, verbosity=0) as ssh: 27 | ssh_cb(ssh) 28 | 29 | @contextlib.contextmanager 30 | def _connect_sftp(ssh, *args, **kwargs): 31 | """A "managed" SFTP session. When the SSH session and an additional SFTP 32 | session are ready, invoke the sftp_cb callback. 33 | """ 34 | 35 | with SftpSession(ssh) as sftp: 36 | yield (ssh, sftp) 37 | 38 | # TODO(dustin): Deprecate this call. 39 | def connect_sftp_with_cb(sftp_cb, *args, **kwargs): 40 | """A "managed" SFTP session. When the SSH session and an additional SFTP 41 | session are ready, invoke the sftp_cb callback. 42 | """ 43 | 44 | with _connect_sftp(*args, **kwargs) as (ssh, sftp): 45 | sftp_cb(ssh, sftp) 46 | 47 | def get_key_auth_cb(key_filepath): 48 | """This is just a convenience function for key-based login.""" 49 | 50 | def auth_cb(ssh): 51 | key = ssh_pki_import_privkey_file(key_filepath) 52 | ssh.userauth_publickey(key) 53 | 54 | return auth_cb 55 | 56 | def get_password_auth_cb(password): 57 | """This is just a convenience function for password-based login.""" 58 | 59 | def auth_cb(ssh): 60 | ssh.userauth_password(password) 61 | 62 | return auth_cb 63 | 64 | class EasySsh(object): 65 | """This class allows a connection to be opened and closed at two separate 66 | points (as opposed to the callback methods, above). 67 | """ 68 | 69 | def __init__(self, user, host, auth_cb, allow_new=True, **session_args): 70 | self.__user = user 71 | self.__host = host 72 | self.__auth_cb = auth_cb 73 | self.__allow_new = allow_new 74 | self.__session_args = session_args 75 | 76 | self.__log = logging.getLogger('EasySsh') 77 | 78 | self.__ssh_session = None 79 | self.__ssh_opened = False 80 | 81 | self.__sftp_session = None 82 | self.__sftp_opened = False 83 | 84 | def __del__(self): 85 | if self.__ssh_opened is True: 86 | self.close_ssh() 87 | 88 | def open_ssh(self): 89 | self.__log.debug("Opening SSH.") 90 | 91 | if self.__ssh_opened is True: 92 | raise Exception("Can not open SFTP session that is already open.") 93 | 94 | # TODO: This might be required to only be run once, globally. 95 | self.__system = SshSystem() 96 | self.__system.open() 97 | 98 | self.__ssh_session = SshSession(user=self.__user, host=self.__host, 99 | **self.__session_args) 100 | self.__ssh_session.open() 101 | 102 | self.__connect = SshConnect(self.__ssh_session) 103 | self.__connect.open() 104 | 105 | self.__ssh_session.is_server_known(allow_new=self.__allow_new) 106 | self.__auth_cb(self.__ssh_session) 107 | 108 | self.__ssh_opened = True 109 | 110 | def close_ssh(self): 111 | self.__log.debug("Closing SSH.") 112 | 113 | if self.__ssh_opened is False: 114 | raise Exception("Can not close SSH session that is not currently " 115 | "opened.") 116 | 117 | if self.__sftp_opened is True: 118 | self.close_sftp() 119 | 120 | self.__connect.close() 121 | self.__ssh_session.close() 122 | self.__system.close() 123 | 124 | self.__ssh_session = None 125 | self.__ssh_opened = False 126 | 127 | def open_sftp(self): 128 | self.__log.debug("Opening SFTP.") 129 | 130 | if self.__sftp_opened is True: 131 | raise Exception("Can not open SFTP session that is already open.") 132 | 133 | self.__sftp_session = SftpSession(self.__ssh_session) 134 | self.__sftp_session.open() 135 | 136 | self.__sftp_opened = True 137 | 138 | def close_sftp(self): 139 | self.__log.debug("Closing SFTP.") 140 | 141 | if self.__sftp_opened is False: 142 | raise Exception("Can not close SFTP session that is not currently " 143 | "opened.") 144 | 145 | self.__sftp_session.close() 146 | 147 | self.__sftp_session = None 148 | self.__sftp_opened = False 149 | 150 | @property 151 | def ssh(self): 152 | if self.__ssh_opened is False: 153 | raise Exception("Can not return an SSH session. A session is not " 154 | "open.") 155 | 156 | return self.__ssh_session 157 | 158 | @property 159 | def sftp(self): 160 | if self.__sftp_opened is False: 161 | raise Exception("Can not return an SFTP session. A session is not " 162 | "open.") 163 | 164 | return self.__sftp_session 165 | 166 | -------------------------------------------------------------------------------- /pysecure/utility.py: -------------------------------------------------------------------------------- 1 | from sys import stdout 2 | from collections import deque 3 | from os import listdir, stat, lstat 4 | from os.path import basename, isfile, isdir, islink 5 | from stat import S_ISCHR, S_ISBLK, S_ISREG, S_ISLNK 6 | from os import SEEK_SET, SEEK_CUR, SEEK_END 7 | 8 | from pysecure.exceptions import SshNonblockingTryAgainException 9 | 10 | def dumphex(data): 11 | data_len = len(data) 12 | row_size = 16 13 | 14 | i = 0 15 | while i < data_len: 16 | stdout.write('%05X:' % (i)) 17 | 18 | # Display bytes as hex. 19 | 20 | j = 0 21 | while j < row_size: 22 | index = i + j 23 | 24 | if j == 8: 25 | stdout.write(' ') 26 | 27 | try: 28 | stdout.write(' %02X' % (ord(data[index]))) 29 | except IndexError: 30 | stdout.write(' ') 31 | 32 | j += 1 33 | 34 | stdout.write(' ') 35 | 36 | # Display bytes as ASCII. 37 | 38 | j = 0 39 | while j < row_size: 40 | index = i + j 41 | 42 | try: 43 | byte = data[index] 44 | except IndexError: 45 | break 46 | else: 47 | if ord(byte) < 32: 48 | byte = '.' 49 | 50 | stdout.write('%s' % (byte)) 51 | 52 | j += 1 53 | 54 | # print 55 | 56 | i += row_size 57 | 58 | def sync(cb): 59 | """A function that will repeatedly invoke a callback until it doesn't 60 | return a try-again error. 61 | """ 62 | 63 | while 1: 64 | try: 65 | cb() 66 | except SshNonblockingTryAgainException: 67 | pass 68 | else: 69 | break 70 | 71 | def stat_is_regular(attr): 72 | return S_ISREG(attr.st_mode) 73 | 74 | def stat_is_special(attr): 75 | return S_ISCHR(attr.st_mode) or S_ISBLK(attr.st_mode) 76 | 77 | def stat_is_symlink(attr): 78 | return S_ISLNK(attr.st_mode) 79 | 80 | def local_recurse(path, dir_cb, listing_cb, max_listing_size=0, 81 | max_depth=None): 82 | 83 | def get_flags_from_attr(attr): 84 | return (stat_is_regular(attr), 85 | stat_is_symlink(attr), 86 | stat_is_special(attr)) 87 | 88 | q = deque([(path, 0)]) 89 | while q: 90 | (path, current_depth) = q.popleft() 91 | 92 | entries = listdir(path) 93 | collected = [] 94 | 95 | def push_entry(entry): 96 | collected.append(entry) 97 | if max_listing_size > 0 and \ 98 | max_listing_size <= len(collected): 99 | listing_cb(path, collected) 100 | del collected[:] 101 | 102 | def push_entry_with_filepath(file_path, name, is_link): 103 | attr = lstat(file_path) if is_link else stat(file_path) 104 | entry = (name, 105 | int(attr.st_mtime), 106 | attr.st_size, 107 | get_flags_from_attr(attr)) 108 | 109 | push_entry(entry) 110 | 111 | for name in entries: 112 | file_path = ('%s/%s' % (path, name)) 113 | # print("ENTRY: %s" % (file_path)) 114 | 115 | if islink(file_path): 116 | if listing_cb is not None: 117 | push_entry_with_filepath(file_path, name, True) 118 | elif isdir(file_path): 119 | if name == '.' or name == '..': 120 | continue 121 | 122 | if dir_cb is not None: 123 | dir_cb(path, file_path, name) 124 | 125 | new_depth = current_depth + 1 126 | 127 | if max_depth is not None and max_depth >= new_depth: 128 | q.append((file_path, new_depth)) 129 | elif isfile(file_path): 130 | if listing_cb is not None: 131 | push_entry_with_filepath(file_path, name, False) 132 | 133 | if listing_cb is not None and max_listing_size == 0 or \ 134 | len(collected) > 0: 135 | listing_cb(path, collected) 136 | 137 | def bytify(s): 138 | if issubclass(s.__class__, str): 139 | return s.encode('ascii') 140 | else: 141 | return s 142 | 143 | def stringify(s): 144 | if issubclass(s.__class__, (bytes, bytearray)): 145 | return s.decode('ascii') 146 | else: 147 | return s 148 | 149 | 150 | class ByteStream(object): 151 | def __init__(self): 152 | self.__array = bytearray() 153 | self.__position = 0 154 | 155 | def write(self, buf_): 156 | assert issubclass(buf_.__class__, bytes) 157 | 158 | self.__array.extend(buf_) 159 | self.__position = len(self.__array) 160 | 161 | def read(self, count): 162 | slice_ = self.__array[self.__position:self.__position + count] 163 | self.__position += count 164 | 165 | return bytes(slice_) 166 | 167 | def seek(self, position, whence=SEEK_SET): 168 | if whence == SEEK_CUR: 169 | position_final = self.__position + position 170 | elif whence == SEEK_END: 171 | position_final = self.__position - position 172 | else: 173 | position_final = position 174 | 175 | if position >= len(self.__array): 176 | raise Exception("[Final] position (%d) exceeds buffer length (%d) " 177 | "for seek-type (%d). Argument was (%d)." % 178 | (position_final, len(self.__array), whence, 179 | position)) 180 | 181 | self.__position = position_final 182 | 183 | def tell(self): 184 | return self.__position 185 | 186 | def get_array(self): 187 | return self.__array 188 | 189 | def get_bytes(self): 190 | return bytes(self.__array) 191 | 192 | -------------------------------------------------------------------------------- /pysecure/calls/sshi.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | 3 | from pysecure.library import libssh 4 | from pysecure.types import * 5 | 6 | # Auxiliary calls. 7 | 8 | c_strerror = libssh.strerror 9 | c_free = libssh.free 10 | 11 | # Function calls. 12 | 13 | # LIBSSH_API ssh_session ssh_new(void); 14 | c_ssh_new = libssh.ssh_new 15 | c_ssh_new.argtypes = [] 16 | c_ssh_new.restype = c_ssh_session 17 | 18 | # LIBSSH_API int ssh_options_set(ssh_session session, enum ssh_options_e type, const void *value); 19 | c_ssh_options_set = libssh.ssh_options_set 20 | c_ssh_options_set.argtypes = [c_ssh_session, c_int, c_void_p] 21 | c_ssh_options_set.restype = c_int 22 | 23 | # LIBSSH_API void ssh_free(ssh_session session); 24 | c_ssh_free = libssh.ssh_free 25 | c_ssh_free.argtypes = [c_ssh_session] 26 | c_ssh_free.restype = None 27 | 28 | # LIBSSH_API int ssh_connect(ssh_session session); 29 | c_ssh_connect = libssh.ssh_connect 30 | c_ssh_connect.argtypes = [c_ssh_session] 31 | c_ssh_connect.restype = c_int 32 | 33 | # LIBSSH_API void ssh_disconnect(ssh_session session); 34 | c_ssh_disconnect = libssh.ssh_disconnect 35 | c_ssh_disconnect.argtypes = [c_ssh_session] 36 | c_ssh_disconnect.restype = None 37 | 38 | # LIBSSH_API int ssh_is_server_known(ssh_session session); 39 | c_ssh_is_server_known = libssh.ssh_is_server_known 40 | c_ssh_is_server_known.argtypes = [c_ssh_session] 41 | c_ssh_is_server_known.restype = c_int 42 | 43 | # LIBSSH_API int ssh_get_pubkey_hash(ssh_session session, unsigned char **hash); 44 | c_ssh_get_pubkey_hash = libssh.ssh_get_pubkey_hash 45 | c_ssh_get_pubkey_hash.argtypes = [c_ssh_session, POINTER(POINTER(c_ubyte))] 46 | c_ssh_get_pubkey_hash.restype = c_int 47 | 48 | # LIBSSH_API char *ssh_get_hexa(const unsigned char *what, size_t len); 49 | c_ssh_get_hexa = libssh.ssh_get_hexa 50 | c_ssh_get_hexa.argtypes = [POINTER(c_ubyte), c_size_t] 51 | c_ssh_get_hexa.restype = c_void_p 52 | 53 | # LIBSSH_API void ssh_print_hexa(const char *descr, const unsigned char *what, size_t len); 54 | c_ssh_print_hexa = libssh.ssh_print_hexa 55 | c_ssh_print_hexa.argtypes = [c_char_p, POINTER(c_ubyte), c_size_t] 56 | c_ssh_print_hexa.restype = None 57 | 58 | # LIBSSH_API int ssh_write_knownhost(ssh_session session); 59 | c_ssh_write_knownhost = libssh.ssh_write_knownhost 60 | c_ssh_write_knownhost.argtypes = [c_ssh_session] 61 | c_ssh_write_knownhost.restype = c_int 62 | 63 | # LIBSSH_API int ssh_userauth_privatekey_file(ssh_session session, const char *username, const char *filename, const char *passphrase); 64 | c_ssh_userauth_privatekey_file = libssh.ssh_userauth_privatekey_file 65 | c_ssh_userauth_privatekey_file.argtypes = [c_ssh_session, c_char_p, c_char_p, c_char_p] 66 | c_ssh_userauth_privatekey_file.restype = c_int 67 | 68 | # int ssh_userauth_password (ssh_session session, const char *username, const char *password) 69 | c_ssh_userauth_password = libssh.ssh_userauth_password 70 | c_ssh_userauth_password.argtypes = [c_ssh_session, c_char_p, c_char_p] 71 | c_ssh_userauth_password.restype = c_int 72 | 73 | # int ssh_get_error_code (void *error) 74 | c_ssh_get_error_code = libssh.ssh_get_error_code 75 | c_ssh_get_error_code.argtypes = [c_ssh_session] 76 | c_ssh_get_error_code.restype = c_int 77 | 78 | # const char* ssh_get_error ( void * error) 79 | c_ssh_get_error = libssh.ssh_get_error 80 | c_ssh_get_error.argtypes = [c_ssh_session] 81 | c_ssh_get_error.restype = c_char_p 82 | 83 | # int ssh_init(void) 84 | c_ssh_init = libssh.ssh_init 85 | c_ssh_init.argtypes = [] 86 | c_ssh_init.restype = c_int 87 | 88 | # int ssh_finalize(void) 89 | c_ssh_finalize = libssh.ssh_finalize 90 | c_ssh_finalize.argtypes = [] 91 | c_ssh_finalize.restype = c_int 92 | 93 | # int ssh_forward_listen(ssh_session session, const char *address, int port, int *bound_port) 94 | c_ssh_forward_listen = libssh.ssh_forward_listen 95 | c_ssh_forward_listen.argtypes = [c_ssh_session, c_char_p, c_int, POINTER(c_int)] 96 | c_ssh_forward_listen.restype = c_int 97 | 98 | # ssh_channel ssh_forward_accept(ssh_session session, int timeout_ms) 99 | c_ssh_forward_accept = libssh.ssh_forward_accept 100 | c_ssh_forward_accept.argtypes = [c_ssh_session, c_int] 101 | c_ssh_forward_accept.restype = c_ssh_channel 102 | 103 | # ssh_key ssh_key_new(void) 104 | c_ssh_key_new = libssh.ssh_key_new 105 | c_ssh_key_new.argtypes = [] 106 | c_ssh_key_new.restype = c_ssh_key 107 | 108 | # int ssh_userauth_publickey(ssh_session session, const char *username, const ssh_key privkey) 109 | c_ssh_userauth_publickey = libssh.ssh_userauth_publickey 110 | c_ssh_userauth_publickey.argtypes = [c_ssh_session, c_char_p, c_ssh_key] 111 | c_ssh_userauth_publickey.restype = c_int 112 | 113 | # void ssh_key_free(ssh_key key) 114 | c_ssh_key_free = libssh.ssh_key_free 115 | c_ssh_key_free.argtypes = [c_ssh_key] 116 | c_ssh_key_free.restype = None 117 | 118 | # void ssh_set_blocking(ssh_session session, int blocking) 119 | c_ssh_set_blocking = libssh.ssh_set_blocking 120 | c_ssh_set_blocking.argtypes = [c_ssh_session, c_int] 121 | c_ssh_set_blocking.restype = None 122 | 123 | # int ssh_is_blocking(ssh_session session) 124 | c_ssh_is_blocking = libssh.ssh_is_blocking 125 | c_ssh_is_blocking.argtypes = [c_ssh_session] 126 | c_ssh_is_blocking.restype = c_int 127 | 128 | # Added support in 0.6.0 129 | # int ssh_pki_import_privkey_file(const char *filename, const char *passphrase, ssh_auth_callback auth_fn, void *auth_data, ssh_key *pkey) 130 | c_ssh_pki_import_privkey_file = libssh.ssh_pki_import_privkey_file 131 | c_ssh_pki_import_privkey_file.argtypes = [c_char_p, c_char_p, c_void_p, c_void_p, POINTER(c_ssh_key)] 132 | c_ssh_pki_import_privkey_file.restype = c_int 133 | 134 | # const char* ssh_get_disconnect_message ( ssh_session session ) 135 | c_ssh_get_disconnect_message = libssh.ssh_get_disconnect_message 136 | c_ssh_get_disconnect_message.argtypes = [c_ssh_session] 137 | c_ssh_get_disconnect_message.restype = c_char_p 138 | 139 | # char* ssh_get_issue_banner(ssh_session session) 140 | c_ssh_get_issue_banner = libssh.ssh_get_issue_banner 141 | c_ssh_get_issue_banner.argtypes = [c_ssh_session] 142 | c_ssh_get_issue_banner.restype = c_char_p 143 | 144 | # int ssh_get_openssh_version(ssh_session session) 145 | c_ssh_get_openssh_version = libssh.ssh_get_openssh_version 146 | c_ssh_get_openssh_version.argtypes = [c_ssh_session] 147 | c_ssh_get_openssh_version.restype = c_int 148 | 149 | # int ssh_get_status(ssh_session session) 150 | c_ssh_get_status = libssh.ssh_get_status 151 | c_ssh_get_status.argtypes = [c_ssh_session] 152 | c_ssh_get_status.restype = c_int 153 | 154 | # int ssh_get_version(ssh_session session) 155 | c_ssh_get_version = libssh.ssh_get_version 156 | c_ssh_get_version.argtypes = [c_ssh_session] 157 | c_ssh_get_version.restype = c_int 158 | 159 | # Added support in 0.6.0 160 | # const char* ssh_get_serverbanner(ssh_session session) 161 | c_ssh_get_serverbanner = libssh.ssh_get_serverbanner 162 | c_ssh_get_serverbanner.argtypes = [c_ssh_session] 163 | c_ssh_get_serverbanner.restype = c_char_p 164 | 165 | # void ssh_disconnect(ssh_session session) 166 | c_ssh_disconnect = libssh.ssh_disconnect 167 | c_ssh_disconnect.argtypes = [c_ssh_session] 168 | c_ssh_disconnect.restype = None 169 | 170 | # struct ssh_threads_callbacks_struct *ssh_threads_get_noop() 171 | c_ssh_threads_get_noop = libssh.ssh_threads_get_noop 172 | c_ssh_threads_get_noop.argtypes = [] 173 | c_ssh_threads_get_noop.restype = c_void_p 174 | 175 | # int ssh_threads_set_callbacks (struct ssh_threads_callbacks_struct *cb) 176 | c_ssh_threads_set_callbacks = libssh.ssh_threads_set_callbacks 177 | c_ssh_threads_set_callbacks.argtypes = [c_void_p] 178 | c_ssh_threads_set_callbacks.restype = c_int 179 | 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Introduction 2 | ------------ 3 | 4 | This is a library that provides SSH and SFTP functionality from within Python, 5 | using "libssh". Although libssh2 was considered, it appears that its SFTP 6 | functionality is slower (1). 7 | 8 | This solution exists as an alternative to Paramiko. I love Paramiko, but as 9 | "libssh" is very complete and actively-maintained, it has a greater breadth of 10 | functionality, such as support for elliptic-curve encryption (recently added). 11 | It is also written in C. 12 | 13 | This project is in active development. It -is- Python3 compatible. 14 | 15 | 16 | (1) http://daniel.haxx.se/blog/2010/12/05/re-evaluating-the-criticism/ 17 | 18 | 19 | Status 20 | ------ 21 | 22 | | Done | Description | 23 | |:----:| ----------- | 24 | | X | SFTP functionality | 25 | | X | Local port forwarding | 26 | | X | Reverse port forwarding | 27 | | X | Remote command (single commands) | 28 | | X | Remote execution (shell session) | 29 | | | Threading support | 30 | | | Support X11 forwarding (waiting on libssh) | 31 | | X | Added SFTP "mirror" functionality | 32 | 33 | 34 | Remote execution is currently broken in libssh 0.6.0, but our library is no 35 | longer compatible with 0.5.5 (key-related changes involved new/changed 36 | functions in libssh). 37 | 38 | 39 | Dependencies 40 | ------------ 41 | 42 | - libssh 0.6.0rc1 43 | 44 | 45 | Installing 46 | ---------- 47 | 48 | Just expand, and make sure PYTHONPATH includes the directory. 49 | 50 | NOTE: Though this project is on PyPI, it's -highly- recommended to use 51 | "easy_install" to get it, rather than "pip". The latter has the tendency 52 | to not get the latest version. 53 | 54 | 55 | Logging 56 | ------- 57 | 58 | To allow for standard logging to go out to the console, import 59 | "pysecure.log_config". 60 | 61 | To enable "debug" logging, set the environment variable 62 | "DEBUG" to "1". 63 | 64 | To enable debug verbosity from the "libssh" library, pass the 65 | "verbosity" argument into the connect_* functions with a value of True. 66 | 67 | 68 | Common Setup Code for Examples 69 | ------------------------------ 70 | 71 | To make the examples more concise, some code has been removed, so as to not be 72 | repeated in every case. 73 | 74 | A complete, working example using some included convenience functions would 75 | look like the following: 76 | 77 | ```python 78 | from pysecure.easy import connect_ssh, connect_sftp, get_key_auth_cb 79 | 80 | user = 'dustin' 81 | host = 'localhost' 82 | key_filepath = '/home/dustin/.ssh/id_dsa' 83 | 84 | auth_cb = get_key_auth_cb(key_filepath) 85 | 86 | # For simple SSH functionality. 87 | 88 | with connect_ssh(user, host, auth_cb) as ssh: 89 | # Main logic, here. 90 | pass 91 | 92 | # Or, for SFTP-enabled SSH functionality. 93 | 94 | with connect_sftp(user, host, auth_cb) as (ssh, sftp): 95 | # Main logic, here. 96 | pass 97 | 98 | ``` 99 | 100 | 101 | SFTP Examples 102 | ------------- 103 | 104 | File resources are file-like objects that are similar to standard file objects. 105 | Calls will have traditional methods, as identified here: 106 | 107 | http://docs.python.org/2/library/stdtypes.html#file-objects 108 | 109 | List a directory: 110 | 111 | ```python 112 | from pysecure.adapters.sftpa import SftpFile 113 | 114 | print("Name Size Perms Owner\tGroup\n") 115 | for attributes in sftp.listdir('.'): 116 | print("%-40s %10d %.8o %s(%d)\t%s(%d)" % 117 | (attributes.name[0:40], attributes.size, 118 | attributes.permissions, attributes.owner, 119 | attributes.uid, attributes.group, 120 | attributes.gid)) 121 | ``` 122 | 123 | Recurse a directory: 124 | 125 | ```python 126 | def dir_cb(path, entry): 127 | full_path = ('%s/%s' % (path, entry.name)) 128 | print("DIR: %s" % (full_path)) 129 | 130 | def listing_cb(path, list_): 131 | print("[%s]: (%d) files" % (path, len(list_))) 132 | 133 | sftp.recurse('Pictures', dir_cb, listing_cb) 134 | ``` 135 | 136 | Read through text-file, one line at a time: 137 | 138 | ```python 139 | with SftpFile(sftp, 'text_file.txt') as sf: 140 | i = 0 141 | for data in sf: 142 | stdout.write("> " + data) 143 | 144 | if i >= 30: 145 | break 146 | 147 | i += 1 148 | ``` 149 | 150 | To read a complete file (binary friendly). It could also be read one chunk at a 151 | time: 152 | 153 | ```python 154 | with SftpFile(sftp, 'binary_file.dat') as sf: 155 | buffer_ = sf.read() 156 | 157 | print("Read (%d) bytes." % (len(buffer_))) 158 | ``` 159 | 160 | Mirroring: 161 | 162 | ```python 163 | from pysecure.sftp_mirror import SftpMirror 164 | 165 | mirror = SftpMirror(sftp) 166 | 167 | # Mirror from server to local. 168 | mirror.mirror(mirror.mirror_to_local_no_recursion, 169 | "Pictures", 170 | "/tmp/Pictures") 171 | 172 | # Mirror from local to server. 173 | mirror.mirror(mirror.mirror_to_remote_no_recursion, 174 | "/home/dustin/Pictures", 175 | "/tmp/RemotePictures") 176 | ``` 177 | 178 | Mirroring will ignore special (device) files. It also won't specially 179 | handle hard-links. 180 | 181 | Port-Forwarding Examples 182 | ------------------------ 183 | 184 | Local Forwarding: 185 | 186 | ```python 187 | from pysecure.adapters.channela import SshChannel 188 | 189 | host_source = 'localhost' 190 | port_local = 1111 191 | host_remote = 'localhost' 192 | port_remote = 80 193 | 194 | data = "GET / HTTP/1.1\nHost: localhost\n\n" 195 | 196 | with SshChannel(ssh) as sc: 197 | # The following command activates forwarding, but does not bind any 198 | # ports. Although a "port_local" parameter is expected, this is 199 | # allegedly for little more than logging. Binding is left as a concern 200 | # for the implementing developer. 201 | sc.open_forward(host_remote, port_remote, host_source, port_local) 202 | 203 | sc.write(data) 204 | 205 | received = sc.read(1024) 206 | print("Received:\n\n%s" % (received)) 207 | ``` 208 | 209 | Reverse Forwarding: 210 | 211 | ```python 212 | # This functionality starts with an SshSession. Therefore, an import of 213 | # SshChannel isn't necessary. 214 | 215 | server_address = None 216 | server_port = 8080 217 | accept_timeout_ms = 60000 218 | 219 | port = ssh.forward_listen(server_address, server_port) 220 | with ssh.forward_accept(accept_timeout_ms) as sc: 221 | while 1: 222 | data = sc.read(2048) 223 | if data == '': 224 | continue 225 | 226 | # Do something with the data. 227 | response = "Received." 228 | 229 | sc.write(response) 230 | ``` 231 | 232 | 233 | Remote Execution 234 | ---------------- 235 | 236 | Remote Command (efficient for single command): 237 | 238 | This functionality can be used to execute one command at a time: 239 | 240 | ```python 241 | for line from ssh.execute('lsb_release -a'): 242 | print(line) 243 | 244 | data = ssh.execute('whoami') 245 | print(data) 246 | ``` 247 | 248 | Output: 249 | 250 | ``` 251 | Distributor ID: Ubuntu 252 | Description: Ubuntu 13.04 253 | Release: 13.04 254 | Codename: raring 255 | 256 | dustin 257 | ``` 258 | 259 | Remote Shell (efficient for many commands): 260 | 261 | Example: 262 | 263 | ```python 264 | rsp = RemoteShellProcessor(ssh) 265 | 266 | def shell_context_cb(sc, welcome): 267 | output = rsp.do_command('cat /proc/uptime') 268 | print(output) 269 | 270 | output = rsp.do_command('whoami') 271 | print(output) 272 | 273 | rsp.shell(shell_context_cb) 274 | ``` 275 | 276 | Output: 277 | 278 | ``` 279 | $ PYTHONPATH=. test/example.py 280 | 631852.37 651773.95 281 | dustin 282 | ``` 283 | 284 | 285 | EasySsh 286 | ------- 287 | 288 | We always recommend the use of the connect_ssh and connect_sftp calls mentioned 289 | above, as they are meant to be used with a context-manager ("with") block, and 290 | will automatically be cleaned-up properly. However, this strategy makes it 291 | impossible to keep a connection open while passing control back to the caller 292 | of the function, and therefoe requires that a new, subsequent connection is 293 | opened for the next operation. 294 | 295 | Whereas the above strategy relies on the use of a callback when the SSH or SFTP 296 | session(s) are ready, we also provide the EasySsh class to make it easy to open 297 | a connection and close that connection at two separate times. 298 | 299 | For example: 300 | 301 | ```python 302 | from pysecure.easy import EasySsh, get_key_auth_cb 303 | 304 | auth_cb = get_key_auth_cb(key_filepath) 305 | easy = EasySsh(user, host, auth_cb) 306 | 307 | easy.open_ssh() 308 | easy.open_sftp() 309 | 310 | # easy.ssh and, if opened, easy.sftp are now ready. 311 | 312 | # Do your logic, here. For example, list directory entries. 313 | entries = easy.sftp.listdir('.') 314 | 315 | easy.close_sftp() # We do this just to be explicit. This will 316 | # automatically be done at SSH close. 317 | easy.close_ssh() 318 | ``` 319 | -------------------------------------------------------------------------------- /pysecure/calls/sftpi.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | 3 | from pysecure.library import libssh 4 | from pysecure.types import * 5 | 6 | # Function calls. 7 | 8 | # 9 | 10 | # LIBSSH_API sftp_session sftp_new(ssh_session session); 11 | c_sftp_new = libssh.sftp_new 12 | c_sftp_new.argtypes = [c_ssh_session] 13 | c_sftp_new.restype = c_sftp_session 14 | 15 | # LIBSSH2_SFTP * libssh2_sftp_init(LIBSSH2_SESSION *session); 16 | 17 | # LIBSSH_API int sftp_init(sftp_session sftp); 18 | c_sftp_init = libssh.sftp_init 19 | c_sftp_init.argtypes = [c_sftp_session] 20 | c_sftp_init.restype = c_int 21 | 22 | # 23 | 24 | # LIBSSH_API int sftp_get_error(sftp_session sftp); 25 | c_sftp_get_error = libssh.sftp_get_error 26 | c_sftp_get_error.argtypes = [c_sftp_session] 27 | c_sftp_get_error.restype = c_int 28 | 29 | # 30 | 31 | # LIBSSH_API void sftp_free(sftp_session sftp); 32 | c_sftp_free = libssh.sftp_free 33 | c_sftp_free.argtypes = [c_sftp_session] 34 | c_sftp_free.restype = None 35 | 36 | # LIBSSH2_SFTP_HANDLE * libssh2_sftp_opendir(LIBSSH2_SFTP *sftp, const char *path); 37 | 38 | # LIBSSH_API sftp_dir sftp_opendir(sftp_session session, const char *path); 39 | c_sftp_opendir = libssh.sftp_opendir 40 | c_sftp_opendir.argtypes = [c_sftp_session, c_char_p] 41 | c_sftp_opendir.restype = c_sftp_dir 42 | 43 | # int libssh2_sftp_readdir(LIBSSH2_SFTP_HANDLE *handle, char *buffer, size_t buffer_maxlen, LIBSSH2_SFTP_ATTRIBUTES *attrs); 44 | 45 | # LIBSSH_API sftp_attributes sftp_readdir(sftp_session session, sftp_dir dir); 46 | c_sftp_readdir = libssh.sftp_readdir 47 | c_sftp_readdir.argtypes = [c_sftp_session, c_sftp_dir] 48 | c_sftp_readdir.restype = c_sftp_attributes 49 | 50 | # 51 | 52 | # LIBSSH_API void sftp_attributes_free(sftp_attributes file); 53 | c_sftp_attributes_free = libssh.sftp_attributes_free 54 | c_sftp_attributes_free.argtypes = [c_sftp_attributes] 55 | c_sftp_attributes_free.restype = c_sftp_attributes 56 | 57 | # 58 | 59 | # LIBSSH_API int sftp_dir_eof(sftp_dir dir); 60 | c_sftp_dir_eof = libssh.sftp_dir_eof 61 | c_sftp_dir_eof.argtypes = [c_sftp_dir] 62 | c_sftp_dir_eof.restype = c_int 63 | 64 | # int libssh2_sftp_closedir(LIBSSH2_SFTP_HANDLE *handle) 65 | 66 | # LIBSSH_API int sftp_closedir(sftp_dir dir); 67 | c_sftp_closedir = libssh.sftp_closedir 68 | c_sftp_closedir.argtypes = [c_sftp_dir] 69 | c_sftp_closedir.restype = c_int 70 | 71 | ## int sftp_async_read (sftp_file file, void *data, uint32_t len, uint32_t id) 72 | #c_sftp_async_read = libssh.sftp_async_read 73 | #c_sftp_async_read.argtypes = [c_sftp_file, c_void_p, c_uint32, c_uint32] 74 | #c_sftp_async_read.restype = c_int 75 | 76 | ## int sftp_async_read_begin (sftp_file file, uint32_t len) 77 | #c_sftp_async_read_begin = libssh.sftp_async_read_begin 78 | #c_sftp_async_read_begin.argtypes = [c_sftp_file, c_uint32] 79 | #c_sftp_async_read_begin.restype = c_int 80 | 81 | ## char *sftp_canonicalize_path (sftp_session sftp, const char *path) 82 | #c_sftp_canonicalize_path = libssh.sftp_canonicalize_path 83 | #c_sftp_canonicalize_path.argtypes = [c_sftp_session, c_char_p] 84 | #c_sftp_canonicalize_path.restype = c_char_p 85 | 86 | # TODO: sftp_chmod, sftp_chown are missing from libssh2. 87 | 88 | # int sftp_chmod (sftp_session sftp, const char *file, mode_t mode) 89 | # mode_t = c_int 90 | c_sftp_chmod = libssh.sftp_chmod 91 | c_sftp_chmod.argtypes = [c_sftp_session, c_char_p, c_mode_t] 92 | c_sftp_chmod.restype = c_int 93 | 94 | # int sftp_chown (sftp_session sftp, const char *file, uid_t owner, gid_t group) 95 | c_sftp_chown = libssh.sftp_chown 96 | c_sftp_chown.argtypes = [c_sftp_session, c_char_p, c_uid_t, c_gid_t] 97 | c_sftp_chown.restype = c_int 98 | 99 | # int libssh2_sftp_close(LIBSSH2_SFTP_HANDLE *handle); 100 | 101 | # int sftp_close (sftp_file file) 102 | c_sftp_close = libssh.sftp_close 103 | c_sftp_close.argtypes = [c_sftp_file] 104 | c_sftp_close.restype = c_int 105 | 106 | # TODO: The "extension" functions aren't available from libssh2. 107 | 108 | # int sftp_extension_supported (sftp_session sftp, const char *name, const char *data) 109 | c_sftp_extension_supported = libssh.sftp_extension_supported 110 | c_sftp_extension_supported.argtypes = [c_sftp_session, c_char_p, c_char_p] 111 | c_sftp_extension_supported.restype = c_int 112 | 113 | # unsigned int sftp_extensions_get_count (sftp_session sftp) 114 | c_sftp_extensions_get_count = libssh.sftp_extensions_get_count 115 | c_sftp_extensions_get_count.argtypes = [c_sftp_session] 116 | c_sftp_extensions_get_count.restype = c_int 117 | 118 | # const char * sftp_extensions_get_data (sftp_session sftp, unsigned int indexn) 119 | c_sftp_extensions_get_data = libssh.sftp_extensions_get_data 120 | c_sftp_extensions_get_data.argtypes = [c_sftp_session, c_uint] 121 | c_sftp_extensions_get_data.restype = c_char_p 122 | 123 | # const char * sftp_extensions_get_name (sftp_session sftp, unsigned int indexn) 124 | c_sftp_extensions_get_name = libssh.sftp_extensions_get_name 125 | c_sftp_extensions_get_name.argtypes = [c_sftp_session, c_uint] 126 | c_sftp_extensions_get_name.restype = c_char_p 127 | 128 | # int libssh2_sftp_fstat(LIBSSH2_SFTP_HANDLE *handle, LIBSSH2_SFTP_ATTRIBUTES *attrs); 129 | 130 | # sftp_attributes sftp_fstat (sftp_file file) 131 | c_sftp_fstat = libssh.sftp_fstat 132 | c_sftp_fstat.argtypes = [c_sftp_file] 133 | c_sftp_fstat.restype = c_sftp_attributes 134 | 135 | # 136 | 137 | # sftp_statvfs_t sftp_fstatvfs (sftp_file file) 138 | # c_sftp_statvfs = c_void_p 139 | c_sftp_fstatvfs = libssh.sftp_fstatvfs 140 | c_sftp_fstatvfs.argtypes = [c_sftp_file] 141 | c_sftp_fstatvfs.restype = c_void_p 142 | 143 | # int libssh2_sftp_lstat(LIBSSH2_SFTP *sftp, const char *path, LIBSSH2_SFTP_ATTRIBUTES *attrs); 144 | 145 | # sftp_attributes sftp_lstat (sftp_session session, const char *path) 146 | # c_sftp_statvfs = c_void_p 147 | c_sftp_lstat = libssh.sftp_lstat 148 | c_sftp_lstat.argtypes = [c_sftp_session, c_char_p] 149 | c_sftp_lstat.restype = c_void_p 150 | 151 | # int libssh2_sftp_mkdir(LIBSSH2_SFTP *sftp, const char *path, long mode); 152 | 153 | # int sftp_mkdir (sftp_session sftp, const char *directory, mode_t mode) 154 | c_sftp_mkdir = libssh.sftp_mkdir 155 | c_sftp_mkdir.argtypes = [c_sftp_session, c_char_p, c_mode_t] 156 | c_sftp_mkdir.restype = c_int 157 | 158 | # LIBSSH2_SFTP_HANDLE * libssh2_sftp_open(LIBSSH2_SFTP *sftp, const char *path, unsigned long flags, long mode); 159 | 160 | # sftp_file sftp_open (sftp_session session, const char *file, int accesstype, mode_t mode) 161 | c_sftp_open = libssh.sftp_open 162 | c_sftp_open.argtypes = [c_sftp_session, c_char_p, c_int, c_mode_t] 163 | c_sftp_open.restype = c_sftp_file 164 | 165 | # ssize_t libssh2_sftp_read(LIBSSH2_SFTP_HANDLE *handle, char *buffer, size_t buffer_maxlen); 166 | 167 | # ssize_t sftp_read (sftp_file file, void *buf, size_t count) 168 | c_sftp_read = libssh.sftp_read 169 | c_sftp_read.argtypes = [c_sftp_file, c_void_p, c_size_t] 170 | c_sftp_read.restype = c_ssize_t 171 | 172 | # libssh2_sftp_readlink(sftp, path, target, maxlen) 173 | 174 | # char * sftp_readlink (sftp_session sftp, const char *path) 175 | c_sftp_readlink = libssh.sftp_readlink 176 | c_sftp_readlink.argtypes = [c_sftp_session, c_char_p] 177 | c_sftp_readlink.restype = c_char_p 178 | 179 | # int libssh2_sftp_rename(LIBSSH2_SFTP *sftp, const char *source_filename, const char *destination_filename); 180 | 181 | # int sftp_rename (sftp_session sftp, const char *original, const char *newname) 182 | c_sftp_rename = libssh.sftp_rename 183 | c_sftp_rename.argtypes = [c_sftp_session, c_char_p, c_char_p] 184 | c_sftp_rename.restype = c_int 185 | 186 | # int libssh2_sftp_rewind(LINBSSH2_SFTP_HANDLE *handle); 187 | 188 | # void sftp_rewind (sftp_file file) 189 | c_sftp_rewind = libssh.sftp_rewind 190 | c_sftp_rewind.argtypes = [c_sftp_file] 191 | c_sftp_rewind.restype = None 192 | 193 | # libssh2_sftp_rmdir(sftp, path) 194 | 195 | # int sftp_rmdir (sftp_session sftp, const char *directory) 196 | c_sftp_rmdir = libssh.sftp_rmdir 197 | c_sftp_rmdir.argtypes = [c_sftp_session, c_char_p] 198 | c_sftp_rmdir.restype = c_int 199 | 200 | # void libssh2_sftp_seek(LIBSSH2_SFTP_HANDLE *handle, size_t offset); 201 | 202 | # int sftp_seek (sftp_file file, uint32_t new_offset) 203 | c_sftp_seek = libssh.sftp_seek 204 | c_sftp_seek.argtypes = [c_sftp_file, c_uint32] 205 | c_sftp_seek.restype = c_int 206 | 207 | # void libssh2_sftp_seek64(LIBSSH2_SFTP_HANDLE *handle, libssh2_uint64_t offset); 208 | 209 | # int sftp_seek64 (sftp_file file, uint64_t new_offset) 210 | c_sftp_seek64 = libssh.sftp_seek64 211 | c_sftp_seek64.argtypes = [c_sftp_file, c_uint64] 212 | c_sftp_seek64.restype = c_int 213 | 214 | # 215 | 216 | # int sftp_server_version (sftp_session sftp) 217 | c_sftp_server_version = libssh.sftp_server_version 218 | c_sftp_server_version.argtypes = [c_sftp_session] 219 | c_sftp_server_version.restype = c_int 220 | 221 | # int libssh2_sftp_setstat(LIBSSH2_SFTP *sftp, const char *path, LIBSSH2_SFTP_ATTRIBUTES *attr); 222 | 223 | # int sftp_setstat (sftp_session sftp, const char *file, sftp_attributes attr) 224 | c_sftp_setstat = libssh.sftp_setstat 225 | c_sftp_setstat.argtypes = [c_sftp_session, c_char_p, c_sftp_attributes] 226 | c_sftp_setstat.restype = c_int 227 | 228 | # int libssh2_sftp_stat(LIBSSH2_SFTP *sftp, const char *path, LIBSSH2_STFP_ATTRIBUTES *attrs); 229 | 230 | # sftp_attributes sftp_stat (sftp_session session, const char *path) 231 | c_sftp_stat = libssh.sftp_stat 232 | c_sftp_stat.argtypes = [c_sftp_session, c_char_p] 233 | c_sftp_stat.restype = c_sftp_attributes 234 | 235 | # int libssh2_sftp_statvfs(LIBSSH2_SFTP *sftp, const char *path, size_t path_len, LIBSSH2_SFTP_STATVFS *st); 236 | 237 | # sftp_statvfs_t sftp_statvfs (sftp_session sftp, const char *path) 238 | # c_sftp_statvfs = c_void_p 239 | c_sftp_statvfs = libssh.sftp_statvfs 240 | c_sftp_statvfs.argtypes = [c_sftp_session, c_char_p] 241 | c_sftp_statvfs.restype = c_void_p 242 | 243 | # 244 | 245 | # void sftp_statvfs_free (sftp_statvfs_t statvfs_o) 246 | # c_sftp_statvfs = c_void_p 247 | c_sftp_statvfs_free = libssh.sftp_statvfs_free 248 | c_sftp_statvfs_free.argtypes = [c_void_p] 249 | c_sftp_statvfs_free.restype = None 250 | 251 | # libssh2_sftp_symlink(sftp, orig, linkpath) 252 | 253 | # int sftp_symlink (sftp_session sftp, const char *target, const char *dest) 254 | c_sftp_symlink = libssh.sftp_symlink 255 | c_sftp_symlink.argtypes = [c_sftp_session, c_char_p, c_char_p] 256 | c_sftp_symlink.restype = c_int 257 | 258 | # size_t libssh2_sftp_tell(LIBSSH2_SFTP_HANDLE *handle); 259 | 260 | # unsigned long sftp_tell (sftp_file file) 261 | c_sftp_tell = libssh.sftp_tell 262 | c_sftp_tell.argtypes = [c_sftp_file] 263 | c_sftp_tell.restype = c_ulong 264 | 265 | # libssh2_uint64_t libssh2_sftp_tell64(LIBSSH2_SFTP_HANDLE *handle); 266 | 267 | # uint64_t sftp_tell64 (sftp_file file) 268 | c_sftp_tell64 = libssh.sftp_tell64 269 | c_sftp_tell64.argtypes = [c_sftp_file] 270 | c_sftp_tell64.restype = c_uint64 271 | 272 | # int libssh2_sftp_unlink(LIBSSH2_SFTP *sftp, const char *filename); 273 | 274 | # int sftp_unlink (sftp_session sftp, const char *file) 275 | c_sftp_unlink = libssh.sftp_unlink 276 | c_sftp_unlink.argtypes = [c_sftp_session, c_char_p] 277 | c_sftp_unlink.restype = c_int 278 | 279 | # 280 | 281 | # int sftp_utimes (sftp_session sftp, const char *file, const struct timeval *times) 282 | c_sftp_utimes = libssh.sftp_utimes 283 | c_sftp_utimes.argtypes = [c_sftp_session, c_char_p, POINTER(c_timeval * 2)] 284 | c_sftp_utimes.restype = c_int 285 | 286 | # ssize_t libssh2_sftp_write(LIBSSH2_SFTP_HANDLE *handle, const char *buffer, size_t count); 287 | 288 | # ssize_t sftp_write (sftp_file file, const void *buf, size_t count) 289 | c_sftp_write = libssh.sftp_write 290 | c_sftp_write.argtypes = [c_sftp_file, c_void_p, c_size_t] 291 | c_sftp_write.restype = c_ssize_t 292 | 293 | -------------------------------------------------------------------------------- /pysecure/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (build by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer-0.10+ (https://github.com/warner/python-versioneer) 10 | 11 | """This text is put at the top of _version.py, and can be keyword-replaced with 12 | version information by the VCS. 13 | """ 14 | 15 | # these strings will be replaced by git during git-archive 16 | git_refnames = " (HEAD -> master)" 17 | git_full_revisionid = "ff7e01a0a77e79564cb00b6e38b4e6f9f88674f0" 18 | git_short_revisionid = "ff7e01a" 19 | 20 | # these strings are filled in when 'setup.py versioneer' creates _version.py 21 | tag_prefix = "" 22 | parentdir_prefix = "pysecure-" 23 | versionfile_source = "pysecure/_version.py" 24 | version_string_template = "%(default)s" 25 | 26 | 27 | import subprocess 28 | import sys 29 | import errno 30 | 31 | 32 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): 33 | assert isinstance(commands, list) 34 | p = None 35 | for c in commands: 36 | try: 37 | # remember shell=False, so use git.cmd on windows, not just git 38 | p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, 39 | stderr=(subprocess.PIPE if hide_stderr 40 | else None)) 41 | break 42 | except EnvironmentError: 43 | e = sys.exc_info()[1] 44 | if e.errno == errno.ENOENT: 45 | continue 46 | if verbose: 47 | print("unable to run %s" % args[0]) 48 | print(e) 49 | return None 50 | else: 51 | if verbose: 52 | print("unable to find command, tried %s" % (commands,)) 53 | return None 54 | stdout = p.communicate()[0].strip() 55 | if sys.version >= '3': 56 | stdout = stdout.decode() 57 | if p.returncode != 0: 58 | # TODO(dustin): Maybe we should contemplate raising a SystemError here, rather 59 | # then returning a None. It's almost always preferable that it would default to 60 | # being a terminal error unles specifically caught (rather than vice versa). 61 | if verbose: 62 | print("unable to run %s (error)" % args[0]) 63 | return None 64 | return stdout 65 | 66 | 67 | def versions_from_parentdir(parentdir_prefix, root, verbose=False): 68 | """Return a dictionary of values derived from the name of our parent 69 | directory (useful when a thoughtfully-named directory is created from an 70 | archive). This is the fourth attempt to find information by get_versions(). 71 | """ 72 | 73 | # Source tarballs conventionally unpack into a directory that includes 74 | # both the project name and a version string. 75 | dirname = os.path.basename(root) 76 | if not dirname.startswith(parentdir_prefix): 77 | if verbose: 78 | print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % 79 | (root, dirname, parentdir_prefix)) 80 | return None 81 | version = dirname[len(parentdir_prefix):] 82 | return { "describe": version, 83 | "long": version, 84 | "pep440": version, 85 | } 86 | 87 | import re 88 | 89 | def git_get_keywords(versionfile_abs): 90 | """Return a dictionary of values replaced by the VCS, automatically. This 91 | is the first attempt to find information by get_versions(). 92 | """ 93 | 94 | # the code embedded in _version.py can just fetch the value of these 95 | # keywords. When used from setup.py, we don't want to import _version.py, 96 | # so we do it with a regexp instead. This function is not used from 97 | # _version.py. 98 | keywords = {} 99 | try: 100 | with open(versionfile_abs) as f: 101 | for line in f.readlines(): 102 | if line.strip().startswith("git_refnames ="): 103 | mo = re.search(r'=\s*"(.*)"', line) 104 | if mo: 105 | keywords["refnames"] = mo.group(1) 106 | if line.strip().startswith("git_full_revisionid ="): 107 | mo = re.search(r'=\s*"(.*)"', line) 108 | if mo: 109 | keywords["full_revisionid"] = mo.group(1) 110 | if line.strip().startswith("git_short_revisionid ="): 111 | mo = re.search(r'=\s*"(.*)"', line) 112 | if mo: 113 | keywords["short_revisionid"] = mo.group(1) 114 | except EnvironmentError: 115 | pass 116 | return keywords 117 | 118 | def git_versions_from_keywords(keywords, tag_prefix, verbose=False): 119 | if not keywords: 120 | return {} # keyword-finding function failed to find keywords 121 | refnames = keywords["refnames"].strip() 122 | if refnames.startswith("$Format"): 123 | if verbose: 124 | print("keywords are unexpanded, not using") 125 | return {} # unexpanded, so not in an unpacked git-archive tarball 126 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 127 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 128 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 129 | TAG = "tag: " 130 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 131 | if not tags: 132 | # Either we're using git < 1.8.3, or there really are no tags. We use 133 | # a heuristic: assume all version tags have a digit. The old git %d 134 | # expansion behaves like git log --decorate=short and strips out the 135 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 136 | # between branches and tags. By ignoring refnames without digits, we 137 | # filter out many common branch names like "release" and 138 | # "stabilization", as well as "HEAD" and "master". 139 | tags = set([r for r in refs if re.search(r'\d', r)]) 140 | if verbose: 141 | print("discarding '%s', no digits" % ",".join(refs-tags)) 142 | if verbose: 143 | print("likely tags: %s" % ",".join(sorted(tags))) 144 | shortest_tag = None 145 | for ref in sorted(tags): 146 | # sorting will prefer e.g. "2.0" over "2.0rc1" 147 | if ref.startswith(tag_prefix): 148 | shortest_tag = ref[len(tag_prefix):] 149 | if verbose: 150 | print("picking %s" % shortest_tag) 151 | break 152 | versions = { 153 | "full_revisionid": keywords["full_revisionid"].strip(), 154 | "short_revisionid": keywords["short_revisionid"].strip(), 155 | "dirty": False, "dash_dirty": "", 156 | "closest_tag": shortest_tag, 157 | "closest_tag_or_zero": shortest_tag or "0", 158 | # "distance" is not provided: cannot deduce from keyword expansion 159 | } 160 | if not shortest_tag and verbose: 161 | print("no suitable tags, using full revision id") 162 | composite = shortest_tag or versions["full_revisionid"] 163 | versions["describe"] = composite 164 | versions["long"] = composite 165 | versions["default"] = composite 166 | versions["pep440"] = composite 167 | return versions 168 | 169 | import re 170 | import sys 171 | import os.path 172 | 173 | def git_versions_from_vcs(tag_prefix, root, verbose=False): 174 | """Return a dictionary of values derived directly from the VCS. This is the 175 | third attempt to find information by get_versions(). 176 | """ 177 | 178 | # this runs 'git' from the root of the source tree. This only gets called 179 | # if the git-archive 'subst' keywords were *not* expanded, and 180 | # _version.py hasn't already been rewritten with a short version string, 181 | # meaning we're inside a checked out source tree. 182 | 183 | if not os.path.exists(os.path.join(root, ".git")): 184 | if verbose: 185 | print("no .git in %s" % root) 186 | return {} 187 | 188 | GITS = ["git"] 189 | if sys.platform == "win32": 190 | GITS = ["git.cmd", "git.exe"] 191 | 192 | versions = {} 193 | 194 | full_revisionid = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 195 | if full_revisionid is None: 196 | return {} 197 | versions["full_revisionid"] = full_revisionid.strip() 198 | 199 | d = run_command(GITS, 200 | ["describe", "--tags", "--dirty", "--always", "--long"], 201 | cwd=root) 202 | if d is None: 203 | return {} 204 | d = d.strip() 205 | # "TAG-DIST-gHASH[-dirty]" , where DIST might be "0" 206 | # or just "HASH[-dirty]" if there are no ancestor tags 207 | 208 | versions["long"] = d 209 | 210 | mo1 = re.search(r"^(.*)-(\d+)-g([0-9a-f]+)(-dirty)?$", d) 211 | mo2 = re.search(r"^([0-9a-f]+)(-dirty)?$", d) 212 | if mo1: 213 | rawtag = mo1.group(1) 214 | if not rawtag.startswith(tag_prefix): 215 | if verbose: 216 | print("tag '%s' doesn't start with prefix '%s'" % (rawtag, tag_prefix)) 217 | return {} 218 | tag = rawtag[len(tag_prefix):] 219 | versions["closest_tag"] = tag 220 | versions["distance"] = int(mo1.group(2)) 221 | versions["short_revisionid"] = mo1.group(3) 222 | versions["dirty"] = bool(mo1.group(4)) 223 | versions["pep440"] = tag 224 | if versions["distance"]: 225 | versions["describe"] = d 226 | versions["pep440"] += ".post%d" % versions["distance"] 227 | else: 228 | versions["describe"] = tag 229 | if versions["dirty"]: 230 | versions["describe"] += "-dirty" 231 | if versions["dirty"]: 232 | # not strictly correct, as X.dev0 sorts "earlier" than X, but we 233 | # need some way to distinguish the two. You shouldn't be shipping 234 | # -dirty code anyways. 235 | versions["pep440"] += ".dev0" 236 | versions["default"] = versions["describe"] 237 | 238 | elif mo2: # no ancestor tags 239 | versions["closest_tag"] = None 240 | versions["short_revisionid"] = mo2.group(1) 241 | versions["dirty"] = bool(mo2.group(2)) 242 | # count revisions to compute ["distance"] 243 | commits = run_command(GITS, ["rev-list", "--count", "HEAD"], cwd=root) 244 | if commits is None: 245 | return {} 246 | versions["distance"] = int(commits.strip()) 247 | versions["pep440"] = "0" 248 | if versions["distance"]: 249 | versions["pep440"] += ".post%d" % versions["distance"] 250 | if versions["dirty"]: 251 | versions["pep440"] += ".dev0" # same concern as above 252 | versions["describe"] = d 253 | versions["default"] = "0-%d-g%s" % (versions["distance"], d) 254 | else: 255 | return {} 256 | versions["dash_dirty"] = "-dirty" if versions["dirty"] else "" 257 | versions["closest_tag_or_zero"] = versions["closest_tag"] or "0" 258 | if versions["distance"] == 0: 259 | versions["dash_distance"] = "" 260 | else: 261 | versions["dash_distance"] = "-%d" % versions["distance"] 262 | 263 | return versions 264 | 265 | import os 266 | 267 | def get_versions(default={"version": "unknown", "full": ""}, verbose=False): 268 | """This variation of get_versions() will be used in _version.py .""" 269 | 270 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 271 | # __file__, we can work backwards from there to the root. Some 272 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 273 | # case we can only use expanded keywords. 274 | 275 | keywords = { "refnames": git_refnames, 276 | "full_revisionid": git_full_revisionid, 277 | "short_revisionid": git_short_revisionid } 278 | ver = git_versions_from_keywords(keywords, tag_prefix, verbose) 279 | if ver: 280 | return ver 281 | 282 | try: 283 | root = os.path.abspath(__file__) 284 | # versionfile_source is the relative path from the top of the source 285 | # tree (where the .git directory might live) to this file. Invert 286 | # this to find the root from __file__. 287 | # TODO(dustin): Shouldn't this always loop until it fails? 288 | for i in range(len(versionfile_source.split(os.sep))): 289 | root = os.path.dirname(root) 290 | except NameError: 291 | return default 292 | 293 | return (git_versions_from_vcs(tag_prefix, root, verbose) 294 | or versions_from_parentdir(parentdir_prefix, root, verbose) 295 | or default) 296 | -------------------------------------------------------------------------------- /pysecure/sftp_mirror.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from datetime import datetime 4 | from os import mkdir, unlink, symlink 5 | from collections import deque 6 | from shutil import rmtree 7 | from time import mktime 8 | 9 | from pysecure.config import MAX_MIRROR_LISTING_CHUNK_SIZE 10 | from pysecure.utility import local_recurse, stringify 11 | from pysecure.exceptions import SftpAlreadyExistsError 12 | 13 | 14 | class SftpMirror(object): 15 | def __init__(self, sftp, allow_creates=True, allow_deletes=True, 16 | create_cb=None, delete_cb=None): 17 | self.__sftp_session = sftp 18 | 19 | self.__allow_creates = allow_creates 20 | self.__allow_deletes = allow_deletes 21 | self.__create_cb = create_cb 22 | self.__delete_cb = delete_cb 23 | 24 | self.__log = logging.getLogger('SftpMirror') 25 | 26 | def mirror(self, handler, path_from, path_to, log_files=False): 27 | """Recursively mirror the contents of "path_from" into "path_to". 28 | "handler" should be self.mirror_to_local_no_recursion or 29 | self.mirror_to_remote_no_recursion to represent which way the files are 30 | moving. 31 | """ 32 | 33 | q = deque(['']) 34 | while q: 35 | path = q.popleft() 36 | 37 | full_from = ('%s/%s' % (path_from, path)) if path else path_from 38 | full_to = ('%s/%s' % (path_to, path)) if path else path_to 39 | 40 | subdirs = handler(full_from, full_to, log_files) 41 | for subdir in subdirs: 42 | q.append(('%s/%s' % (path, subdir)) if path else subdir) 43 | 44 | def __get_local_files(self, path): 45 | self.__log.debug("Checking local files.") 46 | 47 | local_dirs = set() 48 | def local_dir_cb(parent_path, full_path, filename): 49 | local_dirs.add(filename) 50 | 51 | local_entities = set() 52 | local_files = set() 53 | local_attributes = {} 54 | def local_listing_cb(parent_path, listing): 55 | for entry in listing: 56 | (filename, mtime, size, flags) = entry 57 | 58 | entity = (filename, mtime, size, flags[1]) 59 | local_entities.add(entity) 60 | local_files.add(filename) 61 | local_attributes[filename] = (datetime.fromtimestamp(mtime), 62 | flags) 63 | 64 | local_recurse(path, 65 | local_dir_cb, 66 | local_listing_cb, 67 | MAX_MIRROR_LISTING_CHUNK_SIZE, 68 | 0) 69 | 70 | self.__log.debug("TO:\n(%d) directories\n(%d) files found." % 71 | (len(local_dirs), len(local_files))) 72 | 73 | return (local_dirs, local_entities, local_files, local_attributes) 74 | 75 | def __get_remote_files(self, path): 76 | self.__log.debug("Checking remote files.") 77 | 78 | # TODO: Decode all read paths/files from ASCII: (str).decode('ascii') 79 | 80 | remote_dirs = set() 81 | def remote_dir_cb(parent_path, full_path, entry): 82 | remote_dirs.add(stringify(entry.name)) 83 | 84 | remote_entities = set() 85 | remote_files = set() 86 | remote_attributes = {} 87 | def remote_listing_cb(parent_path, listing): 88 | for (file_path, entry) in listing: 89 | entity = (stringify(entry.name), entry.modified_time, 90 | entry.size, entry.is_symlink) 91 | 92 | remote_entities.add(entity) 93 | remote_files.add(stringify(entry.name)) 94 | 95 | flags = (entry.is_regular, entry.is_symlink, entry.is_special) 96 | remote_attributes[stringify(entry.name)] = \ 97 | (entry.modified_time_dt, flags) 98 | 99 | self.__sftp_session.recurse(path, 100 | remote_dir_cb, 101 | remote_listing_cb, 102 | MAX_MIRROR_LISTING_CHUNK_SIZE, 103 | 0) 104 | 105 | self.__log.debug("FROM:\n(%d) directories\n(%d) files found." % 106 | (len(remote_dirs), len(remote_files))) 107 | 108 | return (remote_dirs, remote_entities, remote_files, remote_attributes) 109 | 110 | def __get_deltas(self, from_tuple, to_tuple, log_files=False): 111 | (to_dirs, to_entities, to_files, to_attributes) = to_tuple 112 | (from_dirs, from_entities, from_files, from_attributes) = from_tuple 113 | 114 | self.__log.debug("Checking deltas.") 115 | 116 | # Now, calculate the differences. 117 | 118 | new_dirs = from_dirs - to_dirs 119 | 120 | if log_files is True: 121 | for new_dir in new_dirs: 122 | logging.debug("Will CREATE directory: %s" % (new_dir)) 123 | 124 | deleted_dirs = to_dirs - from_dirs 125 | 126 | if log_files is True: 127 | for deleted_dir in deleted_dirs: 128 | logging.debug("Will DELETE directory: %s" % (deleted_dir)) 129 | 130 | # Get the files from FROM that aren't identical to existing TO 131 | # entries. These will be copied. 132 | new_entities = from_entities - to_entities 133 | 134 | if log_files is True: 135 | for new_entity in new_entities: 136 | logging.debug("Will CREATE file: %s" % (new_entity[0])) 137 | 138 | # Get the files from TO that aren't identical to existing FROM 139 | # entries. These will be deleted. 140 | deleted_entities = to_entities - from_entities 141 | 142 | if log_files is True: 143 | for deleted_entity in deleted_entities: 144 | logging.debug("Will DELETE file: %s" % (deleted_entity[0])) 145 | 146 | self.__log.debug("DELTA:\n(%d) new directories\n(%d) deleted " 147 | "directories\n(%d) new files\n(%d) deleted " 148 | "files" % 149 | (len(new_dirs), len(deleted_dirs), 150 | len(new_entities), len(deleted_entities))) 151 | 152 | return (new_dirs, deleted_dirs, new_entities, deleted_entities) 153 | 154 | def __fix_deltas_at_target(self, context, ops): 155 | (from_tuple, path_from, path_to, delta_tuple) = context 156 | (new_dirs, deleted_dirs, new_entities, deleted_entities) = delta_tuple 157 | (unlink_, rmtree_, mkdir_, copy_, symlink_) = ops 158 | 159 | self.__log.debug("Removing (%d) directories." % (len(deleted_dirs))) 160 | 161 | # Delete all FROM-deleted non-directory entries, regardless of type. 162 | 163 | if self.__allow_deletes is True: 164 | for (name, mtime, size, is_link) in deleted_entities: 165 | file_path = ('%s/%s' % (path_to, name)) 166 | self.__log.debug("UPDATE: Removing TO file-path: %s" % 167 | (file_path)) 168 | 169 | if self.__delete_cb is None or \ 170 | self.__delete_cb(file_path, (mtime, size, is_link)) is True: 171 | unlink_(file_path) 172 | 173 | # Delete all FROM-deleted directories. We do this after the 174 | # individual files are created so that, if all of the files from the 175 | # directory are to be removed, we can show progress for each file 176 | # rather than blocking on a tree-delete just to error-out on the 177 | # unlink()'s, later. 178 | if self.__allow_deletes is True: 179 | for name in deleted_dirs: 180 | final_path = ('%s/%s' % (path_to, name)) 181 | self.__log.debug("UPDATE: Removing TO directory: %s" % 182 | (final_path)) 183 | 184 | if self.__delete_cb is None or \ 185 | self.__delete_cb(final_path, None) is True: 186 | rmtree_(final_path) 187 | 188 | # Create new directories. 189 | if self.__allow_creates is True: 190 | for name in new_dirs: 191 | final_path = ('%s/%s' % (path_to, name)) 192 | self.__log.debug("UPDATE: Creating TO directory: %s" % 193 | (final_path)) 194 | 195 | if self.__create_cb is None or \ 196 | self.__create_cb(final_path, None) is True: 197 | mkdir_(final_path) 198 | 199 | (from_dirs, from_entities, from_files, from_attributes) = from_tuple 200 | 201 | # Write new/changed files. Handle all but "unknown" file types. 202 | if self.__allow_creates is True: 203 | for (name, mtime, size, is_link) in new_entities: 204 | attr = from_attributes[name] 205 | (mtime_dt, (is_regular, is_symlink, is_special)) = attr 206 | 207 | filepath_from = ('%s/%s' % (path_from, name)) 208 | filepath_to = ('%s/%s' % (path_to, name)) 209 | 210 | if self.__create_cb is not None and \ 211 | self.__create_cb(filepath_to, (mtime, size, is_link)) is False: 212 | continue 213 | 214 | if is_regular: 215 | self.__log.debug("UPDATE: Creating regular TO file-path: " 216 | "%s" % (filepath_to)) 217 | 218 | copy_(filepath_from, 219 | filepath_to, 220 | mtime_dt) 221 | 222 | elif is_symlink: 223 | linked_to = self.__sftp_session.readlink(filepath_from) 224 | 225 | self.__log.debug("UPDATE: Creating symlink at [%s] to [%s]." % 226 | (filepath_to, linked_to)) 227 | 228 | # filepath_to: The physical file. 229 | # linked_to: The target. 230 | symlink_(linked_to, filepath_to) 231 | 232 | elif is_special: 233 | # SSH can't indulge us for devices, etc.. 234 | self.__log.warn("Skipping 'special' file at origin: %s" % 235 | (filepath_from)) 236 | 237 | return list(from_dirs) 238 | 239 | def __get_local_ops(self): 240 | return (unlink, 241 | rmtree, 242 | mkdir, 243 | self.__sftp_session.write_to_local, 244 | symlink) 245 | 246 | def __get_remote_ops(self): 247 | return (self.__sftp_session.unlink, 248 | self.__sftp_session.rmtree, 249 | self.__sftp_session.mkdir, 250 | self.__sftp_session.write_to_remote, 251 | self.__sftp_session.symlink) 252 | 253 | def mirror_to_local_no_recursion(self, path_from, path_to, 254 | log_files=False): 255 | """Mirror a directory without descending into directories. Return a 256 | list of subdirectory names (do not include full path). We will unlink 257 | existing files without determining if they're just going to be 258 | rewritten and then truncating them because it is our belief, based on 259 | what little we could find, that unlinking is, usually, quicker than 260 | truncating. 261 | """ 262 | 263 | self.__log.debug("Ensuring local target directory exists: %s" % 264 | (path_to)) 265 | 266 | try: 267 | mkdir(path_to) 268 | except OSError: 269 | already_exists = True 270 | self.__log.debug("Local target already exists.") 271 | else: 272 | already_exists = False 273 | self.__log.debug("Local target created.") 274 | 275 | from_tuple = self.__get_remote_files(path_from) 276 | to_tuple = self.__get_local_files(path_to) 277 | delta_tuple = self.__get_deltas(from_tuple, to_tuple, log_files) 278 | 279 | context = (from_tuple, path_from, path_to, delta_tuple) 280 | ops = self.__get_local_ops() 281 | 282 | return self.__fix_deltas_at_target(context, ops) 283 | 284 | def mirror_to_remote_no_recursion(self, path_from, path_to, 285 | log_files=False): 286 | 287 | self.__log.debug("Ensuring remote target directory exists: %s" % 288 | (path_to)) 289 | 290 | try: 291 | self.__sftp_session.mkdir(path_to) 292 | except SftpAlreadyExistsError: 293 | already_exists = True 294 | self.__log.debug("Remote target already exists.") 295 | else: 296 | already_exists = False 297 | self.__log.debug("Remote target created.") 298 | 299 | 300 | from_tuple = self.__get_local_files(path_from) 301 | to_tuple = self.__get_remote_files(path_to) 302 | delta_tuple = self.__get_deltas(from_tuple, to_tuple, log_files) 303 | 304 | context = (from_tuple, path_from, path_to, delta_tuple) 305 | ops = self.__get_remote_ops() 306 | 307 | return self.__fix_deltas_at_target(context, ops) 308 | 309 | -------------------------------------------------------------------------------- /pysecure/adapters/channela.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from ctypes import c_char_p, c_void_p, cast, c_uint32, c_int, \ 4 | create_string_buffer 5 | from time import time 6 | 7 | from pysecure.config import NONBLOCK_READ_TIMEOUT_MS, \ 8 | DEFAULT_SHELL_READ_BLOCK_SIZE 9 | from pysecure.constants.ssh import SSH_OK, SSH_ERROR, SSH_AGAIN 10 | from pysecure.exceptions import SshError, SshNonblockingTryAgainException, \ 11 | SshNoDataReceivedException, SshTimeoutException 12 | from pysecure.utility import sync, bytify, stringify 13 | from pysecure.calls.channeli import c_ssh_channel_new, \ 14 | c_ssh_channel_open_forward, \ 15 | c_ssh_channel_write, c_ssh_channel_free, \ 16 | c_ssh_channel_read, \ 17 | c_ssh_channel_send_eof, \ 18 | c_ssh_channel_is_open, \ 19 | c_ssh_channel_open_session, \ 20 | c_ssh_channel_request_exec, \ 21 | c_ssh_channel_request_shell, \ 22 | c_ssh_channel_request_pty, \ 23 | c_ssh_channel_change_pty_size, \ 24 | c_ssh_channel_is_eof, \ 25 | c_ssh_channel_read_nonblocking, \ 26 | c_ssh_channel_request_env, \ 27 | c_ssh_channel_get_session, \ 28 | c_ssh_channel_accept_x11, \ 29 | c_ssh_channel_request_x11 30 | 31 | from pysecure.error import ssh_get_error, ssh_get_error_code 32 | 33 | def _ssh_channel_new(ssh_session_int): 34 | logging.debug("Opening channel on session.") 35 | 36 | result = c_ssh_channel_new(ssh_session_int) 37 | if result is None: 38 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 39 | error = ssh_get_error(ssh_session_int) 40 | 41 | raise SshError("Could not open channel: %s" % (error)) 42 | 43 | return result 44 | 45 | def _ssh_channel_open_forward(ssh_channel_int, host_remote, port_remote, 46 | host_source, port_local): 47 | 48 | logging.debug("Requesting forward on channel.") 49 | 50 | result = c_ssh_channel_open_forward(ssh_channel_int, 51 | c_char_p(bytify(host_remote)), 52 | c_int(port_remote), 53 | c_char_p(bytify(host_source)), 54 | c_int(port_local)) 55 | 56 | if result == SSH_AGAIN: 57 | raise SshNonblockingTryAgainException() 58 | elif result != SSH_OK: 59 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 60 | error = ssh_get_error(ssh_session_int) 61 | 62 | raise SshError("Forward failed: %s" % (error)) 63 | 64 | def _ssh_channel_write(ssh_channel_int, data): 65 | data_len = len(data) 66 | sent_bytes = c_ssh_channel_write(ssh_channel_int, 67 | cast(c_char_p(data), c_void_p), 68 | c_uint32(data_len)) 69 | 70 | if sent_bytes == SSH_ERROR: 71 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 72 | error = ssh_get_error(ssh_session_int) 73 | 74 | raise SshError("Channel write failed: %s" % (error)) 75 | elif sent_bytes != data_len: 76 | raise SshError("Channel write of (%d) bytes failed for length (%d) of " 77 | "written data." % (data_len, sent_bytes)) 78 | 79 | def _ssh_channel_read(ssh_channel_int, count, is_stderr): 80 | """Do a read on a channel.""" 81 | 82 | buffer_ = create_string_buffer(count) 83 | while 1: 84 | received_bytes = c_ssh_channel_read(ssh_channel_int, 85 | cast(buffer_, c_void_p), 86 | c_uint32(count), 87 | c_int(int(is_stderr))) 88 | 89 | if received_bytes == SSH_ERROR: 90 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 91 | error = ssh_get_error(ssh_session_int) 92 | 93 | raise SshError("Channel read failed: %s" % (error)) 94 | 95 | # BUG: We're not using the nonblocking variant, but this can still 96 | # return SSH_AGAIN due to that call's broken dependencies. 97 | # TODO: This call might return SSH_AGAIN, even though we should always be 98 | # blocking. Reported as bug #115. 99 | elif received_bytes == SSH_AGAIN: 100 | continue 101 | 102 | else: 103 | break 104 | 105 | # TODO: Where is the timeout configured for the read? 106 | return buffer_.raw[0:received_bytes] 107 | 108 | def _ssh_channel_read_nonblocking(ssh_channel_int, count, is_stderr): 109 | buffer_ = create_string_buffer(count) 110 | received_bytes = c_ssh_channel_read_nonblocking(ssh_channel_int, 111 | cast(buffer_, c_void_p), 112 | c_uint32(count), 113 | c_int(int(is_stderr))) 114 | 115 | if received_bytes == SSH_ERROR: 116 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 117 | error = ssh_get_error(ssh_session_int) 118 | 119 | raise SshError("Channel read (non-blocking) failed: %s" % (error)) 120 | 121 | return buffer_.raw[0:received_bytes] 122 | 123 | def _ssh_channel_free(ssh_channel_int): 124 | logging.debug("Freeing channel (%d)." % (ssh_channel_int)) 125 | 126 | c_ssh_channel_free(ssh_channel_int) 127 | 128 | def _ssh_channel_send_eof(ssh_channel_int): 129 | result = c_ssh_channel_send_eof(ssh_channel_int) 130 | if result != SSH_OK: 131 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 132 | error = ssh_get_error(ssh_session_int) 133 | 134 | raise SshError("Could not send EOF: %s" % (error)) 135 | 136 | def _ssh_channel_is_open(ssh_channel_int): 137 | result = c_ssh_channel_is_open(ssh_channel_int) 138 | return (result != 0) 139 | 140 | def _ssh_channel_open_session(ssh_channel_int): 141 | logging.debug("Request channel open-session.") 142 | 143 | result = c_ssh_channel_open_session(ssh_channel_int) 144 | if result != SSH_OK: 145 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 146 | error = ssh_get_error(ssh_session_int) 147 | 148 | raise SshError("Could not open session on channel: %s" % (error)) 149 | 150 | logging.debug("Channel open-session successful.") 151 | 152 | def _ssh_channel_request_exec(ssh_channel_int, cmd): 153 | logging.debug("Requesting channel exec.") 154 | 155 | result = c_ssh_channel_request_exec(ssh_channel_int, 156 | c_char_p(bytify(cmd))) 157 | if result == SSH_AGAIN: 158 | raise SshNonblockingTryAgainException() 159 | elif result != SSH_OK: 160 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 161 | error = ssh_get_error(ssh_session_int) 162 | 163 | raise SshError("Could not execute shell request on channel: %s" % 164 | (error)) 165 | 166 | logging.debug("Channel-exec successful.") 167 | 168 | def _ssh_channel_request_shell(ssh_channel_int): 169 | logging.debug("Requesting channel shell.") 170 | 171 | result = c_ssh_channel_request_shell(ssh_channel_int) 172 | if result == SSH_AGAIN: 173 | raise SshNonblockingTryAgainException() 174 | elif result != SSH_OK: 175 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 176 | error = ssh_get_error(ssh_session_int) 177 | 178 | raise SshError("Shell request failed: %s" % (error)) 179 | 180 | logging.debug("Channel-shell request successful.") 181 | 182 | def _ssh_channel_request_pty(ssh_channel_int): 183 | logging.debug("Requesting channel PTY.") 184 | 185 | result = c_ssh_channel_request_pty(ssh_channel_int) 186 | if result == SSH_AGAIN: 187 | raise SshNonblockingTryAgainException() 188 | elif result != SSH_OK: 189 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 190 | error = ssh_get_error(ssh_session_int) 191 | 192 | raise SshError("PTY request failed: %s" % (error)) 193 | 194 | logging.debug("Channel PTY request successful.") 195 | 196 | def _ssh_channel_change_pty_size(ssh_channel_int, col, row): 197 | result = c_ssh_channel_change_pty_size(ssh_channel_int, c_int(col), c_int(row)) 198 | if result != SSH_OK: 199 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 200 | error = ssh_get_error(ssh_session_int) 201 | 202 | raise SshError("PTY size change failed: %s" % (error)) 203 | 204 | def _ssh_channel_is_eof(ssh_channel_int): 205 | result = c_ssh_channel_is_eof(ssh_channel_int) 206 | 207 | return bool(result) 208 | 209 | def _ssh_channel_request_env(ssh_channel_int, name, value): 210 | logging.debug("Setting remote environment variable [%s] to [%s]." % 211 | (name, value)) 212 | 213 | # TODO: We haven't been able to get this to work. Reported bug #125. 214 | result = c_ssh_channel_request_env(ssh_channel_int, 215 | c_char_p(bytify(name)), 216 | c_char_p(bytify(value))) 217 | 218 | if result == SSH_AGAIN: 219 | raise SshNonblockingTryAgainException() 220 | elif result != SSH_OK: 221 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 222 | error = ssh_get_error(ssh_session_int) 223 | 224 | raise SshError("Request-env failed: %s" % (error)) 225 | 226 | def _ssh_channel_get_session(ssh_channel_int): 227 | return c_ssh_channel_get_session(ssh_channel_int) 228 | 229 | def _ssh_channel_accept_x11(ssh_channel_int, timeout_ms): 230 | ssh_channel_accepted = c_ssh_channel_accept_x11(ssh_channel_int, 231 | timeout_ms) 232 | 233 | if ssh_channel_accepted is None: 234 | raise SshTimeoutException() 235 | 236 | return ssh_channel_accept 237 | 238 | def _ssh_channel_request_x11(ssh_channel_int, screen_number=0, 239 | single_connection=False, protocol=None, 240 | cookie=None): 241 | result = c_ssh_channel_request_x11(ssh_channel_int, int(single_connection), 242 | c_char_p(bytify(protocol)), \ 243 | c_char_p(bytify(cookie)), 244 | screen_number) 245 | 246 | if result == SSH_AGAIN: 247 | raise SshNonblockingTryAgainException() 248 | elif result != SSH_OK: 249 | ssh_session_int = _ssh_channel_get_session(ssh_channel_int) 250 | error = ssh_get_error(ssh_session_int) 251 | 252 | raise SshError("Channel request-X11 failed: %s" % (error)) 253 | 254 | 255 | class SshChannel(object): 256 | def __init__(self, ssh_session, ssh_channel=None): 257 | self.__ssh_session_int = getattr(ssh_session, 258 | 'session_id', 259 | ssh_session) 260 | 261 | self.__ssh_channel_int = getattr(ssh_channel, 262 | 'session_id', 263 | ssh_channel) 264 | 265 | def __enter__(self): 266 | if self.__ssh_channel_int is None: 267 | self.__ssh_channel_int = _ssh_channel_new(self.__ssh_session_int) 268 | 269 | return self 270 | 271 | def __exit__(self, e_type, e_value, e_tb): 272 | # The documentation says that a "free" implies a "close", and that a 273 | # "close" implies a "send eof". From a cursory glance, this seems 274 | # accurate. 275 | _ssh_channel_free(self.__ssh_channel_int) 276 | self.__ssh_channel_int = None 277 | 278 | def __del__(self): 279 | # The documentation says that a "free" implies a "close", and that a 280 | # "close" implies a "send eof". From a cursory glance, this seems 281 | # accurate. 282 | if self.__ssh_channel_int is not None: 283 | _ssh_channel_free(self.__ssh_channel_int) 284 | 285 | def open_forward(self, host_remote, port_remote, host_source, port_local): 286 | _ssh_channel_open_forward(self.__ssh_channel_int, 287 | host_remote, 288 | port_remote, 289 | host_source, 290 | port_local) 291 | 292 | def write(self, data): 293 | _ssh_channel_write(self.__ssh_channel_int, data) 294 | 295 | def read(self, count, is_stderr=False): 296 | return _ssh_channel_read(self.__ssh_channel_int, count, is_stderr) 297 | 298 | def read_nonblocking(self, count, is_stderr=False): 299 | return _ssh_channel_read_nonblocking(self.__ssh_channel_int, 300 | count, 301 | is_stderr) 302 | 303 | def send_eof(self): 304 | _ssh_channel_send_eof(self.__ssh_channel_int) 305 | 306 | def is_open(self): 307 | return _ssh_channel_is_open(self.__ssh_channel_int) 308 | 309 | def open_session(self): 310 | _ssh_channel_open_session(self.__ssh_channel_int) 311 | 312 | def request_exec(self, cmd): 313 | """Execute a command. Note that this can only be done once, and may be 314 | the only operation performed with the current channel. 315 | """ 316 | 317 | return _ssh_channel_request_exec(self.__ssh_channel_int, cmd) 318 | 319 | def request_shell(self): 320 | """Activate shell services on the channel (for PTY emulation).""" 321 | 322 | _ssh_channel_request_shell(self.__ssh_channel_int) 323 | 324 | def request_pty(self): 325 | _ssh_channel_request_pty(self.__ssh_channel_int) 326 | 327 | def change_pty_size(self, col, row): 328 | _ssh_channel_change_pty_size(self.__ssh_channel_int, col, row) 329 | 330 | def is_eof(self): 331 | return _ssh_channel_is_eof(self.__ssh_channel_int) 332 | 333 | def request_env(self, name, value): 334 | return _ssh_channel_request_env(self.__ssh_channel_int, name, value) 335 | 336 | def accept_x11(self, timeout_ms): 337 | ssh_x11_channel_int = _ssh_channel_accept_x11(self.__ssh_channel_int, 338 | timeout_ms) 339 | 340 | return SshChannel(self.__ssh_session_int, ssh_x11_channel_int) 341 | 342 | def request_x11(screen_number=0, single_connection=False, protocol=None, 343 | cookie=None): 344 | return _ssh_channel_request_x11(self.__ssh_channel_int, screen_number, 345 | single_connection, protocol, cookie) 346 | 347 | 348 | class RemoteShellProcessor(object): 349 | def __init__(self, ssh_session, block_size=DEFAULT_SHELL_READ_BLOCK_SIZE): 350 | self.__log = logging.getLogger('RSP') 351 | self.__log.debug("Initializing RSP.") 352 | 353 | self.__ssh_session = ssh_session 354 | self.__block_size = block_size 355 | 356 | def __wait_on_output(self, data_cb): 357 | self.__log.debug("Reading chunked output.") 358 | 359 | start_at = time() 360 | while self.__sc.is_open() and self.__sc.is_eof() is False: 361 | buffer_ = self.__sc.read_nonblocking(self.__block_size) 362 | if buffer_ == b'': 363 | delta = time() - start_at 364 | if delta * 1000 > NONBLOCK_READ_TIMEOUT_MS: 365 | break 366 | 367 | continue 368 | 369 | data_cb(buffer_) 370 | start_at = time() 371 | 372 | def __wait_on_output_all(self, whole_data_cb): 373 | self.__log.debug("Reading complete output.") 374 | 375 | received = bytearray() 376 | def data_cb(buffer_): 377 | received.extend(buffer_) 378 | 379 | self.__wait_on_output(data_cb) 380 | whole_data_cb(bytes(received)) 381 | 382 | def do_command(self, command, block_cb=None, add_nl=True, 383 | drop_last_line=True, drop_first_line=True): 384 | self.__log.debug("Sending shell command: %s" % (command.rstrip())) 385 | 386 | if add_nl is True: 387 | command += '\n' 388 | 389 | self.__sc.write(bytify(command)) 390 | 391 | if block_cb is not None: 392 | self.__wait_on_output(block_cb) 393 | else: 394 | received = bytearray() 395 | def data_cb(buffer_): 396 | received.extend(bytify(buffer_)) 397 | 398 | self.__wait_on_output_all(data_cb) 399 | 400 | if drop_first_line is True: 401 | received = received[received.index(b'\n') + 1:] 402 | 403 | # In all likelihood, the last line is probably the prompt. 404 | if drop_last_line is True: 405 | received = received[:received.rindex(b'\n')] 406 | 407 | return bytes(received) 408 | 409 | def shell(self, ready_cb, cols=80, rows=24): 410 | self.__log.debug("Starting RSP shell.") 411 | 412 | with SshChannel(self.__ssh_session) as sc: 413 | sc.open_session() 414 | sc.request_env('aa', 'bb') 415 | # sc.request_env('LANG', 'en_US.UTF-8') 416 | 417 | sc.request_pty() 418 | sc.change_pty_size(cols, rows) 419 | sc.request_shell() 420 | 421 | self.__log.debug("Waiting for shell welcome message.") 422 | 423 | welcome = bytearray() 424 | def welcome_received_cb(data): 425 | welcome.extend(bytify(data)) 426 | 427 | self.__sc = sc 428 | self.__wait_on_output_all(welcome_received_cb) 429 | 430 | self.__log.debug("RSP shell is ready.") 431 | ready_cb(sc, stringify(welcome)) 432 | self.__sc = None 433 | 434 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | A Python SSH/SFTP library based on libssh. 294 | Copyright (C) 2013 Dustin Oprea 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /pysecure/adapters/ssha.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from ctypes import c_char_p, c_void_p, c_ubyte, byref, cast, c_uint, \ 4 | c_int, c_long 5 | 6 | from pysecure.exceptions import SshError, SshLoginError, SshHostKeyException, \ 7 | SshNonblockingTryAgainException, \ 8 | SshTimeoutException 9 | from pysecure.config import DEFAULT_EXECUTE_READ_BLOCK_SIZE 10 | from pysecure.types import c_ssh_key 11 | from pysecure.constants.ssh import SSH_OK, SSH_ERROR, SSH_AGAIN, SSH_EOF, \ 12 | \ 13 | SSH_AUTH_ERROR, SSH_AUTH_DENIED, \ 14 | SSH_AUTH_PARTIAL, SSH_AUTH_AGAIN, \ 15 | SSH_AUTH_SUCCESS, \ 16 | \ 17 | SSH_SERVER_ERROR, SSH_SERVER_NOT_KNOWN, \ 18 | SSH_SERVER_KNOWN_OK, \ 19 | SSH_SERVER_KNOWN_CHANGED, \ 20 | SSH_SERVER_FOUND_OTHER, SSH_OPTIONS, \ 21 | SSH_SERVER_FILE_NOT_FOUND, \ 22 | \ 23 | SSH_CLOSED, \ 24 | SSH_READ_PENDING, \ 25 | SSH_WRITE_PENDING, \ 26 | SSH_CLOSED_ERROR 27 | 28 | from pysecure.calls.sshi import c_free, c_ssh_pki_import_privkey_file, \ 29 | c_ssh_write_knownhost, c_ssh_get_pubkey_hash, \ 30 | c_ssh_is_server_known, c_ssh_connect, \ 31 | c_ssh_disconnect, c_ssh_print_hexa, \ 32 | c_ssh_get_hexa, c_ssh_free, c_ssh_new, \ 33 | c_ssh_options_set, c_ssh_init, \ 34 | c_ssh_finalize, c_ssh_userauth_password, \ 35 | c_ssh_forward_listen, c_ssh_forward_accept, \ 36 | c_ssh_key_new, c_ssh_userauth_publickey, \ 37 | c_ssh_key_free, c_ssh_get_disconnect_message, \ 38 | c_ssh_get_issue_banner, \ 39 | c_ssh_get_openssh_version, c_ssh_get_status, \ 40 | c_ssh_get_version, c_ssh_get_serverbanner, \ 41 | c_ssh_disconnect, c_ssh_is_blocking, \ 42 | c_ssh_threads_get_noop, \ 43 | c_ssh_threads_set_callbacks, \ 44 | c_ssh_set_blocking 45 | # c_ssh_threads_init, c_ssh_threads_finalize, \ 46 | # c_ssh_threads_get_type 47 | 48 | 49 | from pysecure.adapters.channela import SshChannel 50 | from pysecure.error import ssh_get_error, ssh_get_error_code 51 | from pysecure.utility import bytify, stringify 52 | 53 | def _ssh_options_set_string(ssh_session, type_, value): 54 | assert issubclass(value.__class__, str) 55 | 56 | value_charp = c_char_p(bytify(value)) 57 | 58 | result = c_ssh_options_set(c_void_p(ssh_session), 59 | c_int(type_), 60 | cast(value_charp, c_void_p)) 61 | 62 | if result < 0: 63 | error = ssh_get_error(ssh_session) 64 | raise SshError("Could not set STRING option (%d) to [%s]: %s" % 65 | (type_, value, error)) 66 | 67 | def _ssh_options_set_uint(ssh_session, type_, value): 68 | value_uint = c_uint(value) 69 | result = c_ssh_options_set(c_void_p(ssh_session), 70 | c_int(type_), 71 | cast(byref(value_uint), c_void_p)) 72 | 73 | if result < 0: 74 | error = ssh_get_error(ssh_session) 75 | raise SshError("Could not set UINT option (%d) to (%d): %s" % 76 | (type_, value, error)) 77 | 78 | def _ssh_options_set_int(ssh_session, type_, value): 79 | value_int = c_int(value) 80 | result = c_ssh_options_set(c_void_p(ssh_session), 81 | c_int(type_), 82 | cast(byref(value_int), c_void_p)) 83 | 84 | if result < 0: 85 | error = ssh_get_error(ssh_session) 86 | raise SshError("Could not set INT option (%d) to (%d): %s" % 87 | (type_, value, error)) 88 | 89 | def _ssh_options_set_long(ssh_session, type_, value): 90 | value_long = c_long(value) 91 | result = c_ssh_options_set(c_void_p(ssh_session), 92 | c_int(type_), 93 | cast(byref(value_long), c_void_p)) 94 | 95 | if result < 0: 96 | error = ssh_get_error(ssh_session) 97 | raise SshError("Could not set LONG option (%d) to (%d): %s" % 98 | (type_, value, error)) 99 | 100 | def _ssh_new(): 101 | ssh_session = c_ssh_new() 102 | if ssh_session is None: 103 | raise SshError("Could not create session.") 104 | 105 | return ssh_session 106 | 107 | def _ssh_free(ssh_session): 108 | c_ssh_free(c_void_p(ssh_session)) 109 | 110 | def _ssh_connect(ssh_session): 111 | result = c_ssh_connect(c_void_p(ssh_session)) 112 | if result == SSH_AGAIN: 113 | raise SshNonblockingTryAgainException() 114 | elif result != SSH_OK: 115 | error = ssh_get_error(ssh_session) 116 | raise SshError("Connect failed: %s" % (error)) 117 | 118 | def _ssh_disconnect(ssh_session): 119 | c_ssh_disconnect(c_void_p(ssh_session)) 120 | 121 | def _ssh_is_server_known(ssh_session, allow_new=False, cb=None): 122 | result = c_ssh_is_server_known(c_void_p(ssh_session)) 123 | 124 | if result == SSH_SERVER_KNOWN_OK: 125 | if cb is not None: 126 | hk = repr(PublicKeyHash(ssh_session)) 127 | allow_auth = cb(hk, True) 128 | 129 | logging.debug("Host-key callback returned [%s] when a host-key has " 130 | "already been accepted." % (allow_auth)) 131 | 132 | if allow_auth is False: 133 | raise SshHostKeyException("Existing host-key was failed by " 134 | "callback.") 135 | 136 | logging.debug("Server host-key authenticated.") 137 | 138 | return 139 | 140 | if result == SSH_SERVER_KNOWN_CHANGED: 141 | raise SshHostKeyException("Host key: Server has changed.") 142 | elif result == SSH_SERVER_FOUND_OTHER: 143 | raise SshHostKeyException("Host key: Server -type- has changed.") 144 | elif result == SSH_SERVER_FILE_NOT_FOUND or result == SSH_SERVER_NOT_KNOWN: 145 | logging.warn("Server is not already known.") 146 | if allow_new is False: 147 | if result == SSH_SERVER_FILE_NOT_FOUND: 148 | raise SshHostKeyException("Host key: The known-hosts file was " 149 | "not found, and we are not " 150 | "accepting new hosts.") 151 | 152 | raise SshHostKeyException("An existing host-key was not found. " 153 | "Our policy is to deny new hosts.") 154 | 155 | if cb is not None: 156 | hk = repr(PublicKeyHash(ssh_session)) 157 | allow_auth = cb(hk, allow_new) 158 | 159 | logging.debug("Host-key callback returned [%s] when no host-key " 160 | "yet available." % (allow_auth)) 161 | 162 | if allow_auth is False: 163 | raise SshHostKeyException("New host-key was failed by " 164 | "callback.") 165 | 166 | logging.warn("Recording host-key for server.") 167 | c_ssh_write_knownhost(ssh_session) 168 | elif result == SSH_SERVER_ERROR: 169 | raise SshHostKeyException("Host key: Server error.") 170 | else: 171 | raise SshHostKeyException("Host key: Failed (unexpected error).") 172 | 173 | def _ssh_print_hexa(title, hash_, hlen): 174 | assert issubclass(title.__class__, str) 175 | 176 | c_ssh_print_hexa(c_char_p(bytify(title)), hash_, c_int(hlen)) 177 | 178 | def _ssh_get_hexa(hash_, hlen): 179 | hexa = c_ssh_get_hexa(hash_, c_int(hlen)) 180 | if hexa is None: 181 | raise SshError("Could not build hex-string.") 182 | 183 | return hexa 184 | 185 | def _ssh_write_knownhost(ssh_session): 186 | logging.debug("Updating known-hosts file.") 187 | 188 | result = c_ssh_write_knownhost(c_void_p(ssh_session)) 189 | if result != SSH_OK: 190 | error = ssh_get_error(ssh_session) 191 | raise SshError("Could not update known-hosts file: %s" % (error)) 192 | 193 | def _check_auth_response(result): 194 | if result == SSH_AUTH_ERROR: 195 | raise SshLoginError("Login failed: Auth error.") 196 | elif result == SSH_AUTH_DENIED: 197 | raise SshLoginError("Login failed: Auth denied.") 198 | elif result == SSH_AUTH_PARTIAL: 199 | raise SshLoginError("Login failed: Auth partial.") 200 | elif result == SSH_AUTH_AGAIN: 201 | raise SshLoginError("Login failed: Auth again.") 202 | elif result != SSH_AUTH_SUCCESS: 203 | raise SshLoginError("Login failed (unexpected error).") 204 | 205 | def _ssh_userauth_password(ssh_session, username, password): 206 | if username is not None: 207 | assert issubclass(username.__class__, str) 208 | 209 | assert issubclass(password.__class__, str) 210 | 211 | logging.debug("Authenticating with a password for user [%s]." % (username)) 212 | 213 | result = c_ssh_userauth_password(c_void_p(ssh_session), \ 214 | c_char_p(bytify(username)), \ 215 | c_char_p(bytify(password))) 216 | 217 | _check_auth_response(result) 218 | 219 | def _ssh_init(): 220 | result = c_ssh_init() 221 | if result < 0: 222 | raise SshError("Could not initialize SSH.") 223 | 224 | def _ssh_finalize(): 225 | result = c_ssh_finalize() 226 | if result < 0: 227 | raise SshError("Could not finalize SSH.") 228 | 229 | def _ssh_forward_listen(ssh_session, address, port): 230 | if address is not None: 231 | assert issubclass(address.__class__, str) 232 | address = bytify(address) 233 | 234 | bound_port = c_int() 235 | # BUG: Currently always returns SSH_AGAIN in 0.6.0 . Registered as bug #126. 236 | result = c_ssh_forward_listen(ssh_session, 237 | address, 238 | port, 239 | byref(bound_port)) 240 | 241 | if result == SSH_AGAIN: 242 | raise SshNonblockingTryAgainException() 243 | elif result != SSH_OK: 244 | error = ssh_get_error(ssh_session) 245 | raise SshError("Forward-listen failed: %s" % (error)) 246 | 247 | return bound_port.value 248 | 249 | def _ssh_forward_accept(ssh_session, timeout_ms): 250 | """Waiting for an incoming connection from a reverse forwarded port. Note 251 | that this results in a kernel block until a connection is received. 252 | """ 253 | 254 | ssh_channel = c_ssh_forward_accept(c_void_p(ssh_session), 255 | c_int(timeout_ms)) 256 | 257 | if ssh_channel is None: 258 | raise SshTimeoutException() 259 | 260 | return ssh_channel 261 | 262 | def _ssh_key_new(): 263 | key = c_ssh_key_new() 264 | if key is None: 265 | raise SshError("Could not create empty key.") 266 | 267 | return key 268 | 269 | def _ssh_userauth_publickey(ssh_session, priv_key): 270 | result = c_ssh_userauth_publickey(c_void_p(ssh_session), 271 | None, 272 | priv_key) 273 | 274 | _check_auth_response(result) 275 | 276 | def ssh_pki_import_privkey_file(file_path, pass_phrase=None): 277 | assert issubclass(file_path.__class__, str) 278 | 279 | logging.debug("Importing private-key from [%s]." % (file_path)) 280 | 281 | key = c_ssh_key() 282 | # TODO: This needs to be freed. Use our key class. 283 | 284 | file_path = bytify(file_path) 285 | if pass_phrase is not None: 286 | assert issubclass(pass_phrase.__class__, str) 287 | pass_phrase = bytify(pass_phrase) 288 | 289 | result = c_ssh_pki_import_privkey_file(c_char_p(file_path), 290 | c_char_p(pass_phrase), 291 | None, 292 | None, 293 | byref(key)) 294 | 295 | if result == SSH_EOF: 296 | raise SshError("Key file [%s] does not exist or could not be read." % 297 | (file_path)) 298 | elif result != SSH_OK: 299 | raise SshError("Could not import key.") 300 | 301 | return key 302 | 303 | def _ssh_is_blocking(ssh_session): 304 | result = c_ssh_is_blocking(c_void_p(ssh_session)) 305 | return bool(result) 306 | 307 | def _ssh_get_disconnect_message(ssh_session): 308 | message = c_ssh_get_disconnect_message(c_void_p(ssh_session)) 309 | if message is None: 310 | return (ssh_get_error_code(ssh_session), True) 311 | 312 | return (message, False) 313 | 314 | def _ssh_get_issue_banner(ssh_session): 315 | """Get the "issue banner" for the server. Note that this function may/will 316 | fail if the server isn't configured for such a message (like some/all 317 | Ubuntu installs). In the event of failure, we'll just return an empty 318 | string. 319 | """ 320 | 321 | message = c_ssh_get_issue_banner(c_void_p(ssh_session)) 322 | # TODO: Does "newly allocated" string have to be freed? We might have to reallocate it as a Python string. 323 | if message is None: 324 | return '' 325 | 326 | return stringify(message) 327 | 328 | def _ssh_get_openssh_version(ssh_session): 329 | """Returns an encoded version. Comparisons can be done with the 330 | SSH_INT_VERSION macro. 331 | """ 332 | 333 | openssh_server_version = c_ssh_get_openssh_version(c_void_p(ssh_session)) 334 | if openssh_server_version == 0: 335 | raise SshError("Could not get OpenSSH version. Server may not be " 336 | "OpenSSH.") 337 | 338 | return openssh_server_version 339 | 340 | def _ssh_get_status(ssh_session): 341 | result = c_ssh_get_status(c_void_p(ssh_session)) 342 | 343 | # TODO: This is returning bad flags (SSH_CLOSED_ERROR is True). Reported as bug 344 | # #119. 345 | return { 'SSH_CLOSED': (result & SSH_CLOSED) > 0, 346 | 'SSH_READ_PENDING': (result & SSH_READ_PENDING) > 0, 347 | 'SSH_WRITE_PENDING': (result & SSH_WRITE_PENDING) > 0, 348 | 'SSH_CLOSED_ERROR': (result & SSH_CLOSED_ERROR) > 0 } 349 | 350 | def _ssh_get_version(ssh_session): 351 | protocol_version = c_ssh_get_version(ssh_session) 352 | if protocol_version < 0: 353 | raise SshError("Could not determine protocol version.") 354 | 355 | return protocol_version 356 | 357 | def _ssh_get_serverbanner(ssh_session): 358 | result = c_ssh_get_serverbanner(c_void_p(ssh_session)) 359 | if result is None: 360 | raise SshError("Could not get server-banner.") 361 | 362 | return result 363 | 364 | def _ssh_disconnect(ssh_session): 365 | c_ssh_disconnect(c_void_p(ssh_session)) 366 | 367 | def ssh_threads_get_noop(): 368 | return c_ssh_threads_get_noop() 369 | 370 | def ssh_threads_set_callbacks(cb): 371 | result = c_ssh_threads_set_callbacks(c_void_p(cb)) 372 | if result != SSH_OK: 373 | raise SshError("Could not set callbacks.") 374 | 375 | def _ssh_set_blocking(ssh_session, blocking): 376 | c_ssh_set_blocking(c_void_p(ssh_session), c_int(blocking)) 377 | 378 | 379 | class SshSystem(object): 380 | def __enter__(self): 381 | return self.open() 382 | 383 | def open(self): 384 | logging.debug("Initializing SSH system.") 385 | _ssh_init() 386 | 387 | def __exit__(self, e_type, e_value, e_tb): 388 | self.close() 389 | 390 | def close(self): 391 | logging.debug("Cleaning-up SSH system.") 392 | _ssh_finalize 393 | 394 | class SshSession(object): 395 | def __init__(self, **options): 396 | self.__options = options 397 | 398 | self.__ssh_session_ptr = _ssh_new() 399 | self.__log = logging.getLogger('SSH_SESSION(%d)' % 400 | (self.__ssh_session_ptr)) 401 | 402 | self.__log.debug("Created session.") 403 | 404 | 405 | if 'blocking' in options: 406 | self.set_blocking(options['blocking']) 407 | # SSH_OPTIONS doesn't contain blocking and will crash if it finds it 408 | del self.__options['blocking'] 409 | 410 | def __enter__(self): 411 | return self.open() 412 | 413 | def open(self): 414 | for k, v in self.__options.items(): 415 | (option_id, type_) = SSH_OPTIONS[k] 416 | 417 | if type_ == 'string': 418 | option_setter = _ssh_options_set_string 419 | elif type_ == 'uint': 420 | option_setter = _ssh_options_set_uint 421 | elif type_ == 'int': 422 | option_setter = _ssh_options_set_int 423 | elif type_ == 'long': 424 | option_setter = _ssh_options_set_long 425 | elif type_ == 'bool': 426 | v = 0 if v is False else 1 427 | option_setter = _ssh_options_set_int 428 | else: 429 | raise SshError("Option type [%s] is invalid." % (type_)) 430 | 431 | self.__log.debug("Setting option [%s] (%d) to [%s]." % 432 | (k, option_id, v)) 433 | 434 | option_setter(self.__ssh_session_ptr, option_id, v) 435 | 436 | return self 437 | 438 | def __exit__(self, e_type, e_value, e_tb): 439 | self.close() 440 | 441 | def close(self): 442 | # _ssh_free doesn't seem to imply a formal disconnect. 443 | self.disconnect() 444 | 445 | (message, is_error) = self.get_disconnect_message() 446 | self.__log.debug("Disconnect message: %s (error= %s)" % 447 | (message, is_error)) 448 | 449 | self.__log.debug("Freeing SSH session: %d" % (self.__ssh_session_ptr)) 450 | 451 | _ssh_free(self.__ssh_session_ptr) 452 | 453 | def forward_listen(self, address, port): 454 | return _ssh_forward_listen(self.__ssh_session_ptr, address, port) 455 | 456 | def forward_accept(self, timeout_ms): 457 | ssh_channel_int = _ssh_forward_accept(self.__ssh_session_ptr, \ 458 | timeout_ms) 459 | 460 | return SshChannel(self, ssh_channel_int) 461 | 462 | def is_server_known(self, allow_new=False, cb=None): 463 | return _ssh_is_server_known(self.__ssh_session_ptr, allow_new, cb) 464 | 465 | def write_knownhost(self): 466 | return _ssh_write_knownhost(self.__ssh_session_ptr) 467 | 468 | def userauth_password(self, password): 469 | return _ssh_userauth_password(self.__ssh_session_ptr, None, password) 470 | 471 | def userauth_publickey(self, privkey): 472 | """This is the recommended function. Supports EC keys.""" 473 | 474 | return _ssh_userauth_publickey(self.__ssh_session_ptr, privkey) 475 | 476 | def execute(self, cmd, block_size=DEFAULT_EXECUTE_READ_BLOCK_SIZE): 477 | """Execute a remote command. This functionality does not support more 478 | than one command to be executed on the same channel, so we create a 479 | dedicated channel at the session level than allowing direct access at 480 | the channel level. 481 | """ 482 | 483 | with SshChannel(self) as sc: 484 | self.__log.debug("Executing command: %s" % (cmd)) 485 | 486 | sc.open_session() 487 | sc.request_exec(cmd) 488 | 489 | buffer_ = bytearray() 490 | while 1: 491 | bytes = sc.read(block_size) 492 | yield bytes 493 | 494 | if len(bytes) < block_size: 495 | break 496 | 497 | def is_blocking(self): 498 | return _ssh_is_blocking(self.__ssh_session_ptr) 499 | 500 | def set_blocking(self, blocking=True): 501 | _ssh_set_blocking(self.__ssh_session_ptr, blocking) 502 | 503 | def get_error_code(self): 504 | return ssh_get_error_code(self.__ssh_session_ptr) 505 | 506 | def get_error(self): 507 | return ssh_get_error(self.__ssh_session_ptr) 508 | 509 | def get_disconnect_message(self): 510 | return _ssh_get_disconnect_message(self.__ssh_session_ptr) 511 | 512 | def get_issue_banner(self): 513 | return _ssh_get_issue_banner(self.__ssh_session_ptr) 514 | 515 | def get_openssh_version(self): 516 | return _ssh_get_openssh_version(self.__ssh_session_ptr) 517 | 518 | def get_status(self): 519 | return _ssh_get_status(self.__ssh_session_ptr) 520 | 521 | def get_version(self): 522 | return _ssh_get_version(self.__ssh_session_ptr) 523 | 524 | def get_serverbanner(self): 525 | return _ssh_get_serverbanner(self.__ssh_session_ptr) 526 | 527 | def disconnect(self): 528 | return _ssh_disconnect(self.__ssh_session_ptr) 529 | 530 | @property 531 | def session_id(self): 532 | return self.__ssh_session_ptr 533 | 534 | 535 | class SshConnect(object): 536 | def __init__(self, ssh_session): 537 | self.__ssh_session_ptr = getattr(ssh_session, 538 | 'session_id', 539 | ssh_session) 540 | 541 | def __enter__(self): 542 | return self.open() 543 | 544 | def open(self): 545 | logging.debug("Connecting SSH.") 546 | _ssh_connect(self.__ssh_session_ptr) 547 | 548 | def __exit__(self, e_type, e_value, e_tb): 549 | self.close() 550 | 551 | def close(self): 552 | logging.debug("Disconnecting SSH.") 553 | _ssh_disconnect(self.__ssh_session_ptr) 554 | 555 | 556 | class _PublicKeyHashString(object): 557 | def __init__(self, hash_, hlen): 558 | self.__hexa = _ssh_get_hexa(hash_, hlen) 559 | 560 | def __repr__(self): 561 | hexa_string = cast(self.__hexa, c_char_p) 562 | # TODO: We do an empty concatenate just to ensure that we are making a copy. 563 | return hexa_string.value + "" 564 | 565 | def __del__(self): 566 | c_free(self.__hexa) 567 | 568 | 569 | class PublicKeyHash(object): 570 | def __init__(self, ssh_session): 571 | ssh_session_int = getattr(ssh_session, 'session_id', ssh_session) 572 | self.__hasht = _ssh_get_pubkey_hash(ssh_session_int) 573 | 574 | def __del__(self): 575 | c_free(self.__hasht[0]) 576 | 577 | def print_string(self, title="Public key"): 578 | _ssh_print_hexa(title, *self.__hasht) 579 | 580 | def __repr__(self): 581 | pks = _PublicKeyHashString(*self.__hasht) 582 | return repr(pks) 583 | 584 | -------------------------------------------------------------------------------- /pysecure/adapters/sftpa.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from datetime import datetime 4 | from os import SEEK_SET, SEEK_CUR, SEEK_END, utime 5 | from ctypes import create_string_buffer, cast, c_void_p, c_int, c_char_p, \ 6 | c_size_t, byref 7 | from collections import deque 8 | from time import mktime 9 | 10 | from pysecure.constants.ssh import SSH_NO_ERROR 11 | from pysecure.constants.sftp import O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, \ 12 | O_TRUNC 13 | from pysecure.types import CTimeval 14 | from pysecure.constants import SERVER_RESPONSES 15 | from pysecure.config import DEFAULT_CREATE_MODE, \ 16 | MAX_MIRROR_WRITE_CHUNK_SIZE, \ 17 | MAX_MIRROR_LISTING_CHUNK_SIZE, \ 18 | MAX_REMOTE_RECURSION_DEPTH 19 | from pysecure.calls.sftpi import c_sftp_get_error, c_sftp_new, c_sftp_init, \ 20 | c_sftp_open, c_sftp_write, c_sftp_free, \ 21 | c_sftp_opendir, c_sftp_closedir, \ 22 | c_sftp_readdir, c_sftp_attributes_free, \ 23 | c_sftp_dir_eof, c_sftp_tell, c_sftp_seek, \ 24 | c_sftp_read, c_sftp_fstat, c_sftp_rewind, \ 25 | c_sftp_close, c_sftp_rename, c_sftp_chmod, \ 26 | c_sftp_chown, c_sftp_mkdir, c_sftp_rmdir, \ 27 | c_sftp_stat, c_sftp_utimes, c_sftp_readlink, \ 28 | c_sftp_symlink, c_sftp_lstat, c_sftp_unlink 29 | 30 | from pysecure.exceptions import SftpError, SftpAlreadyExistsError 31 | from pysecure.utility import bytify, ByteStream, stringify 32 | 33 | def sftp_get_error(sftp_session_int): 34 | return c_sftp_get_error(sftp_session_int) 35 | 36 | def sftp_get_error_string(code): 37 | return ('%s [%s]' % (SERVER_RESPONSES[code][1], SERVER_RESPONSES[code][0])) 38 | 39 | def _sftp_new(ssh_session_int): 40 | logging.debug("Creating SFTP session.") 41 | 42 | session = c_sftp_new(ssh_session_int) 43 | if session is None: 44 | raise SftpError("Could not create SFTP session.") 45 | 46 | logging.debug("New SFTP session: %s" % (session)) 47 | return session 48 | 49 | def _sftp_free(sftp_session_int): 50 | logging.debug("Freeing SFTP session: %d" % (sftp_session_int)) 51 | 52 | c_sftp_free(sftp_session_int) 53 | 54 | def _sftp_init(sftp_session_int): 55 | logging.debug("Initializing SFTP session: %d" % (sftp_session_int)) 56 | 57 | result = c_sftp_init(sftp_session_int) 58 | if result < 0: 59 | type_ = sftp_get_error(sftp_session_int) 60 | if type_ >= 0: 61 | raise SftpError("Could not create SFTP session: %s" % 62 | (sftp_get_error_string(type_))) 63 | else: 64 | raise SftpError("Could not create SFTP session. There was an " 65 | "unspecified error.") 66 | 67 | def _sftp_opendir(sftp_session_int, path): 68 | logging.debug("Opening directory: %s" % (path)) 69 | 70 | sd = c_sftp_opendir(sftp_session_int, bytify(path)) 71 | if sd is None: 72 | type_ = sftp_get_error(sftp_session_int) 73 | if type_ >= 0: 74 | raise SftpError("Could not open directory [%s]: %s" % 75 | (path, sftp_get_error_string(type_))) 76 | else: 77 | raise SftpError("Could not open directory [%s]. There was an " 78 | "unspecified error." % (path)) 79 | 80 | logging.debug("Directory resource ID is (%d)." % (sd)) 81 | return sd 82 | 83 | def _sftp_closedir(sd): 84 | logging.debug("Closing directory: %d" % (sd)) 85 | 86 | result = c_sftp_closedir(sd) 87 | if result != SSH_NO_ERROR: 88 | raise SftpError("Could not close directory.") 89 | 90 | def _sftp_readdir(sftp_session_int, sd): 91 | attr = c_sftp_readdir(sftp_session_int, sd) 92 | 93 | if not attr: 94 | return None 95 | 96 | return EntryAttributes(attr) 97 | 98 | def _sftp_attributes_free(attr): 99 | c_sftp_attributes_free(attr) 100 | 101 | def _sftp_dir_eof(sd): 102 | return (c_sftp_dir_eof(sd) == 1) 103 | 104 | def _sftp_open(sftp_session_int, filepath, access_type, mode): 105 | logging.debug("Opening file: %s" % (filepath)) 106 | 107 | sf = c_sftp_open(sftp_session_int, 108 | bytify(filepath), 109 | access_type, 110 | mode) 111 | 112 | if sf is None: 113 | type_ = sftp_get_error(sftp_session_int) 114 | if type_ >= 0: 115 | raise SftpError("Could not open file [%s]: %s" % 116 | (filepath, sftp_get_error_string(type_))) 117 | else: 118 | raise SftpError("Could not open file [%s]. There was an " 119 | "unspecified error." % (filepath)) 120 | 121 | logging.debug("File [%s] opened as [%s]." % (filepath, sf)) 122 | return sf 123 | 124 | def _sftp_close(sf): 125 | logging.debug("Closing file: %s" % (sf)) 126 | 127 | result = c_sftp_close(sf) 128 | if result != SSH_NO_ERROR: 129 | raise SftpError("Close failed with code (%d)." % (result)) 130 | 131 | def _sftp_write(sf, buffer_): 132 | buffer_raw = create_string_buffer(buffer_) 133 | result = c_sftp_write(sf, cast(buffer_raw, c_void_p), len(buffer_)) 134 | if result < 0: 135 | raise SftpError("Could not write to file.") 136 | 137 | def _sftp_tell(sf): 138 | position = c_sftp_tell(sf) 139 | if position < 0: 140 | raise SftpError("Could not read current position in file.") 141 | 142 | return position 143 | 144 | def _sftp_seek(sf, position): 145 | if c_sftp_seek(sf, position) < 0: 146 | raise SftpError("Could not seek to the position (%d)." % (position)) 147 | 148 | def _sftp_read(sf, count): 149 | buffer_ = create_string_buffer(count) 150 | received_bytes = c_sftp_read(sf, cast(buffer_, c_void_p), c_size_t(count)) 151 | if received_bytes < 0: 152 | raise SftpError("Read failed.") 153 | 154 | return buffer_.raw[0:received_bytes] 155 | 156 | def _sftp_fstat(sf): 157 | attr = c_sftp_fstat(sf) 158 | if attr is None: 159 | raise SftpError("Could not acquire attributes for FSTAT.") 160 | 161 | return EntryAttributes(attr) 162 | 163 | def _sftp_rewind(sf): 164 | # Returns VOID. 165 | c_sftp_rewind(sf) 166 | 167 | def _sftp_stat(sftp_session_int, file_path): 168 | attr = c_sftp_stat(sftp_session_int, c_char_p(bytify(file_path))) 169 | if attr is None: 170 | type_ = sftp_get_error(sftp_session_int) 171 | if type_ >= 0: 172 | raise SftpError("Could not acquire attributes for STAT of [%s]: " 173 | "%s" % (file_path, sftp_get_error_string(type_))) 174 | else: 175 | raise SftpError("Could not acquire attributes for STAT of [%s]. " 176 | "There was an unspecified error." % (file_path)) 177 | 178 | return EntryAttributes(attr) 179 | 180 | def _sftp_rename(sftp_session_int, filepath_old, filepath_new): 181 | result = c_sftp_rename(sftp_session_int, 182 | c_char_p(bytify(filepath_old)), 183 | c_char_p(bytify(filepath_new))) 184 | 185 | if result < 0: 186 | type_ = sftp_get_error(sftp_session_int) 187 | if type_ >= 0: 188 | raise SftpError("Rename of [%s] to [%s] failed: %s" % 189 | (filepath_old, 190 | filepath_new, 191 | sftp_get_error_string(type_))) 192 | else: 193 | raise SftpError("Rename of [%s] to [%s] failed. There was an " 194 | "unspecified error." % 195 | (filepath_old, filespace_new)) 196 | 197 | def _sftp_chmod(sftp_session_int, file_path, mode): 198 | result = c_sftp_chmod(sftp_session_int, 199 | c_char_p(bytify(file_path)), 200 | c_int(mode)) 201 | 202 | if result < 0: 203 | type_ = sftp_get_error(sftp_session_int) 204 | if type_ >= 0: 205 | raise SftpError("CHMOD of [%s] for mode [%o] failed: %s" % 206 | (file_path, mode, sftp_get_error_string(type_))) 207 | else: 208 | raise SftpError("CHMOD of [%s] for mode [%o] failed. There was " % 209 | "an unspecified error." % (file_path, mode)) 210 | 211 | def _sftp_chown(sftp_session_int, file_path, uid, gid): 212 | result = c_sftp_chown(sftp_session_int, 213 | c_char_p(bytify(file_path)), 214 | c_int(uid), 215 | c_int(gid)) 216 | 217 | if result < 0: 218 | type_ = sftp_get_error(sftp_session_int) 219 | if type_ >= 0: 220 | raise SftpError("CHOWN of [%s] for UID (%d) and GID (%d) failed: " 221 | "%s" % 222 | (file_path, uid, gid, 223 | sftp_get_error_string(type_))) 224 | else: 225 | raise SftpError("CHOWN of [%s] for UID (%d) and GID (%d) failed. " 226 | "There was an unspecified error." % 227 | (file_path, mode)) 228 | 229 | def _sftp_exists(sftp_session_int, path): 230 | try: 231 | _sftp_stat(sftp_session_int, path) 232 | except SftpError: 233 | return False 234 | else: 235 | return True 236 | 237 | def _sftp_mkdir(sftp_session_int, path, mode, check_exists_on_fail=True): 238 | logging.debug("Creating directory: %s" % (path)) 239 | 240 | result = c_sftp_mkdir(sftp_session_int, 241 | c_char_p(bytify(path)), 242 | c_int(mode)) 243 | 244 | if result < 0: 245 | if check_exists_on_fail is not False: 246 | if _sftp_exists(sftp_session_int, path) is True: 247 | raise SftpAlreadyExistsError("Path already exists: %s" % 248 | (path)) 249 | 250 | type_ = sftp_get_error(sftp_session_int) 251 | if type_ >= 0: 252 | raise SftpError("MKDIR of [%s] for mode [%o] failed: %s" % 253 | (path, mode, sftp_get_error_string(type_))) 254 | else: 255 | raise SftpError("MKDIR of [%s] for mode [%o] failed. There was " % 256 | "an unspecified error." % (path, mode)) 257 | 258 | def _sftp_rmdir(sftp_session_int, path): 259 | logging.debug("Deleting directory: %s" % (path)) 260 | 261 | result = c_sftp_rmdir(sftp_session_int, c_char_p(bytify(path))) 262 | 263 | if result < 0: 264 | type_ = sftp_get_error(sftp_session_int) 265 | if type_ >= 0: 266 | raise SftpError("RMDIR of [%s] failed: %s" % 267 | (path, sftp_get_error_string(type_))) 268 | else: 269 | raise SftpError("RMDIR of [%s] failed. There was an unspecified " 270 | "error." % (path)) 271 | 272 | def _sftp_lstat(sftp_session_int, file_path): 273 | attr = c_sftp_lstat(sftp_session_int, c_char_p(bytify(file_path))) 274 | 275 | if attr is None: 276 | type_ = sftp_get_error(sftp_session_int) 277 | if type_ >= 0: 278 | raise SftpError("LSTAT of [%s] failed: %s" % 279 | (file_path, sftp_get_error_string(type_))) 280 | else: 281 | raise SftpError("LSTAT of [%s] failed. There was an unspecified " 282 | "error." % (file_path)) 283 | 284 | return EntryAttributes(attr) 285 | 286 | def _sftp_unlink(sftp_session_int, file_path): 287 | logging.debug("Deleting file: %s" % (file_path)) 288 | 289 | result = c_sftp_unlink(sftp_session_int, 290 | c_char_p(bytify(file_path))) 291 | # TODO: This seems to be a large integer. What is it? 292 | if result < 0: 293 | type_ = sftp_get_error(sftp_session_int) 294 | if type_ >= 0: 295 | raise SftpError("Unlink of [%s] failed: %s" % 296 | (file_path, sftp_get_error_string(type_))) 297 | else: 298 | raise SftpError("Unlink of [%s] failed. There was an unspecified " 299 | "error." % (file_path)) 300 | 301 | def _sftp_readlink(sftp_session_int, file_path): 302 | target = c_sftp_readlink(sftp_session_int, c_char_p(bytify(file_path))) 303 | 304 | if target is None: 305 | type_ = sftp_get_error(sftp_session_int) 306 | if type_ >= 0: 307 | raise SftpError("Read of link [%s] failed: %s" % 308 | (file_path, sftp_get_error_string(type_))) 309 | else: 310 | raise SftpError("Read of link [%s] failed. There was an " 311 | "unspecified error." % (file_path)) 312 | 313 | return target 314 | 315 | def _sftp_symlink(sftp_session_int, to, from_): 316 | result = c_sftp_symlink(sftp_session_int, 317 | c_char_p(bytify(to)), 318 | c_char_p(bytify(from_))) 319 | 320 | if result < 0: 321 | type_ = sftp_get_error(sftp_session_int) 322 | if type_ >= 0: 323 | raise SftpError("Symlink of [%s] to target [%s] failed: %s" % 324 | (from_, to, sftp_get_error_string(type_))) 325 | else: 326 | raise SftpError("Symlink of [%s] to target [%s] failed. There was " 327 | "an unspecified error." % (from_, to)) 328 | 329 | def _sftp_setstat(sftp_session_int, file_path, entry_attributes): 330 | result = c_sftp_setstat(sftp_session_int, 331 | c_char_p(bytify(file_path)), 332 | entry_attributes.raw) 333 | 334 | if result < 0: 335 | type_ = sftp_get_error(sftp_session_int) 336 | if type_ >= 0: 337 | raise SftpError("Set-stat on [%s] failed: %s" % 338 | (file_path, sftp_get_error_string(type_))) 339 | else: 340 | raise SftpError("Set-stat on [%s] failed. There was an " 341 | "unspecified error." % (file_path)) 342 | 343 | def _sftp_listdir(sftp_session_int, 344 | path, 345 | get_directories=True, 346 | get_files=True): 347 | logging.debug("Listing directory: %s" % (path)) 348 | 349 | with SftpDirectory(sftp_session_int, bytify(path)) as sd_: 350 | while 1: 351 | attributes = _sftp_readdir(sftp_session_int, sd_) 352 | if attributes is None: 353 | break 354 | 355 | if attributes.is_directory is True and get_directories is True or \ 356 | attributes.is_directory is False and get_files is True: 357 | yield attributes 358 | 359 | if not _sftp_dir_eof(sd_): 360 | raise SftpError("We're done iterating the directory, but it's not " 361 | "at EOF.") 362 | 363 | def _sftp_utimes(sftp_session_int, file_path, atime_epoch, mtime_epoch): 364 | atime = CTimeval() 365 | mtime = CTimeval() 366 | 367 | atime.tv_sec = int(atime_epoch) 368 | atime.tv_usec = 0 369 | 370 | mtime.tv_sec = int(mtime_epoch) 371 | mtime.tv_usec = 0 372 | 373 | times = (CTimeval * 2)(atime, mtime) 374 | 375 | result = c_sftp_utimes(sftp_session_int, 376 | c_char_p(bytify(file_path)), 377 | byref(times)) 378 | 379 | if result < 0: 380 | raise SftpError("Times updated of [%s] failed." % (file_path)) 381 | 382 | def _sftp_utimes_dt(sftp_session_int, file_path, atime_dt, mtime_dt): 383 | _sftp_utimes(sftp_session_int, 384 | bytify(file_path), 385 | mktime(atime_dt.timetuple()), 386 | mktime(mtime_dt.timetuple())) 387 | 388 | 389 | class SftpSession(object): 390 | def __init__(self, ssh_session): 391 | self.__ssh_session_int = getattr(ssh_session, 392 | 'session_id', 393 | ssh_session) 394 | self.__log = logging.getLogger('SSH_SESSION(%s)' % 395 | (self.__ssh_session_int)) 396 | 397 | def __enter__(self): 398 | return self.open() 399 | 400 | def open(self): 401 | self.__sftp_session_int = _sftp_new(self.__ssh_session_int) 402 | _sftp_init(self.__sftp_session_int) 403 | 404 | return self 405 | 406 | def __exit__(self, e_type, e_value, e_tb): 407 | self.close() 408 | 409 | def close(self): 410 | _sftp_free(self.__sftp_session_int) 411 | 412 | def stat(self, file_path): 413 | return _sftp_stat(self.__sftp_session_int, file_path) 414 | 415 | def rename(self, filepath_old, filepath_new): 416 | return _sftp_rename(self.__sftp_session_int, filepath_old, filepath_new) 417 | 418 | def chmod(self, file_path, mode): 419 | return _sftp_chmod(self.__sftp_session_int, file_path, mode) 420 | 421 | def chown(self, file_path, uid, gid): 422 | return _sftp_chown(self.__sftp_session_int, file_path, uid, gid) 423 | 424 | def exists(self, path): 425 | return _sftp_exists(self.__sftp_session_int, path) 426 | 427 | def mkdir(self, path, mode=0o755): 428 | return _sftp_mkdir(self.__sftp_session_int, path, mode) 429 | 430 | def rmdir(self, path): 431 | return _sftp_rmdir(self.__sftp_session_int, path) 432 | 433 | def lstat(self, file_path): 434 | return _sftp_lstat(self.__sftp_session_int, file_path) 435 | 436 | def unlink(self, file_path): 437 | return _sftp_unlink(self.__sftp_session_int, file_path) 438 | 439 | def readlink(self, file_path): 440 | return _sftp_readlink(self.__sftp_session_int, file_path) 441 | 442 | def symlink(self, to, from_): 443 | return _sftp_symlink(self.__sftp_session_int, to, from_) 444 | 445 | def setstat(self, file_path, entry_attributes): 446 | return _sftp_setstat(self.__sftp_session_int, file_path, entry_attributes) 447 | 448 | def listdir(self, path, get_directories=True, get_files=True): 449 | return _sftp_listdir(self.__sftp_session_int, 450 | path, 451 | get_directories, 452 | get_files) 453 | 454 | def rmtree(self, path): 455 | self.__log.debug("REMOTE: Doing recursive remove: %s" % (path)) 456 | 457 | # Collect names and heirarchy of subdirectories. Also, delete files 458 | # that we encounter. 459 | 460 | # If we don't put our root path in here, the recursive removal (at the 461 | # end, below) will fail once it get's back to the top. 462 | path_relations = { path: None } 463 | parents = {} 464 | def remote_dir_cb(parent_path, full_path, entry): 465 | path_relations[full_path] = parent_path 466 | 467 | if parent_path not in parents: 468 | parents[parent_path] = [full_path] 469 | else: 470 | parents[parent_path].append(full_path) 471 | 472 | def remote_listing_cb(parent_path, listing): 473 | for (file_path, entry) in listing: 474 | self.__log.debug("REMOTE: Unlinking: %s" % (file_path)) 475 | self.unlink(file_path) 476 | 477 | self.recurse(path, 478 | remote_dir_cb, 479 | remote_listing_cb, 480 | MAX_MIRROR_LISTING_CHUNK_SIZE) 481 | 482 | # Now, delete the directories. Descend to leaves and work our way back. 483 | 484 | self.__log.debug("REMOTE: Removing (%d) directories/subdirectories." % 485 | (len(path_relations))) 486 | 487 | def remove_directory(node_path, depth=0): 488 | if depth > MAX_REMOTE_RECURSION_DEPTH: 489 | raise SftpError("Remote rmtree recursed too deeply. Either " 490 | "the directories run very deep, or we've " 491 | "encountered a cycle (probably via hard-" 492 | "links).") 493 | 494 | if node_path in parents: 495 | while parents[node_path]: 496 | remove_directory(parents[node_path][0], depth + 1) 497 | del parents[node_path][0] 498 | 499 | self.__log.debug("REMOTE: Removing directory: %s" % (node_path)) 500 | 501 | self.rmdir(node_path) 502 | 503 | # All children subdirectories have been deleted. Delete our parent 504 | # record. 505 | 506 | try: 507 | del parents[node_path] 508 | except KeyError: 509 | pass 510 | 511 | # Delete the mapping from us to our parent. 512 | 513 | del path_relations[node_path] 514 | 515 | remove_directory(path) 516 | 517 | def recurse(self, root_path, dir_cb, listing_cb, max_listing_size=0, 518 | max_depth=MAX_REMOTE_RECURSION_DEPTH): 519 | """Recursively iterate a directory. Invoke callbacks for directories 520 | and entries (both are optional, but it doesn't make sense unless one is 521 | provided). "max_listing_size" will allow for the file-listing to be 522 | chunked into manageable pieces. "max_depth" limited how deep recursion 523 | goes. This can be used to make it easy to simply read a single 524 | directory in chunks. 525 | """ 526 | 527 | q = deque([(root_path, 0)]) 528 | collected = [] 529 | 530 | def push_file(path, file_path, entry): 531 | collected.append((file_path, entry)) 532 | if max_listing_size > 0 and \ 533 | len(collected) >= max_listing_size: 534 | listing_cb(path, collected) 535 | 536 | # Clear contents on the list. We delete it this way so that 537 | # we're only -modifying- the list rather than replacing it (a 538 | # requirement of a closure). 539 | del collected[:] 540 | 541 | while q: 542 | (path, current_depth) = q.popleft() 543 | 544 | entries = self.listdir(path) 545 | for entry in entries: 546 | filename = stringify(entry.name) 547 | file_path = ('%s/%s' % (path, filename)) 548 | 549 | if entry.is_symlink: 550 | push_file(path, file_path, entry) 551 | elif entry.is_directory: 552 | if filename == '.' or filename == '..': 553 | continue 554 | 555 | if dir_cb is not None: 556 | dir_cb(path, file_path, entry) 557 | 558 | new_depth = current_depth + 1 559 | 560 | if max_depth is None or new_depth <= max_depth: 561 | q.append((file_path, new_depth)) 562 | elif entry.is_regular: 563 | if listing_cb is not None: 564 | push_file(path, file_path, entry) 565 | 566 | if listing_cb is not None and (max_listing_size == 0 or 567 | len(collected) > 0): 568 | listing_cb(path, collected) 569 | 570 | def write_to_local(self, filepath_from, filepath_to, mtime_dt=None): 571 | """Open a remote file and write it locally.""" 572 | 573 | self.__log.debug("Writing R[%s] -> L[%s]." % (filepath_from, 574 | filepath_to)) 575 | 576 | with SftpFile(self, filepath_from, 'r') as sf_from: 577 | with open(filepath_to, 'wb') as file_to: 578 | while 1: 579 | part = sf_from.read(MAX_MIRROR_WRITE_CHUNK_SIZE) 580 | file_to.write(part) 581 | 582 | if len(part) < MAX_MIRROR_WRITE_CHUNK_SIZE: 583 | break 584 | 585 | if mtime_dt is None: 586 | mtime_dt = datetime.now() 587 | 588 | mtime_epoch = mktime(mtime_dt.timetuple()) 589 | utime(filepath_to, (mtime_epoch, mtime_epoch)) 590 | 591 | def write_to_remote(self, filepath_from, filepath_to, mtime_dt=None): 592 | """Open a local file and write it remotely.""" 593 | 594 | self.__log.debug("Writing L[%s] -> R[%s]." % (filepath_from, 595 | filepath_to)) 596 | 597 | with open(filepath_from, 'rb') as file_from: 598 | with SftpFile(self, filepath_to, 'w') as sf_to: 599 | while 1: 600 | part = file_from.read(MAX_MIRROR_WRITE_CHUNK_SIZE) 601 | sf_to.write(part) 602 | 603 | if len(part) < MAX_MIRROR_WRITE_CHUNK_SIZE: 604 | break 605 | 606 | if mtime_dt is None: 607 | mtime_dt = datetime.now() 608 | 609 | self.utimes_dt(filepath_to, mtime_dt, mtime_dt) 610 | 611 | def utimes(self, file_path, atime_epoch, mtime_epoch): 612 | _sftp_utimes(self.__sftp_session_int, 613 | file_path, 614 | atime_epoch, 615 | mtime_epoch) 616 | 617 | def utimes_dt(self, file_path, atime_dt, mtime_dt): 618 | _sftp_utimes_dt(self.__sftp_session_int, file_path, atime_dt, mtime_dt) 619 | 620 | @property 621 | def session_id(self): 622 | return self.__sftp_session_int 623 | 624 | 625 | class SftpDirectory(object): 626 | def __init__(self, sftp_session, path): 627 | self.__sftp_session_int = getattr(sftp_session, 628 | 'session_id', 629 | sftp_session) 630 | self.__path = path 631 | 632 | def __enter__(self): 633 | return self.open() 634 | 635 | def open(self): 636 | self.__sd = _sftp_opendir(self.__sftp_session_int, self.__path) 637 | return self.__sd 638 | 639 | def __exit__(self, e_type, e_value, e_tb): 640 | self.close() 641 | 642 | def close(self): 643 | _sftp_closedir(self.__sd) 644 | 645 | 646 | class SftpFile(object): 647 | def __init__(self, sftp_session, filepath, access_type_om='r', 648 | create_mode=DEFAULT_CREATE_MODE): 649 | 650 | at_im = self.__at_om_to_im(access_type_om) 651 | 652 | self.__sftp_session_int = getattr(sftp_session, 653 | 'session_id', 654 | sftp_session) 655 | 656 | self.__filepath = filepath 657 | self.__access_type = at_im 658 | self.__create_mode = create_mode 659 | 660 | def __repr__(self): 661 | return ('' % 662 | (self.__access_type[0], self.__filepath)) 663 | 664 | def __at_om_to_im(self, om): 665 | """Convert an "outer" access mode to an "inner" access mode. 666 | Returns a tuple of: 667 | 668 | (, , ). 669 | """ 670 | 671 | original_om = om 672 | 673 | if om[0] == 'U': 674 | om = om[1:] 675 | is_um = True 676 | else: 677 | is_um = False 678 | 679 | if om == 'r': 680 | return (original_om, O_RDONLY, False, is_um) 681 | elif om == 'w': 682 | return (original_om, O_WRONLY | O_CREAT | O_TRUNC, False, is_um) 683 | elif om == 'a': 684 | return (original_om, O_WRONLY | O_CREAT, False, is_um) 685 | elif om == 'r+': 686 | return (original_om, O_RDWR | O_CREAT, False, is_um) 687 | elif om == 'w+': 688 | return (original_om, O_RDWR | O_CREAT | O_TRUNC, False, is_um) 689 | elif om == 'a+': 690 | return (original_om, O_RDWR | O_CREAT, True, is_um) 691 | else: 692 | raise Exception("Outer access mode [%s] is invalid." % 693 | (original_om)) 694 | 695 | def __enter__(self): 696 | return self.open() 697 | 698 | def open(self): 699 | """This is the only way to open a file resource.""" 700 | 701 | self.__sf = _sftp_open(self.__sftp_session_int, 702 | self.__filepath, 703 | self.access_type_int, 704 | self.__create_mode) 705 | 706 | if self.access_type_is_append is True: 707 | self.seek(self.filesize) 708 | 709 | return SftpFileObject(self) 710 | 711 | def __exit__(self, e_type, e_value, e_tb): 712 | self.close() 713 | 714 | def close(self): 715 | _sftp_close(self.__sf) 716 | 717 | def write(self, buffer_): 718 | return _sftp_write(self.__sf, buffer_) 719 | 720 | def seek(self, position): 721 | return _sftp_seek(self.__sf, position) 722 | 723 | def read(self, size): 724 | """Read a length of bytes. Return empty on EOF.""" 725 | 726 | return _sftp_read(self.__sf, size) 727 | 728 | def fstat(self): 729 | return _sftp_fstat(self.__sf) 730 | 731 | def rewind(self): 732 | return _sftp_rewind(self.__sf) 733 | 734 | @property 735 | def sf(self): 736 | return self.__sf 737 | 738 | @property 739 | def position(self): 740 | return _sftp_tell(self.__sf) 741 | 742 | @property 743 | def filesize(self): 744 | return self.fstat().size 745 | 746 | @property 747 | def filepath(self): 748 | return self.__filepath 749 | 750 | @property 751 | def access_type_str(self): 752 | return self.__access_type[0] 753 | 754 | @property 755 | def access_type_int(self): 756 | return self.__access_type[1] 757 | 758 | @property 759 | def access_type_is_append(self): 760 | return self.__access_type[2] 761 | 762 | @property 763 | def access_type_has_universal_nl(self): 764 | return self.__access_type[3] 765 | 766 | 767 | class EntryAttributes(object): 768 | """This wraps the raw attribute type, and frees it at destruction.""" 769 | 770 | def __init__(self, attr_raw): 771 | self.__attr_raw = attr_raw 772 | 773 | def __del__(self): 774 | _sftp_attributes_free(self.__attr_raw) 775 | 776 | def __getattr__(self, key): 777 | return getattr(self.__attr_raw.contents, key) 778 | 779 | def __repr__(self): 780 | return repr(self.__attr_raw.contents) 781 | 782 | def __str__(self): 783 | return str(self.__attr_raw.contents) 784 | 785 | @property 786 | def raw(self): 787 | return self.__attr_raw 788 | 789 | 790 | class StreamBuffer(object): 791 | def __init__(self): 792 | self.__stream = ByteStream() 793 | 794 | def read_until_nl(self, read_cb): 795 | captured = ByteStream() 796 | 797 | i = 0 798 | found = False 799 | nl = None 800 | done = False 801 | 802 | while found is False and done is False: 803 | position = self.__stream.tell() 804 | couplet = self.__stream.read(2) 805 | 806 | if len(couplet) < 2: 807 | logging.debug("Couplet is a dwarf of (%d) bytes." % 808 | (len(couplet))) 809 | 810 | more_data = read_cb() 811 | assert issubclass(more_data.__class__, bytes) 812 | logging.debug("Retrieved (%d) more bytes." % (len(more_data))) 813 | 814 | if more_data != b'': 815 | self.__stream.write(more_data) 816 | self.__stream.seek(position) 817 | 818 | # Re-read. 819 | couplet = self.__stream.read(2) 820 | logging.debug("Couplet is now (%d) bytes." % 821 | (len(couplet))) 822 | elif couplet == b'': 823 | done = True 824 | continue 825 | 826 | if len(couplet) == 2: 827 | # We represent a \r\n newline. 828 | if couplet == b'\r\n': 829 | nl = couplet 830 | found = True 831 | 832 | captured.write(couplet) 833 | 834 | # We represent a one-byte newline that's in the first position. 835 | elif couplet[0:1] == b'\r' or couplet[0:1] == b'\n': 836 | nl = couplet[0] 837 | found = True 838 | 839 | captured.write(couplet[0:1]) 840 | self.__stream.seek(-1, SEEK_CUR) 841 | 842 | # The first position is an ordinary character. If there's a 843 | # newline in the second position, we'll pick it up on the next 844 | # round. 845 | else: 846 | captured.write(couplet[0:1]) 847 | self.__stream.seek(-1, SEEK_CUR) 848 | elif len(couplet) == 1: 849 | # This is the last [odd] byte of the file. 850 | 851 | if couplet[0:1] == b'\r' or couplet[0:1] == b'\n': 852 | nl = couplet[0] 853 | found = True 854 | 855 | captured.write(couplet[0:1]) 856 | 857 | done = True 858 | 859 | i += 1 860 | 861 | return (stringify(captured.get_bytes()), nl) 862 | 863 | 864 | class SftpFileObject(object): 865 | """A file-like object interface for SFTP resources.""" 866 | 867 | __block_size = 8192 868 | 869 | def __init__(self, sf): 870 | self.__sf = sf 871 | self.__buffer = StreamBuffer() 872 | self.__offset = 0 873 | self.__buffer_offset = 0 874 | self.__newlines = {} 875 | self.__eof = False 876 | self.__log = logging.getLogger('FILE(%s)' % (sf)) 877 | 878 | def __repr__(self): 879 | return ('' % 880 | (self.mode, self.name.replace('"', '\\"'))) 881 | 882 | def write(self, buffer_): 883 | # self.__log.debug("Writing (%d) bytes." % (len(buffer_))) 884 | self.__sf.write(buffer_) 885 | 886 | def read(self, size=None): 887 | """Read a length of bytes. Return empty on EOF. If 'size' is omitted, 888 | return whole file. 889 | """ 890 | 891 | if size is not None: 892 | return self.__sf.read(size) 893 | 894 | block_size = self.__class__.__block_size 895 | 896 | b = bytearray() 897 | received_bytes = 0 898 | while 1: 899 | partial = self.__sf.read(block_size) 900 | # self.__log.debug("Reading (%d) bytes. (%d) bytes returned." % 901 | # (block_size, len(partial))) 902 | 903 | b.extend(partial) 904 | received_bytes += len(partial) 905 | 906 | if len(partial) < block_size: 907 | self.__log.debug("End of file.") 908 | break 909 | 910 | self.__log.debug("Read (%d) bytes for total-file." % (received_bytes)) 911 | 912 | return b 913 | 914 | def close(self): 915 | """Close the resource.""" 916 | 917 | self.__sf.close() 918 | 919 | def seek(self, offset, whence=SEEK_SET): 920 | """Reposition the file pointer.""" 921 | 922 | if whence == SEEK_SET: 923 | self.__sf.seek(offset) 924 | elif whence == SEEK_CUR: 925 | self.__sf.seek(self.tell() + offset) 926 | elif whence == SEEK_END: 927 | self.__sf.seek(self.__sf.filesize - offset) 928 | 929 | def tell(self): 930 | """Report the current position.""" 931 | 932 | return self.__sf.position 933 | 934 | def flush(self): 935 | """Flush data. This is a no-op in our context.""" 936 | 937 | pass 938 | 939 | def isatty(self): 940 | """Only return True if connected to a TTY device.""" 941 | 942 | return False 943 | 944 | def __iter__(self): 945 | return self 946 | 947 | def __next__(self): 948 | """Iterate through lines of text.""" 949 | 950 | next_line = self.readline() 951 | if next_line == '': 952 | self.__log.debug("No more lines (EOF).") 953 | raise StopIteration() 954 | 955 | return next_line 956 | 957 | # For Python 2.x compatibility. 958 | next = __next__ 959 | 960 | def readline(self, size=None): 961 | """Read a single line of text with EOF.""" 962 | 963 | # TODO: Add support for Unicode. 964 | (line, nl) = self.__buffer.read_until_nl(self.__retrieve_data) 965 | 966 | if self.__sf.access_type_has_universal_nl and nl is not None: 967 | self.__newlines[nl] = True 968 | 969 | return line 970 | 971 | def __retrieve_data(self): 972 | """Read more data from the file.""" 973 | 974 | if self.__eof is True: 975 | return b'' 976 | 977 | logging.debug("Reading another block.") 978 | block = self.read(self.__block_size) 979 | if block == b'': 980 | self.__log.debug("We've encountered the EOF.") 981 | self.__eof = True 982 | 983 | return block 984 | 985 | def readlines(self, sizehint=None): 986 | self.__log.debug("Reading all lines.") 987 | 988 | collected = [] 989 | total = 0 990 | for line in iter(self): 991 | collected.append(line) 992 | total += len(line) 993 | 994 | if sizehint is not None and total > sizehint: 995 | break 996 | 997 | self.__log.debug("Read whole file as (%d) lines." % (len(collected))) 998 | return ''.join(collected) 999 | 1000 | @property 1001 | def closed(self): 1002 | raise False 1003 | 1004 | @property 1005 | def encoding(self): 1006 | return None 1007 | 1008 | @property 1009 | def mode(self): 1010 | return self.__sf.access_type_str 1011 | 1012 | @property 1013 | def name(self): 1014 | return self.__sf.filepath 1015 | 1016 | @property 1017 | def newlines(self): 1018 | if self.__sf.access_type_has_universal_nl is False: 1019 | raise AttributeError("Universal newlines are unavailable since " 1020 | "not requested.") 1021 | 1022 | return tuple(self.__newlines.keys()) 1023 | 1024 | @property 1025 | def raw(self): 1026 | return self.__sf 1027 | 1028 | # c_sftp_seek64 1029 | # c_sftp_tell64 1030 | 1031 | # c_sftp_extensions_get_count 1032 | # c_sftp_extensions_get_name 1033 | # c_sftp_fstatvfs 1034 | # c_sftp_server_version 1035 | # c_sftp_statvfs 1036 | # c_sftp_statvfs_free 1037 | 1038 | --------------------------------------------------------------------------------