├── 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 |
--------------------------------------------------------------------------------