Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a key if keyfile is not provided. #15

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 26 additions & 31 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,53 +1,48 @@
sftpserver
==========

``sftpserver`` is a simple single-threaded SFTP server based on
Paramiko's SFTPServer.
``sftpserver`` is a skeletal SFTP server written using `Paramiko`_

I needed a simple server that could be used as a stub for testing
Python SFTP clients so I whipped out one.
This project exists to serve as a starting point / demonstration of how to build
an SFTP server or as something to be used in tests. As such the goal is *not* to
provide a full featured sftp server.


Installation
------------

Using ``pip``::

$ [sudo] pip install sftpserver
This was initially a simple fork of `@rspivak`'s `sftpserver`_, which in turn
was an adaptation of the code from Paramiko's tests. However, I updated it
further to demonstrate the use of different `threaded` and `forked` modes of
operation.


Examples
--------

::

$ sftpserver
Usage: sftpserver [options]
-k/--keyfile should be specified
# run sftpserver with defaults (serving current dir, at
# localhost:3373, in threaded mode)

$ python -m sftpserver

Options:
-h, --help show this help message and exit
--host=HOST listen on HOST [default: localhost]
-p PORT, --port=PORT listen on PORT [default: 3373]
-l LEVEL, --level=LEVEL
Debug level: WARNING, INFO, DEBUG [default: INFO]
-k FILE, --keyfile=FILE
Path to private key, for example /tmp/test_rsa.key
# run sftpserver with defaults (serving dir /tmp, at
# localhost:3373, in forked mode, using server key /tmp/test_rsa.key)

$ sftpserver -k /tmp/test_rsa.key -l DEBUG
$ sftpserver -r /tmp -k /tmp/test_rsa.key -l DEBUG -m forked


Generating a test private key::

$ openssl req -out CSR.csr -new -newkey rsa:2048 -nodes -keyout /tmp/test_rsa.key

Connecting with a Python client to our server:
Connecting with a Python client to our server::

>>> import paramiko
>>> pkey = paramiko.RSAKey.from_private_key_file('/tmp/test_rsa.key')
>>> transport = paramiko.Transport(('localhost', 3373))
>>> transport.connect(username='admin', password='admin', pkey=pkey)
>>> sftp = paramiko.SFTPClient.from_transport(transport)
>>> sftp.listdir('.')
['loop.py', 'stub_sftp.py']


