Unverified Commit 7295a354 authored by 老广's avatar 老广 Committed by GitHub

Merge pull request #66 from jumpserver/feature_telnet

[Feature] 添加功能,支持 telnet server.
parents 1df7614a 581b66ea
......@@ -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:
......@@ -148,3 +154,133 @@ 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.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
......@@ -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,6 +18,8 @@ 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:
......@@ -34,13 +36,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 or 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)
......@@ -74,15 +87,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 +140,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()
......
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