Last active
January 14, 2023 14:36
-
-
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]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 ) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I've taken to maintaining this as a project instead.