>>> import paramiko
>>> pkey = paramiko.RSAKey.from_private_key_file('/tmp/test_rsa.key')
>>> transport = paramiko.Transport(('localhost', 3373))
>>> transport.connect(username='admin', password='admin', pkey=pkey)
>>> sftp = paramiko.SFTPClient.from_transport(transport)
>>> sftp.listdir('.')
['loop.py', 'stub_sftp.py']
.. _Paramiko: https://www.paramiko.org/
.. _sftpserver: https://github.com/rspivak/sftpserver
13 changes: 7 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,26 @@
Operating System :: Unix
"""


def read(*rel_names):
return open(os.path.join(os.path.dirname(__file__), *rel_names)).read()


setup(
name='sftpserver',
version='0.3',
url='http://github.com/rspivak/sftpserver',
version='0.5',
url='http://github.com/lonetwin/sftpserver',
license='MIT',
description='sftpserver - a simple single-threaded sftp server',
author='Ruslan Spivak',
author_email='[email protected]',
description='sftpserver - a skeletal SFTP server written using Paramiko',
author='Steven Fernandez',
author_email='[email protected]',
packages=find_packages('src'),
package_dir={'': 'src'},
install_requires=['setuptools>=0.7', 'paramiko'],
zip_safe=False,
entry_points="""\
[console_scripts]
sftpserver = sftpserver:main
sftpserver = sftpserver.__main__:main
""",
classifiers=filter(None, classifiers.split('\n')),
long_description=read('README.rst'),
Expand Down
100 changes: 0 additions & 100 deletions src/sftpserver/__init__.py
Original file line number Diff line number Diff line change
@@ -1,100 +0,0 @@
###############################################################################
#
# Copyright (c) 2011-2017 Ruslan Spivak
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

__author__ = 'Ruslan Spivak <[email protected]>'

import time
import socket
import argparse
import sys
import textwrap

import paramiko

from sftpserver.stub_sftp import StubServer, StubSFTPServer

HOST, PORT = 'localhost', 3373
BACKLOG = 10


def start_server(host, port, keyfile, level):
paramiko_level = getattr(paramiko.common, level)
paramiko.common.logging.basicConfig(level=paramiko_level)

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
server_socket.bind((host, port))
server_socket.listen(BACKLOG)

while True:
conn, addr = server_socket.accept()

host_key = paramiko.RSAKey.from_private_key_file(keyfile)
transport = paramiko.Transport(conn)
transport.add_server_key(host_key)
transport.set_subsystem_handler(
'sftp', paramiko.SFTPServer, StubSFTPServer)

server = StubServer()
transport.start_server(server=server)

channel = transport.accept()
while transport.is_active():
time.sleep(1)


def main():
usage = """\
usage: sftpserver [options]
-k/--keyfile should be specified
"""
parser = argparse.ArgumentParser(usage=textwrap.dedent(usage))
parser.add_argument(
'--host', dest='host', default=HOST,
help='listen on HOST [default: %(default)s]'
)
parser.add_argument(
'-p', '--port', dest='port', type=int, default=PORT,
help='listen on PORT [default: %(default)d]'
)
parser.add_argument(
'-l', '--level', dest='level', default='INFO',
help='Debug level: WARNING, INFO, DEBUG [default: %(default)s]'
)
parser.add_argument(
'-k', '--keyfile', dest='keyfile', metavar='FILE',
help='Path to private key, for example /tmp/test_rsa.key'
)

args = parser.parse_args()

if args.keyfile is None:
parser.print_help()
sys.exit(-1)

start_server(args.host, args.port, args.keyfile, args.level)


if __name__ == '__main__':
main()
149 changes: 149 additions & 0 deletions src/sftpserver/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
###############################################################################
#
# Copyright (c) 2011-2017 Ruslan Spivak
# Copyright (c) 2020 Steven Fernandez <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

__author__ = 'Steven Fernandez <[email protected]>'

import argparse
import logging
import os
import pwd
import socket
import sys

import paramiko

from sftpserver.stub_sftp import StubSFTPServer, ssh_server

# - Defaults
HOST, PORT = 'localhost', 3373
ROOT = StubSFTPServer.ROOT
LOG_LEVEL = logging.getLevelName(logging.INFO)
MODE = 'threaded'

BACKLOG = 10


def setup_logging(level, mode):
if mode == 'threaded':
log_format = logging.BASIC_FORMAT
else:
log_format = '%(process)d:' + logging.BASIC_FORMAT

logging.basicConfig(format=log_format)

# - setup paramiko logging
paramiko_logger = logging.getLogger('paramiko')
paramiko_logger.setLevel(logging.INFO)

logger = logging.getLogger(__name__)
logger.setLevel(level)
return logger


def start_server(host=HOST, port=PORT, root=ROOT, keyfile=None, level=LOG_LEVEL, mode=MODE):
logger = setup_logging(level, mode)

if keyfile is None:
server_key = paramiko.RSAKey.generate(bits=1024)
else:
server_key = paramiko.RSAKey.from_private_key_file(keyfile)

StubSFTPServer.ROOT = root

logger.debug('Serving %s over sftp at %s:%s', root, host, port)

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
server_socket.bind((host, port))
server_socket.listen(BACKLOG)

sessions = []
while True:
connection, _ = server_socket.accept()
transport = paramiko.Transport(connection)
transport.add_server_key(server_key)
transport.set_subsystem_handler('sftp', paramiko.SFTPServer, StubSFTPServer)

if mode == 'threaded':
logger.debug('Starting a new thread')
transport.start_server(server=ssh_server)
sessions.append(transport.accept())
elif mode == 'forked':
pid = os.fork()
if pid == 0:
logger.debug('Starting a new process')
transport.start_server(server=ssh_server)
sessions.append(transport.accept())
if os.geteuid() == 0:
user = pwd.getpwnam(transport.get_username())
logger.debug('Dropping privileges, will run as %s', user)
os.setuid(user.pw_uid)
os.setgid(user.pw_gid)
else:
transport.atfork()
os.waitpid(-1, 0)

logger.debug('%s active sessions', len(sessions))


def main():
usage = """usage: sftpserver [options]"""
parser = argparse.ArgumentParser(usage=usage)
parser.add_argument(
'--host', dest='host', default=HOST,
help='listen on HOST [default: %(default)s]'
)
parser.add_argument(
'-p', '--port', dest='port', type=int, default=PORT,
help='listen on PORT [default: %(default)d]'
)
parser.add_argument(
'-l', '--level', dest='level', default=LOG_LEVEL,
help='Debug level: WARNING, INFO, DEBUG [default: %(default)s]'
)
parser.add_argument(
'-k', '--keyfile', dest='keyfile', metavar='FILE',
help='Path to private key, for example /tmp/test_rsa.key'
)
parser.add_argument(
'-r', '--root', dest='root', default=ROOT,
help='Directory to serve as root for the server'
)
parser.add_argument(
'-m', '--mode', default=MODE, const=MODE, nargs='?', choices=('threaded', 'forked'),
help='Mode to run server in [default: %(default)s]'
)

args = parser.parse_args()

if not os.path.isdir(args.root):
parser.print_help()
sys.exit(-1)

start_server(args.host, args.port, args.root, args.keyfile, args.level, args.mode)


if __name__ == '__main__':
main()
Loading