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 = []
...@@ -2,15 +2,15 @@ ...@@ -2,15 +2,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import os import os
import socket
import uuid import uuid
from flask_socketio import SocketIO, Namespace, join_room from flask_socketio import SocketIO, Namespace, join_room
from flask import Flask, request, current_app, redirect from flask import Flask, request
from .models import Request, Client, WSProxy from .models import Connection, WSProxy
from .proxy import ProxyServer from .proxy import ProxyServer
from .utils import get_logger from .utils import get_logger
from .ctx import current_app, app_service from .ctx import app_service
from .config import config
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__)
...@@ -26,16 +26,14 @@ class BaseNamespace(Namespace): ...@@ -26,16 +26,14 @@ class BaseNamespace(Namespace):
def get_current_user(self): def get_current_user(self):
session_id = request.cookies.get('sessionid', '') session_id = request.cookies.get('sessionid', '')
csrf_token = request.cookies.get('csrftoken', '') csrf_token = request.cookies.get('csrftoken', '')
token = request.headers.get("Authorization")
user = None user = None
if session_id and csrf_token: if session_id and csrf_token:
user = app_service.check_user_cookie(session_id, csrf_token) user = app_service.check_user_cookie(session_id, csrf_token)
if token: msg = "Get current user: session_id<{}> => {}".format(
user = app_service.check_user_with_token(token) session_id, user
msg = "Get current user: session_id<{}> token<{}> => {}".format(
session_id, token, user
) )
logger.debug(msg) logger.debug(msg)
request.current_user = user
return user return user
...@@ -46,77 +44,54 @@ class ProxyNamespace(BaseNamespace): ...@@ -46,77 +44,54 @@ class ProxyNamespace(BaseNamespace):
:param kwargs: :param kwargs:
self.connections = { self.connections = {
"request_sid": { "request_sid": connection,
"room_id": {
"id": room_id,
"proxy": None,
"client": None,
"forwarder": None,
"request": None,
"cols": 80,
"rows": 24
},
...
},
... ...
} }
""" """
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.connections = dict()
self.win_size = None self.win_size = None
def new_connection(self): def new_connection(self):
self.connections[request.sid] = dict()
def new_room(self, current_user, cols=80, rows=24):
room_id = str(uuid.uuid4())
req = self.make_coco_request(current_user, cols=cols, rows=rows)
room = {
"id": room_id,
"proxy": None,
"client": None,
"forwarder": None,
"request": req,
"cols": cols,
"rows": rows
}
self.connections[request.sid][room_id] = room
return room
@staticmethod
def make_coco_request(user, cols=80, rows=24):
x_forwarded_for = request.headers.get("X-Forwarded-For", '').split(',') x_forwarded_for = request.headers.get("X-Forwarded-For", '').split(',')
if x_forwarded_for and x_forwarded_for[0]: if x_forwarded_for and x_forwarded_for[0]:
remote_ip = x_forwarded_for[0] remote_ip = x_forwarded_for[0]
else: else:
remote_ip = request.remote_addr remote_ip = request.remote_addr
connection = Connection.new_connection(
req = Request((remote_ip, 0)) addr=(remote_ip, 0), cid=request.sid, sock=self
req.user = user )
req.meta = {"width": cols, "height": rows} connection.user = request.current_user
return req connection.login_from = 'WT'
def on_connect(self): def on_connect(self):
logger.debug("On connect event trigger") logger.debug("On connect event trigger")
self.get_current_user()
super().on_connect() super().on_connect()
self.new_connection() self.new_connection()
def on_host(self, message): def on_host(self, message):
# 此处获取主机的信息 # 此处获取主机的信息
logger.debug("On host event trigger") logger.debug("On host event trigger")
current_user = self.get_current_user() self.connect_host(message)
self.connect_host(current_user, message)
def connect_host(self, current_user, message): def connect_host(self, message):
asset_id = message.get('uuid', None) asset_id = message.get('uuid', None)
system_user_id = message.get('userid', None) system_user_id = message.get('userid', None)
secret = message.get('secret', None) secret = message.get('secret', None)
cols, rows = message.get('size', (80, 24)) cols, rows = message.get('size', (80, 24))
room = self.new_room(current_user, cols=cols, rows=rows)
self.emit('room', {'room': room["id"], 'secret': secret}) connection = Connection.get_connection(request.sid)
join_room(room["id"]) client_id = str(uuid.uuid4())
client = connection.new_client(client_id)
client.request.kind = 'session'
client.request.type = 'pty'
client.request.meta.update({
'pty': b'xterm', 'width': cols, 'height': rows,
})
ws_proxy = WSProxy(self, client_id)
client.chan = ws_proxy
self.emit('room', {'room': client_id, 'secret': secret})
join_room(client_id)
if not asset_id or not system_user_id: if not asset_id or not system_user_id:
return return
...@@ -125,16 +100,12 @@ class ProxyNamespace(BaseNamespace): ...@@ -125,16 +100,12 @@ class ProxyNamespace(BaseNamespace):
if not asset or not system_user: if not asset or not system_user:
return return
forwarder = ProxyServer(client, asset, system_user)
child, parent = socket.socketpair() def proxy():
client = Client(parent, room["request"]) forwarder.proxy()
forwarder = ProxyServer(client, login_from='WT') self.logout(client_id, connection)
room["client"] = client self.socketio.start_background_task(proxy)
room["forwarder"] = forwarder
room["proxy"] = WSProxy(self, child, room["id"])
self.socketio.start_background_task(
forwarder.proxy, asset, system_user
)
def on_data(self, message): def on_data(self, message):
""" """
...@@ -142,80 +113,98 @@ class ProxyNamespace(BaseNamespace): ...@@ -142,80 +113,98 @@ class ProxyNamespace(BaseNamespace):
:param message: {"data": "xxx", "room": "xxx"} :param message: {"data": "xxx", "room": "xxx"}
:return: :return:
""" """
room_id = message.get('room') client_id = message.get('room')
room = self.connections.get(request.sid, {}).get(room_id) connection = Connection.get_connection(request.sid)
if not room: if not connection:
return return
room["proxy"].send({"data": message['data']}) client = connection.clients.get(client_id)
if not client:
def on_token(self, message): return
# 此处获取token含有的主机的信息 client.chan.write(message.get("data", ""))
logger.debug("On token trigger")
token = message.get('token', None)
secret = message.get("secret", None)
win_size = message.get('size', (80, 24))
room = self.new_room(None)
self.emit('room', {'room': room["id"], 'secret': secret})
join_room(room['id'])
if not token or not secret: def check_token(self, token, secret, client_id):
if not token or secret:
msg = "Token or secret is None: {} {}".format(token, secret) msg = "Token or secret is None: {} {}".format(token, secret)
logger.error(msg) logger.error(msg)
self.emit('data', {'data': msg, 'room': room['id']}, room=room['id']) self.emit('data', {'data': msg, 'room': client_id}, room=client_id)
self.emit('disconnect') self.emit('disconnect')
return return False, None
info = app_service.get_token_asset(token) info = app_service.get_token_asset(token)
logger.debug(info) logger.debug(info)
if not info: if not info:
msg = "Token info is none, maybe token expired" msg = "Token info is none, maybe token expired"
logger.error(msg) logger.error(msg)
self.emit('data', {'data': msg, 'room': room['id']}, room=room['id']) self.emit('data', {'data': msg, 'room': client_id}, room=client_id)
self.emit('disconnect') self.emit('disconnect')
return None return False, None
return True, info
def on_token(self, message):
# 此处获取token含有的主机的信息
logger.debug("On token trigger")
token = message.get('token', None)
secret = message.get("secret", None)
win_size = message.get('size', (80, 24))
client_id = str(uuid.uuid4())
self.emit('room', {'room': client_id, 'secret': secret})
join_room(client_id)
valid, info = self.check_token(token, secret, client_id)
if not valid:
return
user_id = info.get('user', None) user_id = info.get('user', None)
current_user = app_service.get_user_profile(user_id) request.current_user = app_service.get_user_profile(user_id)
message = { message = {
'secret': secret, 'secret': secret,
'uuid': info['asset'], 'uuid': info['asset'],
'userid': info['system_user'], 'userid': info['system_user'],
'size': win_size, 'size': win_size,
} }
self.connect_host(current_user, message) self.connect_host(message)
def on_resize(self, message): def on_resize(self, message):
cols, rows = message.get('cols', None), message.get('rows', None) cols, rows = message.get('cols', None), message.get('rows', None)
logger.debug("On resize event trigger: {}*{}".format(cols, rows)) logger.debug("On resize event trigger: {}*{}".format(cols, rows))
rooms = self.connections.get(request.sid, {}) connection = Connection.get_connection(request.sid)
if not connection:
logger.error("Not connection found")
return
logger.debug("Start change win size: {}*{}".format(cols, rows)) logger.debug("Start change win size: {}*{}".format(cols, rows))
for room in rooms.values(): for client in connection.clients.values():
if (room["cols"], room["rows"]) == (cols, rows): if (client.request.meta["width"], client.request.meta["height"]) == (cols, rows):
continue continue
room["request"].meta.update({ client.request.meta.update({
'width': cols, 'height': rows 'width': cols, 'height': rows
}) })
room["request"].change_size_event.set() client.change_size_evt.set()
room.update({"cols": cols, "rows": rows})
def on_disconnect(self): def on_disconnect(self):
logger.debug("On disconnect event trigger") logger.debug("On disconnect event trigger")
rooms = {k: v for k, v in self.connections.get(request.sid, {}).items()} connection = Connection.get_connection(request.sid)
for room_id in rooms: if not connection:
return
clients_copy = list(connection.clients.keys())
for client_id in clients_copy:
try: try:
self.on_logout(room_id) self.on_logout(client_id)
except Exception as e: except Exception as e:
logger.warn(e) logger.warn(e)
del self.connections[request.sid] Connection.remove_connection(connection.id)
def on_logout(self, room_id): def logout(self, client_id, connection):
room = self.connections.get(request.sid, {}).get(room_id) connection.remove_client(client_id)
if room: self.emit('logout', {"room": client_id}, room=client_id)
room.get("proxy") and room["proxy"].close()
self.close_room(room_id) def on_logout(self, client_id):
del self.connections[request.sid][room_id] logger.debug("On logout event trigger: {}".format(client_id))
del room connection = Connection.get_connection(request.sid)
if not connection:
return
self.logout(client_id, connection)
def close(self):
pass
def on_ping(self): def on_ping(self):
self.emit('pong') self.emit('pong')
...@@ -238,7 +227,6 @@ class HttpServer: ...@@ -238,7 +227,6 @@ class HttpServer:
) )
def __init__(self): def __init__(self):
config = {k: v for k, v in current_app.config.items()}
config.update(self.config) config.update(self.config)
self.flask_app = Flask(__name__, template_folder='dist') self.flask_app = Flask(__name__, template_folder='dist')
self.flask_app.config.update(config) self.flask_app.config.update(config)
...@@ -257,8 +245,9 @@ class HttpServer: ...@@ -257,8 +245,9 @@ class HttpServer:
self.socket_io.on_error_default(self.on_error_default) self.socket_io.on_error_default(self.on_error_default)
def run(self): def run(self):
host = self.flask_app.config["BIND_HOST"] # return
port = self.flask_app.config["HTTPD_PORT"] host = config["BIND_HOST"]
port = config["HTTPD_PORT"]
print('Starting websocket server at {}:{}'.format(host, port)) print('Starting websocket server at {}:{}'.format(host, port))
self.socket_io.init_app( self.socket_io.init_app(
self.flask_app, self.flask_app,
......
...@@ -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={
......
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import threading
import datetime
import weakref import weakref
import time import uuid
import socket
from .struct import SizedList, SelectEvent
from . import char from . import char
from . import utils from . import utils
...@@ -12,34 +12,85 @@ BUF_SIZE = 4096 ...@@ -12,34 +12,85 @@ BUF_SIZE = 4096
logger = utils.get_logger(__file__) logger = utils.get_logger(__file__)
class Request: class Connection:
def __init__(self, addr): connections = {}
self.type = [] clients_num = 0
self.meta = {"width": 80, "height": 24}
self.user = None def __init__(self, cid=None, sock=None, addr=None):
if not cid:
cid = str(uuid.uuid4())
self.id = cid
self.sock = sock
self.addr = addr self.addr = addr
self.remote_ip = self.addr[0] self.user = None
self.change_size_event = threading.Event() self.otp_auth = False
self.date_start = datetime.datetime.now() self.login_from = 'ST'
self.clients = {}
def __str__(self):
return '<{} from {}>'.format(self.user, self.addr)
def new_client(self, tid):
client = Client(
tid=tid, user=self.user, addr=self.addr,
login_from=self.login_from
)
client.connection_id = self.id
self.clients[tid] = client
self.__class__.clients_num += 1
logger.info("New client {} join, total {} now".format(
client, self.__class__.clients_num
))
return client
def get_client(self, tid):
if hasattr(tid, 'get_id'):
tid = tid.get_id()
client = self.clients.get(tid)
return client
def remove_client(self, tid):
client = self.get_client(tid)
if not client:
return
client.close()
self.__class__.clients_num -= 1
del self.clients[tid]
logger.info("Client {} leave, total {} now".format(
client, self.__class__.clients_num
))
def close(self):
clients_copy = [k for k in self.clients]
for tid in clients_copy:
self.remove_client(tid)
self.sock.close()
# def __del__(self): @classmethod
# print("GC: Request object gc") def new_connection(cls, addr, sock=None, cid=None):
if not cid:
cid = str(uuid.uuid4())
connection = cls(cid=cid, sock=sock, addr=addr)
cls.connections[cid] = connection
return connection
@classmethod
def remove_connection(cls, cid):
connection = cls.get_connection(cid)
connection.close()
del cls.connections[cid]
class SizedList(list): @classmethod
def __init__(self, maxsize=0): def get_connection(cls, cid):
self.maxsize = maxsize return cls.connections.get(cid)
self.size = 0
super().__init__()
def append(self, b):
if self.maxsize == 0 or self.size < self.maxsize:
super().append(b)
self.size += len(b)
def clean(self): class Request:
self.size = 0 def __init__(self):
del self[:] self.type = None
self.x11 = None
self.kind = None
self.meta = {'env': {}}
class Client: class Client:
...@@ -51,11 +102,17 @@ class Client: ...@@ -51,11 +102,17 @@ class Client:
``` ```
""" """
def __init__(self, chan, request): def __init__(self, tid=None, user=None, addr=None, login_from=None):
self.chan = chan if tid is None:
self.request = request tid = str(uuid.uuid4())
self.user = request.user self.id = tid
self.addr = request.addr self.user = user
self.addr = addr
self.chan = None
self.request = Request()
self.connection_id = None
self.login_from = login_from
self.change_size_evt = SelectEvent()
def fileno(self): def fileno(self):
return self.chan.fileno() return self.chan.fileno()
...@@ -82,9 +139,6 @@ class Client: ...@@ -82,9 +139,6 @@ class Client:
def __str__(self): def __str__(self):
return "<%s from %s:%s>" % (self.user, self.addr[0], self.addr[1]) return "<%s from %s:%s>" % (self.user, self.addr[0], self.addr[1])
# def __del__(self):
# print("GC: Client object has been gc")
class BaseServer: class BaseServer:
""" """
...@@ -94,9 +148,7 @@ class BaseServer: ...@@ -94,9 +148,7 @@ class BaseServer:
""" """
def __init__(self): def __init__(self):
self.send_bytes = 0 self.chan = None
self.recv_bytes = 0
self.stop_evt = threading.Event()
self.input_data = SizedList(maxsize=1024) self.input_data = SizedList(maxsize=1024)
self.output_data = SizedList(maxsize=1024) self.output_data = SizedList(maxsize=1024)
...@@ -107,6 +159,13 @@ class BaseServer: ...@@ -107,6 +159,13 @@ class BaseServer:
self._input = "" self._input = ""
self._output = "" self._output = ""
self._session_ref = None self._session_ref = None
self._zmodem_recv_start_mark = b'rz waiting to receive.**\x18B0100'
self._zmodem_send_start_mark = b'**\x18B00000000000000'
self._zmodem_cancel_mark = b'\x18\x18\x18\x18\x18'
self._zmodem_end_mark = b'**\x18B0800000000022d'
self._zmodem_state_send = 'send'
self._zmodem_state_recv = 'recv'
self._zmodem_state = ''
def set_session(self, session): def set_session(self, session):
self._session_ref = weakref.ref(session) self._session_ref = weakref.ref(session)
...@@ -118,16 +177,17 @@ class BaseServer: ...@@ -118,16 +177,17 @@ class BaseServer:
else: else:
return None return None
def parse(self, b): def initial_filter(self):
if isinstance(b, str):
b = b.encode("utf-8")
if not self._input_initial: if not self._input_initial:
self._input_initial = True self._input_initial = True
if self._have_enter_char(b): def parse_cmd_filter(self, data):
# 输入了回车键, 开始计算输入的内容
if self._have_enter_char(data):
self._in_input_state = False self._in_input_state = False
self._input = self._parse_input() self._input = self._parse_input()
else: return data
# 用户输入了内容,但是如果没在输入状态,也就是用户刚开始输入了,结算上次输出内容
if not self._in_input_state: if not self._in_input_state:
self._output = self._parse_output() self._output = self._parse_output()
logger.debug("\n{}\nInput: {}\nOutput: {}\n{}".format( logger.debug("\n{}\nInput: {}\nOutput: {}\n{}".format(
...@@ -140,6 +200,56 @@ class BaseServer: ...@@ -140,6 +200,56 @@ class BaseServer:
self.input_data.clean() self.input_data.clean()
self.output_data.clean() self.output_data.clean()
self._in_input_state = True self._in_input_state = True
return data
def send(self, data):
self.initial_filter()
self.parse_cmd_filter(data)
return self.chan.send(data)
def replay_filter(self, data):
if not self._zmodem_state:
self.session.put_replay(data)
def input_output_filter(self, data):
if not self._input_initial:
return
if self._zmodem_state:
return
if self._in_input_state:
self.input_data.append(data)
else:
self.output_data.append(data)
def zmodem_state_filter(self, data):
if not self._zmodem_state:
if data[:50].find(self._zmodem_recv_start_mark) != -1:
logger.debug("Zmodem state => recv")
self._zmodem_state = self._zmodem_state_recv
elif data[:24].find(self._zmodem_send_start_mark) != -1:
logger.debug("Zmodem state => send")
self._zmodem_state = self._zmodem_state_send
if self._zmodem_state:
if data[:24].find(self._zmodem_end_mark) != -1:
logger.debug("Zmodem state => end")
self._zmodem_state = ''
elif data[:24].find(self._zmodem_cancel_mark) != -1:
logger.debug("Zmodem state => cancel")
self._zmodem_state = ''
def zmodem_cancel_filter(self):
if self._zmodem_state:
pass
# self.chan.send(self._zmodem_cancel_mark)
# self.chan.send("Zmodem disabled")
def recv(self, size):
data = self.chan.recv(size)
self.zmodem_state_filter(data)
self.zmodem_cancel_filter()
self.replay_filter(data)
self.input_output_filter(data)
return data
@staticmethod @staticmethod
def _have_enter_char(s): def _have_enter_char(s):
...@@ -155,107 +265,38 @@ class BaseServer: ...@@ -155,107 +265,38 @@ class BaseServer:
return parser.parse_output(self.output_data) return parser.parse_output(self.output_data)
def _parse_input(self): def _parse_input(self):
if not self.input_data or self.input_data[0] == char.RZ_PROTOCOL_CHAR: if not self.input_data:
return return
parser = utils.TtyIOParser() parser = utils.TtyIOParser()
return parser.parse_input(self.input_data) return parser.parse_input(self.input_data)
class TelnetServer(BaseServer):
"""
Telnet server
"""
def __init__(self, sock, asset, system_user):
super(TelnetServer, self).__init__()
self.sock = sock
self.asset = asset
self.system_user = system_user
def fileno(self): def fileno(self):
return self.sock.fileno() return self.chan.fileno()
def send(self, b):
self.parse(b)
return self.sock.send(b)
def recv(self, size):
data = self.sock.recv(size)
self.session.put_replay(data)
if self._input_initial:
if self._in_input_state:
self.input_data.append(data)
else:
self.output_data.append(data)
return data
def close(self): def close(self):
logger.info("Closed server {}".format(self)) logger.info("Closed server {}".format(self))
self.parse(b'') self.input_output_filter(b'')
self.stop_evt.set() self.chan.close()
self.sock.close()
def __getattr__(self, item): def __getattr__(self, item):
return getattr(self.sock, item) return getattr(self.chan, item)
def __str__(self): def __str__(self):
return "<To: {}>".format(str(self.asset)) return "<To: {}>".format(str(self.asset))
class Server(BaseServer): class TelnetServer(BaseServer):
""" """
SSH Server Telnet server
Server object like client, a wrapper object, a connection to the asset,
Because we don't want to using python dynamic feature, such asset
have the chan and system_user attr.
""" """
def __init__(self, sock, asset, system_user):
# Todo: Server name is not very suitable super(TelnetServer, self).__init__()
def __init__(self, chan, sock, asset, system_user): self.chan = sock
super(Server, self).__init__()
self.chan = chan
self.sock = sock
self.asset = asset self.asset = asset
self.system_user = system_user self.system_user = system_user
def fileno(self):
return self.chan.fileno()
def send(self, b):
self.parse(b)
return self.chan.send(b)
def recv(self, size):
data = self.chan.recv(size)
self.session.put_replay(data)
if self._input_initial:
if self._in_input_state:
self.input_data.append(data)
else:
self.output_data.append(data)
return data
def close(self):
logger.info("Closed server {}".format(self))
self.parse(b'')
self.stop_evt.set()
self.chan.close()
self.chan.transport.close()
if self.sock:
self.sock.transport.close()
def __getattr__(self, item):
return getattr(self.chan, item)
def __str__(self):
return "<To: {}>".format(str(self.asset))
# def __del__(self):
# print("GC: Server object has been gc")
''' class Server(BaseServer):
class Server:
""" """
SSH Server SSH Server
Server object like client, a wrapper object, a connection to the asset, Server object like client, a wrapper object, a connection to the asset,
...@@ -265,185 +306,39 @@ class Server: ...@@ -265,185 +306,39 @@ class Server:
# Todo: Server name is not very suitable # Todo: Server name is not very suitable
def __init__(self, chan, sock, asset, system_user): def __init__(self, chan, sock, asset, system_user):
super(Server, self).__init__()
self.chan = chan self.chan = chan
self.sock = sock self.sock = sock
self.asset = asset self.asset = asset
self.system_user = system_user self.system_user = system_user
self.send_bytes = 0
self.recv_bytes = 0
self.stop_evt = threading.Event()
self.input_data = SizedList(maxsize=1024)
self.output_data = SizedList(maxsize=1024)
self._in_input_state = True
self._input_initial = False
self._in_vim_state = False
self._input = ""
self._output = ""
self._session_ref = None
def fileno(self):
return self.chan.fileno()
def set_session(self, session):
self._session_ref = weakref.ref(session)
@property
def session(self):
if self._session_ref:
return self._session_ref()
else:
return None
def parse(self, b):
if isinstance(b, str):
b = b.encode("utf-8")
if not self._input_initial:
self._input_initial = True
if self._have_enter_char(b):
self._in_input_state = False
self._input = self._parse_input()
else:
if not self._in_input_state:
self._output = self._parse_output()
logger.debug("\n{}\nInput: {}\nOutput: {}\n{}".format(
"#" * 30 + " Command " + "#" * 30,
self._input, self._output,
"#" * 30 + " End " + "#" * 30,
))
if self._input:
self.session.put_command(self._input, self._output)
self.input_data.clean()
self.output_data.clean()
self._in_input_state = True
def send(self, b):
self.parse(b)
return self.chan.send(b)
def recv(self, size):
data = self.chan.recv(size)
self.session.put_replay(data)
if self._input_initial:
if self._in_input_state:
self.input_data.append(data)
else:
self.output_data.append(data)
return data
def close(self): def close(self):
logger.info("Closed server {}".format(self)) super().close()
self.parse(b'')
self.stop_evt.set()
self.chan.close()
self.chan.transport.close() self.chan.transport.close()
if self.sock: if self.sock:
self.sock.transport.close() self.sock.transport.close()
@staticmethod
def _have_enter_char(s):
for c in char.ENTER_CHAR:
if c in s:
return True
return False
def _parse_output(self):
if not self.output_data:
return ''
parser = utils.TtyIOParser()
return parser.parse_output(self.output_data)
def _parse_input(self):
if not self.input_data or self.input_data[0] == char.RZ_PROTOCOL_CHAR:
return
parser = utils.TtyIOParser()
return parser.parse_input(self.input_data)
def __getattr__(self, item):
return getattr(self.chan, item)
def __str__(self):
return "<To: {}>".format(str(self.asset))
# def __del__(self):
# print("GC: Server object has been gc")
'''
class WSProxy: class WSProxy:
""" def __init__(self, ws, client_id):
WSProxy is websocket proxy channel object.
Because tornado or flask websocket base event, if we want reuse func
with sshd, we need change it to socket, so we implement a proxy.
we should use socket pair implement it. usage:
```
child, parent = socket.socketpair()
# self must have write_message method, write message to ws
proxy = WSProxy(self, child)
client = Client(parent, user)
```
"""
def __init__(self, ws, child, room_id):
"""
:param ws: websocket instance or handler, have write_message method
:param child: sock child pair
"""
self.ws = ws self.ws = ws
self.child = child self.client_id = client_id
self.stop_event = threading.Event() self.sock, self.proxy = socket.socketpair()
self.room_id = room_id
self.auto_forward()
def send(self, msg): def send(self, data):
""" _data = {'data': data.decode(errors="ignore"), 'room': self.client_id},
If ws use proxy send data, then send the data to child sock, then self.ws.emit("data", _data, room=self.client_id)
the parent sock recv
:param msg: terminal write message {"data": "message"} @property
:return: def closed(self):
""" return self.sock._closed
data = msg["data"]
if isinstance(data, str):
data = data.encode('utf-8')
self.child.send(data)
def forward(self): def write(self, data):
while not self.stop_event.is_set(): self.proxy.send(data.encode())
try:
data = self.child.recv(BUF_SIZE)
except (OSError, EOFError):
self.close()
break
if not data:
self.close()
break
data = data.decode(errors="ignore")
self.ws.emit("data", {'data': data, 'room': self.room_id},
room=self.room_id)
if len(data) == BUF_SIZE:
time.sleep(0.1)
def auto_forward(self):
thread = threading.Thread(target=self.forward, args=())
thread.daemon = True
thread.start()
def close(self): def close(self):
self.ws.emit("logout", {"room": self.room_id}, room=self.room_id) self.proxy.close()
self.stop_event.set()
try: def __getattr__(self, item):
self.child.shutdown(1) return getattr(self.sock, item)
self.child.close()
except (OSError, EOFError):
pass
logger.debug("Proxy {} closed".format(self))
...@@ -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