Skip to content

Instantly share code, notes, and snippets.

@lonetwin
Last active January 14, 2023 14:36
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save lonetwin/3b5982cf88c598c0e169 to your computer and use it in GitHub Desktop.
Save lonetwin/3b5982cf88c598c0e169 to your computer and use it in GitHub Desktop.
Replacing openssh's external sftp server with a custom sftp server based on paramiko [DEPRECATED -- please see comment]
#!/usr/bin/env python
# A more detailed explaination will come as a blog post soon, but in brief,
# here is the problem that led to this:
#
# For various reasons we have a requirement to implement an 'sftp' like
# abstraction/interface for a service that we provide. Basically we need to
# present objects in our application as a filesystem accessible over sftp.
#
# The way we have decided to go about this is to replace the 'standard' openssh
# sftp subsystem command entry in /etc/ssh/sshd_config on our servers with our
# own customized sftp server.
#
# This would allow openssh to handle all the heavy lifting of authentication
# and setting up the transport and we'd ^just^ have to implement the a sftp
# server which would communicate over its stdin/stdout with the openssh
# established connection.
#
# I decided to use paramiko to help me build the sftp server side of things,
# but in the process had to only adapt stdin/stdout for a socket api
# (SocketAdapter below) -- neat stuff !!
import sys
import logging
from paramiko.server import ServerInterface
from paramiko.transport import Transport
from paramiko.sftp_server import SFTPServer, SFTPServerInterface
# - an example implementation that might provide an abstraction of a filesystem
# this one just serves the actual filesystem, but you could choose to implement
# whatever is required
from sftp_implementation import StubSFTPServer
logging.basicConfig(filename='/tmp/sftpd.log', level='DEBUG')
log = logging.getLogger(__name__)
# This class adapts the sys.std{in,out} to the socket interface for providing
# the recv() and send() api which paramiko calls to interact with the connected
# client channel
class SocketAdapter(object):
""" Class that adapts stdout and stdin to the socket api to keep paramiko
happy
"""
def __init__(self, stdin, stdout):
self._stdin = stdin
self._stdout = stdout
self._transport = None
def send(self, data, flags=0):
self._stdout.flush()
self._stdout.write(data)
self._stdout.flush()
return len(data)
def recv(self, bufsize, flags=0):
data = self._stdin.read(bufsize)
return data
def close(self):
self._stdin.close()
self._stdout.close()
def settimeout(self, ignored):
pass
def get_name(self):
# required at https://github.com/paramiko/paramiko/blob/master/paramiko/sftp_server.py#L86-L91
return 'sftp'
def get_transport(self):
if not self._transport:
self._transport = Transport(self)
return self._transport
# - the main entry point
def start_server(params):
server_type = {'local' : StubSFTPServer,
# ...any other custom implementation
}
# - choose the implementation of the filesystem abstraction, defaults to
# the 'normal' local filesystem as implemented in localfs.py
fs_type = params[1] if len(params) > 1 else 'local'
server_type = server_type[ fs_type ]
log.debug('going to setup adapter...')
server_socket = SocketAdapter(sys.stdin, sys.stdout)
log.debug('going to setup server...')
si = ServerInterface()
sftp_server = SFTPServer(server_socket, 'sftp', server=si, sftp_si=server_type)
log.debug('going to start server...')
sftp_server.start()
# If you'd like to quickly try this out, save this file someplace, replace the
# Subsystem entry in your /etc/ssh/sshd_config file to point to the full path
# of this file and restart sshd
if __name__ == '__main__':
log.debug('starting the sftp_server subsystem...')
start_server( sys.argv )
import os
from paramiko import SFTPServerInterface, SFTPServer, SFTPAttributes, SFTP_OK, SFTPHandle
class StubSFTPHandle (SFTPHandle):
def stat(self):
try:
return SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
except OSError as e:
return SFTPServer.convert_errno(e.errno)
def chattr(self, attr):
# python doesn't have equivalents to fchown or fchmod, so we have to
# use the stored filename
try:
SFTPServer.set_file_attr(self.filename, attr)
return SFTP_OK
except OSError as e:
return SFTPServer.convert_errno(e.errno)
class StubSFTPServer(SFTPServerInterface):
# I shall be changing this eventually to adapt it for our requirement of an
# sftp like abstraction to our objects.
# Borrowed from sftpserver: https://github.com/rspivak/sftpserver
# assume current folder is a fine root
# (the tests always create and eventualy delete a subfolder, so there shouldn't be any mess)
ROOT = os.getcwd()
def _realpath(self, path):
return self.ROOT + self.canonicalize(path)
def list_folder(self, path):
path = self._realpath(path)
try:
out = [ ]
flist = os.listdir(path)
for fname in flist:
attr = SFTPAttributes.from_stat(os.stat(os.path.join(path, fname)))
attr.filename = fname
out.append(attr)
return out
except OSError as e:
return SFTPServer.convert_errno(e.errno)
def stat(self, path):
path = self._realpath(path)
try:
return SFTPAttributes.from_stat(os.stat(path))
except OSError as e:
return SFTPServer.convert_errno(e.errno)
def lstat(self, path):
path = self._realpath(path)
try:
return SFTPAttributes.from_stat(os.lstat(path))
except OSError as e:
return SFTPServer.convert_errno(e.errno)
def open(self, path, flags, attr):
path = self._realpath(path)
try:
binary_flag = getattr(os, 'O_BINARY', 0)
flags |= binary_flag
mode = getattr(attr, 'st_mode', None)
if mode is not None:
fd = os.open(path, flags, mode)
else:
# os.open() defaults to 0777 which is
# an odd default mode for files
fd = os.open(path, flags, 0o666)
except OSError as e:
return SFTPServer.convert_errno(e.errno)
if (flags & os.O_CREAT) and (attr is not None):
attr._flags &= ~attr.FLAG_PERMISSIONS
SFTPServer.set_file_attr(path, attr)
if flags & os.O_WRONLY:
if flags & os.O_APPEND:
fstr = 'ab'
else:
fstr = 'wb'
elif flags & os.O_RDWR:
if flags & os.O_APPEND:
fstr = 'a+b'
else:
fstr = 'r+b'
else:
# O_RDONLY (== 0)
fstr = 'rb'
try:
f = os.fdopen(fd, fstr)
except OSError as e:
return SFTPServer.convert_errno(e.errno)
fobj = StubSFTPHandle(flags)
fobj.filename = path
fobj.readfile = f
fobj.writefile = f
return fobj
def remove(self, path):
path = self._realpath(path)
try:
os.remove(path)
except OSError as e:
return SFTPServer.convert_errno(e.errno)
return SFTP_OK
def rename(self, oldpath, newpath):
oldpath = self._realpath(oldpath)
newpath = self._realpath(newpath)
try:
os.rename(oldpath, newpath)
except OSError as e:
return SFTPServer.convert_errno(e.errno)
return SFTP_OK
def mkdir(self, path, attr):
path = self._realpath(path)
try:
os.mkdir(path)
if attr is not None:
SFTPServer.set_file_attr(path, attr)
except OSError as e:
return SFTPServer.convert_errno(e.errno)
return SFTP_OK
def rmdir(self, path):
path = self._realpath(path)
try:
os.rmdir(path)
except OSError as e:
return SFTPServer.convert_errno(e.errno)
return SFTP_OK
def chattr(self, path, attr):
path = self._realpath(path)
try:
SFTPServer.set_file_attr(path, attr)
except OSError as e:
return SFTPServer.convert_errno(e.errno)
return SFTP_OK
def symlink(self, target_path, path):
path = self._realpath(path)
if (len(target_path) > 0) and (target_path[0] == '/'):
# absolute symlink
target_path = os.path.join(self.ROOT, target_path[1:])
if target_path[:2] == '//':
# bug in os.path.join
target_path = target_path[1:]
else:
# compute relative to path
abspath = os.path.join(os.path.dirname(path), target_path)
if abspath[:len(self.ROOT)] != self.ROOT:
# this symlink isn't going to work anyway -- just break it immediately
target_path = '<error>'
try:
os.symlink(target_path, path)
except OSError as e:
return SFTPServer.convert_errno(e.errno)
return SFTP_OK
def readlink(self, path):
path = self._realpath(path)
try:
symlink = os.readlink(path)
except OSError as e:
return SFTPServer.convert_errno(e.errno)
# if it's absolute, remove the root
if os.path.isabs(symlink):
if symlink[:len(self.ROOT)] == self.ROOT:
symlink = symlink[len(self.ROOT):]
if (len(symlink) == 0) or (symlink[0] != '/'):
symlink = '/' + symlink
else:
symlink = '<error>'
return symlink
@lonetwin
Copy link
Author

lonetwin commented Oct 4, 2020

I've taken to maintaining this as a project instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment