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 ...@@ -25,7 +25,7 @@ from .utils import get_logger, register_app, register_service
eventlet.monkey_patch() eventlet.monkey_patch()
hub_prevent_multiple_readers(False) hub_prevent_multiple_readers(False)
__version__ = '1.3.2' __version__ = '1.3.3'
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DIR = os.path.dirname(os.path.dirname(__file__))
logger = get_logger(__file__) logger = get_logger(__file__)
......
...@@ -2,16 +2,22 @@ ...@@ -2,16 +2,22 @@
# #
import os import os
import re
import socket import socket
import selectors
import telnetlib
import paramiko import paramiko
from paramiko.ssh_exception import SSHException from paramiko.ssh_exception import SSHException
from .ctx import app_service 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__) logger = get_logger(__file__)
TIMEOUT = 10 TIMEOUT = 10
BUF_SIZE = 1024
MANUAL_LOGIN = 'manual'
AUTO_LOGIN = 'auto'
class SSHConnection: class SSHConnection:
...@@ -104,7 +110,8 @@ class SSHConnection: ...@@ -104,7 +110,8 @@ class SSHConnection:
ssh.connect(gateway.ip, port=gateway.port, ssh.connect(gateway.ip, port=gateway.port,
username=gateway.username, username=gateway.username,
password=gateway.password, password=gateway.password,
pkey=gateway.private_key_obj) pkey=gateway.private_key_obj,
timeout=TIMEOUT)
except(paramiko.AuthenticationException, except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType, paramiko.BadAuthenticationType,
SSHException): SSHException):
...@@ -148,3 +155,134 @@ class SSHConnection: ...@@ -148,3 +155,134 @@ class SSHConnection:
logger.error(e) logger.error(e)
continue continue
return sock 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): ...@@ -21,8 +21,6 @@ class BaseNamespace(Namespace):
def on_connect(self): def on_connect(self):
self.current_user = self.get_current_user() 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)) logger.debug("{} connect websocket".format(self.current_user))
def get_current_user(self): def get_current_user(self):
...@@ -34,6 +32,10 @@ class BaseNamespace(Namespace): ...@@ -34,6 +32,10 @@ class BaseNamespace(Namespace):
user = app_service.check_user_cookie(session_id, csrf_token) user = app_service.check_user_cookie(session_id, csrf_token)
if token: if token:
user = app_service.check_user_with_token(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 return user
...@@ -123,7 +125,7 @@ class ProxyNamespace(BaseNamespace): ...@@ -123,7 +125,7 @@ class ProxyNamespace(BaseNamespace):
child, parent = socket.socketpair() child, parent = socket.socketpair()
client = Client(parent, room["request"]) client = Client(parent, room["request"])
forwarder = ProxyServer(client) forwarder = ProxyServer(client, login_from='WT')
room["client"] = client room["client"] = client
room["forwarder"] = forwarder room["forwarder"] = forwarder
room["proxy"] = WSProxy(self, child, room["id"]) room["proxy"] = WSProxy(self, child, room["id"])
...@@ -149,27 +151,30 @@ class ProxyNamespace(BaseNamespace): ...@@ -149,27 +151,30 @@ class ProxyNamespace(BaseNamespace):
token = message.get('token', None) token = message.get('token', None)
secret = message.get('secret', None) secret = message.get('secret', None)
win_size = message.get('size', (80, 24)) win_size = message.get('size', (80, 24))
room = self.new_room() room = self.new_room()
self.emit('room', {'room': room["id"], 'secret': secret}) self.emit('room', {'room': room["id"], 'secret': secret})
join_room(room['id'])
if not token or not secret: if not token or not secret:
logger.debug("Token or secret is None: {}".format(token, secret)) msg = "Token or secret is None: {} {}".format(token, secret)
self.emit('data', {'data': "\nOperation not permitted!", logger.error(msg)
'room': room["id"]}) self.emit('data', {'data': msg, 'room': room['id']}, room=room['id'])
self.emit('disconnect') self.emit('disconnect')
return None return
info = app_service.get_token_asset(token) info = app_service.get_token_asset(token)
logger.debug(info) logger.debug(info)
if not info: if not info:
logger.debug("Token info is None") msg = "Token info is none, maybe token expired"
self.emit('data', {'data': "\nOperation not permitted!", logger.error(msg)
'room': room["id"]}) self.emit('data', {'data': msg, 'room': room['id']}, room=room['id'])
self.emit('disconnect') self.emit('disconnect')
return None return None
user_id = info.get('user', None) user_id = info.get('user', None)
self.current_user = app_service.get_user_profile(user_id) 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({ self.on_host({
'secret': secret, 'secret': secret,
'uuid': info['asset'], 'uuid': info['asset'],
...@@ -180,7 +185,7 @@ class ProxyNamespace(BaseNamespace): ...@@ -180,7 +185,7 @@ class ProxyNamespace(BaseNamespace):
def on_resize(self, message): def on_resize(self, message):
cols, rows = message.get('cols', None), message.get('rows', None) cols, rows = message.get('cols', None), message.get('rows', None)
logger.debug("On resize event trigger: {}*{}".format(cols, rows)) 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): if self.win_size != (cols, rows):
logger.debug("Start change win size: {}*{}".format(cols, rows)) logger.debug("Start change win size: {}*{}".format(cols, rows))
for room in rooms.values(): for room in rooms.values():
...@@ -203,7 +208,7 @@ class ProxyNamespace(BaseNamespace): ...@@ -203,7 +208,7 @@ class ProxyNamespace(BaseNamespace):
def on_logout(self, room_id): def on_logout(self, room_id):
room = self.connections.get(request.sid, {}).get(room_id) room = self.connections.get(request.sid, {}).get(room_id)
if room: if room:
room["proxy"].close() room.get("proxy") and room["proxy"].close()
self.close_room(room_id) self.close_room(room_id)
del self.connections[request.sid][room_id] del self.connections[request.sid][room_id]
del room del room
......
...@@ -260,7 +260,7 @@ class InteractiveServer: ...@@ -260,7 +260,7 @@ class InteractiveServer:
if system_user is None: if system_user is None:
self.client.send(_("没有系统用户")) self.client.send(_("没有系统用户"))
return return
forwarder = ProxyServer(self.client) forwarder = ProxyServer(self.client, login_from='ST')
forwarder.proxy(asset, system_user) forwarder.proxy(asset, system_user)
def interact(self): def interact(self):
......
...@@ -86,8 +86,178 @@ class Client: ...@@ -86,8 +86,178 @@ class Client:
# print("GC: Client object has been gc") # 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: class Server:
""" """
SSH Server
Server object like client, a wrapper object, a connection to the asset, Server object like client, a wrapper object, a connection to the asset,
Because we don't want to using python dynamic feature, such asset Because we don't want to using python dynamic feature, such asset
have the chan and system_user attr. have the chan and system_user attr.
...@@ -199,6 +369,7 @@ class Server: ...@@ -199,6 +369,7 @@ class Server:
# def __del__(self): # def __del__(self):
# print("GC: Server object has been gc") # print("GC: Server object has been gc")
'''
class WSProxy: class WSProxy:
......
...@@ -8,8 +8,8 @@ import time ...@@ -8,8 +8,8 @@ import time
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, TelnetServer
from .connection import SSHConnection from .connection import SSHConnection, TelnetConnection
from .ctx import current_app, app_service from .ctx import current_app, app_service
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_logger, net_input get_logger, net_input
...@@ -18,12 +18,15 @@ from .utils import wrap_with_line_feed as wr, wrap_with_warning as warning, \ ...@@ -18,12 +18,15 @@ from .utils import wrap_with_line_feed as wr, wrap_with_warning as warning, \
logger = get_logger(__file__) logger = get_logger(__file__)
TIMEOUT = 10 TIMEOUT = 10
BUF_SIZE = 4096 BUF_SIZE = 4096
MANUAL_LOGIN = 'manual'
AUTO_LOGIN = 'auto'
class ProxyServer: class ProxyServer:
def __init__(self, client): def __init__(self, client, login_from):
self.client = client self.client = client
self.server = None self.server = None
self.login_from = login_from
self.connecting = True self.connecting = True
self.stop_event = threading.Event() self.stop_event = threading.Event()
...@@ -34,13 +37,24 @@ class ProxyServer: ...@@ -34,13 +37,24 @@ class ProxyServer:
""" """
password, private_key = \ password, private_key = \
app_service.get_system_user_auth_info(system_user) 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) prompt = "{}'s password: ".format(system_user.username)
password = net_input(self.client, prompt=prompt, sensitive=True) password = net_input(self.client, prompt=prompt, sensitive=True)
system_user.password = password system_user.password = password
system_user.private_key = private_key system_user.private_key = private_key
def proxy(self, asset, system_user): 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.get_system_user_auth(system_user)
self.send_connecting_message(asset, system_user) self.send_connecting_message(asset, system_user)
self.server = self.get_server_conn(asset, system_user) self.server = self.get_server_conn(asset, system_user)
...@@ -49,7 +63,7 @@ class ProxyServer: ...@@ -49,7 +63,7 @@ class ProxyServer:
command_recorder = current_app.new_command_recorder() command_recorder = current_app.new_command_recorder()
replay_recorder = current_app.new_replay_recorder() replay_recorder = current_app.new_replay_recorder()
session = Session( session = Session(
self.client, self.server, self.client, self.server, self.login_from,
command_recorder=command_recorder, command_recorder=command_recorder,
replay_recorder=replay_recorder, replay_recorder=replay_recorder,
) )
...@@ -74,15 +88,26 @@ class ProxyServer: ...@@ -74,15 +88,26 @@ class ProxyServer:
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
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) server = self.get_ssh_server_conn(asset, system_user)
else: else:
server = self.get_ssh_server_conn(asset, system_user) server = None
return server return server
# Todo: Support telnet # Todo: Support telnet
def get_telnet_server_conn(self, asset, system_user): 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): def get_ssh_server_conn(self, asset, system_user):
request = self.client.request request = self.client.request
...@@ -116,6 +141,8 @@ class ProxyServer: ...@@ -116,6 +141,8 @@ class ProxyServer:
break break
def watch_win_size_change_async(self): def watch_win_size_change_async(self):
if not isinstance(self.server, Server):
return
thread = threading.Thread(target=self.watch_win_size_change) thread = threading.Thread(target=self.watch_win_size_change)
thread.daemon = True thread.daemon = True
thread.start() thread.start()
......
...@@ -14,10 +14,11 @@ logger = get_logger(__file__) ...@@ -14,10 +14,11 @@ logger = get_logger(__file__)
class Session: 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.id = str(uuid.uuid4())
self.client = client # Master of the session, it's a client sock self.client = client # Master of the session, it's a client sock
self.server = server # Server channel self.server = server # Server channel
self.login_from = login_from # Login from
self._watchers = [] # Only watch session self._watchers = [] # Only watch session
self._sharers = [] # Join to the session, read and write self._sharers = [] # Join to the session, read and write
self.replaying = True self.replaying = True
...@@ -174,7 +175,7 @@ class Session: ...@@ -174,7 +175,7 @@ class Session:
"user": self.client.user.username, "user": self.client.user.username,
"asset": self.server.asset.hostname, "asset": self.server.asset.hostname,
"system_user": self.server.system_user.username, "system_user": self.server.system_user.username,
"login_from": "ST", "login_from": self.login_from,
"remote_addr": self.client.addr[0], "remote_addr": self.client.addr[0],
"is_finished": True if self.stop_evt.is_set() else False, "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", "date_last_active": self.date_last_active.strftime("%Y-%m-%d %H:%M:%S") + " +0000",
......
...@@ -18,8 +18,8 @@ idna==2.6 ...@@ -18,8 +18,8 @@ idna==2.6
itsdangerous==0.24 itsdangerous==0.24
Jinja2==2.10 Jinja2==2.10
jmespath==0.9.3 jmespath==0.9.3
jms-storage==0.0.17 jms-storage==0.0.18
jumpserver-python-sdk==0.0.42 jumpserver-python-sdk==0.0.44
MarkupSafe==1.0 MarkupSafe==1.0
oss2==2.4.0 oss2==2.4.0
paramiko==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