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):
self.request.type.append('subsystem')
self.request.meta.update({'channel': channel.get_id(), 'name': name})
self.event.set()
return False
return super().check_channel_subsystem_request(channel, name)
def check_channel_window_change_request(self, channel, width, height,
pixelwidth, pixelheight):
......
......@@ -2,19 +2,17 @@
# -*- coding: utf-8 -*-
#
import socket
import threading
import time
import weakref
import os
import paramiko
from paramiko.ssh_exception import SSHException
from .session import Session
from .models import Server
from .connection import SSHConnection
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__)
......@@ -62,20 +60,11 @@ class ProxyServer:
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):
logger.info("Connect to {}".format(asset.hostname))
if not self.validate_permission(asset, system_user):
self.client.send(warning('No permission'))
return None
self.get_system_user_auth(system_user)
if True:
server = self.get_ssh_server_conn(asset, system_user)
else:
......@@ -86,85 +75,18 @@ class ProxyServer:
def get_telnet_server_conn(self, asset, system_user):
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):
ssh = paramiko.SSHClient()
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')
ssh = SSHConnection(self.app)
request = self.client.request
term = request.meta.get('term', 'xterm')
width = request.meta.get('width', 80)
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)
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 tempfile
import time
import socket
import argparse
import sys
import textwrap
import paramiko
paramiko.util.log_to_file('/tmp/ftpserver.log', 'DEBUG')
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
from .connection import SSHConnection
def check_channel_request(self, kind, chanid):
return paramiko.OPEN_SUCCEEDED
def get_allowed_auths(self, username):
"""List availble auth mechanisms."""
return "password,publickey"
class SFTPServer(paramiko.SFTPServerInterface):
root = '/tmp'
def __init__(self, server, **kwargs):
super().__init__(server, **kwargs)
self.server = server
self._sftp = {}
self.hosts = self.get_perm_hosts()
class StubSFTPServer(paramiko.SFTPServerInterface):
hosts = None
def get_host_sftp(self, host, su):
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):
super().__init__(*args, **kwargs)
self.get_perm_hosts()
self._sftp = {}
if not asset or not system_user:
raise OSError("No asset or system user explicit")
def get_host_sftp(self, host):
if host not in self._sftp:
t = paramiko.Transport(('192.168.244.176', 22))
t.connect(username='root', password='redhat123')
sftp = paramiko.SFTPClient.from_transport(t)
ssh = SSHConnection(self.server.app)
sftp, msg = ssh.get_sftp(asset, system_user)
if sftp:
self._sftp[host] = sftp
return sftp
else:
raise OSError("Can not connect asset sftp server")
else:
return self._sftp[host]
def get_perm_hosts(self):
self.hosts = ['centos', 'localhost']
@staticmethod
def parse_path(path):
host, *rpath = path.lstrip('/').split('/')
rpath = '/' + '/'.join(rpath)
return host, rpath
assets = self.server.app.service.get_user_assets(
self.server.request.user
)
return {asset.hostname: asset for asset in assets}
def parse_path(self, path):
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):
host, rpath = self.parse_path(path)
sftp = self.get_host_sftp(host) if host else None
host, su, rpath = self.parse_path(path)
sftp = self.get_host_sftp(host, su) if host and su else None
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
def stat_host_dir():
tmp = tempfile.TemporaryDirectory()
......@@ -90,10 +74,16 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
def list_folder(self, path):
output = []
if path == "/":
for filename in self.hosts:
host, su, rpath = self.parse_path(path)
if not host:
for hostname in self.hosts:
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)
else:
sftp, rpath = self.get_sftp_rpath(path)
......@@ -105,23 +95,23 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
return output
def stat(self, path):
host, rpath = self.parse_path(path)
if host and not rpath:
host, su, rpath = self.parse_path(path)
if not rpath or rpath == "/":
attr = self.stat_host_dir()
attr.filename = host
attr.filename = su or host
return attr
else:
sftp = self.get_host_sftp(host)
sftp = self.get_host_sftp(host, su)
return sftp.stat(rpath)
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.filename = host
attr.filename = su or host
else:
sftp = self.get_host_sftp(host)
sftp = self.get_host_sftp(host, su)
attr = sftp.stat(rpath)
attr.filename = os.path.basename(path)
return attr
......@@ -165,11 +155,11 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
return paramiko.SFTP_FAILURE
def rename(self, src, dest):
host1, rsrc = self.parse_path(src)
host2, rdest = self.parse_path(dest)
host1, su1, rsrc = self.parse_path(src)
host2, su2, rdest = self.parse_path(dest)
if host1 == host2 and host1:
sftp = self.get_host_sftp(host2)
if host1 == host2 and su1 == su2 and host1:
sftp = self.get_host_sftp(host1, su1)
try:
sftp.rename(rsrc, rdest)
except OSError as e:
......@@ -179,7 +169,7 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
def mkdir(self, path, attr):
sftp, rpath = self.get_sftp_rpath(path)
if sftp is not None:
if sftp is not None and rpath != '/':
try:
sftp.mkdir(rpath)
except OSError as e:
......@@ -188,7 +178,6 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
return paramiko.SFTP_FAILURE
def rmdir(self, path):
print("Call {}".format("Rmdir"))
sftp, rpath = self.get_sftp_rpath(path)
if sftp is not None:
try:
......@@ -198,7 +187,6 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
return paramiko.SFTP_OK
def chattr(self, path, attr):
print("Call {}".format("Chattr"))
sftp, rpath = self.get_sftp_rpath(path)
if sftp is not None:
if attr._flags & attr.FLAG_PERMISSIONS:
......@@ -210,69 +198,3 @@ class StubSFTPServer(paramiko.SFTPServerInterface):
if attr._flags & attr.FLAG_SIZE:
sftp.truncate(rpath, attr.st_size)
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
import socket
import threading
import paramiko
import time
from .utils import ssh_key_gen, get_logger
from .interface import SSHInterface
from .interactive import InteractiveServer
from .models import Client, Request
from .sftp import SFTPServer
logger = get_logger(__file__)
BACKLOG = 5
paramiko.util.log_to_file('/tmp/ftpserver.log', 'DEBUG')
class SSHServer:
......@@ -60,6 +64,9 @@ class SSHServer:
logger.warning("Failed load moduli -- gex will be unsupported")
transport.add_server_key(self.host_key)
transport.set_subsystem_handler(
'sftp', paramiko.SFTPServer, SFTPServer
)
request = Request(addr)
server = SSHInterface(self.app, request)
try:
......@@ -101,6 +108,9 @@ class SSHServer:
logger.info("Request type `pty`, dispatch to interactive mode")
InteractiveServer(self.app, client).interact()
else:
logger.info("Request type `{}`".format(request_type))
print(request_type)
time.sleep(100)
client.send("Not support request type: %s" % request_type)
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