Commit 2486cc83 authored by ibuler's avatar ibuler

[Feature] 支持sftp

parent 67ff6b5c
# -*- coding: utf-8 -*-
#
import weakref
import os
import socket
import paramiko
from paramiko.ssh_exception import SSHException
from .utils import get_logger, get_private_key_fingerprint
logger = get_logger(__file__)
TIMEOUT = 10
class SSHConnection:
def __init__(self, app):
self._app = weakref.ref(app)
@property
def app(self):
return self._app()
def get_ssh_client(self, asset, system_user):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
sock = None
self.get_system_user_auth(system_user)
if asset.domain:
sock = self.get_proxy_sock(asset)
try:
ssh.connect(
asset.ip, port=asset.port, username=system_user.username,
password=system_user.password, pkey=system_user.private_key,
timeout=TIMEOUT, compress=True, auth_timeout=TIMEOUT,
look_for_keys=False, sock=sock
)
except (paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
SSHException) as e:
password_short = "None"
key_fingerprint = "None"
if system_user.password:
password_short = system_user.password[:5] + \
(len(system_user.password) - 5) * '*'
if system_user.private_key:
key_fingerprint = get_private_key_fingerprint(
system_user.private_key
)
logger.error("Connect {}@{}:{} auth failed, password: \
{}, key: {}".format(
system_user.username, asset.ip, asset.port,
password_short, key_fingerprint,
))
return None, str(e)
except (socket.error, TimeoutError) as e:
return None, str(e)
return ssh, None
def get_transport(self, asset, system_user):
ssh, msg = self.get_ssh_client(asset, system_user)
if ssh:
return ssh.get_transport(), None
else:
return None, msg
def get_channel(self, asset, system_user, term="xterm", width=80, height=24):
ssh, msg = self.get_ssh_client(asset, system_user)
if ssh:
chan = ssh.invoke_shell(term, width=width, height=height)
return chan, None
else:
return None, msg
def get_sftp(self, asset, system_user):
ssh, msg = self.get_ssh_client(asset, system_user)
if ssh:
return ssh.open_sftp(), None
else:
return None, msg
def get_system_user_auth(self, system_user):
"""
获取系统用户的认证信息,密码或秘钥
:return: system user have full info
"""
system_user.password, system_user.private_key = \
self.app.service.get_system_user_auth_info(system_user)
def get_proxy_sock(self, asset):
sock = None
domain = self.app.service.get_domain_detail_with_gateway(
asset.domain
)
if not domain.has_ssh_gateway():
return None
for i in domain.gateways:
gateway = domain.random_ssh_gateway()
proxy_command = [
"ssh", "-o", "StrictHostKeyChecking=no",
"-p", str(gateway.port),
"{}@{}".format(gateway.username, gateway.ip),
"-W", "{}:{}".format(asset.ip, asset.port), "-q",
]
if gateway.password:
proxy_command.insert(0, "sshpass -p {}".format(gateway.password))
if gateway.private_key:
gateway.set_key_dir(os.path.join(self.app.root_path, 'keys'))
proxy_command.append("-i {}".format(gateway.private_key_file))
proxy_command = ' '.join(proxy_command)
try:
sock = paramiko.ProxyCommand(proxy_command)
break
except (paramiko.AuthenticationException,
paramiko.BadAuthenticationType, SSHException,
TimeoutError) as e:
logger.error(e)
continue
return sock
...@@ -147,7 +147,7 @@ class SSHInterface(paramiko.ServerInterface): ...@@ -147,7 +147,7 @@ class SSHInterface(paramiko.ServerInterface):
self.request.type.append('subsystem') self.request.type.append('subsystem')
self.request.meta.update({'channel': channel.get_id(), 'name': name}) self.request.meta.update({'channel': channel.get_id(), 'name': name})
self.event.set() self.event.set()
return False return super().check_channel_subsystem_request(channel, name)
def check_channel_window_change_request(self, channel, width, height, def check_channel_window_change_request(self, channel, width, height,
pixelwidth, pixelheight): pixelwidth, pixelheight):
......
...@@ -2,19 +2,17 @@ ...@@ -2,19 +2,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import socket
import threading import threading
import time import time
import weakref import weakref
import os
import paramiko
from paramiko.ssh_exception import SSHException from paramiko.ssh_exception import SSHException
from .session import Session from .session import Session
from .models import Server from .models import Server
from .connection import SSHConnection
from .utils import wrap_with_line_feed as wr, wrap_with_warning as warning, \ from .utils import wrap_with_line_feed as wr, wrap_with_warning as warning, \
get_private_key_fingerprint, get_logger get_logger
logger = get_logger(__file__) logger = get_logger(__file__)
...@@ -62,20 +60,11 @@ class ProxyServer: ...@@ -62,20 +60,11 @@ class ProxyServer:
self.client.user.id, asset.id, system_user.id self.client.user.id, asset.id, system_user.id
) )
def get_system_user_auth(self, system_user):
"""
获取系统用户的认证信息,密码或秘钥
:return: system user have full info
"""
system_user.password, system_user.private_key = \
self.app.service.get_system_user_auth_info(system_user)
def get_server_conn(self, asset, system_user): def get_server_conn(self, asset, system_user):
logger.info("Connect to {}".format(asset.hostname)) logger.info("Connect to {}".format(asset.hostname))
if not self.validate_permission(asset, system_user): if not self.validate_permission(asset, system_user):
self.client.send(warning('No permission')) self.client.send(warning('No permission'))
return None return None
self.get_system_user_auth(system_user)
if True: if True:
server = self.get_ssh_server_conn(asset, system_user) server = self.get_ssh_server_conn(asset, system_user)
else: else:
...@@ -86,85 +75,18 @@ class ProxyServer: ...@@ -86,85 +75,18 @@ class ProxyServer:
def get_telnet_server_conn(self, asset, system_user): def get_telnet_server_conn(self, asset, system_user):
pass pass
def get_proxy_sock(self, asset):
sock = None
domain = self.app.service.get_domain_detail_with_gateway(
asset.domain
)
if not domain.has_ssh_gateway():
return None
for i in domain.gateways:
gateway = domain.random_ssh_gateway()
proxy_command = [
"ssh", "-o", "StrictHostKeyChecking=no",
"-p", str(gateway.port),
"{}@{}".format(gateway.username, gateway.ip),
"-W", "{}:{}".format(asset.ip, asset.port), "-q",
]
if gateway.password:
proxy_command.insert(0, "sshpass -p {}".format(gateway.password))
if gateway.private_key:
gateway.set_key_dir(os.path.join(self.app.root_path, 'keys'))
proxy_command.append("-i {}".format(gateway.private_key_file))
proxy_command = ' '.join(proxy_command)
try:
sock = paramiko.ProxyCommand(proxy_command)
break
except (paramiko.AuthenticationException,
paramiko.BadAuthenticationType, SSHException,
TimeoutError) as e:
logger.error(e)
continue
return sock
def get_ssh_server_conn(self, asset, system_user): def get_ssh_server_conn(self, asset, system_user):
ssh = paramiko.SSHClient() ssh = SSHConnection(self.app)
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
sock = None
if asset.domain:
sock = self.get_proxy_sock(asset)
try:
ssh.connect(
asset.ip, port=asset.port, username=system_user.username,
password=system_user.password, pkey=system_user.private_key,
timeout=TIMEOUT, compress=True, auth_timeout=TIMEOUT,
look_for_keys=False, sock=sock
)
except (paramiko.AuthenticationException, paramiko.BadAuthenticationType, SSHException):
admins = self.app.config['ADMINS'] or 'administrator'
self.client.send(warning(wr(
"Authenticate with server failed, contact {}".format(admins),
before=1, after=0
)))
password_short = "None"
key_fingerprint = "None"
if system_user.password:
password_short = system_user.password[:5] + (len(system_user.password)-5) * '*'
if system_user.private_key:
key_fingerprint = get_private_key_fingerprint(system_user.private_key)
logger.error("Connect {}@{}:{} auth failed, password: {}, key: {}".format(
system_user.username, asset.ip, asset.port,
password_short, key_fingerprint,
))
return None
except (socket.error, TimeoutError) as e:
self.client.send(wr(" {}".format(e)))
return None
finally:
self.connecting = False
self.client.send(b'\r\n')
request = self.client.request request = self.client.request
term = request.meta.get('term', 'xterm') term = request.meta.get('term', 'xterm')
width = request.meta.get('width', 80) width = request.meta.get('width', 80)
height = request.meta.get('height', 24) height = request.meta.get('height', 24)
chan = ssh.invoke_shell(term, width=width, height=height) chan, msg = ssh.get_channel(asset, system_user, term=term,
width=width, height=height)
if not chan:
self.client.send(warning(wr(msg, before=1, after=0)))
self.connecting = False
self.client.send(b'\r\n')
return Server(chan, asset, system_user) return Server(chan, asset, system_user)
def watch_win_size_change(self): def watch_win_size_change(self):
......
# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
#
# This file is part of paramiko.
#
# Paramiko is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2.1 of the License, or (at your option)
# any later version.
#
# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
"""
A stub SFTP server for loopback SFTP testing.
"""
import os import os
import tempfile import tempfile
import time
import socket
import argparse
import sys
import textwrap
import paramiko import paramiko
paramiko.util.log_to_file('/tmp/ftpserver.log', 'DEBUG') from .connection import SSHConnection
class StubServer(paramiko.ServerInterface):
def check_auth_password(self, username, password):
# all are allowed
return paramiko.AUTH_SUCCESSFUL
def check_auth_publickey(self, username, key):
# all are allowed
return paramiko.AUTH_SUCCESSFUL
def check_channel_request(self, kind, chanid): class SFTPServer(paramiko.SFTPServerInterface):
return paramiko.OPEN_SUCCEEDED root = '/tmp'
def get_allowed_auths(self, username):
"""List availble auth mechanisms."""
return "password,publickey"
def __init__(self, server, **kwargs):
super().__init__(server, **kwargs)
self.server = server
self._sftp = {}
self.hosts = self.get_perm_hosts()
class StubSFTPServer(paramiko.SFTPServerInterface): def get_host_sftp(self, host, su):
hosts = None asset = self.hosts.get(host)
system_user = None
for system_user in self.get_asset_system_users(host):
if system_user.name == su:
break
def __init__(self, *args, **kwargs): if not asset or not system_user:
super().__init__(*args, **kwargs) raise OSError("No asset or system user explicit")
self.get_perm_hosts()
self._sftp = {}
def get_host_sftp(self, host):
if host not in self._sftp: if host not in self._sftp:
t = paramiko.Transport(('192.168.244.176', 22)) ssh = SSHConnection(self.server.app)
t.connect(username='root', password='redhat123') sftp, msg = ssh.get_sftp(asset, system_user)
sftp = paramiko.SFTPClient.from_transport(t) if sftp:
self._sftp[host] = sftp self._sftp[host] = sftp
return sftp return sftp
else:
raise OSError("Can not connect asset sftp server")
else: else:
return self._sftp[host] return self._sftp[host]
def get_perm_hosts(self): def get_perm_hosts(self):
self.hosts = ['centos', 'localhost'] assets = self.server.app.service.get_user_assets(
self.server.request.user
@staticmethod )
def parse_path(path): return {asset.hostname: asset for asset in assets}
host, *rpath = path.lstrip('/').split('/')
rpath = '/' + '/'.join(rpath) def parse_path(self, path):
return host, rpath data = path.lstrip('/').split('/')
su = rpath = ''
if len(data) == 1:
host = data[0]
elif len(data) == 2:
host, su = data
rpath = self.root
else:
host, su, *rpath = data
rpath = os.path.join(self.root, '/'.join(rpath))
return host, su, rpath
def get_sftp_rpath(self, path): def get_sftp_rpath(self, path):
host, rpath = self.parse_path(path) host, su, rpath = self.parse_path(path)
sftp = self.get_host_sftp(host) if host else None sftp = self.get_host_sftp(host, su) if host and su else None
return sftp, rpath return sftp, rpath
def get_asset_system_users(self, host):
asset = self.hosts.get(host)
if not asset:
return []
return [su for su in asset.system_users_granted if su.protocol == "ssh"]
@staticmethod @staticmethod
def stat_host_dir(): def stat_host_dir():
tmp = tempfile.TemporaryDirectory() tmp = tempfile.TemporaryDirectory()
...@@ -90,10 +74,16 @@ class StubSFTPServer(paramiko.SFTPServerInterface): ...@@ -90,10 +74,16 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
def list_folder(self, path): def list_folder(self, path):
output = [] output = []
if path == "/": host, su, rpath = self.parse_path(path)
for filename in self.hosts: if not host:
for hostname in self.hosts:
attr = self.stat_host_dir() attr = self.stat_host_dir()
attr.filename = filename attr.filename = hostname
output.append(attr)
elif not su:
for su in self.get_asset_system_users(host):
attr = self.stat_host_dir()
attr.filename = su.name
output.append(attr) output.append(attr)
else: else:
sftp, rpath = self.get_sftp_rpath(path) sftp, rpath = self.get_sftp_rpath(path)
...@@ -105,23 +95,23 @@ class StubSFTPServer(paramiko.SFTPServerInterface): ...@@ -105,23 +95,23 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
return output return output
def stat(self, path): def stat(self, path):
host, rpath = self.parse_path(path) host, su, rpath = self.parse_path(path)
if host and not rpath: if not rpath or rpath == "/":
attr = self.stat_host_dir() attr = self.stat_host_dir()
attr.filename = host attr.filename = su or host
return attr return attr
else: else:
sftp = self.get_host_sftp(host) sftp = self.get_host_sftp(host, su)
return sftp.stat(rpath) return sftp.stat(rpath)
def lstat(self, path): def lstat(self, path):
host, rpath = self.parse_path(path) host, su, rpath = self.parse_path(path)
if host == '': if not rpath or rpath == "/":
attr = self.stat_host_dir() attr = self.stat_host_dir()
attr.filename = host attr.filename = su or host
else: else:
sftp = self.get_host_sftp(host) sftp = self.get_host_sftp(host, su)
attr = sftp.stat(rpath) attr = sftp.stat(rpath)
attr.filename = os.path.basename(path) attr.filename = os.path.basename(path)
return attr return attr
...@@ -165,11 +155,11 @@ class StubSFTPServer(paramiko.SFTPServerInterface): ...@@ -165,11 +155,11 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
return paramiko.SFTP_FAILURE return paramiko.SFTP_FAILURE
def rename(self, src, dest): def rename(self, src, dest):
host1, rsrc = self.parse_path(src) host1, su1, rsrc = self.parse_path(src)
host2, rdest = self.parse_path(dest) host2, su2, rdest = self.parse_path(dest)
if host1 == host2 and host1: if host1 == host2 and su1 == su2 and host1:
sftp = self.get_host_sftp(host2) sftp = self.get_host_sftp(host1, su1)
try: try:
sftp.rename(rsrc, rdest) sftp.rename(rsrc, rdest)
except OSError as e: except OSError as e:
...@@ -179,7 +169,7 @@ class StubSFTPServer(paramiko.SFTPServerInterface): ...@@ -179,7 +169,7 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
def mkdir(self, path, attr): def mkdir(self, path, attr):
sftp, rpath = self.get_sftp_rpath(path) sftp, rpath = self.get_sftp_rpath(path)
if sftp is not None: if sftp is not None and rpath != '/':
try: try:
sftp.mkdir(rpath) sftp.mkdir(rpath)
except OSError as e: except OSError as e:
...@@ -188,7 +178,6 @@ class StubSFTPServer(paramiko.SFTPServerInterface): ...@@ -188,7 +178,6 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
return paramiko.SFTP_FAILURE return paramiko.SFTP_FAILURE
def rmdir(self, path): def rmdir(self, path):
print("Call {}".format("Rmdir"))
sftp, rpath = self.get_sftp_rpath(path) sftp, rpath = self.get_sftp_rpath(path)
if sftp is not None: if sftp is not None:
try: try:
...@@ -198,7 +187,6 @@ class StubSFTPServer(paramiko.SFTPServerInterface): ...@@ -198,7 +187,6 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
return paramiko.SFTP_OK return paramiko.SFTP_OK
def chattr(self, path, attr): def chattr(self, path, attr):
print("Call {}".format("Chattr"))
sftp, rpath = self.get_sftp_rpath(path) sftp, rpath = self.get_sftp_rpath(path)
if sftp is not None: if sftp is not None:
if attr._flags & attr.FLAG_PERMISSIONS: if attr._flags & attr.FLAG_PERMISSIONS:
...@@ -210,69 +198,3 @@ class StubSFTPServer(paramiko.SFTPServerInterface): ...@@ -210,69 +198,3 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
if attr._flags & attr.FLAG_SIZE: if attr._flags & attr.FLAG_SIZE:
sftp.truncate(rpath, attr.st_size) sftp.truncate(rpath, attr.st_size)
return paramiko.SFTP_OK return paramiko.SFTP_OK
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()
\ No newline at end of file
...@@ -6,15 +6,19 @@ import os ...@@ -6,15 +6,19 @@ import os
import socket import socket
import threading import threading
import paramiko import paramiko
import time
from .utils import ssh_key_gen, get_logger from .utils import ssh_key_gen, get_logger
from .interface import SSHInterface from .interface import SSHInterface
from .interactive import InteractiveServer from .interactive import InteractiveServer
from .models import Client, Request from .models import Client, Request
from .sftp import SFTPServer
logger = get_logger(__file__) logger = get_logger(__file__)
BACKLOG = 5 BACKLOG = 5
paramiko.util.log_to_file('/tmp/ftpserver.log', 'DEBUG')
class SSHServer: class SSHServer:
...@@ -60,6 +64,9 @@ class SSHServer: ...@@ -60,6 +64,9 @@ class SSHServer:
logger.warning("Failed load moduli -- gex will be unsupported") logger.warning("Failed load moduli -- gex will be unsupported")
transport.add_server_key(self.host_key) transport.add_server_key(self.host_key)
transport.set_subsystem_handler(
'sftp', paramiko.SFTPServer, SFTPServer
)
request = Request(addr) request = Request(addr)
server = SSHInterface(self.app, request) server = SSHInterface(self.app, request)
try: try:
...@@ -101,6 +108,9 @@ class SSHServer: ...@@ -101,6 +108,9 @@ class SSHServer:
logger.info("Request type `pty`, dispatch to interactive mode") logger.info("Request type `pty`, dispatch to interactive mode")
InteractiveServer(self.app, client).interact() InteractiveServer(self.app, client).interact()
else: else:
logger.info("Request type `{}`".format(request_type))
print(request_type)
time.sleep(100)
client.send("Not support request type: %s" % request_type) client.send("Not support request type: %s" % request_type)
def shutdown(self): def shutdown(self):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment