Unverified Commit 8a60c4eb authored by 老广's avatar 老广 Committed by GitHub

Merge pull request #95 from jumpserver/dev

Dev
parents a2ec9e77 4b742525
......@@ -2,66 +2,40 @@
# -*- coding: utf-8 -*-
#
import eventlet
from eventlet.debug import hub_prevent_multiple_readers
eventlet.monkey_patch()
hub_prevent_multiple_readers(False)
import datetime
import os
import time
import threading
import socket
import json
import signal
import eventlet
from eventlet.debug import hub_prevent_multiple_readers
from jms.service import AppService
from .config import Config
from .config import config
from .sshd import SSHServer
from .httpd import HttpServer
from .logger import create_logger
from .tasks import TaskHandler
from .recorder import ReplayRecorder, CommandRecorder
from .utils import get_logger, register_app, register_service
from .utils import get_logger, ugettext as _, \
ignore_error
from .service import init_app
from .ctx import app_service
from .recorder import get_replay_recorder
from .session import Session
from .models import Connection
eventlet.monkey_patch()
hub_prevent_multiple_readers(False)
__version__ = '1.3.3'
__version__ = '1.4.1'
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
logger = get_logger(__file__)
class Coco:
config_class = Config
default_config = {
'DEFAULT_NAME': socket.gethostname(),
'NAME': None,
'CORE_HOST': 'http://127.0.0.1:8080',
'DEBUG': True,
'BIND_HOST': '0.0.0.0',
'SSHD_PORT': 2222,
'HTTPD_PORT': 5000,
'ACCESS_KEY': '',
'ACCESS_KEY_ENV': 'COCO_ACCESS_KEY',
'ACCESS_KEY_FILE': os.path.join(BASE_DIR, 'keys', '.access_key'),
'SECRET_KEY': None,
'LOG_LEVEL': 'DEBUG',
'LOG_DIR': os.path.join(BASE_DIR, 'logs'),
'SESSION_DIR': os.path.join(BASE_DIR, 'sessions'),
'ASSET_LIST_SORT_BY': 'hostname', # hostname, ip
'PASSWORD_AUTH': True,
'PUBLIC_KEY_AUTH': True,
'HEARTBEAT_INTERVAL': 5,
'MAX_CONNECTIONS': 500,
'ADMINS': '',
'COMMAND_STORAGE': {'TYPE': 'server'}, # server
'REPLAY_STORAGE': {'TYPE': 'server'},
}
def __init__(self, root_path=None):
self.root_path = root_path if root_path else BASE_DIR
self.sessions = []
self.clients = []
def __init__(self):
self.lock = threading.Lock()
self.stop_evt = threading.Event()
self._service = None
......@@ -70,28 +44,8 @@ class Coco:
self.replay_recorder_class = None
self.command_recorder_class = None
self._task_handler = None
self.config = None
self.init_config()
register_app(self)
def init_config(self):
self.config = self.config_class(
self.root_path, defaults=self.default_config
)
@property
def name(self):
if self.config['NAME']:
return self.config['NAME']
else:
return self.config['DEFAULT_NAME']
@property
def service(self):
if self._service is None:
self._service = AppService(self)
register_service(self._service)
return self._service
self.config = config
init_app(self)
@property
def sshd(self):
......@@ -114,32 +68,26 @@ class Coco:
def make_logger(self):
create_logger(self)
def load_extra_conf_from_server(self):
configs = self.service.load_config_from_server()
@staticmethod
def load_extra_conf_from_server():
configs = app_service.load_config_from_server()
logger.debug("Loading config from server: {}".format(
json.dumps(configs)
))
self.config.update(configs)
@staticmethod
def new_command_recorder():
return CommandRecorder()
@staticmethod
def new_replay_recorder():
return ReplayRecorder()
config.update(configs)
def bootstrap(self):
self.make_logger()
self.service.initial()
app_service.initial()
self.load_extra_conf_from_server()
self.keep_heartbeat()
self.monitor_sessions()
self.monitor_sessions_replay()
@ignore_error
def heartbeat(self):
_sessions = [s.to_json() for s in self.sessions]
tasks = self.service.terminal_heartbeat(_sessions)
_sessions = [s.to_json() for s in Session.sessions.values()]
tasks = app_service.terminal_heartbeat(_sessions)
if tasks:
self.handle_task(tasks)
if tasks is False:
......@@ -159,19 +107,17 @@ class Coco:
def func():
while not self.stop_evt.is_set():
self.heartbeat()
time.sleep(self.config["HEARTBEAT_INTERVAL"])
time.sleep(config["HEARTBEAT_INTERVAL"])
thread = threading.Thread(target=func)
thread.start()
def monitor_sessions_replay(self):
interval = 10
recorder = self.new_replay_recorder()
log_dir = os.path.join(self.config['LOG_DIR'])
log_dir = os.path.join(config['LOG_DIR'])
def func():
while not self.stop_evt.is_set():
active_sessions = [str(session.id) for session in self.sessions]
active_sessions = [sid for sid in Session.sessions]
for filename in os.listdir(log_dir):
session_id = filename.split('.')[0]
full_path = os.path.join(log_dir, filename)
......@@ -179,6 +125,7 @@ class Coco:
if len(session_id) != 36:
continue
recorder = get_replay_recorder()
if session_id not in active_sessions:
recorder.file_path = full_path
ok = recorder.upload_replay(session_id, 1)
......@@ -190,21 +137,33 @@ class Coco:
thread.start()
def monitor_sessions(self):
interval = self.config["HEARTBEAT_INTERVAL"]
interval = config["HEARTBEAT_INTERVAL"]
def check_session_idle_too_long(s):
delta = datetime.datetime.utcnow() - s.date_last_active
max_idle_seconds = config['SECURITY_MAX_IDLE_TIME'] * 60
if delta.seconds > max_idle_seconds:
msg = _(
"Connect idle more than {} minutes, disconnect").format(
config['SECURITY_MAX_IDLE_TIME']
)
s.terminate(msg=msg)
return True
def func():
while not self.stop_evt.is_set():
for s in self.sessions:
if not s.stop_evt.is_set():
sessions_copy = [s for s in Session.sessions.values()]
for s in sessions_copy:
# Session 没有正常关闭,
if s.closed_unexpected:
Session.remove_session(s.id)
continue
if s.date_end is None:
self.remove_session(s)
continue
delta = datetime.datetime.now() - s.date_end
if delta > datetime.timedelta(seconds=interval*5):
self.remove_session(s)
# Session已正常关闭
if s.closed:
Session.remove_session(s)
else:
check_session_idle_too_long(s)
time.sleep(interval)
thread = threading.Thread(target=func)
thread.start()
......@@ -215,10 +174,10 @@ class Coco:
print('Quit the server with CONTROL-C.')
try:
if self.config["SSHD_PORT"] != 0:
if config["SSHD_PORT"] != 0:
self.run_sshd()
if self.config['HTTPD_PORT'] != 0:
if config['HTTPD_PORT'] != 0:
self.run_httpd()
signal.signal(signal.SIGTERM, lambda x, y: self.shutdown())
......@@ -228,7 +187,6 @@ class Coco:
break
time.sleep(3)
except KeyboardInterrupt:
self.stop_evt.set()
self.shutdown()
def run_sshd(self):
......@@ -242,46 +200,11 @@ class Coco:
thread.start()
def shutdown(self):
for client in self.clients:
self.remove_client(client)
logger.info("Grace shutdown the server")
for connection in Connection.connections.values():
connection.close()
time.sleep(1)
self.heartbeat()
self.stop_evt.set()
self.sshd.shutdown()
self.httpd.shutdown()
logger.info("Grace shutdown the server")
def add_client(self, client):
with self.lock:
self.clients.append(client)
logger.info("New client {} join, total {} now".format(
client, len(self.clients)
)
)
def remove_client(self, client):
with self.lock:
try:
self.clients.remove(client)
logger.info("Client {} leave, total {} now".format(
client, len(self.clients)
)
)
client.close()
except:
pass
def add_session(self, session):
with self.lock:
self.sessions.append(session)
self.service.create_session(session.to_json())
def remove_session(self, session):
with self.lock:
try:
logger.info("Remove session: {}".format(session))
self.sessions.remove(session)
self.service.finish_session(session.to_json())
except ValueError:
msg = "Remove session: {} fail, maybe already removed"
logger.warning(msg.format(session))
......@@ -17,10 +17,17 @@ import os
import types
import errno
import json
import socket
from werkzeug.utils import import_string
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
root_path = os.environ.get("COCO_PATH")
if not root_path:
root_path = BASE_DIR
class ConfigAttribute(object):
"""Makes an attribute forward to the config"""
......@@ -233,7 +240,7 @@ class Config(dict):
The resulting dictionary `image_store_config` would look like::
{
'type': 'fs',
'types': 'fs',
'path': '/var/app/images',
'base_url': 'http://img.website.com'
}
......@@ -266,4 +273,41 @@ class Config(dict):
return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self))
access_key_path = os.path.abspath(os.path.join(root_path, 'keys', '.access_key'))
default_config = {
'NAME': socket.gethostname(),
'CORE_HOST': 'http://127.0.0.1:8080',
'ROOT_PATH': root_path,
'DEBUG': True,
'BIND_HOST': '0.0.0.0',
'SSHD_PORT': 2222,
'HTTPD_PORT': 5000,
'COCO_ACCESS_KEY': '',
'ACCESS_KEY_FILE': access_key_path,
'SECRET_KEY': 'SDK29K03%MM0ksf&#2',
'LOG_LEVEL': 'DEBUG',
'LOG_DIR': os.path.join(root_path, 'logs'),
'SESSION_DIR': os.path.join(root_path, 'sessions'),
'ASSET_LIST_SORT_BY': 'hostname', # hostname, ip
'PASSWORD_AUTH': True,
'PUBLIC_KEY_AUTH': True,
'SSH_TIMEOUT': 10,
'ALLOW_SSH_USER': [],
'BLOCK_SSH_USER': [],
'HEARTBEAT_INTERVAL': 5,
'MAX_CONNECTIONS': 500, # Not use now
'ADMINS': '',
'COMMAND_STORAGE': {'TYPE': 'server'}, # server
'REPLAY_STORAGE': {'TYPE': 'server'},
'LANGUAGE_CODE': 'zh',
'SECURITY_MAX_IDLE_TIME': 60,
}
config = Config(root_path, default_config)
config.from_pyfile('conf.py')
try:
from conf import config as _conf
config.from_object(_conf)
except ImportError:
pass
......@@ -11,10 +11,11 @@ import paramiko
from paramiko.ssh_exception import SSHException
from .ctx import app_service
from .utils import get_logger, get_private_key_fingerprint, net_input
from .config import config
from .utils import get_logger, get_private_key_fingerprint
logger = get_logger(__file__)
TIMEOUT = 10
BUF_SIZE = 1024
MANUAL_LOGIN = 'manual'
AUTO_LOGIN = 'auto'
......@@ -46,9 +47,12 @@ class SSHConnection:
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,
timeout=config['SSH_TIMEOUT'],
compress=True, auth_timeout=config['SSH_TIMEOUT'],
look_for_keys=False, sock=sock
)
transport = ssh.get_transport()
transport.set_keepalive(300)
except (paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
SSHException) as e:
......@@ -111,7 +115,7 @@ class SSHConnection:
username=gateway.username,
password=gateway.password,
pkey=gateway.private_key_obj,
timeout=TIMEOUT)
timeout=config['SSH_TIMEOUT'])
except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
SSHException):
......@@ -142,7 +146,7 @@ class SSHConnection:
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'))
gateway.set_key_dir(os.path.join(config['ROOT_PATH'], 'keys'))
proxy_command.append("-i {}".format(gateway.private_key_file))
proxy_command = ' '.join(proxy_command)
......
......@@ -4,8 +4,11 @@
from werkzeug.local import LocalProxy
from functools import partial
from .config import config
from jms.service import AppService
stack = {}
__db_sessions = []
def _find(name):
......@@ -15,8 +18,6 @@ def _find(name):
raise ValueError("Not found in stack: {}".format(name))
current_app = LocalProxy(partial(_find, 'app'))
app_service = LocalProxy(partial(_find, 'service'))
# current_app = []
# current_service = []
app_service = AppService(config)
current_app = LocalProxy(partial(_find, 'current_app'))
# app_service = LocalProxy(partial(_find, 'app_service'))
This diff is collapsed.
......@@ -7,11 +7,12 @@ import threading
import os
from . import char
from .config import config
from .utils import wrap_with_line_feed as wr, wrap_with_title as title, \
wrap_with_warning as warning, is_obj_attr_has, is_obj_attr_eq, \
sort_assets, ugettext as _, get_logger, net_input, format_with_zh, \
item_max_length, size_of_str_with_zh
from .ctx import current_app, app_service
item_max_length, size_of_str_with_zh, switch_lang
from .ctx import app_service
from .proxy import ProxyServer
logger = get_logger(__file__)
......@@ -22,8 +23,8 @@ class InteractiveServer:
def __init__(self, client):
self.client = client
self.request = client.request
self.assets = None
self.closed = False
self._search_result = None
self.nodes = None
self.get_user_assets_async()
......@@ -44,28 +45,39 @@ class InteractiveServer:
value = self.filter_system_users(value)
self._search_result = value
def display_logo(self):
logo_path = os.path.join(config['ROOT_PATH'], "logo.txt")
if not os.path.isfile(logo_path):
return
with open(logo_path, 'rb') as f:
for i in f:
if i.decode('utf-8').startswith('#'):
continue
self.client.send(i.decode('utf-8').replace('\n', '\r\n'))
def display_banner(self):
self.client.send(char.CLEAR_CHAR)
logo_path = os.path.join(current_app.root_path, "logo.txt")
if os.path.isfile(logo_path):
with open(logo_path, 'rb') as f:
for i in f:
if i.decode('utf-8').startswith('#'):
continue
self.client.send(i.decode('utf-8').replace('\n', '\r\n'))
banner = _("""\n {title} {user}, 欢迎使用Jumpserver开源跳板机系统 {end}\r\n\r
1) 输入 {green}ID{end} 直接登录 或 输入{green}部分 IP,主机名,备注{end} 进行搜索登录(如果唯一).\r
2) 输入 {green}/{end} + {green}IP, 主机名{end} or {green}备注 {end}搜索. 如: /ip\r
3) 输入 {green}p{end} 显示您有权限的主机.\r
4) 输入 {green}g{end} 显示您有权限的节点\r
5) 输入 {green}g{end} + {green}组ID{end} 显示节点下主机. 如: g1\r
6) 输入 {green}h{end} 帮助.\r
0) 输入 {green}q{end} 退出.\r\n""").format(
title="\033[1;32m", green="\033[32m",
end="\033[0m", user=self.client.user
)
self.client.send(banner)
self.display_logo()
header = _("\n{T}{T}{title} {user}, Welcome to use Jumpserver open source fortress system {end}{R}{R}")
menus = [
_("{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, Comment{end} to search login(if unique).{R}"),
_("{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} search, such as: /ip.{R}"),
_("{T}3) Enter {green}p{end} to display the host you have permission.{R}"),
_("{T}4) Enter {green}g{end} to display the node that you have permission.{R}"),
_("{T}5) Enter {green}g{end} + {green}Group ID{end} to display the host under the node, such as g1.{R}"),
_("{T}6) Enter {green}s{end} Chinese-english switch.{R}"),
_("{T}7) Enter {green}h{end} help.{R}"),
_("{T}0) Enter {green}q{end} exit.{R}")
]
self.client.send(header.format(
title="\033[1;32m", user=self.client.user, end="\033[0m",
T='\t', R='\r\n\r'
))
for menu in menus:
self.client.send(menu.format(
green="\033[32m", end="\033[0m",
T='\t', R='\r\n\r'
))
def dispatch(self, opt):
if opt is None:
......@@ -80,6 +92,9 @@ class InteractiveServer:
self.display_node_assets(int(opt.lstrip("g")))
elif opt in ['q', 'Q', 'exit', 'quit']:
return self._sentinel
elif opt in ['s', 'S']:
switch_lang()
self.display_banner()
elif opt in ['h', 'H']:
self.display_banner()
else:
......@@ -124,33 +139,27 @@ class InteractiveServer:
self.get_user_nodes()
if len(self.nodes) == 0:
self.client.send(warning(_("")))
self.client.send(warning(_("No")))
return
id_length = max(len(str(len(self.nodes))), 5)
name_length = item_max_length(self.nodes, 15, key=lambda x: x.name)
amount_length = item_max_length(self.nodes, 10,
key=lambda x: x.assets_amount)
amount_length = item_max_length(self.nodes, 10, key=lambda x: x.assets_amount)
size_list = [id_length, name_length, amount_length]
fake_data = ['ID', _("Name"), _("Assets")]
header_without_comment = format_with_zh(size_list, *fake_data)
comment_length = max(
self.request.meta["width"] -
size_of_str_with_zh(header_without_comment) - 1,
2
)
size_list.append(comment_length)
fake_data.append(_("Comment"))
self.client.send(title(format_with_zh(size_list, *fake_data)))
for index, group in enumerate(self.nodes, 1):
data = [index, group.name, group.assets_amount, group.comment]
self.client.send(wr(title(format_with_zh(size_list, *fake_data))))
for index, node in enumerate(self.nodes, 1):
data = [index, node.name, node.assets_amount]
self.client.send(wr(format_with_zh(size_list, *data)))
self.client.send(wr(_("总共: {}").format(len(self.nodes)), before=1))
self.client.send(wr(_("Total: {}").format(len(self.nodes)), before=1))
def display_node_assets(self, _id):
if self.nodes is None:
self.get_user_nodes()
if _id > len(self.nodes) or _id <= 0:
self.client.send(wr(warning("没有匹配分组,请重新输入")))
msg = wr(warning(_("There is no matched node, please re-enter")))
self.client.send(msg)
self.display_nodes()
return
......@@ -158,7 +167,7 @@ class InteractiveServer:
self.display_search_result()
def display_search_result(self):
sort_by = current_app.config["ASSET_LIST_SORT_BY"]
sort_by = config["ASSET_LIST_SORT_BY"]
self.search_result = sort_assets(self.search_result, sort_by)
fake_data = [_("ID"), _("Hostname"), _("IP"), _("LoginAs")]
id_length = max(len(str(len(self.search_result))), 4)
......@@ -169,7 +178,7 @@ class InteractiveServer:
size_list = [id_length, hostname_length, 16, sysuser_length]
header_without_comment = format_with_zh(size_list, *fake_data)
comment_length = max(
self.request.meta["width"] -
self.client.request.meta["width"] -
size_of_str_with_zh(header_without_comment) - 1,
2
)
......@@ -182,7 +191,7 @@ class InteractiveServer:
asset.system_users_name_list, asset.comment
]
self.client.send(wr(format_with_zh(size_list, *data)))
self.client.send(wr(_("总共: {} 匹配: {}").format(
self.client.send(wr(_("Total: {} Match: {}").format(
len(self.assets), len(self.search_result)), before=1)
)
......@@ -225,7 +234,7 @@ class InteractiveServer:
return None
while True:
self.client.send(wr(_("选择一个登录: "), after=1))
self.client.send(wr(_("Select a login:: "), after=1))
self.display_system_users(system_users)
opt = net_input(self.client, prompt="ID> ")
if opt.isdigit() and len(system_users) > int(opt):
......@@ -248,7 +257,8 @@ class InteractiveServer:
self.search_result = None
if asset.platform == "Windows":
self.client.send(warning(
_("终端不支持登录windows, 请使用web terminal访问"))
_("Terminal does not support login Windows, "
"please use web terminal to access"))
)
return
self.proxy(asset)
......@@ -258,20 +268,21 @@ class InteractiveServer:
def proxy(self, asset):
system_user = self.choose_system_user(asset.system_users_granted)
if system_user is None:
self.client.send(_("没有系统用户"))
self.client.send(_("No system user"))
return
forwarder = ProxyServer(self.client, login_from='ST')
forwarder.proxy(asset, system_user)
forwarder = ProxyServer(self.client, asset, system_user)
forwarder.proxy()
def interact(self):
self.display_banner()
while True:
while not self.closed:
try:
opt = net_input(self.client, prompt='Opt> ', before=1)
rv = self.dispatch(opt)
if rv is self._sentinel:
break
except socket.error:
except socket.error as e:
logger.debug("Socket error: {}".format(e))
break
self.close()
......@@ -281,7 +292,9 @@ class InteractiveServer:
thread.start()
def close(self):
current_app.remove_client(self.client)
logger.debug("Interactive server server close: {}".format(self))
self.closed = True
# current_app.remove_client(self.client)
# def __del__(self):
# print("GC: Interactive class been gc")
......@@ -4,9 +4,11 @@
import paramiko
import threading
from collections import Iterable
from .utils import get_logger
from .ctx import current_app, app_service
from .config import config
from .ctx import app_service
logger = get_logger(__file__)
......@@ -19,12 +21,13 @@ class SSHInterface(paramiko.ServerInterface):
https://github.com/paramiko/paramiko/blob/master/demos/demo_server.py
"""
def __init__(self, request):
self.request = request
def __init__(self, connection):
self.connection = connection
self.event = threading.Event()
self.auth_valid = False
self.otp_auth = False
self.info = None
self.user = None
def check_auth_interactive(self, username, submethods):
logger.info("Check auth interactive: %s %s" % (username, submethods))
......@@ -58,9 +61,9 @@ class SSHInterface(paramiko.ServerInterface):
supported = []
if self.otp_auth:
return 'keyboard-interactive'
if current_app.config["PASSWORD_AUTH"]:
if config["PASSWORD_AUTH"]:
supported.append("password")
if current_app.config["PUBLIC_KEY_AUTH"]:
if config["PUBLIC_KEY_AUTH"]:
supported.append("publickey")
return ",".join(supported)
......@@ -69,6 +72,7 @@ class SSHInterface(paramiko.ServerInterface):
def check_auth_password(self, username, password):
user = self.validate_auth(username, password=password)
if not user:
logger.warning("Password and public key auth <%s> failed, reject it" % username)
return paramiko.AUTH_FAILED
......@@ -81,6 +85,7 @@ class SSHInterface(paramiko.ServerInterface):
def check_auth_publickey(self, username, key):
key = key.get_base64()
user = self.validate_auth(username, public_key=key)
if not user:
logger.debug("Public key auth <%s> failed, try to password" % username)
return paramiko.AUTH_FAILED
......@@ -90,113 +95,150 @@ class SSHInterface(paramiko.ServerInterface):
return paramiko.AUTH_PARTIALLY_SUCCESSFUL
return paramiko.AUTH_SUCCESSFUL
@staticmethod
def check_block_ssh_user(username):
block_ssh_user = config['BLOCK_SSH_USER']
if not block_ssh_user or not isinstance(block_ssh_user, Iterable):
return False
if username in block_ssh_user:
return True
else:
return False
@staticmethod
def check_allow_ssh_user(username):
allow_ssh_user = config["ALLOW_SSH_USER"]
if not allow_ssh_user or not isinstance(allow_ssh_user, Iterable):
return True
if username in allow_ssh_user:
return True
else:
return False
def validate_auth(self, username, password="", public_key=""):
if self.check_block_ssh_user(username) or \
not self.check_allow_ssh_user(username):
logger.warn("User in black list or not allowed: {}".format(username))
return None
info = app_service.authenticate(
username, password=password, public_key=public_key,
remote_addr=self.request.remote_ip
remote_addr=self.connection.addr[0]
)
user = info.get('user', None)
if user:
self.request.user = user
self.connection.user = user
self.info = info
seed = info.get('seed', None)
token = info.get('token', None)
if seed and not token:
self.connection.otp_auth = True
self.otp_auth = True
return user
def check_channel_direct_tcpip_request(self, chanid, origin, destination):
def check_channel_direct_tcpip_request(self, chan_id, origin, destination):
logger.debug("Check channel direct tcpip request: %d %s %s" %
(chanid, origin, destination))
self.request.type.append('direct-tcpip')
self.request.meta.update({
'chanid': chanid, 'origin': origin,
'destination': destination,
(chan_id, origin, destination))
client = self.connection.new_client(chan_id)
client.request.kind = 'direct-tcpip'
client.request.type = 'direct-tcpip'
client.request.meta.update({
'origin': origin, 'destination': destination
})
self.event.set()
return 0
def check_port_forward_request(self, address, port):
logger.info(
"Check channel port forward request: %s %s" % (address, port)
)
self.event.set()
return False
def check_channel_request(self, kind, chan_id):
logger.info("Check channel request: %s %d" % (kind, chan_id))
client = self.connection.new_client(chan_id)
client.request.kind = kind
return paramiko.OPEN_SUCCEEDED
def check_channel_env_request(self, channel, name, value):
logger.debug("Check channel env request: %s, %s, %s" %
(channel, name, value))
self.request.type.append('env')
(channel.get_id(), name, value))
client = self.connection.get_client(channel)
client.request.meta['env'][name] = value
return False
def check_channel_exec_request(self, channel, command):
logger.debug("Check channel exec request: `%s`" % command)
self.request.type.append('exec')
self.request.meta.update({'channel': channel.get_id(), 'command': command})
client = self.connection.get_client(channel)
client.request.type = 'exec'
client.request.meta.update({
'command': command
})
self.event.set()
return False
def check_channel_forward_agent_request(self, channel):
logger.debug("Check channel forward agent request: %s" % channel)
self.request.type.append("forward-agent")
self.request.meta.update({'channel': channel.get_id()})
client = self.connection.get_client(channel)
client.request.meta['forward-agent'] = True
self.event.set()
return False
return True
def check_channel_pty_request(
self, channel, term, width, height,
pixelwidth, pixelheight, modes):
logger.info("Check channel pty request: %s %s %s %s %s" %
(term, width, height, pixelwidth, pixelheight))
self.request.type.append('pty')
self.request.meta.update({
'channel': channel, 'term': term, 'width': width,
(term, width, height, pixelwidth, pixelheight))
client = self.connection.get_client(channel)
client.request.type = 'pty'
client.request.meta.update({
'term': term, 'width': width,
'height': height, 'pixelwidth': pixelwidth,
'pixelheight': pixelheight,
})
self.event.set()
return True
def check_channel_request(self, kind, chanid):
logger.info("Check channel request: %s %d" % (kind, chanid))
return paramiko.OPEN_SUCCEEDED
def check_channel_shell_request(self, channel):
logger.info("Check channel shell request: %s" % channel.get_id())
self.event.set()
client = self.connection.get_client(channel)
client.request.meta['shell'] = True
return True
def check_channel_subsystem_request(self, channel, name):
logger.info("Check channel subsystem request: %s %s" % (channel, name))
self.request.type.append('subsystem')
self.request.meta.update({'channel': channel.get_id(), 'name': name})
logger.info("Check channel subsystem request: %s" % name)
client = self.connection.get_client(channel)
client.request.type = 'subsystem'
client.request.meta['subsystem'] = name
self.event.set()
return super().check_channel_subsystem_request(channel, name)
def check_channel_window_change_request(self, channel, width, height,
pixelwidth, pixelheight):
self.request.meta.update({
client = self.connection.get_client(channel)
client.request.meta.update({
'width': width,
'height': height,
'pixelwidth': pixelwidth,
'pixelheight': pixelheight,
})
self.request.change_size_event.set()
client.change_size_evt.set()
return True
def check_channel_x11_request(self, channel, single_connection,
auth_protocol, auth_cookie, screen_number):
logger.info("Check channel x11 request %s %s %s %s %s" %
(channel, single_connection, auth_protocol,
logger.info("Check channel x11 request %s %s %s %s" %
(single_connection, auth_protocol,
auth_cookie, screen_number))
self.request.type.append('x11')
self.request.meta.update({
'channel': channel.get_id(), 'single_connection': single_connection,
'auth_protocol': auth_protocol, 'auth_cookie': auth_cookie,
client = self.connection.get_client(channel)
# client.request_x11_event.set()
client.request.meta.update({
'single_connection': single_connection,
'auth_protocol': auth_protocol,
'auth_cookie': auth_cookie,
'screen_number': screen_number,
})
self.event.set()
return False
def check_port_forward_request(self, address, port):
logger.info("Check channel port forward request: %s %s" % (address, port))
self.request.type.append('port-forward')
self.request.meta.update({'address': address, 'port': port})
self.event.set()
return False
def get_banner(self):
......
......@@ -40,9 +40,12 @@ def create_logger(app):
},
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'class': 'logging.handlers.TimedRotatingFileHandler',
'formatter': 'main',
'filename': log_path,
'when': "D",
'interval': 1,
"backupCount": 7
},
},
loggers={
......
This diff is collapsed.
......@@ -5,159 +5,136 @@
import threading
import time
from paramiko.ssh_exception import SSHException
from .session import Session
from .models import Server, TelnetServer
from .connection import SSHConnection, TelnetConnection
from .ctx import current_app, app_service
from .ctx import app_service
from .config import config
from .utils import wrap_with_line_feed as wr, wrap_with_warning as warning, \
get_logger, net_input
get_logger, net_input, ugettext as _
logger = get_logger(__file__)
TIMEOUT = 10
BUF_SIZE = 4096
MANUAL_LOGIN = 'manual'
AUTO_LOGIN = 'auto'
class ProxyServer:
def __init__(self, client, login_from):
def __init__(self, client, asset, system_user):
self.client = client
self.asset = asset
self.system_user = system_user
self.server = None
self.login_from = login_from
self.connecting = True
self.stop_event = threading.Event()
def get_system_user_auth(self, system_user):
def get_system_user_auth_or_manual_set(self):
"""
获取系统用户的认证信息,密码或秘钥
:return: system user have full info
"""
password, private_key = \
app_service.get_system_user_auth_info(system_user)
if system_user.login_mode == MANUAL_LOGIN or (not password and not private_key):
prompt = "{}'s password: ".format(system_user.username)
app_service.get_system_user_auth_info(self.system_user)
if self.system_user.login_mode == MANUAL_LOGIN \
or (not password and not private_key):
prompt = "{}'s password: ".format(self.system_user.username)
password = net_input(self.client, prompt=prompt, sensitive=True)
system_user.password = password
system_user.private_key = private_key
self.system_user.password = password
self.system_user.private_key = private_key
def proxy(self, asset, system_user):
if asset.protocol != system_user.protocol:
def check_protocol(self):
if self.asset.protocol != self.system_user.protocol:
msg = 'System user <{}> and asset <{}> protocol are inconsistent.'.format(
system_user.name, asset.hostname
self.system_user.name, self.asset.hostname
)
self.client.send(warning(wr(msg, before=1, after=0)))
return False
return True
def manual_set_system_user_username_if_need(self):
if self.system_user.login_mode == MANUAL_LOGIN and \
not self.system_user.username:
username = net_input(self.client, prompt='username: ', before=1)
self.system_user.username = username
return True
return False
def proxy(self):
if not self.check_protocol():
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)
self.manual_set_system_user_username_if_need()
self.get_system_user_auth_or_manual_set()
self.server = self.get_server_conn()
if self.server is None:
return
command_recorder = current_app.new_command_recorder()
replay_recorder = current_app.new_replay_recorder()
session = Session(
self.client, self.server, self.login_from,
command_recorder=command_recorder,
replay_recorder=replay_recorder,
)
current_app.add_session(session)
self.watch_win_size_change_async()
session = Session.new_session(self.client, self.server)
session.bridge()
self.stop_event.set()
self.end_watch_win_size_change()
current_app.remove_session(session)
Session.remove_session(session.id)
self.server.close()
def validate_permission(self, asset, system_user):
def validate_permission(self):
"""
验证用户是否有连接改资产的权限
:return: True or False
"""
return app_service.validate_user_asset_permission(
self.client.user.id, asset.id, system_user.id
self.client.user.id, self.asset.id, self.system_user.id
)
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
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)
def get_server_conn(self):
logger.info("Connect to {}".format(self.asset.hostname))
self.send_connecting_message()
if not self.validate_permission():
self.client.send(warning(_('No permission')))
server = None
elif self.system_user.protocol == self.asset.protocol == 'telnet':
server = self.get_telnet_server_conn()
elif self.system_user.protocol == self.asset.protocol == 'ssh':
server = self.get_ssh_server_conn()
else:
server = None
self.connecting = False
self.client.send(b'\r\n')
return server
# Todo: Support telnet
def get_telnet_server_conn(self, asset, system_user):
telnet = TelnetConnection(asset, system_user, self.client)
def get_telnet_server_conn(self):
telnet = TelnetConnection(self.asset, self.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
server = TelnetServer(sock, self.asset, self.system_user)
return server
def get_ssh_server_conn(self, asset, system_user):
def get_ssh_server_conn(self):
request = self.client.request
term = request.meta.get('term', 'xterm')
width = request.meta.get('width', 80)
height = request.meta.get('height', 24)
ssh = SSHConnection()
chan, sock, msg = ssh.get_channel(
asset, system_user, term=term, width=width, height=height
self.asset, self.system_user, term=term,
width=width, height=height
)
if not chan:
self.client.send(warning(wr(msg, before=1, after=0)))
server = None
else:
server = Server(chan, sock, asset, system_user)
self.connecting = False
self.client.send(b'\r\n')
server = Server(chan, sock, self.asset, self.system_user)
return server
def watch_win_size_change(self):
while self.client.request.change_size_event.wait():
if self.stop_event.is_set():
break
self.client.request.change_size_event.clear()
width = self.client.request.meta.get('width', 80)
height = self.client.request.meta.get('height', 24)
logger.debug("Change win size: %s - %s" % (width, height))
try:
self.server.chan.resize_pty(width=width, height=height)
except SSHException:
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()
def end_watch_win_size_change(self):
self.client.request.change_size_event.set()
def send_connecting_message(self, asset, system_user):
def send_connecting_message(self):
def func():
delay = 0.0
self.client.send('Connecting to {}@{} {:.1f}'.format(
system_user, asset, delay)
self.client.send(_('Connecting to {}@{} {:.1f}').format(
self.system_user, self.asset, delay)
)
while self.connecting and delay < TIMEOUT:
self.client.send('\x08\x08\x08{:.1f}'.format(delay).encode())
while self.connecting and delay < config['SSH_TIMEOUT']:
if 0 <= delay < 10:
self.client.send('\x08\x08\x08{:.1f}'.format(delay).encode())
else:
self.client.send('\x08\x08\x08\x08{:.1f}'.format(delay).encode())
time.sleep(0.1)
delay += 0.1
thread = threading.Thread(target=func)
......
......@@ -12,9 +12,10 @@ from copy import deepcopy
import jms_storage
from .config import config
from .utils import get_logger, Singleton
from .alignment import MemoryQueue
from .ctx import current_app, app_service
from .struct import MemoryQueue
from .ctx import app_service
logger = get_logger(__file__)
BUF_SIZE = 1024
......@@ -48,7 +49,7 @@ class ReplayRecorder(metaclass=abc.ABCMeta):
def session_start(self, session_id):
self.time_start = time.time()
filename = session_id + '.replay.gz'
self.file_path = os.path.join(current_app.config['LOG_DIR'], filename)
self.file_path = os.path.join(config['LOG_DIR'], filename)
self.file = gzip.open(self.file_path, 'at')
self.file.write('{')
......@@ -58,9 +59,9 @@ class ReplayRecorder(metaclass=abc.ABCMeta):
self.upload_replay(session_id)
def get_storage(self):
config = deepcopy(current_app.config["REPLAY_STORAGE"])
config["SERVICE"] = app_service
self.storage = jms_storage.get_object_storage(config)
conf = deepcopy(config["REPLAY_STORAGE"])
conf["SERVICE"] = app_service
self.storage = jms_storage.get_object_storage(conf)
def upload_replay(self, session_id, times=3):
if times < 1:
......@@ -130,9 +131,9 @@ class CommandRecorder(metaclass=Singleton):
self.queue.put(data)
def get_storage(self):
config = deepcopy(current_app.config["COMMAND_STORAGE"])
config['SERVICE'] = app_service
self.storage = jms_storage.get_log_storage(config)
conf = deepcopy(config["COMMAND_STORAGE"])
conf['SERVICE'] = app_service
self.storage = jms_storage.get_log_storage(conf)
def push_to_server_async(self):
def func():
......@@ -153,9 +154,19 @@ class CommandRecorder(metaclass=Singleton):
thread.start()
def session_start(self, session_id):
print("Session start: {}".format(session_id))
pass
def session_end(self, session_id):
print("Session end: {}".format(session_id))
pass
def get_command_recorder():
return CommandRecorder()
def get_replay_recorder():
return ReplayRecorder()
def get_recorder():
return get_command_recorder(), get_replay_recorder()
\ No newline at end of file
# -*- coding: utf-8 -*-
#
from .ctx import stack
def init_app(app):
stack['current_app'] = app
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import threading
import uuid
import datetime
import selectors
import time
from .utils import get_logger
from .utils import get_logger, wrap_with_warning as warn, \
wrap_with_line_feed as wr, ugettext as _, ignore_error
from .ctx import app_service
from .struct import SelectEvent
from .recorder import get_recorder
BUF_SIZE = 1024
logger = get_logger(__file__)
class Session:
def __init__(self, client, server, login_from, command_recorder=None, replay_recorder=None):
sessions = {}
def __init__(self, client, server):
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
self.date_created = datetime.datetime.utcnow()
self.date_start = datetime.datetime.utcnow()
self.date_end = None
self.stop_evt = threading.Event()
self.is_finished = False
self.closed = False
self.sel = selectors.DefaultSelector()
self._command_recorder = command_recorder
self._replay_recorder = replay_recorder
self._command_recorder = None
self._replay_recorder = None
self.stop_evt = SelectEvent()
self.server.set_session(self)
self.date_last_active = datetime.datetime.utcnow()
@classmethod
def new_session(cls, client, server):
session = cls(client, server)
command_recorder, replay_recorder = get_recorder()
session.set_command_recorder(command_recorder)
session.set_replay_recorder(replay_recorder)
cls.sessions[session.id] = session
app_service.create_session(session.to_json())
return session
@classmethod
def get_session(cls, sid):
return cls.sessions.get(sid)
@classmethod
def remove_session(cls, sid):
session = cls.get_session(sid)
if session:
session.close()
app_service.finish_session(session.to_json())
app_service.finish_replay(sid)
del cls.sessions[sid]
def add_watcher(self, watcher, silent=False):
"""
Add a watcher, and will be transport server side msg to it.
......@@ -64,6 +93,10 @@ class Session:
self.sel.register(sharer, selectors.EVENT_READ)
self._sharers.append(sharer)
@property
def closed_unexpected(self):
return not self.is_finished and (self.client.closed or self.server.closed)
def remove_sharer(self, sharer):
logger.info("Session %s remove sharer %s" % (self.id, sharer))
sharer.send("Leave session {} at {}"
......@@ -79,8 +112,6 @@ class Session:
self._replay_recorder = recorder
def put_command(self, _input, _output):
if not _input:
return
self._command_recorder.record({
"session": self.id,
"org_id": self.server.asset.org_id,
......@@ -107,13 +138,14 @@ class Session:
self._replay_recorder.session_end(self.id)
self._command_recorder.session_end(self.id)
def terminate(self):
msg = b"Terminate by administrator\r\n"
def terminate(self, msg=None):
if not msg:
msg = _("Terminated by administrator")
try:
self.client.send(msg)
self.client.send(wr(warn(msg), before=1))
except OSError:
pass
self.close()
self.stop_evt.set()
def bridge(self):
"""
......@@ -124,16 +156,17 @@ class Session:
self.pre_bridge()
self.sel.register(self.client, selectors.EVENT_READ)
self.sel.register(self.server, selectors.EVENT_READ)
while not self.stop_evt.is_set():
events = self.sel.select()
self.sel.register(self.stop_evt, selectors.EVENT_READ)
self.sel.register(self.client.change_size_evt, selectors.EVENT_READ)
while not self.is_finished:
events = self.sel.select(timeout=60)
for sock in [key.fileobj for key, _ in events]:
data = sock.recv(BUF_SIZE)
# self.put_replay(data)
if sock == self.server:
if len(data) == 0:
msg = "Server close the connection"
logger.info(msg)
self.close()
self.is_finished = True
break
self.date_last_active = datetime.datetime.utcnow()
......@@ -145,30 +178,32 @@ class Session:
logger.info(msg)
for watcher in self._watchers + self._sharers:
watcher.send(msg.encode("utf-8"))
self.close()
self.is_finished = True
break
self.server.send(data)
elif sock in self._sharers:
if len(data) == 0:
logger.info("Sharer {} leave the session {}".format(sock, self.id))
self.remove_sharer(sock)
self.server.send(data)
elif sock in self._watchers:
if len(data) == 0:
self._watchers.remove(sock)
logger.info("Watcher {} leave the session {}".format(sock, self.id))
elif sock == self.stop_evt:
self.is_finished = True
break
elif sock == self.client.change_size_evt:
self.resize_win_size()
logger.info("Session stop event set: {}".format(self.id))
def set_size(self, width, height):
def resize_win_size(self):
width, height = self.client.request.meta['width'], \
self.client.request.meta['height']
logger.debug("Resize server chan size {}*{}".format(width, height))
self.server.resize_pty(width=width, height=height)
@ignore_error
def close(self):
if self.closed:
logger.info("Session has been closed: {} ".format(self.id))
return
logger.info("Close the session: {} ".format(self.id))
self.stop_evt.set()
self.is_finished = True
self.closed = True
self.post_bridge()
self.date_end = datetime.datetime.utcnow()
self.server.close()
def to_json(self):
return {
......@@ -177,11 +212,10 @@ class Session:
"asset": self.server.asset.hostname,
"org_id": self.server.asset.org_id,
"system_user": self.server.system_user.username,
"login_from": self.login_from,
"login_from": self.client.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",
"date_start": self.date_created.strftime("%Y-%m-%d %H:%M:%S") + " +0000",
"is_finished": self.is_finished,
"date_start": self.date_start.strftime("%Y-%m-%d %H:%M:%S") + " +0000",
"date_end": self.date_end.strftime("%Y-%m-%d %H:%M:%S") + " +0000" if self.date_end else None
}
......
......@@ -2,9 +2,9 @@ import os
import tempfile
import paramiko
import time
from .ctx import app_service
from datetime import datetime
from .ctx import app_service
from .connection import SSHConnection
......@@ -53,7 +53,7 @@ class SFTPServer(paramiko.SFTPServerInterface):
def get_perm_hosts(self):
hosts = {}
assets = app_service.get_user_assets(
self.server.request.user
self.server.connection.user
)
for asset in assets:
key = asset.hostname
......
......@@ -5,14 +5,15 @@
import os
import socket
import threading
import paramiko
from .utils import ssh_key_gen, get_logger
from .interface import SSHInterface
from .interactive import InteractiveServer
from .models import Client, Request
from .models import Connection
from .sftp import SFTPServer
from .ctx import current_app
from .config import config
logger = get_logger(__file__)
BACKLOG = 5
......@@ -24,10 +25,11 @@ class SSHServer:
self.stop_evt = threading.Event()
self.workers = []
self.pipe = None
self.connections = []
@property
def host_key(self):
host_key_path = os.path.join(current_app.root_path, 'keys', 'host_rsa_key')
host_key_path = os.path.join(config['ROOT_PATH'], 'keys', 'host_rsa_key')
if not os.path.isfile(host_key_path):
self.gen_host_key(host_key_path)
return paramiko.RSAKey(filename=host_key_path)
......@@ -39,24 +41,27 @@ class SSHServer:
f.write(ssh_key)
def run(self):
host = current_app.config["BIND_HOST"]
port = current_app.config["SSHD_PORT"]
host = config["BIND_HOST"]
port = config["SSHD_PORT"]
print('Starting ssh server at {}:{}'.format(host, port))
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
sock.bind((host, port))
sock.listen(BACKLOG)
while not self.stop_evt.is_set():
try:
client, addr = sock.accept()
logger.info("Get ssh request from {}: {}".format(*addr))
thread = threading.Thread(target=self.handle_connection,
args=(client, addr))
thread.daemon = True
thread.start()
t = threading.Thread(target=self.handle_connection, args=(client, addr))
t.daemon = True
t.start()
except IndexError as e:
logger.error("Start SSH server error: {}".format(e))
def new_connection(self, addr, sock):
connection = Connection.new_connection(addr=addr, sock=sock)
self.connections.append(connection)
return connection
def handle_connection(self, sock, addr):
transport = paramiko.Transport(sock, gss_kex=False)
try:
......@@ -68,52 +73,52 @@ class SSHServer:
transport.set_subsystem_handler(
'sftp', paramiko.SFTPServer, SFTPServer
)
request = Request(addr)
server = SSHInterface(request)
connection = self.new_connection(addr, sock=sock)
server = SSHInterface(connection)
try:
transport.start_server(server=server)
except paramiko.SSHException:
logger.warning("SSH negotiation failed")
return
except EOFError:
logger.warning("Handle EOF Error")
except EOFError as e:
logger.warning("Handle EOF Error: {}".format(e))
return
while True:
if not transport.is_active():
transport.close()
sock.close()
break
while transport.is_active():
chan = transport.accept()
server.event.wait(5)
if chan is None:
continue
if not server.event.is_set():
logger.warning("Client not request a valid request, exiting")
sock.close()
return
else:
server.event.clear()
t = threading.Thread(target=self.handle_chan, args=(chan, request))
client = connection.clients.get(chan.get_id())
client.chan = chan
t = threading.Thread(target=self.dispatch, args=(client,))
t.daemon = True
t.start()
Connection.remove_connection(connection.id)
def handle_chan(self, chan, request):
client = Client(chan, request)
current_app.add_client(client)
self.dispatch(client)
def dispatch(self, client):
@staticmethod
def dispatch(client):
supported = {'pty', 'x11', 'forward-agent'}
request_type = set(client.request.type)
if supported & request_type:
logger.info("Request type `pty`, dispatch to interactive mode")
chan_type = client.request.type
kind = client.request.kind
if kind == 'session' and chan_type in supported:
logger.info("Request type `{}:{}`, dispatch to interactive mode".format(kind, chan_type))
InteractiveServer(client).interact()
elif 'subsystem' in request_type:
connection = Connection.get_connection(client.connection_id)
connection.remove_client(client.id)
elif chan_type == 'subsystem':
pass
else:
logger.info("Request type `{}`".format(request_type))
client.send("Not support request type: %s" % request_type)
msg = "Request type `{}:{}` not support now".format(kind, chan_type)
logger.info(msg)
client.send(msg)
def shutdown(self):
self.stop_evt.set()
......@@ -3,6 +3,7 @@
#
import queue
import socket
class MultiQueueMixin:
......@@ -24,16 +25,31 @@ class MemoryQueue(MultiQueueMixin, queue.Queue):
pass
def get_queue(config):
queue_engine = config['QUEUE_ENGINE']
queue_size = config['QUEUE_MAX_SIZE']
class SizedList(list):
def __init__(self, maxsize=0):
self.maxsize = maxsize
self.size = 0
super().__init__()
if queue_engine == "server":
replay_queue = MemoryQueue(queue_size)
command_queue = MemoryQueue(queue_size)
else:
replay_queue = MemoryQueue(queue_size)
command_queue = MemoryQueue(queue_size)
def append(self, b):
if self.maxsize == 0 or self.size < self.maxsize:
super().append(b)
self.size += len(b)
return replay_queue, command_queue
def clean(self):
self.size = 0
del self[:]
class SelectEvent:
def __init__(self):
self.p1, self.p2 = socket.socketpair()
def set(self):
self.p2.send(b'0')
def fileno(self):
return self.p1.fileno()
def __getattr__(self, item):
return getattr(self.p1, item)
......@@ -2,8 +2,9 @@
# -*- coding: utf-8 -*-
#
from .ctx import current_app, app_service
from .ctx import app_service
from .utils import get_logger
from .session import Session
logger = get_logger(__file__)
......@@ -18,12 +19,7 @@ class TaskHandler:
def handle_kill_session(task):
logger.info("Handle kill session task: {}".format(task.args))
session_id = task.args
session = None
for s in current_app.sessions:
if s.id == session_id:
session = s
break
session = Session.sessions.get(session_id)
if session:
session.terminate()
app_service.finish_task(task.id)
......
......@@ -10,15 +10,21 @@ import os
import gettext
from io import StringIO
from binascii import hexlify
from werkzeug.local import Local, LocalProxy
from functools import partial, wraps
import builtins
import paramiko
import pyte
from . import char
from .ctx import stack
from .config import config
BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
APP_NAME = "coco"
LOCALE_DIR = os.path.join(BASE_DIR, 'locale')
class Singleton(type):
def __init__(cls, *args, **kwargs):
......@@ -270,12 +276,6 @@ def sort_assets(assets, order_by='hostname'):
return assets
def _gettext():
gettext.bindtextdomain("coco", os.path.join(BASE_DIR, "locale"))
gettext.textdomain("coco")
return gettext.gettext
def get_private_key_fingerprint(key):
line = hexlify(key.get_fingerprint())
return b':'.join([line[i:i+2] for i in range(0, len(line), 2)])
......@@ -342,7 +342,7 @@ def net_input(client, prompt='Opt> ', sensitive=False, before=0, after=0):
input_data.append(data[:-1])
multi_char_with_enter = True
# If user type ENTER we should get user input
# If user types ENTER we should get user input
if data in char.ENTER_CHAR or multi_char_with_enter:
client.send(wrap_with_line_feed(b'', after=2))
option = parser.parse_input(input_data)
......@@ -356,14 +356,6 @@ def net_input(client, prompt='Opt> ', sensitive=False, before=0, after=0):
input_data.append(data)
def register_app(app):
stack['app'] = app
def register_service(service):
stack['service'] = service
zh_pattern = re.compile(r'[\u4e00-\u9fa5]')
......@@ -419,5 +411,62 @@ def int_length(i):
return len(str(i))
ugettext = _gettext()
def _get_trans():
gettext.install(APP_NAME, LOCALE_DIR)
zh = gettext.translation(APP_NAME, LOCALE_DIR, ["zh_CN"])
en = gettext.translation(APP_NAME, LOCALE_DIR, ["en"])
return zh, en
trans_zh, trans_en = _get_trans()
_thread_locals = Local()
def set_current_lang(lang):
setattr(_thread_locals, 'LANGUAGE_CODE', lang)
def get_current_lang(attr):
return getattr(_thread_locals, attr, None)
def _gettext(lang):
if lang == 'en':
trans_en.install()
else:
trans_zh.install()
return builtins.__dict__['_']
def _find(attr):
lang = get_current_lang(attr)
if lang is None:
lang = config['LANGUAGE_CODE']
set_current_lang(lang)
return _gettext(lang)
def switch_lang():
lang = get_current_lang('LANGUAGE_CODE')
if lang == 'zh':
set_current_lang('en')
elif lang == 'en':
set_current_lang('zh')
logger = get_logger(__file__)
def ignore_error(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
resp = func(*args, **kwargs)
return resp
except Exception as e:
logger.error("Error occur: {} {}".format(func.__name__, e))
raise e
return wrapper
ugettext = LocalProxy(partial(_find, 'LANGUAGE_CODE'))
......@@ -53,6 +53,12 @@ class Config:
# 登录是否支持秘钥认证
# PUBLIC_KEY_AUTH = True
# SSH白名单
# ALLOW_SSH_USER = 'all' # ['test', 'test2']
# SSH黑名单, 如果用户同时在白名单和黑名单,黑名单优先生效
# BLOCK_SSH_USER = []
# 和Jumpserver 保持心跳时间间隔
# HEARTBEAT_INTERVAL = 5
......@@ -66,5 +72,11 @@ class Config:
"TYPE": "server"
}
# SSH连接超时时间 (default 15 seconds)
# SSH_TIMEOUT = 15
# 语言 = en
LANGUAGE_CODE = 'zh'
config = Config()
# Language locale/en/LC translations for PACKAGE package.
# Copyright (C) 2018 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# BaiJiangjie <bugatti_it@163.com>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-03 10:39+0800\n"
"PO-Revision-Date: 2018-08-10 10:42+0800\n"
"Last-Translator: BaiJiangjie <bugatti_it@163.com>\n"
"Language-Team: Language locale/en/LC\n"
"Language: locale/en/LC_MESSAGES/coco\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: coco/app.py:147
msgid "Connect idle more than {} minutes, disconnect"
msgstr ""
#: coco/interactive.py:61
#, python-brace-format
msgid ""
"\n"
"{T}{T}{title} {user}, Welcome to use Jumpserver open source fortress system "
"{end}{R}{R}"
msgstr ""
#: coco/interactive.py:63
#, python-brace-format
msgid ""
"{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, "
"Comment{end} to search login(if unique).{R}"
msgstr ""
#: coco/interactive.py:64
#, python-brace-format
msgid ""
"{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} "
"search, such as: /ip.{R}"
msgstr ""
#: coco/interactive.py:65
#, python-brace-format
msgid "{T}3) Enter {green}p{end} to display the host you have permission.{R}"
msgstr ""
#: coco/interactive.py:66
#, python-brace-format
msgid ""
"{T}4) Enter {green}g{end} to display the node that you have permission.{R}"
msgstr ""
#: coco/interactive.py:67
#, python-brace-format
msgid ""
"{T}5) Enter {green}g{end} + {green}Group ID{end} to display the host under "
"the node, such as g1.{R}"
msgstr ""
#: coco/interactive.py:68
#, python-brace-format
msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}"
msgstr ""
#: coco/interactive.py:69
#, python-brace-format
msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr ""
#: coco/interactive.py:70
#, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr ""
#: coco/interactive.py:142
msgid "No"
msgstr ""
#: coco/interactive.py:149
msgid "Name"
msgstr ""
#: coco/interactive.py:149
msgid "Assets"
msgstr ""
#: coco/interactive.py:155
msgid "Total: {}"
msgstr ""
#: coco/interactive.py:159
msgid "There is no matched node, please re-enter"
msgstr ""
#: coco/interactive.py:170
msgid "ID"
msgstr ""
#: coco/interactive.py:170
msgid "Hostname"
msgstr ""
#: coco/interactive.py:170
msgid "IP"
msgstr ""
#: coco/interactive.py:170
msgid "LoginAs"
msgstr ""
#: coco/interactive.py:184
msgid "Comment"
msgstr ""
#: coco/interactive.py:192
msgid "Total: {} Match: {}"
msgstr ""
#: coco/interactive.py:235
msgid "Select a login:: "
msgstr ""
#: coco/interactive.py:258
msgid ""
"Terminal does not support login Windows, please use web terminal to access"
msgstr ""
#: coco/interactive.py:269
msgid "No system user"
msgstr ""
#: coco/proxy.py:88
msgid "No permission"
msgstr ""
#: coco/proxy.py:130
msgid "Connecting to {}@{} {:.1f}"
msgstr ""
#: coco/session.py:143
msgid "Terminated by administrator"
msgstr ""
# Language locale/en/LC translations for PACKAGE package.
# Copyright (C) 2018 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# BaiJiangjie <bugatti_it@163.com>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-03 10:36+0800\n"
"PO-Revision-Date: 2018-08-10 10:42+0800\n"
"Last-Translator: BaiJiangjie <bugatti_it@163.com>\n"
"Language-Team: Language locale/en/LC\n"
"Language: locale/en/LC_MESSAGES/coco\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: coco/app.py:147
msgid "Connect idle more than {} minutes, disconnect"
msgstr ""
#: coco/interactive.py:61
#, python-brace-format
msgid ""
"\n"
"{T}{T}{title} {user}, Welcome to use Jumpserver open source fortress system "
"{end}{R}{R}"
msgstr ""
#: coco/interactive.py:63
#, python-brace-format
msgid ""
"{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, "
"Comment{end} to search login(if unique).{R}"
msgstr ""
#: coco/interactive.py:64
#, python-brace-format
msgid ""
"{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} "
"search, such as: /ip.{R}"
msgstr ""
#: coco/interactive.py:65
#, python-brace-format
msgid "{T}3) Enter {green}p{end} to display the host you have permission.{R}"
msgstr ""
#: coco/interactive.py:66
#, python-brace-format
msgid ""
"{T}4) Enter {green}g{end} to display the node that you have permission.{R}"
msgstr ""
#: coco/interactive.py:67
#, python-brace-format
msgid ""
"{T}5) Enter {green}g{end} + {green}Group ID{end} to display the host under "
"the node, such as g1.{R}"
msgstr ""
#: coco/interactive.py:68
#, python-brace-format
msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}"
msgstr ""
#: coco/interactive.py:69
#, python-brace-format
msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr ""
#: coco/interactive.py:70
#, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr ""
#: coco/interactive.py:142
msgid "No"
msgstr ""
#: coco/interactive.py:149
msgid "Name"
msgstr ""
#: coco/interactive.py:149
msgid "Assets"
msgstr ""
#: coco/interactive.py:155
msgid "Total: {}"
msgstr ""
#: coco/interactive.py:159
msgid "There is no matched node, please re-enter"
msgstr ""
#: coco/interactive.py:170
msgid "ID"
msgstr ""
#: coco/interactive.py:170
msgid "Hostname"
msgstr ""
#: coco/interactive.py:170
msgid "IP"
msgstr ""
#: coco/interactive.py:170
msgid "LoginAs"
msgstr ""
#: coco/interactive.py:184
msgid "Comment"
msgstr ""
#: coco/interactive.py:192
msgid "Total: {} Match: {}"
msgstr ""
#: coco/interactive.py:235
msgid "Select a login:: "
msgstr ""
#: coco/interactive.py:258
msgid ""
"Terminal does not support login Windows, please use web terminal to access"
msgstr ""
#: coco/interactive.py:269
msgid "No system user"
msgstr ""
#: coco/session.py:143
msgid "Terminated by administrator"
msgstr ""
# Language locale/zh translations for PACKAGE package.
# Copyright (C) 2018 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# BaiJiangjie <bugatti_it@163.com>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-03 10:39+0800\n"
"PO-Revision-Date: 2018-08-10 10:42+0800\n"
"Last-Translator: BaiJiangjie <bugatti_it@163.com>\n"
"Language-Team: Language locale/zh\n"
"Language: locale/zh_CN/LC_MESSAGES/coco\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: coco/app.py:147
msgid "Connect idle more than {} minutes, disconnect"
msgstr "空闲时间超过 {} 分钟,断开连接"
#: coco/interactive.py:61
#, python-brace-format
msgid ""
"\n"
"{T}{T}{title} {user}, Welcome to use Jumpserver open source fortress system "
"{end}{R}{R}"
msgstr ""
"\n"
"{T}{T}{title} {user}, 欢迎使用Jumpserver开源跳板机系统 {end}{R}{R}"
#: coco/interactive.py:63
#, python-brace-format
msgid ""
"{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, "
"Comment{end} to search login(if unique).{R}"
msgstr ""
"{T}1) 输入 {green}ID{end} 直接登录 或 输入{green}部分 IP,主机名,备注{end} 进"
"行搜索登录(如果唯一).{R}"
#: coco/interactive.py:64
#, python-brace-format
msgid ""
"{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} "
"search, such as: /ip.{R}"
msgstr ""
"{T}2) 输入 {green}/{end} + {green}IP, 主机名{end} or {green}备注 {end}搜索. "
"如: /ip{R}"
#: coco/interactive.py:65
#, python-brace-format
msgid "{T}3) Enter {green}p{end} to display the host you have permission.{R}"
msgstr "{T}3) 输入 {green}p{end} 显示您有权限的主机.{R}"
#: coco/interactive.py:66
#, python-brace-format
msgid ""
"{T}4) Enter {green}g{end} to display the node that you have permission.{R}"
msgstr "{T}4) 输入 {green}g{end} 显示您有权限的节点.{R}"
#: coco/interactive.py:67
#, python-brace-format
msgid ""
"{T}5) Enter {green}g{end} + {green}Group ID{end} to display the host under "
"the node, such as g1.{R}"
msgstr "{T}5) 输入 {green}g{end} + {green}组ID{end} 显示节点下主机. 如: g1{R}"
#: coco/interactive.py:68
#, python-brace-format
msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}"
msgstr "{T}6) 输入 {green}s{end} 中/英文切换.{R}"
#: coco/interactive.py:69
#, python-brace-format
msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr "{T}7) 输入 {green}h{end} 帮助.{R}"
#: coco/interactive.py:70
#, fuzzy, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr "{T}0) 输入 {green}q{end} 退出.{R}\n"
#: coco/interactive.py:142
msgid "No"
msgstr "无"
#: coco/interactive.py:149
msgid "Name"
msgstr "名称"
#: coco/interactive.py:149
msgid "Assets"
msgstr "资产"
#: coco/interactive.py:155
msgid "Total: {}"
msgstr "总共: {}"
#: coco/interactive.py:159
msgid "There is no matched node, please re-enter"
msgstr "没有匹配分组,请重新输入"
#: coco/interactive.py:170
msgid "ID"
msgstr ""
#: coco/interactive.py:170
msgid "Hostname"
msgstr "主机名"
#: coco/interactive.py:170
msgid "IP"
msgstr ""
#: coco/interactive.py:170
msgid "LoginAs"
msgstr ""
#: coco/interactive.py:184
msgid "Comment"
msgstr "备注"
#: coco/interactive.py:192
msgid "Total: {} Match: {}"
msgstr "总共: {} 匹配: {}"
#: coco/interactive.py:235
msgid "Select a login:: "
msgstr "选择一个登录:"
#: coco/interactive.py:258
msgid ""
"Terminal does not support login Windows, please use web terminal to access"
msgstr "终端不支持登录windows, 请使用web terminal访问"
#: coco/interactive.py:269
msgid "No system user"
msgstr "没有系统用户"
#: coco/proxy.py:88
msgid "No permission"
msgstr "没有权限"
#: coco/proxy.py:130
msgid "Connecting to {}@{} {:.1f}"
msgstr "开始连接到 {}@{} {:.1f}"
#: coco/session.py:143
msgid "Terminated by administrator"
msgstr "被管理员中断"
# Language locale/zh translations for PACKAGE package.
# Copyright (C) 2018 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# BaiJiangjie <bugatti_it@163.com>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-03 10:36+0800\n"
"PO-Revision-Date: 2018-08-10 10:42+0800\n"
"Last-Translator: BaiJiangjie <bugatti_it@163.com>\n"
"Language-Team: Language locale/zh\n"
"Language: locale/zh_CN/LC_MESSAGES/coco\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: coco/app.py:147
msgid "Connect idle more than {} minutes, disconnect"
msgstr "空闲时间超过 {} 分钟,断开连接"
#: coco/interactive.py:61
#, python-brace-format
msgid ""
"\n"
"{T}{T}{title} {user}, Welcome to use Jumpserver open source fortress system "
"{end}{R}{R}"
msgstr ""
"\n"
"{T}{T}{title} {user}, 欢迎使用Jumpserver开源跳板机系统 {end}{R}{R}"
#: coco/interactive.py:63
#, python-brace-format
msgid ""
"{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, "
"Comment{end} to search login(if unique).{R}"
msgstr ""
"{T}1) 输入 {green}ID{end} 直接登录 或 输入{green}部分 IP,主机名,备注{end} 进"
"行搜索登录(如果唯一).{R}"
#: coco/interactive.py:64
#, python-brace-format
msgid ""
"{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} "
"search, such as: /ip.{R}"
msgstr ""
"{T}2) 输入 {green}/{end} + {green}IP, 主机名{end} or {green}备注 {end}搜索. "
"如: /ip{R}"
#: coco/interactive.py:65
#, python-brace-format
msgid "{T}3) Enter {green}p{end} to display the host you have permission.{R}"
msgstr "{T}3) 输入 {green}p{end} 显示您有权限的主机.{R}"
#: coco/interactive.py:66
#, python-brace-format
msgid ""
"{T}4) Enter {green}g{end} to display the node that you have permission.{R}"
msgstr "{T}4) 输入 {green}g{end} 显示您有权限的节点.{R}"
#: coco/interactive.py:67
#, python-brace-format
msgid ""
"{T}5) Enter {green}g{end} + {green}Group ID{end} to display the host under "
"the node, such as g1.{R}"
msgstr "{T}5) 输入 {green}g{end} + {green}组ID{end} 显示节点下主机. 如: g1{R}"
#: coco/interactive.py:68
#, python-brace-format
msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}"
msgstr "{T}6) 输入 {green}s{end} 中/英文切换.{R}"
#: coco/interactive.py:69
#, python-brace-format
msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr "{T}7) 输入 {green}h{end} 帮助.{R}"
#: coco/interactive.py:70
#, fuzzy, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr "{T}0) 输入 {green}q{end} 退出.{R}\n"
#: coco/interactive.py:142
msgid "No"
msgstr "无"
#: coco/interactive.py:149
msgid "Name"
msgstr "名称"
#: coco/interactive.py:149
msgid "Assets"
msgstr "资产"
#: coco/interactive.py:155
msgid "Total: {}"
msgstr "总共: {}"
#: coco/interactive.py:159
msgid "There is no matched node, please re-enter"
msgstr "没有匹配分组,请重新输入"
#: coco/interactive.py:170
msgid "ID"
msgstr ""
#: coco/interactive.py:170
msgid "Hostname"
msgstr "主机名"
#: coco/interactive.py:170
msgid "IP"
msgstr ""
#: coco/interactive.py:170
msgid "LoginAs"
msgstr ""
#: coco/interactive.py:184
msgid "Comment"
msgstr "备注"
#: coco/interactive.py:192
msgid "Total: {} Match: {}"
msgstr "总共: {} 匹配: {}"
#: coco/interactive.py:235
msgid "Select a login:: "
msgstr "选择一个登录:"
#: coco/interactive.py:258
msgid ""
"Terminal does not support login Windows, please use web terminal to access"
msgstr "终端不支持登录windows, 请使用web terminal访问"
#: coco/interactive.py:269
msgid "No system user"
msgstr "没有系统用户"
#: coco/session.py:143
msgid "Terminated by administrator"
msgstr "被管理员中断"
......@@ -19,7 +19,7 @@ itsdangerous==0.24
Jinja2==2.10
jmespath==0.9.3
jms-storage==0.0.18
jumpserver-python-sdk==0.0.47
jumpserver-python-sdk==0.0.48
MarkupSafe==1.0
oss2==2.4.0
paramiko==2.4.1
......
libffi-devel sshpass
\ No newline at end of file
libffi-devel sshpass krb5-devel
#!/bin/bash
#
function init_message() {
xgettext -k_ -o pot/coco.pot --from-code=UTF-8 coco/*.py
msginit -l locale/zh_CN/LC_MESSAGES/coco -i pot/coco.pot
msginit -l locale/en/LC_MESSAGES/coco -i pot/coco.pot
}
function make_message() {
xgettext -k_ -o pot/coco.pot --from-code=UTF-8 coco/*.py
msgmerge -U locale/zh_CN/LC_MESSAGES/coco.po pot/coco.pot
msgmerge -U locale/en/LC_MESSAGES/coco.po pot/coco.pot
}
function compile_message() {
msgfmt -o locale/zh_CN/LC_MESSAGES/coco.mo locale/zh_CN/LC_MESSAGES/coco.po
msgfmt -o locale/en/LC_MESSAGES/coco.mo locale/en/LC_MESSAGES/coco.po
}
action=$1
if [ -z "$action" ];then
action="make"
fi
case $action in
m|make)
make_message;;
i|init)
init_message;;
c|compile)
compile_message;;
*)
echo "Usage: $0 [m|make i|init | c|compile]"
exit 1
;;
esac
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