Unverified Commit da2bd7cd authored by 老广's avatar 老广 Committed by GitHub

Merge pull request #71 from jumpserver/dev

Dev
parents a518b9a7 c31b22d4
......@@ -25,7 +25,7 @@ from .utils import get_logger, register_app, register_service
eventlet.monkey_patch()
hub_prevent_multiple_readers(False)
__version__ = '1.3.2'
__version__ = '1.3.3'
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
logger = get_logger(__file__)
......
......@@ -2,16 +2,22 @@
#
import os
import re
import socket
import selectors
import telnetlib
import paramiko
from paramiko.ssh_exception import SSHException
from .ctx import app_service
from .utils import get_logger, get_private_key_fingerprint
from .utils import get_logger, get_private_key_fingerprint, net_input
logger = get_logger(__file__)
TIMEOUT = 10
BUF_SIZE = 1024
MANUAL_LOGIN = 'manual'
AUTO_LOGIN = 'auto'
class SSHConnection:
......@@ -104,7 +110,8 @@ class SSHConnection:
ssh.connect(gateway.ip, port=gateway.port,
username=gateway.username,
password=gateway.password,
pkey=gateway.private_key_obj)
pkey=gateway.private_key_obj,
timeout=TIMEOUT)
except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
SSHException):
......@@ -148,3 +155,134 @@ class SSHConnection:
logger.error(e)
continue
return sock
class TelnetConnection:
def __init__(self, asset, system_user, client):
self.client = client
self.asset = asset
self.system_user = system_user
self.sock = None
self.sel = selectors.DefaultSelector()
self.incorrect_pattern = re.compile(
r'incorrect|failed|失败|错误', re.I
)
self.username_pattern = re.compile(
r'login:\s*$|username:\s*$|用户名:\s*$|账\s*号:\s*$', re.I
)
self.password_pattern = re.compile(
r'password:\s*$|passwd:\s*$|密\s*码:\s*$', re.I
)
self.success_pattern = re.compile(
r'Last\s*login|success|成功', re.I
)
def get_socket(self):
logger.info('Get telnet server socket. {}'.format(self.client.user))
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(10)
self.sock.connect((self.asset.ip, self.asset.port))
# Send SGA and ECHO options to Telnet Server
self.sock.send(telnetlib.IAC + telnetlib.DO + telnetlib.SGA)
self.sock.send(telnetlib.IAC + telnetlib.DO + telnetlib.ECHO)
self.sel.register(self.sock, selectors.EVENT_READ)
while True:
events = self.sel.select()
for sock in [key.fileobj for key, _ in events]:
data = sock.recv(BUF_SIZE)
if sock == self.sock:
logger.info(b'[Telnet server send]: ' + data)
if not data:
self.sock.close()
msg = 'The server <{}> closes the connection.'.format(
self.asset.hostname
)
logger.info(msg)
return None, msg
if data.startswith(telnetlib.IAC):
self.option_negotiate(data)
else:
result = self.login_auth(data)
if result:
msg = 'Successful asset connection.<{}>/<{}>/<{}>.'.format(
self.client.user, self.system_user.username,
self.asset.hostname
)
logger.info(msg)
self.client.send(b'\r\n' + data)
return self.sock, None
elif result is False:
self.sock.close()
msg = 'Authentication failed.\r\n'
logger.info(msg)
return None, msg
elif result is None:
continue
def option_negotiate(self, data):
"""
Telnet server option negotiate before connection
:param data: option negotiate data
:return:
"""
logger.info(b'[Server options negotiate]: ' + data)
data_list = data.split(telnetlib.IAC)
new_data_list = []
for x in data_list:
if x == telnetlib.DO + telnetlib.ECHO:
new_data_list.append(telnetlib.WONT + telnetlib.ECHO)
elif x == telnetlib.WILL + telnetlib.ECHO:
new_data_list.append(telnetlib.DO + telnetlib.ECHO)
elif x == telnetlib.WILL + telnetlib.SGA:
new_data_list.append(telnetlib.DO + telnetlib.SGA)
elif x == telnetlib.DO + telnetlib.TTYPE:
new_data_list.append(telnetlib.WILL + telnetlib.TTYPE)
elif x == telnetlib.SB + telnetlib.TTYPE + b'\x01':
new_data_list.append(telnetlib.SB + telnetlib.TTYPE + b'\x00' + b'XTERM-256COLOR')
elif telnetlib.DO in x:
new_data_list.append(x.replace(telnetlib.DO, telnetlib.WONT))
elif telnetlib.WILL in x:
new_data_list.append(x.replace(telnetlib.WILL, telnetlib.DONT))
elif telnetlib.WONT in x:
new_data_list.append(x.replace(telnetlib.WONT, telnetlib.DONT))
elif telnetlib.DONT in x:
new_data_list.append(x.replace(telnetlib.DONT, telnetlib.WONT))
else:
new_data_list.append(x)
new_data = telnetlib.IAC.join(new_data_list)
logger.info(b'[Client options negotiate]: ' + new_data)
self.sock.send(new_data)
def login_auth(self, raw_data):
logger.info('[Telnet login auth]: ({})'.format(self.client.user))
try:
data = raw_data.decode('utf-8')
except UnicodeDecodeError:
try:
data = raw_data.decode('gbk')
except UnicodeDecodeError:
logger.info(b'[Decode error]: ' + b'>>' + raw_data + b'<<')
return None
if self.incorrect_pattern.search(data):
logger.info(b'[Login incorrect prompt]: ' + b'>>' + raw_data + b'<<')
return False
elif self.username_pattern.search(data):
logger.info(b'[Username prompt]: ' + b'>>' + raw_data + b'<<')
self.sock.send(self.system_user.username.encode('utf-8') + b'\r\n')
return None
elif self.password_pattern.search(data):
logger.info(b'[Password prompt]: ' + b'>>' + raw_data + b'<<')
self.sock.send(self.system_user.password.encode('utf-8') + b'\r\n')
return None
elif self.success_pattern.search(data):
logger.info(b'[Login Success prompt]: ' + b'>>' + raw_data + b'<<')
return True
else:
logger.info(b'[No match]: ' + b'>>' + raw_data + b'<<')
return None
......@@ -21,8 +21,6 @@ class BaseNamespace(Namespace):
def on_connect(self):
self.current_user = self.get_current_user()
if self.current_user is None:
return redirect(self.socketio.config['LOGIN_URL'])
logger.debug("{} connect websocket".format(self.current_user))
def get_current_user(self):
......@@ -34,6 +32,10 @@ class BaseNamespace(Namespace):
user = app_service.check_user_cookie(session_id, csrf_token)
if token:
user = app_service.check_user_with_token(token)
msg = "Get current user: session_id<{}> token<{}> => {}".format(
session_id, token, user
)
logger.debug(msg)
return user
......@@ -123,7 +125,7 @@ class ProxyNamespace(BaseNamespace):
child, parent = socket.socketpair()
client = Client(parent, room["request"])
forwarder = ProxyServer(client)
forwarder = ProxyServer(client, login_from='WT')
room["client"] = client
room["forwarder"] = forwarder
room["proxy"] = WSProxy(self, child, room["id"])
......@@ -149,27 +151,30 @@ class ProxyNamespace(BaseNamespace):
token = message.get('token', None)
secret = message.get('secret', None)
win_size = message.get('size', (80, 24))
room = self.new_room()
self.emit('room', {'room': room["id"], 'secret': secret})
join_room(room['id'])
if not token or not secret:
logger.debug("Token or secret is None: {}".format(token, secret))
self.emit('data', {'data': "\nOperation not permitted!",
'room': room["id"]})
msg = "Token or secret is None: {} {}".format(token, secret)
logger.error(msg)
self.emit('data', {'data': msg, 'room': room['id']}, room=room['id'])
self.emit('disconnect')
return None
return
info = app_service.get_token_asset(token)
logger.debug(info)
if not info:
logger.debug("Token info is None")
self.emit('data', {'data': "\nOperation not permitted!",
'room': room["id"]})
msg = "Token info is none, maybe token expired"
logger.error(msg)
self.emit('data', {'data': msg, 'room': room['id']}, room=room['id'])
self.emit('disconnect')
return None
user_id = info.get('user', None)
self.current_user = app_service.get_user_profile(user_id)
room["request"].user = self.current_user
# room["request"].user = self.current_user
self.on_host({
'secret': secret,
'uuid': info['asset'],
......@@ -180,7 +185,7 @@ class ProxyNamespace(BaseNamespace):
def on_resize(self, message):
cols, rows = message.get('cols', None), message.get('rows', None)
logger.debug("On resize event trigger: {}*{}".format(cols, rows))
rooms = self.connections.get(request.sid)
rooms = self.connections.get(request.sid, {})
if self.win_size != (cols, rows):
logger.debug("Start change win size: {}*{}".format(cols, rows))
for room in rooms.values():
......@@ -203,7 +208,7 @@ class ProxyNamespace(BaseNamespace):
def on_logout(self, room_id):
room = self.connections.get(request.sid, {}).get(room_id)
if room:
room["proxy"].close()
room.get("proxy") and room["proxy"].close()
self.close_room(room_id)
del self.connections[request.sid][room_id]
del room
......
......@@ -260,7 +260,7 @@ class InteractiveServer:
if system_user is None:
self.client.send(_("没有系统用户"))
return
forwarder = ProxyServer(self.client)
forwarder = ProxyServer(self.client, login_from='ST')
forwarder.proxy(asset, system_user)
def interact(self):
......
......@@ -86,8 +86,178 @@ class Client:
# print("GC: Client object has been gc")
class BaseServer:
"""
Base Server
Achieve command record
sub-class: Server, Telnet Server
"""
def __init__(self):
self.send_bytes = 0
self.recv_bytes = 0
self.stop_evt = threading.Event()
self.input_data = SizedList(maxsize=1024)
self.output_data = SizedList(maxsize=1024)
self._in_input_state = True
self._input_initial = False
self._in_vim_state = False
self._input = ""
self._output = ""
self._session_ref = None
def set_session(self, session):
self._session_ref = weakref.ref(session)
@property
def session(self):
if self._session_ref:
return self._session_ref()
else:
return None
def parse(self, b):
if isinstance(b, str):
b = b.encode("utf-8")
if not self._input_initial:
self._input_initial = True
if self._have_enter_char(b):
self._in_input_state = False
self._input = self._parse_input()
else:
if not self._in_input_state:
self._output = self._parse_output()
logger.debug("\n{}\nInput: {}\nOutput: {}\n{}".format(
"#" * 30 + " Command " + "#" * 30,
self._input, self._output,
"#" * 30 + " End " + "#" * 30,
))
if self._input:
self.session.put_command(self._input, self._output)
self.input_data.clean()
self.output_data.clean()
self._in_input_state = True
@staticmethod
def _have_enter_char(s):
for c in char.ENTER_CHAR:
if c in s:
return True
return False
def _parse_output(self):
if not self.output_data:
return ''
parser = utils.TtyIOParser()
return parser.parse_output(self.output_data)
def _parse_input(self):
if not self.input_data or self.input_data[0] == char.RZ_PROTOCOL_CHAR:
return
parser = utils.TtyIOParser()
return parser.parse_input(self.input_data)
class TelnetServer(BaseServer):
"""
Telnet server
"""
def __init__(self, sock, asset, system_user):
super(TelnetServer, self).__init__()
self.sock = sock
self.asset = asset
self.system_user = system_user
def fileno(self):
return self.sock.fileno()
def send(self, b):
self.parse(b)
return self.sock.send(b)
def recv(self, size):
data = self.sock.recv(size)
self.session.put_replay(data)
if self._input_initial:
if self._in_input_state:
self.input_data.append(data)
else:
self.output_data.append(data)
return data
def close(self):
logger.info("Closed server {}".format(self))
self.parse(b'')
self.stop_evt.set()
self.sock.close()
def __getattr__(self, item):
return getattr(self.sock, item)
def __str__(self):
return "<To: {}>".format(str(self.asset))
class Server(BaseServer):
"""
SSH Server
Server object like client, a wrapper object, a connection to the asset,
Because we don't want to using python dynamic feature, such asset
have the chan and system_user attr.
"""
# Todo: Server name is not very suitable
def __init__(self, chan, sock, asset, system_user):
super(Server, self).__init__()
self.chan = chan
self.sock = sock
self.asset = asset
self.system_user = system_user
def fileno(self):
return self.chan.fileno()
def send(self, b):
self.parse(b)
return self.chan.send(b)
def recv(self, size):
data = self.chan.recv(size)
self.session.put_replay(data)
if self._input_initial:
if self._in_input_state:
self.input_data.append(data)
else:
self.output_data.append(data)
return data
def close(self):
logger.info("Closed server {}".format(self))
self.parse(b'')
self.stop_evt.set()
self.chan.close()
self.chan.transport.close()
if self.sock:
self.sock.transport.close()
def __getattr__(self, item):
return getattr(self.chan, item)
def __str__(self):
return "<To: {}>".format(str(self.asset))
# def __del__(self):
# print("GC: Server object has been gc")
'''
class Server:
"""
SSH Server
Server object like client, a wrapper object, a connection to the asset,
Because we don't want to using python dynamic feature, such asset
have the chan and system_user attr.
......@@ -199,6 +369,7 @@ class Server:
# def __del__(self):
# print("GC: Server object has been gc")
'''
class WSProxy:
......
......@@ -8,8 +8,8 @@ import time
from paramiko.ssh_exception import SSHException
from .session import Session
from .models import Server
from .connection import SSHConnection
from .models import Server, TelnetServer
from .connection import SSHConnection, TelnetConnection
from .ctx import current_app, app_service
from .utils import wrap_with_line_feed as wr, wrap_with_warning as warning, \
get_logger, net_input
......@@ -18,12 +18,15 @@ from .utils import wrap_with_line_feed as wr, wrap_with_warning as warning, \
logger = get_logger(__file__)
TIMEOUT = 10
BUF_SIZE = 4096
MANUAL_LOGIN = 'manual'
AUTO_LOGIN = 'auto'
class ProxyServer:
def __init__(self, client):
def __init__(self, client, login_from):
self.client = client
self.server = None
self.login_from = login_from
self.connecting = True
self.stop_event = threading.Event()
......@@ -34,13 +37,24 @@ class ProxyServer:
"""
password, private_key = \
app_service.get_system_user_auth_info(system_user)
if not password and not private_key:
if system_user.login_mode == MANUAL_LOGIN or (not password and not private_key):
prompt = "{}'s password: ".format(system_user.username)
password = net_input(self.client, prompt=prompt, sensitive=True)
system_user.password = password
system_user.private_key = private_key
def proxy(self, asset, system_user):
if asset.protocol != system_user.protocol:
msg = 'System user <{}> and asset <{}> protocol are inconsistent.'.format(
system_user.name, asset.hostname
)
self.client.send(warning(wr(msg, before=1, after=0)))
return
if system_user.login_mode == MANUAL_LOGIN and not system_user.username:
system_user_name = net_input(self.client, prompt='username: ', before=1)
system_user.username = system_user_name
self.get_system_user_auth(system_user)
self.send_connecting_message(asset, system_user)
self.server = self.get_server_conn(asset, system_user)
......@@ -49,7 +63,7 @@ class ProxyServer:
command_recorder = current_app.new_command_recorder()
replay_recorder = current_app.new_replay_recorder()
session = Session(
self.client, self.server,
self.client, self.server, self.login_from,
command_recorder=command_recorder,
replay_recorder=replay_recorder,
)
......@@ -74,15 +88,26 @@ class ProxyServer:
if not self.validate_permission(asset, system_user):
self.client.send(warning('No permission'))
return None
if True:
if system_user.protocol == asset.protocol == 'telnet':
server = self.get_telnet_server_conn(asset, system_user)
elif system_user.protocol == asset.protocol == 'ssh':
server = self.get_ssh_server_conn(asset, system_user)
else:
server = self.get_ssh_server_conn(asset, system_user)
server = None
return server
# Todo: Support telnet
def get_telnet_server_conn(self, asset, system_user):
pass
telnet = TelnetConnection(asset, system_user, self.client)
sock, msg = telnet.get_socket()
if not sock:
self.client.send(warning(wr(msg, before=1, after=0)))
server = None
else:
server = TelnetServer(sock, asset, system_user)
# self.client.send(b'\r\n')
self.connecting = False
return server
def get_ssh_server_conn(self, asset, system_user):
request = self.client.request
......@@ -116,6 +141,8 @@ class ProxyServer:
break
def watch_win_size_change_async(self):
if not isinstance(self.server, Server):
return
thread = threading.Thread(target=self.watch_win_size_change)
thread.daemon = True
thread.start()
......
......@@ -14,10 +14,11 @@ logger = get_logger(__file__)
class Session:
def __init__(self, client, server, command_recorder=None, replay_recorder=None):
def __init__(self, client, server, login_from, command_recorder=None, replay_recorder=None):
self.id = str(uuid.uuid4())
self.client = client # Master of the session, it's a client sock
self.server = server # Server channel
self.login_from = login_from # Login from
self._watchers = [] # Only watch session
self._sharers = [] # Join to the session, read and write
self.replaying = True
......@@ -174,7 +175,7 @@ class Session:
"user": self.client.user.username,
"asset": self.server.asset.hostname,
"system_user": self.server.system_user.username,
"login_from": "ST",
"login_from": self.login_from,
"remote_addr": self.client.addr[0],
"is_finished": True if self.stop_evt.is_set() else False,
"date_last_active": self.date_last_active.strftime("%Y-%m-%d %H:%M:%S") + " +0000",
......
......@@ -18,8 +18,8 @@ idna==2.6
itsdangerous==0.24
Jinja2==2.10
jmespath==0.9.3
jms-storage==0.0.17
jumpserver-python-sdk==0.0.42
jms-storage==0.0.18
jumpserver-python-sdk==0.0.44
MarkupSafe==1.0
oss2==2.4.0
paramiko==2.4.0
......
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