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 @@ ...@@ -2,66 +2,40 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import eventlet
from eventlet.debug import hub_prevent_multiple_readers
eventlet.monkey_patch()
hub_prevent_multiple_readers(False)
import datetime import datetime
import os import os
import time import time
import threading import threading
import socket
import json import json
import signal 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 .sshd import SSHServer
from .httpd import HttpServer from .httpd import HttpServer
from .logger import create_logger from .logger import create_logger
from .tasks import TaskHandler from .tasks import TaskHandler
from .recorder import ReplayRecorder, CommandRecorder from .utils import get_logger, ugettext as _, \
from .utils import get_logger, register_app, register_service 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__)) BASE_DIR = os.path.dirname(os.path.dirname(__file__))
logger = get_logger(__file__) logger = get_logger(__file__)
class Coco: class Coco:
config_class = Config def __init__(self):
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 = []
self.lock = threading.Lock() self.lock = threading.Lock()
self.stop_evt = threading.Event() self.stop_evt = threading.Event()
self._service = None self._service = None
...@@ -70,28 +44,8 @@ class Coco: ...@@ -70,28 +44,8 @@ class Coco:
self.replay_recorder_class = None self.replay_recorder_class = None
self.command_recorder_class = None self.command_recorder_class = None
self._task_handler = None self._task_handler = None
self.config = None self.config = config
self.init_config() init_app(self)
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
@property @property
def sshd(self): def sshd(self):
...@@ -114,32 +68,26 @@ class Coco: ...@@ -114,32 +68,26 @@ class Coco:
def make_logger(self): def make_logger(self):
create_logger(self) create_logger(self)
def load_extra_conf_from_server(self): @staticmethod
configs = self.service.load_config_from_server() def load_extra_conf_from_server():
configs = app_service.load_config_from_server()
logger.debug("Loading config from server: {}".format( logger.debug("Loading config from server: {}".format(
json.dumps(configs) json.dumps(configs)
)) ))
self.config.update(configs) config.update(configs)
@staticmethod
def new_command_recorder():
return CommandRecorder()
@staticmethod
def new_replay_recorder():
return ReplayRecorder()
def bootstrap(self): def bootstrap(self):
self.make_logger() self.make_logger()
self.service.initial() app_service.initial()
self.load_extra_conf_from_server() self.load_extra_conf_from_server()
self.keep_heartbeat() self.keep_heartbeat()
self.monitor_sessions() self.monitor_sessions()
self.monitor_sessions_replay() self.monitor_sessions_replay()
@ignore_error
def heartbeat(self): def heartbeat(self):
_sessions = [s.to_json() for s in self.sessions] _sessions = [s.to_json() for s in Session.sessions.values()]
tasks = self.service.terminal_heartbeat(_sessions) tasks = app_service.terminal_heartbeat(_sessions)
if tasks: if tasks:
self.handle_task(tasks) self.handle_task(tasks)
if tasks is False: if tasks is False:
...@@ -159,19 +107,17 @@ class Coco: ...@@ -159,19 +107,17 @@ class Coco:
def func(): def func():
while not self.stop_evt.is_set(): while not self.stop_evt.is_set():
self.heartbeat() self.heartbeat()
time.sleep(self.config["HEARTBEAT_INTERVAL"]) time.sleep(config["HEARTBEAT_INTERVAL"])
thread = threading.Thread(target=func) thread = threading.Thread(target=func)
thread.start() thread.start()
def monitor_sessions_replay(self): def monitor_sessions_replay(self):
interval = 10 interval = 10
recorder = self.new_replay_recorder() log_dir = os.path.join(config['LOG_DIR'])
log_dir = os.path.join(self.config['LOG_DIR'])
def func(): def func():
while not self.stop_evt.is_set(): 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): for filename in os.listdir(log_dir):
session_id = filename.split('.')[0] session_id = filename.split('.')[0]
full_path = os.path.join(log_dir, filename) full_path = os.path.join(log_dir, filename)
...@@ -179,6 +125,7 @@ class Coco: ...@@ -179,6 +125,7 @@ class Coco:
if len(session_id) != 36: if len(session_id) != 36:
continue continue
recorder = get_replay_recorder()
if session_id not in active_sessions: if session_id not in active_sessions:
recorder.file_path = full_path recorder.file_path = full_path
ok = recorder.upload_replay(session_id, 1) ok = recorder.upload_replay(session_id, 1)
...@@ -190,21 +137,33 @@ class Coco: ...@@ -190,21 +137,33 @@ class Coco:
thread.start() thread.start()
def monitor_sessions(self): 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(): def func():
while not self.stop_evt.is_set(): while not self.stop_evt.is_set():
for s in self.sessions: sessions_copy = [s for s in Session.sessions.values()]
if not s.stop_evt.is_set(): for s in sessions_copy:
continue # Session 没有正常关闭,
if s.date_end is None: if s.closed_unexpected:
self.remove_session(s) Session.remove_session(s.id)
continue continue
delta = datetime.datetime.now() - s.date_end # Session已正常关闭
if delta > datetime.timedelta(seconds=interval*5): if s.closed:
self.remove_session(s) Session.remove_session(s)
else:
check_session_idle_too_long(s)
time.sleep(interval) time.sleep(interval)
thread = threading.Thread(target=func) thread = threading.Thread(target=func)
thread.start() thread.start()
...@@ -215,10 +174,10 @@ class Coco: ...@@ -215,10 +174,10 @@ class Coco:
print('Quit the server with CONTROL-C.') print('Quit the server with CONTROL-C.')
try: try:
if self.config["SSHD_PORT"] != 0: if config["SSHD_PORT"] != 0:
self.run_sshd() self.run_sshd()
if self.config['HTTPD_PORT'] != 0: if config['HTTPD_PORT'] != 0:
self.run_httpd() self.run_httpd()
signal.signal(signal.SIGTERM, lambda x, y: self.shutdown()) signal.signal(signal.SIGTERM, lambda x, y: self.shutdown())
...@@ -228,7 +187,6 @@ class Coco: ...@@ -228,7 +187,6 @@ class Coco:
break break
time.sleep(3) time.sleep(3)
except KeyboardInterrupt: except KeyboardInterrupt:
self.stop_evt.set()
self.shutdown() self.shutdown()
def run_sshd(self): def run_sshd(self):
...@@ -242,46 +200,11 @@ class Coco: ...@@ -242,46 +200,11 @@ class Coco:
thread.start() thread.start()
def shutdown(self): def shutdown(self):
for client in self.clients: logger.info("Grace shutdown the server")
self.remove_client(client) for connection in Connection.connections.values():
connection.close()
time.sleep(1) time.sleep(1)
self.heartbeat() self.heartbeat()
self.stop_evt.set() self.stop_evt.set()
self.sshd.shutdown() self.sshd.shutdown()
self.httpd.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 ...@@ -17,10 +17,17 @@ import os
import types import types
import errno import errno
import json import json
import socket
from werkzeug.utils import import_string 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): class ConfigAttribute(object):
"""Makes an attribute forward to the config""" """Makes an attribute forward to the config"""
...@@ -233,7 +240,7 @@ class Config(dict): ...@@ -233,7 +240,7 @@ class Config(dict):
The resulting dictionary `image_store_config` would look like:: The resulting dictionary `image_store_config` would look like::
{ {
'type': 'fs', 'types': 'fs',
'path': '/var/app/images', 'path': '/var/app/images',
'base_url': 'http://img.website.com' 'base_url': 'http://img.website.com'
} }
...@@ -266,4 +273,41 @@ class Config(dict): ...@@ -266,4 +273,41 @@ class Config(dict):
return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) 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 ...@@ -11,10 +11,11 @@ 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, net_input from .config import config
from .utils import get_logger, get_private_key_fingerprint
logger = get_logger(__file__) logger = get_logger(__file__)
TIMEOUT = 10
BUF_SIZE = 1024 BUF_SIZE = 1024
MANUAL_LOGIN = 'manual' MANUAL_LOGIN = 'manual'
AUTO_LOGIN = 'auto' AUTO_LOGIN = 'auto'
...@@ -46,9 +47,12 @@ class SSHConnection: ...@@ -46,9 +47,12 @@ class SSHConnection:
ssh.connect( ssh.connect(
asset.ip, port=asset.port, username=system_user.username, asset.ip, port=asset.port, username=system_user.username,
password=system_user.password, pkey=system_user.private_key, 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 look_for_keys=False, sock=sock
) )
transport = ssh.get_transport()
transport.set_keepalive(300)
except (paramiko.AuthenticationException, except (paramiko.AuthenticationException,
paramiko.BadAuthenticationType, paramiko.BadAuthenticationType,
SSHException) as e: SSHException) as e:
...@@ -111,7 +115,7 @@ class SSHConnection: ...@@ -111,7 +115,7 @@ class SSHConnection:
username=gateway.username, username=gateway.username,
password=gateway.password, password=gateway.password,
pkey=gateway.private_key_obj, pkey=gateway.private_key_obj,
timeout=TIMEOUT) timeout=config['SSH_TIMEOUT'])
except(paramiko.AuthenticationException, except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType, paramiko.BadAuthenticationType,
SSHException): SSHException):
...@@ -142,7 +146,7 @@ class SSHConnection: ...@@ -142,7 +146,7 @@ class SSHConnection:
proxy_command.insert(0, "sshpass -p {}".format(gateway.password)) proxy_command.insert(0, "sshpass -p {}".format(gateway.password))
if gateway.private_key: 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.append("-i {}".format(gateway.private_key_file))
proxy_command = ' '.join(proxy_command) proxy_command = ' '.join(proxy_command)
......
...@@ -4,8 +4,11 @@ ...@@ -4,8 +4,11 @@
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from functools import partial from functools import partial
from .config import config
from jms.service import AppService
stack = {} stack = {}
__db_sessions = []
def _find(name): def _find(name):
...@@ -15,8 +18,6 @@ def _find(name): ...@@ -15,8 +18,6 @@ def _find(name):
raise ValueError("Not found in stack: {}".format(name)) raise ValueError("Not found in stack: {}".format(name))
current_app = LocalProxy(partial(_find, 'app')) app_service = AppService(config)
app_service = LocalProxy(partial(_find, 'service')) current_app = LocalProxy(partial(_find, 'current_app'))
# app_service = LocalProxy(partial(_find, 'app_service'))
# current_app = []
# current_service = []
This diff is collapsed.
...@@ -7,11 +7,12 @@ import threading ...@@ -7,11 +7,12 @@ import threading
import os import os
from . import char from . import char
from .config import config
from .utils import wrap_with_line_feed as wr, wrap_with_title as title, \ 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, \ wrap_with_warning as warning, is_obj_attr_has, is_obj_attr_eq, \
sort_assets, ugettext as _, get_logger, net_input, format_with_zh, \ sort_assets, ugettext as _, get_logger, net_input, format_with_zh, \
item_max_length, size_of_str_with_zh item_max_length, size_of_str_with_zh, switch_lang
from .ctx import current_app, app_service from .ctx import app_service
from .proxy import ProxyServer from .proxy import ProxyServer
logger = get_logger(__file__) logger = get_logger(__file__)
...@@ -22,8 +23,8 @@ class InteractiveServer: ...@@ -22,8 +23,8 @@ class InteractiveServer:
def __init__(self, client): def __init__(self, client):
self.client = client self.client = client
self.request = client.request
self.assets = None self.assets = None
self.closed = False
self._search_result = None self._search_result = None
self.nodes = None self.nodes = None
self.get_user_assets_async() self.get_user_assets_async()
...@@ -44,28 +45,39 @@ class InteractiveServer: ...@@ -44,28 +45,39 @@ class InteractiveServer:
value = self.filter_system_users(value) value = self.filter_system_users(value)
self._search_result = value self._search_result = value
def display_banner(self): def display_logo(self):
self.client.send(char.CLEAR_CHAR) logo_path = os.path.join(config['ROOT_PATH'], "logo.txt")
logo_path = os.path.join(current_app.root_path, "logo.txt") if not os.path.isfile(logo_path):
if os.path.isfile(logo_path): return
with open(logo_path, 'rb') as f: with open(logo_path, 'rb') as f:
for i in f: for i in f:
if i.decode('utf-8').startswith('#'): if i.decode('utf-8').startswith('#'):
continue continue
self.client.send(i.decode('utf-8').replace('\n', '\r\n')) self.client.send(i.decode('utf-8').replace('\n', '\r\n'))
banner = _("""\n {title} {user}, 欢迎使用Jumpserver开源跳板机系统 {end}\r\n\r def display_banner(self):
1) 输入 {green}ID{end} 直接登录 或 输入{green}部分 IP,主机名,备注{end} 进行搜索登录(如果唯一).\r self.client.send(char.CLEAR_CHAR)
2) 输入 {green}/{end} + {green}IP, 主机名{end} or {green}备注 {end}搜索. 如: /ip\r self.display_logo()
3) 输入 {green}p{end} 显示您有权限的主机.\r header = _("\n{T}{T}{title} {user}, Welcome to use Jumpserver open source fortress system {end}{R}{R}")
4) 输入 {green}g{end} 显示您有权限的节点\r menus = [
5) 输入 {green}g{end} + {green}组ID{end} 显示节点下主机. 如: g1\r _("{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, Comment{end} to search login(if unique).{R}"),
6) 输入 {green}h{end} 帮助.\r _("{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} search, such as: /ip.{R}"),
0) 输入 {green}q{end} 退出.\r\n""").format( _("{T}3) Enter {green}p{end} to display the host you have permission.{R}"),
title="\033[1;32m", green="\033[32m", _("{T}4) Enter {green}g{end} to display the node that you have permission.{R}"),
end="\033[0m", user=self.client.user _("{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}"),
self.client.send(banner) _("{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): def dispatch(self, opt):
if opt is None: if opt is None:
...@@ -80,6 +92,9 @@ class InteractiveServer: ...@@ -80,6 +92,9 @@ class InteractiveServer:
self.display_node_assets(int(opt.lstrip("g"))) self.display_node_assets(int(opt.lstrip("g")))
elif opt in ['q', 'Q', 'exit', 'quit']: elif opt in ['q', 'Q', 'exit', 'quit']:
return self._sentinel return self._sentinel
elif opt in ['s', 'S']:
switch_lang()
self.display_banner()
elif opt in ['h', 'H']: elif opt in ['h', 'H']:
self.display_banner() self.display_banner()
else: else:
...@@ -124,33 +139,27 @@ class InteractiveServer: ...@@ -124,33 +139,27 @@ class InteractiveServer:
self.get_user_nodes() self.get_user_nodes()
if len(self.nodes) == 0: if len(self.nodes) == 0:
self.client.send(warning(_(""))) self.client.send(warning(_("No")))
return return
id_length = max(len(str(len(self.nodes))), 5) id_length = max(len(str(len(self.nodes))), 5)
name_length = item_max_length(self.nodes, 15, key=lambda x: x.name) name_length = item_max_length(self.nodes, 15, key=lambda x: x.name)
amount_length = item_max_length(self.nodes, 10, amount_length = item_max_length(self.nodes, 10, key=lambda x: x.assets_amount)
key=lambda x: x.assets_amount)
size_list = [id_length, name_length, amount_length] size_list = [id_length, name_length, amount_length]
fake_data = ['ID', _("Name"), _("Assets")] 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))) self.client.send(wr(title(format_with_zh(size_list, *fake_data))))
for index, group in enumerate(self.nodes, 1): for index, node in enumerate(self.nodes, 1):
data = [index, group.name, group.assets_amount, group.comment] data = [index, node.name, node.assets_amount]
self.client.send(wr(format_with_zh(size_list, *data))) 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): def display_node_assets(self, _id):
if self.nodes is None:
self.get_user_nodes()
if _id > len(self.nodes) or _id <= 0: 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() self.display_nodes()
return return
...@@ -158,7 +167,7 @@ class InteractiveServer: ...@@ -158,7 +167,7 @@ class InteractiveServer:
self.display_search_result() self.display_search_result()
def display_search_result(self): 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) self.search_result = sort_assets(self.search_result, sort_by)
fake_data = [_("ID"), _("Hostname"), _("IP"), _("LoginAs")] fake_data = [_("ID"), _("Hostname"), _("IP"), _("LoginAs")]
id_length = max(len(str(len(self.search_result))), 4) id_length = max(len(str(len(self.search_result))), 4)
...@@ -169,7 +178,7 @@ class InteractiveServer: ...@@ -169,7 +178,7 @@ class InteractiveServer:
size_list = [id_length, hostname_length, 16, sysuser_length] size_list = [id_length, hostname_length, 16, sysuser_length]
header_without_comment = format_with_zh(size_list, *fake_data) header_without_comment = format_with_zh(size_list, *fake_data)
comment_length = max( comment_length = max(
self.request.meta["width"] - self.client.request.meta["width"] -
size_of_str_with_zh(header_without_comment) - 1, size_of_str_with_zh(header_without_comment) - 1,
2 2
) )
...@@ -182,7 +191,7 @@ class InteractiveServer: ...@@ -182,7 +191,7 @@ class InteractiveServer:
asset.system_users_name_list, asset.comment asset.system_users_name_list, asset.comment
] ]
self.client.send(wr(format_with_zh(size_list, *data))) 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) len(self.assets), len(self.search_result)), before=1)
) )
...@@ -225,7 +234,7 @@ class InteractiveServer: ...@@ -225,7 +234,7 @@ class InteractiveServer:
return None return None
while True: while True:
self.client.send(wr(_("选择一个登录: "), after=1)) self.client.send(wr(_("Select a login:: "), after=1))
self.display_system_users(system_users) self.display_system_users(system_users)
opt = net_input(self.client, prompt="ID> ") opt = net_input(self.client, prompt="ID> ")
if opt.isdigit() and len(system_users) > int(opt): if opt.isdigit() and len(system_users) > int(opt):
...@@ -248,7 +257,8 @@ class InteractiveServer: ...@@ -248,7 +257,8 @@ class InteractiveServer:
self.search_result = None self.search_result = None
if asset.platform == "Windows": if asset.platform == "Windows":
self.client.send(warning( self.client.send(warning(
_("终端不支持登录windows, 请使用web terminal访问")) _("Terminal does not support login Windows, "
"please use web terminal to access"))
) )
return return
self.proxy(asset) self.proxy(asset)
...@@ -258,20 +268,21 @@ class InteractiveServer: ...@@ -258,20 +268,21 @@ class InteractiveServer:
def proxy(self, asset): def proxy(self, asset):
system_user = self.choose_system_user(asset.system_users_granted) system_user = self.choose_system_user(asset.system_users_granted)
if system_user is None: if system_user is None:
self.client.send(_("没有系统用户")) self.client.send(_("No system user"))
return return
forwarder = ProxyServer(self.client, login_from='ST') forwarder = ProxyServer(self.client, asset, system_user)
forwarder.proxy(asset, system_user) forwarder.proxy()
def interact(self): def interact(self):
self.display_banner() self.display_banner()
while True: while not self.closed:
try: try:
opt = net_input(self.client, prompt='Opt> ', before=1) opt = net_input(self.client, prompt='Opt> ', before=1)
rv = self.dispatch(opt) rv = self.dispatch(opt)
if rv is self._sentinel: if rv is self._sentinel:
break break
except socket.error: except socket.error as e:
logger.debug("Socket error: {}".format(e))
break break
self.close() self.close()
...@@ -281,7 +292,9 @@ class InteractiveServer: ...@@ -281,7 +292,9 @@ class InteractiveServer:
thread.start() thread.start()
def close(self): 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): # def __del__(self):
# print("GC: Interactive class been gc") # print("GC: Interactive class been gc")
...@@ -4,9 +4,11 @@ ...@@ -4,9 +4,11 @@
import paramiko import paramiko
import threading import threading
from collections import Iterable
from .utils import get_logger 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__) logger = get_logger(__file__)
...@@ -19,12 +21,13 @@ class SSHInterface(paramiko.ServerInterface): ...@@ -19,12 +21,13 @@ class SSHInterface(paramiko.ServerInterface):
https://github.com/paramiko/paramiko/blob/master/demos/demo_server.py https://github.com/paramiko/paramiko/blob/master/demos/demo_server.py
""" """
def __init__(self, request): def __init__(self, connection):
self.request = request self.connection = connection
self.event = threading.Event() self.event = threading.Event()
self.auth_valid = False self.auth_valid = False
self.otp_auth = False self.otp_auth = False
self.info = None self.info = None
self.user = None
def check_auth_interactive(self, username, submethods): def check_auth_interactive(self, username, submethods):
logger.info("Check auth interactive: %s %s" % (username, submethods)) logger.info("Check auth interactive: %s %s" % (username, submethods))
...@@ -58,9 +61,9 @@ class SSHInterface(paramiko.ServerInterface): ...@@ -58,9 +61,9 @@ class SSHInterface(paramiko.ServerInterface):
supported = [] supported = []
if self.otp_auth: if self.otp_auth:
return 'keyboard-interactive' return 'keyboard-interactive'
if current_app.config["PASSWORD_AUTH"]: if config["PASSWORD_AUTH"]:
supported.append("password") supported.append("password")
if current_app.config["PUBLIC_KEY_AUTH"]: if config["PUBLIC_KEY_AUTH"]:
supported.append("publickey") supported.append("publickey")
return ",".join(supported) return ",".join(supported)
...@@ -69,6 +72,7 @@ class SSHInterface(paramiko.ServerInterface): ...@@ -69,6 +72,7 @@ class SSHInterface(paramiko.ServerInterface):
def check_auth_password(self, username, password): def check_auth_password(self, username, password):
user = self.validate_auth(username, password=password) user = self.validate_auth(username, password=password)
if not user: if not user:
logger.warning("Password and public key auth <%s> failed, reject it" % username) logger.warning("Password and public key auth <%s> failed, reject it" % username)
return paramiko.AUTH_FAILED return paramiko.AUTH_FAILED
...@@ -81,6 +85,7 @@ class SSHInterface(paramiko.ServerInterface): ...@@ -81,6 +85,7 @@ class SSHInterface(paramiko.ServerInterface):
def check_auth_publickey(self, username, key): def check_auth_publickey(self, username, key):
key = key.get_base64() key = key.get_base64()
user = self.validate_auth(username, public_key=key) user = self.validate_auth(username, public_key=key)
if not user: if not user:
logger.debug("Public key auth <%s> failed, try to password" % username) logger.debug("Public key auth <%s> failed, try to password" % username)
return paramiko.AUTH_FAILED return paramiko.AUTH_FAILED
...@@ -90,113 +95,150 @@ class SSHInterface(paramiko.ServerInterface): ...@@ -90,113 +95,150 @@ class SSHInterface(paramiko.ServerInterface):
return paramiko.AUTH_PARTIALLY_SUCCESSFUL return paramiko.AUTH_PARTIALLY_SUCCESSFUL
return paramiko.AUTH_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=""): 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( info = app_service.authenticate(
username, password=password, public_key=public_key, username, password=password, public_key=public_key,
remote_addr=self.request.remote_ip remote_addr=self.connection.addr[0]
) )
user = info.get('user', None) user = info.get('user', None)
if user: if user:
self.request.user = user self.connection.user = user
self.info = info self.info = info
seed = info.get('seed', None) seed = info.get('seed', None)
token = info.get('token', None) token = info.get('token', None)
if seed and not token: if seed and not token:
self.connection.otp_auth = True
self.otp_auth = True self.otp_auth = True
return user 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" % logger.debug("Check channel direct tcpip request: %d %s %s" %
(chanid, origin, destination)) (chan_id, origin, destination))
self.request.type.append('direct-tcpip') client = self.connection.new_client(chan_id)
self.request.meta.update({ client.request.kind = 'direct-tcpip'
'chanid': chanid, 'origin': origin, client.request.type = 'direct-tcpip'
'destination': destination, client.request.meta.update({
'origin': origin, 'destination': destination
}) })
self.event.set() self.event.set()
return 0 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): def check_channel_env_request(self, channel, name, value):
logger.debug("Check channel env request: %s, %s, %s" % logger.debug("Check channel env request: %s, %s, %s" %
(channel, name, value)) (channel.get_id(), name, value))
self.request.type.append('env') client = self.connection.get_client(channel)
client.request.meta['env'][name] = value
return False return False
def check_channel_exec_request(self, channel, command): def check_channel_exec_request(self, channel, command):
logger.debug("Check channel exec request: `%s`" % command) logger.debug("Check channel exec request: `%s`" % command)
self.request.type.append('exec') client = self.connection.get_client(channel)
self.request.meta.update({'channel': channel.get_id(), 'command': command}) client.request.type = 'exec'
client.request.meta.update({
'command': command
})
self.event.set() self.event.set()
return False return False
def check_channel_forward_agent_request(self, channel): def check_channel_forward_agent_request(self, channel):
logger.debug("Check channel forward agent request: %s" % channel) logger.debug("Check channel forward agent request: %s" % channel)
self.request.type.append("forward-agent") client = self.connection.get_client(channel)
self.request.meta.update({'channel': channel.get_id()}) client.request.meta['forward-agent'] = True
self.event.set() self.event.set()
return False return True
def check_channel_pty_request( def check_channel_pty_request(
self, channel, term, width, height, self, channel, term, width, height,
pixelwidth, pixelheight, modes): pixelwidth, pixelheight, modes):
logger.info("Check channel pty request: %s %s %s %s %s" % logger.info("Check channel pty request: %s %s %s %s %s" %
(term, width, height, pixelwidth, pixelheight)) (term, width, height, pixelwidth, pixelheight))
self.request.type.append('pty') client = self.connection.get_client(channel)
self.request.meta.update({ client.request.type = 'pty'
'channel': channel, 'term': term, 'width': width, client.request.meta.update({
'term': term, 'width': width,
'height': height, 'pixelwidth': pixelwidth, 'height': height, 'pixelwidth': pixelwidth,
'pixelheight': pixelheight, 'pixelheight': pixelheight,
}) })
self.event.set() self.event.set()
return True 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): def check_channel_shell_request(self, channel):
logger.info("Check channel shell request: %s" % channel.get_id()) 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 return True
def check_channel_subsystem_request(self, channel, name): def check_channel_subsystem_request(self, channel, name):
logger.info("Check channel subsystem request: %s %s" % (channel, name)) logger.info("Check channel subsystem request: %s" % name)
self.request.type.append('subsystem') client = self.connection.get_client(channel)
self.request.meta.update({'channel': channel.get_id(), 'name': name}) client.request.type = 'subsystem'
client.request.meta['subsystem'] = name
self.event.set() self.event.set()
return super().check_channel_subsystem_request(channel, name) return super().check_channel_subsystem_request(channel, name)
def check_channel_window_change_request(self, channel, width, height, def check_channel_window_change_request(self, channel, width, height,
pixelwidth, pixelheight): pixelwidth, pixelheight):
self.request.meta.update({ client = self.connection.get_client(channel)
client.request.meta.update({
'width': width, 'width': width,
'height': height, 'height': height,
'pixelwidth': pixelwidth, 'pixelwidth': pixelwidth,
'pixelheight': pixelheight, 'pixelheight': pixelheight,
}) })
self.request.change_size_event.set() client.change_size_evt.set()
return True return True
def check_channel_x11_request(self, channel, single_connection, def check_channel_x11_request(self, channel, single_connection,
auth_protocol, auth_cookie, screen_number): auth_protocol, auth_cookie, screen_number):
logger.info("Check channel x11 request %s %s %s %s %s" % logger.info("Check channel x11 request %s %s %s %s" %
(channel, single_connection, auth_protocol, (single_connection, auth_protocol,
auth_cookie, screen_number)) auth_cookie, screen_number))
self.request.type.append('x11') client = self.connection.get_client(channel)
self.request.meta.update({ # client.request_x11_event.set()
'channel': channel.get_id(), 'single_connection': single_connection, client.request.meta.update({
'auth_protocol': auth_protocol, 'auth_cookie': auth_cookie, 'single_connection': single_connection,
'auth_protocol': auth_protocol,
'auth_cookie': auth_cookie,
'screen_number': screen_number, '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 return False
def get_banner(self): def get_banner(self):
......
...@@ -40,9 +40,12 @@ def create_logger(app): ...@@ -40,9 +40,12 @@ def create_logger(app):
}, },
'file': { 'file': {
'level': 'DEBUG', 'level': 'DEBUG',
'class': 'logging.FileHandler', 'class': 'logging.handlers.TimedRotatingFileHandler',
'formatter': 'main', 'formatter': 'main',
'filename': log_path, 'filename': log_path,
'when': "D",
'interval': 1,
"backupCount": 7
}, },
}, },
loggers={ loggers={
......
This diff is collapsed.
...@@ -5,159 +5,136 @@ ...@@ -5,159 +5,136 @@
import threading import threading
import time import time
from paramiko.ssh_exception import SSHException
from .session import Session from .session import Session
from .models import Server, TelnetServer from .models import Server, TelnetServer
from .connection import SSHConnection, TelnetConnection 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, \ 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__) logger = get_logger(__file__)
TIMEOUT = 10
BUF_SIZE = 4096 BUF_SIZE = 4096
MANUAL_LOGIN = 'manual' MANUAL_LOGIN = 'manual'
AUTO_LOGIN = 'auto' AUTO_LOGIN = 'auto'
class ProxyServer: class ProxyServer:
def __init__(self, client, login_from): def __init__(self, client, asset, system_user):
self.client = client self.client = client
self.asset = asset
self.system_user = system_user
self.server = None self.server = None
self.login_from = login_from
self.connecting = True 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 :return: system user have full info
""" """
password, private_key = \ password, private_key = \
app_service.get_system_user_auth_info(system_user) app_service.get_system_user_auth_info(self.system_user)
if system_user.login_mode == MANUAL_LOGIN or (not password and not private_key): if self.system_user.login_mode == MANUAL_LOGIN \
prompt = "{}'s password: ".format(system_user.username) or (not password and not private_key):
prompt = "{}'s password: ".format(self.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 self.system_user.password = password
system_user.private_key = private_key self.system_user.private_key = private_key
def proxy(self, asset, system_user): def check_protocol(self):
if asset.protocol != system_user.protocol: if self.asset.protocol != self.system_user.protocol:
msg = 'System user <{}> and asset <{}> protocol are inconsistent.'.format( 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))) 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 return
self.manual_set_system_user_username_if_need()
if system_user.login_mode == MANUAL_LOGIN and not system_user.username: self.get_system_user_auth_or_manual_set()
system_user_name = net_input(self.client, prompt='username: ', before=1) self.server = self.get_server_conn()
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)
if self.server is None: if self.server is None:
return return
command_recorder = current_app.new_command_recorder() session = Session.new_session(self.client, self.server)
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.bridge() session.bridge()
self.stop_event.set() Session.remove_session(session.id)
self.end_watch_win_size_change() self.server.close()
current_app.remove_session(session)
def validate_permission(self, asset, system_user): def validate_permission(self):
""" """
验证用户是否有连接改资产的权限 验证用户是否有连接改资产的权限
:return: True or False :return: True or False
""" """
return app_service.validate_user_asset_permission( 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): def get_server_conn(self):
logger.info("Connect to {}".format(asset.hostname)) logger.info("Connect to {}".format(self.asset.hostname))
if not self.validate_permission(asset, system_user): self.send_connecting_message()
self.client.send(warning('No permission')) if not self.validate_permission():
return None self.client.send(warning(_('No permission')))
if system_user.protocol == asset.protocol == 'telnet': server = None
server = self.get_telnet_server_conn(asset, system_user) elif self.system_user.protocol == self.asset.protocol == 'telnet':
elif system_user.protocol == asset.protocol == 'ssh': server = self.get_telnet_server_conn()
server = self.get_ssh_server_conn(asset, system_user) elif self.system_user.protocol == self.asset.protocol == 'ssh':
server = self.get_ssh_server_conn()
else: else:
server = None server = None
self.connecting = False
self.client.send(b'\r\n')
return server return server
# Todo: Support telnet def get_telnet_server_conn(self):
def get_telnet_server_conn(self, asset, system_user): telnet = TelnetConnection(self.asset, self.system_user, self.client)
telnet = TelnetConnection(asset, system_user, self.client)
sock, msg = telnet.get_socket() sock, msg = telnet.get_socket()
if not sock: if not sock:
self.client.send(warning(wr(msg, before=1, after=0))) self.client.send(warning(wr(msg, before=1, after=0)))
server = None server = None
else: else:
server = TelnetServer(sock, asset, system_user) server = TelnetServer(sock, self.asset, self.system_user)
# self.client.send(b'\r\n')
self.connecting = False
return server return server
def get_ssh_server_conn(self, asset, system_user): def get_ssh_server_conn(self):
request = self.client.request request = self.client.request
term = request.meta.get('term', 'xterm') term = request.meta.get('term', 'xterm')
width = request.meta.get('width', 80) width = request.meta.get('width', 80)
height = request.meta.get('height', 24) height = request.meta.get('height', 24)
ssh = SSHConnection() ssh = SSHConnection()
chan, sock, msg = ssh.get_channel( 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: if not chan:
self.client.send(warning(wr(msg, before=1, after=0))) self.client.send(warning(wr(msg, before=1, after=0)))
server = None server = None
else: else:
server = Server(chan, sock, asset, system_user) server = Server(chan, sock, self.asset, self.system_user)
self.connecting = False
self.client.send(b'\r\n')
return server return server
def watch_win_size_change(self): def send_connecting_message(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 func(): def func():
delay = 0.0 delay = 0.0
self.client.send('Connecting to {}@{} {:.1f}'.format( self.client.send(_('Connecting to {}@{} {:.1f}').format(
system_user, asset, delay) self.system_user, self.asset, delay)
) )
while self.connecting and delay < TIMEOUT: while self.connecting and delay < config['SSH_TIMEOUT']:
if 0 <= delay < 10:
self.client.send('\x08\x08\x08{:.1f}'.format(delay).encode()) 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) time.sleep(0.1)
delay += 0.1 delay += 0.1
thread = threading.Thread(target=func) thread = threading.Thread(target=func)
......
...@@ -12,9 +12,10 @@ from copy import deepcopy ...@@ -12,9 +12,10 @@ from copy import deepcopy
import jms_storage import jms_storage
from .config import config
from .utils import get_logger, Singleton from .utils import get_logger, Singleton
from .alignment import MemoryQueue from .struct import MemoryQueue
from .ctx import current_app, app_service from .ctx import app_service
logger = get_logger(__file__) logger = get_logger(__file__)
BUF_SIZE = 1024 BUF_SIZE = 1024
...@@ -48,7 +49,7 @@ class ReplayRecorder(metaclass=abc.ABCMeta): ...@@ -48,7 +49,7 @@ class ReplayRecorder(metaclass=abc.ABCMeta):
def session_start(self, session_id): def session_start(self, session_id):
self.time_start = time.time() self.time_start = time.time()
filename = session_id + '.replay.gz' 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 = gzip.open(self.file_path, 'at')
self.file.write('{') self.file.write('{')
...@@ -58,9 +59,9 @@ class ReplayRecorder(metaclass=abc.ABCMeta): ...@@ -58,9 +59,9 @@ class ReplayRecorder(metaclass=abc.ABCMeta):
self.upload_replay(session_id) self.upload_replay(session_id)
def get_storage(self): def get_storage(self):
config = deepcopy(current_app.config["REPLAY_STORAGE"]) conf = deepcopy(config["REPLAY_STORAGE"])
config["SERVICE"] = app_service conf["SERVICE"] = app_service
self.storage = jms_storage.get_object_storage(config) self.storage = jms_storage.get_object_storage(conf)
def upload_replay(self, session_id, times=3): def upload_replay(self, session_id, times=3):
if times < 1: if times < 1:
...@@ -130,9 +131,9 @@ class CommandRecorder(metaclass=Singleton): ...@@ -130,9 +131,9 @@ class CommandRecorder(metaclass=Singleton):
self.queue.put(data) self.queue.put(data)
def get_storage(self): def get_storage(self):
config = deepcopy(current_app.config["COMMAND_STORAGE"]) conf = deepcopy(config["COMMAND_STORAGE"])
config['SERVICE'] = app_service conf['SERVICE'] = app_service
self.storage = jms_storage.get_log_storage(config) self.storage = jms_storage.get_log_storage(conf)
def push_to_server_async(self): def push_to_server_async(self):
def func(): def func():
...@@ -153,9 +154,19 @@ class CommandRecorder(metaclass=Singleton): ...@@ -153,9 +154,19 @@ class CommandRecorder(metaclass=Singleton):
thread.start() thread.start()
def session_start(self, session_id): def session_start(self, session_id):
print("Session start: {}".format(session_id))
pass pass
def session_end(self, session_id): def session_end(self, session_id):
print("Session end: {}".format(session_id))
pass 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 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import threading
import uuid import uuid
import datetime import datetime
import selectors import selectors
import time 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 BUF_SIZE = 1024
logger = get_logger(__file__) logger = get_logger(__file__)
class Session: 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.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
self.date_created = datetime.datetime.utcnow() self.date_start = datetime.datetime.utcnow()
self.date_end = None self.date_end = None
self.stop_evt = threading.Event() self.is_finished = False
self.closed = False
self.sel = selectors.DefaultSelector() self.sel = selectors.DefaultSelector()
self._command_recorder = command_recorder self._command_recorder = None
self._replay_recorder = replay_recorder self._replay_recorder = None
self.stop_evt = SelectEvent()
self.server.set_session(self) self.server.set_session(self)
self.date_last_active = datetime.datetime.utcnow() 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): def add_watcher(self, watcher, silent=False):
""" """
Add a watcher, and will be transport server side msg to it. Add a watcher, and will be transport server side msg to it.
...@@ -64,6 +93,10 @@ class Session: ...@@ -64,6 +93,10 @@ class Session:
self.sel.register(sharer, selectors.EVENT_READ) self.sel.register(sharer, selectors.EVENT_READ)
self._sharers.append(sharer) 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): def remove_sharer(self, sharer):
logger.info("Session %s remove sharer %s" % (self.id, sharer)) logger.info("Session %s remove sharer %s" % (self.id, sharer))
sharer.send("Leave session {} at {}" sharer.send("Leave session {} at {}"
...@@ -79,8 +112,6 @@ class Session: ...@@ -79,8 +112,6 @@ class Session:
self._replay_recorder = recorder self._replay_recorder = recorder
def put_command(self, _input, _output): def put_command(self, _input, _output):
if not _input:
return
self._command_recorder.record({ self._command_recorder.record({
"session": self.id, "session": self.id,
"org_id": self.server.asset.org_id, "org_id": self.server.asset.org_id,
...@@ -107,13 +138,14 @@ class Session: ...@@ -107,13 +138,14 @@ class Session:
self._replay_recorder.session_end(self.id) self._replay_recorder.session_end(self.id)
self._command_recorder.session_end(self.id) self._command_recorder.session_end(self.id)
def terminate(self): def terminate(self, msg=None):
msg = b"Terminate by administrator\r\n" if not msg:
msg = _("Terminated by administrator")
try: try:
self.client.send(msg) self.client.send(wr(warn(msg), before=1))
except OSError: except OSError:
pass pass
self.close() self.stop_evt.set()
def bridge(self): def bridge(self):
""" """
...@@ -124,16 +156,17 @@ class Session: ...@@ -124,16 +156,17 @@ class Session:
self.pre_bridge() self.pre_bridge()
self.sel.register(self.client, selectors.EVENT_READ) self.sel.register(self.client, selectors.EVENT_READ)
self.sel.register(self.server, selectors.EVENT_READ) self.sel.register(self.server, selectors.EVENT_READ)
while not self.stop_evt.is_set(): self.sel.register(self.stop_evt, selectors.EVENT_READ)
events = self.sel.select() 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]: for sock in [key.fileobj for key, _ in events]:
data = sock.recv(BUF_SIZE) data = sock.recv(BUF_SIZE)
# self.put_replay(data)
if sock == self.server: if sock == self.server:
if len(data) == 0: if len(data) == 0:
msg = "Server close the connection" msg = "Server close the connection"
logger.info(msg) logger.info(msg)
self.close() self.is_finished = True
break break
self.date_last_active = datetime.datetime.utcnow() self.date_last_active = datetime.datetime.utcnow()
...@@ -145,30 +178,32 @@ class Session: ...@@ -145,30 +178,32 @@ class Session:
logger.info(msg) logger.info(msg)
for watcher in self._watchers + self._sharers: for watcher in self._watchers + self._sharers:
watcher.send(msg.encode("utf-8")) watcher.send(msg.encode("utf-8"))
self.close() self.is_finished = True
break break
self.server.send(data) self.server.send(data)
elif sock in self._sharers: elif sock == self.stop_evt:
if len(data) == 0: self.is_finished = True
logger.info("Sharer {} leave the session {}".format(sock, self.id)) break
self.remove_sharer(sock) elif sock == self.client.change_size_evt:
self.server.send(data) self.resize_win_size()
elif sock in self._watchers:
if len(data) == 0:
self._watchers.remove(sock)
logger.info("Watcher {} leave the session {}".format(sock, self.id))
logger.info("Session stop event set: {}".format(self.id)) 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)) logger.debug("Resize server chan size {}*{}".format(width, height))
self.server.resize_pty(width=width, height=height) self.server.resize_pty(width=width, height=height)
@ignore_error
def close(self): def close(self):
if self.closed:
logger.info("Session has been closed: {} ".format(self.id))
return
logger.info("Close the session: {} ".format(self.id)) logger.info("Close the session: {} ".format(self.id))
self.stop_evt.set() self.is_finished = True
self.closed = True
self.post_bridge() self.post_bridge()
self.date_end = datetime.datetime.utcnow() self.date_end = datetime.datetime.utcnow()
self.server.close()
def to_json(self): def to_json(self):
return { return {
...@@ -177,11 +212,10 @@ class Session: ...@@ -177,11 +212,10 @@ class Session:
"asset": self.server.asset.hostname, "asset": self.server.asset.hostname,
"org_id": self.server.asset.org_id, "org_id": self.server.asset.org_id,
"system_user": self.server.system_user.username, "system_user": self.server.system_user.username,
"login_from": self.login_from, "login_from": self.client.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": self.is_finished,
"date_last_active": self.date_last_active.strftime("%Y-%m-%d %H:%M:%S") + " +0000", "date_start": self.date_start.strftime("%Y-%m-%d %H:%M:%S") + " +0000",
"date_start": self.date_created.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 "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 ...@@ -2,9 +2,9 @@ import os
import tempfile import tempfile
import paramiko import paramiko
import time import time
from .ctx import app_service
from datetime import datetime from datetime import datetime
from .ctx import app_service
from .connection import SSHConnection from .connection import SSHConnection
...@@ -53,7 +53,7 @@ class SFTPServer(paramiko.SFTPServerInterface): ...@@ -53,7 +53,7 @@ class SFTPServer(paramiko.SFTPServerInterface):
def get_perm_hosts(self): def get_perm_hosts(self):
hosts = {} hosts = {}
assets = app_service.get_user_assets( assets = app_service.get_user_assets(
self.server.request.user self.server.connection.user
) )
for asset in assets: for asset in assets:
key = asset.hostname key = asset.hostname
......
...@@ -5,14 +5,15 @@ ...@@ -5,14 +5,15 @@
import os import os
import socket import socket
import threading import threading
import paramiko import paramiko
from .utils import ssh_key_gen, get_logger from .utils import ssh_key_gen, get_logger
from .interface import SSHInterface from .interface import SSHInterface
from .interactive import InteractiveServer from .interactive import InteractiveServer
from .models import Client, Request from .models import Connection
from .sftp import SFTPServer from .sftp import SFTPServer
from .ctx import current_app from .config import config
logger = get_logger(__file__) logger = get_logger(__file__)
BACKLOG = 5 BACKLOG = 5
...@@ -24,10 +25,11 @@ class SSHServer: ...@@ -24,10 +25,11 @@ class SSHServer:
self.stop_evt = threading.Event() self.stop_evt = threading.Event()
self.workers = [] self.workers = []
self.pipe = None self.pipe = None
self.connections = []
@property @property
def host_key(self): 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): if not os.path.isfile(host_key_path):
self.gen_host_key(host_key_path) self.gen_host_key(host_key_path)
return paramiko.RSAKey(filename=host_key_path) return paramiko.RSAKey(filename=host_key_path)
...@@ -39,24 +41,27 @@ class SSHServer: ...@@ -39,24 +41,27 @@ class SSHServer:
f.write(ssh_key) f.write(ssh_key)
def run(self): def run(self):
host = current_app.config["BIND_HOST"] host = config["BIND_HOST"]
port = current_app.config["SSHD_PORT"] port = config["SSHD_PORT"]
print('Starting ssh server at {}:{}'.format(host, port)) print('Starting ssh server at {}:{}'.format(host, port))
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 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.bind((host, port))
sock.listen(BACKLOG) sock.listen(BACKLOG)
while not self.stop_evt.is_set(): while not self.stop_evt.is_set():
try: try:
client, addr = sock.accept() client, addr = sock.accept()
logger.info("Get ssh request from {}: {}".format(*addr)) t = threading.Thread(target=self.handle_connection, args=(client, addr))
thread = threading.Thread(target=self.handle_connection, t.daemon = True
args=(client, addr)) t.start()
thread.daemon = True
thread.start()
except IndexError as e: except IndexError as e:
logger.error("Start SSH server error: {}".format(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): def handle_connection(self, sock, addr):
transport = paramiko.Transport(sock, gss_kex=False) transport = paramiko.Transport(sock, gss_kex=False)
try: try:
...@@ -68,52 +73,52 @@ class SSHServer: ...@@ -68,52 +73,52 @@ class SSHServer:
transport.set_subsystem_handler( transport.set_subsystem_handler(
'sftp', paramiko.SFTPServer, SFTPServer 'sftp', paramiko.SFTPServer, SFTPServer
) )
request = Request(addr) connection = self.new_connection(addr, sock=sock)
server = SSHInterface(request) server = SSHInterface(connection)
try: try:
transport.start_server(server=server) transport.start_server(server=server)
except paramiko.SSHException: except paramiko.SSHException:
logger.warning("SSH negotiation failed") logger.warning("SSH negotiation failed")
return return
except EOFError: except EOFError as e:
logger.warning("Handle EOF Error") logger.warning("Handle EOF Error: {}".format(e))
return return
while transport.is_active():
while True:
if not transport.is_active():
transport.close()
sock.close()
break
chan = transport.accept() chan = transport.accept()
server.event.wait(5) server.event.wait(5)
if chan is None: if chan is None:
continue continue
if not server.event.is_set(): if not server.event.is_set():
logger.warning("Client not request a valid request, exiting") logger.warning("Client not request a valid request, exiting")
sock.close()
return 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.daemon = True
t.start() t.start()
Connection.remove_connection(connection.id)
def handle_chan(self, chan, request): @staticmethod
client = Client(chan, request) def dispatch(client):
current_app.add_client(client)
self.dispatch(client)
def dispatch(self, client):
supported = {'pty', 'x11', 'forward-agent'} supported = {'pty', 'x11', 'forward-agent'}
request_type = set(client.request.type) chan_type = client.request.type
if supported & request_type: kind = client.request.kind
logger.info("Request type `pty`, dispatch to interactive mode") if kind == 'session' and chan_type in supported:
logger.info("Request type `{}:{}`, dispatch to interactive mode".format(kind, chan_type))
InteractiveServer(client).interact() 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 pass
else: else:
logger.info("Request type `{}`".format(request_type)) msg = "Request type `{}:{}` not support now".format(kind, chan_type)
client.send("Not support request type: %s" % request_type) logger.info(msg)
client.send(msg)
def shutdown(self): def shutdown(self):
self.stop_evt.set() self.stop_evt.set()
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
# #
import queue import queue
import socket
class MultiQueueMixin: class MultiQueueMixin:
...@@ -24,16 +25,31 @@ class MemoryQueue(MultiQueueMixin, queue.Queue): ...@@ -24,16 +25,31 @@ class MemoryQueue(MultiQueueMixin, queue.Queue):
pass pass
def get_queue(config): class SizedList(list):
queue_engine = config['QUEUE_ENGINE'] def __init__(self, maxsize=0):
queue_size = config['QUEUE_MAX_SIZE'] self.maxsize = maxsize
self.size = 0
super().__init__()
if queue_engine == "server": def append(self, b):
replay_queue = MemoryQueue(queue_size) if self.maxsize == 0 or self.size < self.maxsize:
command_queue = MemoryQueue(queue_size) super().append(b)
else: self.size += len(b)
replay_queue = MemoryQueue(queue_size)
command_queue = MemoryQueue(queue_size)
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 @@ ...@@ -2,8 +2,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .ctx import current_app, app_service from .ctx import app_service
from .utils import get_logger from .utils import get_logger
from .session import Session
logger = get_logger(__file__) logger = get_logger(__file__)
...@@ -18,12 +19,7 @@ class TaskHandler: ...@@ -18,12 +19,7 @@ class TaskHandler:
def handle_kill_session(task): def handle_kill_session(task):
logger.info("Handle kill session task: {}".format(task.args)) logger.info("Handle kill session task: {}".format(task.args))
session_id = task.args session_id = task.args
session = None session = Session.sessions.get(session_id)
for s in current_app.sessions:
if s.id == session_id:
session = s
break
if session: if session:
session.terminate() session.terminate()
app_service.finish_task(task.id) app_service.finish_task(task.id)
......
...@@ -10,15 +10,21 @@ import os ...@@ -10,15 +10,21 @@ import os
import gettext import gettext
from io import StringIO from io import StringIO
from binascii import hexlify from binascii import hexlify
from werkzeug.local import Local, LocalProxy
from functools import partial, wraps
import builtins
import paramiko import paramiko
import pyte import pyte
from . import char from . import char
from .ctx import stack from .config import config
BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) 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): class Singleton(type):
def __init__(cls, *args, **kwargs): def __init__(cls, *args, **kwargs):
...@@ -270,12 +276,6 @@ def sort_assets(assets, order_by='hostname'): ...@@ -270,12 +276,6 @@ def sort_assets(assets, order_by='hostname'):
return assets 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): def get_private_key_fingerprint(key):
line = hexlify(key.get_fingerprint()) line = hexlify(key.get_fingerprint())
return b':'.join([line[i:i+2] for i in range(0, len(line), 2)]) 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): ...@@ -342,7 +342,7 @@ def net_input(client, prompt='Opt> ', sensitive=False, before=0, after=0):
input_data.append(data[:-1]) input_data.append(data[:-1])
multi_char_with_enter = True 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: if data in char.ENTER_CHAR or multi_char_with_enter:
client.send(wrap_with_line_feed(b'', after=2)) client.send(wrap_with_line_feed(b'', after=2))
option = parser.parse_input(input_data) option = parser.parse_input(input_data)
...@@ -356,14 +356,6 @@ def net_input(client, prompt='Opt> ', sensitive=False, before=0, after=0): ...@@ -356,14 +356,6 @@ def net_input(client, prompt='Opt> ', sensitive=False, before=0, after=0):
input_data.append(data) 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]') zh_pattern = re.compile(r'[\u4e00-\u9fa5]')
...@@ -419,5 +411,62 @@ def int_length(i): ...@@ -419,5 +411,62 @@ def int_length(i):
return len(str(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'))
...@@ -54,6 +54,12 @@ class Config: ...@@ -54,6 +54,12 @@ class Config:
# 登录是否支持秘钥认证 # 登录是否支持秘钥认证
# PUBLIC_KEY_AUTH = True # PUBLIC_KEY_AUTH = True
# SSH白名单
# ALLOW_SSH_USER = 'all' # ['test', 'test2']
# SSH黑名单, 如果用户同时在白名单和黑名单,黑名单优先生效
# BLOCK_SSH_USER = []
# 和Jumpserver 保持心跳时间间隔 # 和Jumpserver 保持心跳时间间隔
# HEARTBEAT_INTERVAL = 5 # HEARTBEAT_INTERVAL = 5
...@@ -66,5 +72,11 @@ class Config: ...@@ -66,5 +72,11 @@ class Config:
"TYPE": "server" "TYPE": "server"
} }
# SSH连接超时时间 (default 15 seconds)
# SSH_TIMEOUT = 15
# 语言 = en
LANGUAGE_CODE = 'zh'
config = Config() 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 ...@@ -19,7 +19,7 @@ itsdangerous==0.24
Jinja2==2.10 Jinja2==2.10
jmespath==0.9.3 jmespath==0.9.3
jms-storage==0.0.18 jms-storage==0.0.18
jumpserver-python-sdk==0.0.47 jumpserver-python-sdk==0.0.48
MarkupSafe==1.0 MarkupSafe==1.0
oss2==2.4.0 oss2==2.4.0
paramiko==2.4.1 paramiko==2.4.1
......
libffi-devel sshpass libffi-devel sshpass krb5-devel
\ No newline at end of file
#!/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