Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
C
coco
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ops
coco
Commits
a07ede5b
Commit
a07ede5b
authored
Sep 03, 2018
by
ibuler
Browse files
Options
Browse Files
Download
Plain Diff
[Update] Merge
parents
c13956a4
9f0a1581
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
25 changed files
with
1181 additions
and
434 deletions
+1181
-434
app.py
coco/app.py
+55
-133
config.py
coco/config.py
+42
-2
connection.py
coco/connection.py
+9
-6
ctx.py
coco/ctx.py
+6
-5
httpd.py
coco/httpd.py
+0
-0
interactive.py
coco/interactive.py
+57
-37
interface.py
coco/interface.py
+90
-48
models.py
coco/models.py
+0
-0
proxy.py
coco/proxy.py
+60
-85
recorder.py
coco/recorder.py
+23
-11
service.py
coco/service.py
+9
-0
session.py
coco/session.py
+69
-35
sftp.py
coco/sftp.py
+2
-2
sshd.py
coco/sshd.py
+40
-35
struct.py
coco/struct.py
+26
-10
tasks.py
coco/tasks.py
+3
-7
utils.py
coco/utils.py
+66
-17
conf_example.py
conf_example.py
+10
-1
coco.mo
locale/en/LC_MESSAGES/coco.mo
+0
-0
coco.po
locale/en/LC_MESSAGES/coco.po
+145
-0
coco.po~
locale/en/LC_MESSAGES/coco.po~
+137
-0
coco.mo
locale/zh_CN/LC_MESSAGES/coco.mo
+0
-0
coco.po
locale/zh_CN/LC_MESSAGES/coco.po
+151
-0
coco.po~
locale/zh_CN/LC_MESSAGES/coco.po~
+143
-0
messages.sh
utils/messages.sh
+38
-0
No files found.
coco/app.py
View file @
a07ede5b
...
...
@@ -2,28 +2,31 @@
# -*- coding: utf-8 -*-
#
import
eventlet
from
eventlet.debug
import
hub_prevent_multiple_readers
eventlet
.
monkey_patch
()
hub_prevent_multiple_readers
(
False
)
import
datetime
import
os
import
time
import
threading
import
socket
import
json
import
signal
import
eventlet
from
eventlet.debug
import
hub_prevent_multiple_readers
from
jms.service
import
AppService
from
.config
import
C
onfig
from
.config
import
c
onfig
from
.sshd
import
SSHServer
from
.httpd
import
HttpServer
from
.logger
import
create_logger
from
.tasks
import
TaskHandler
from
.recorder
import
ReplayRecorder
,
CommandRecorder
from
.utils
import
get_logger
,
register_app
,
register_service
from
.utils
import
get_logger
,
ugettext
as
_
,
\
ignore_error
from
.service
import
init_app
from
.ctx
import
app_service
from
.recorder
import
get_replay_recorder
from
.session
import
Session
from
.models
import
Connection
eventlet
.
monkey_patch
()
hub_prevent_multiple_readers
(
False
)
__version__
=
'1.4.0'
...
...
@@ -32,37 +35,7 @@ logger = get_logger(__file__)
class
Coco
:
config_class
=
Config
default_config
=
{
'DEFAULT_NAME'
:
socket
.
gethostname
(),
'NAME'
:
None
,
'CORE_HOST'
:
'http://127.0.0.1:8080'
,
'DEBUG'
:
True
,
'BIND_HOST'
:
'0.0.0.0'
,
'SSHD_PORT'
:
2222
,
'HTTPD_PORT'
:
5000
,
'ACCESS_KEY'
:
''
,
'ACCESS_KEY_ENV'
:
'COCO_ACCESS_KEY'
,
'ACCESS_KEY_FILE'
:
os
.
path
.
join
(
BASE_DIR
,
'keys'
,
'.access_key'
),
'SECRET_KEY'
:
None
,
'LOG_LEVEL'
:
'DEBUG'
,
'LOG_DIR'
:
os
.
path
.
join
(
BASE_DIR
,
'logs'
),
'SESSION_DIR'
:
os
.
path
.
join
(
BASE_DIR
,
'sessions'
),
'ASSET_LIST_SORT_BY'
:
'hostname'
,
# hostname, ip
'PASSWORD_AUTH'
:
True
,
'PUBLIC_KEY_AUTH'
:
True
,
'HEARTBEAT_INTERVAL'
:
5
,
'MAX_CONNECTIONS'
:
500
,
'ADMINS'
:
''
,
'SSH_TIMEOUT'
:
15
,
'COMMAND_STORAGE'
:
{
'TYPE'
:
'server'
},
# server
'REPLAY_STORAGE'
:
{
'TYPE'
:
'server'
},
}
def
__init__
(
self
,
root_path
=
None
):
self
.
root_path
=
root_path
if
root_path
else
BASE_DIR
self
.
sessions
=
[]
self
.
clients
=
[]
def
__init__
(
self
):
self
.
lock
=
threading
.
Lock
()
self
.
stop_evt
=
threading
.
Event
()
self
.
_service
=
None
...
...
@@ -71,28 +44,8 @@ class Coco:
self
.
replay_recorder_class
=
None
self
.
command_recorder_class
=
None
self
.
_task_handler
=
None
self
.
config
=
None
self
.
init_config
()
register_app
(
self
)
def
init_config
(
self
):
self
.
config
=
self
.
config_class
(
self
.
root_path
,
defaults
=
self
.
default_config
)
@property
def
name
(
self
):
if
self
.
config
[
'NAME'
]:
return
self
.
config
[
'NAME'
]
else
:
return
self
.
config
[
'DEFAULT_NAME'
]
@property
def
service
(
self
):
if
self
.
_service
is
None
:
self
.
_service
=
AppService
(
self
)
register_service
(
self
.
_service
)
return
self
.
_service
self
.
config
=
config
init_app
(
self
)
@property
def
sshd
(
self
):
...
...
@@ -115,32 +68,26 @@ class Coco:
def
make_logger
(
self
):
create_logger
(
self
)
def
load_extra_conf_from_server
(
self
):
configs
=
self
.
service
.
load_config_from_server
()
@staticmethod
def
load_extra_conf_from_server
():
configs
=
app_service
.
load_config_from_server
()
logger
.
debug
(
"Loading config from server: {}"
.
format
(
json
.
dumps
(
configs
)
))
self
.
config
.
update
(
configs
)
@staticmethod
def
new_command_recorder
():
return
CommandRecorder
()
@staticmethod
def
new_replay_recorder
():
return
ReplayRecorder
()
config
.
update
(
configs
)
def
bootstrap
(
self
):
self
.
make_logger
()
self
.
service
.
initial
()
app_
service
.
initial
()
self
.
load_extra_conf_from_server
()
self
.
keep_heartbeat
()
self
.
monitor_sessions
()
self
.
monitor_sessions_replay
()
@ignore_error
def
heartbeat
(
self
):
_sessions
=
[
s
.
to_json
()
for
s
in
self
.
sessions
]
tasks
=
self
.
service
.
terminal_heartbeat
(
_sessions
)
_sessions
=
[
s
.
to_json
()
for
s
in
Session
.
sessions
.
values
()
]
tasks
=
app_
service
.
terminal_heartbeat
(
_sessions
)
if
tasks
:
self
.
handle_task
(
tasks
)
if
tasks
is
False
:
...
...
@@ -160,19 +107,17 @@ class Coco:
def
func
():
while
not
self
.
stop_evt
.
is_set
():
self
.
heartbeat
()
time
.
sleep
(
self
.
config
[
"HEARTBEAT_INTERVAL"
])
time
.
sleep
(
config
[
"HEARTBEAT_INTERVAL"
])
thread
=
threading
.
Thread
(
target
=
func
)
thread
.
start
()
def
monitor_sessions_replay
(
self
):
interval
=
10
recorder
=
self
.
new_replay_recorder
()
log_dir
=
os
.
path
.
join
(
self
.
config
[
'LOG_DIR'
])
log_dir
=
os
.
path
.
join
(
config
[
'LOG_DIR'
])
def
func
():
while
not
self
.
stop_evt
.
is_set
():
active_sessions
=
[
s
tr
(
session
.
id
)
for
session
in
self
.
sessions
]
active_sessions
=
[
s
id
for
sid
in
Session
.
sessions
]
for
filename
in
os
.
listdir
(
log_dir
):
session_id
=
filename
.
split
(
'.'
)[
0
]
full_path
=
os
.
path
.
join
(
log_dir
,
filename
)
...
...
@@ -180,6 +125,7 @@ class Coco:
if
len
(
session_id
)
!=
36
:
continue
recorder
=
get_replay_recorder
()
if
session_id
not
in
active_sessions
:
recorder
.
file_path
=
full_path
ok
=
recorder
.
upload_replay
(
session_id
,
1
)
...
...
@@ -191,21 +137,33 @@ class Coco:
thread
.
start
()
def
monitor_sessions
(
self
):
interval
=
self
.
config
[
"HEARTBEAT_INTERVAL"
]
interval
=
config
[
"HEARTBEAT_INTERVAL"
]
def
check_session_idle_too_long
(
s
):
delta
=
datetime
.
datetime
.
utcnow
()
-
s
.
date_last_active
max_idle_seconds
=
config
[
'SECURITY_MAX_IDLE_TIME'
]
*
60
if
delta
.
seconds
>
max_idle_seconds
:
msg
=
_
(
"Connect idle more than {} minutes, disconnect"
)
.
format
(
config
[
'SECURITY_MAX_IDLE_TIME'
]
)
s
.
terminate
(
msg
=
msg
)
return
True
def
func
():
while
not
self
.
stop_evt
.
is_set
():
for
s
in
self
.
sessions
:
if
not
s
.
stop_evt
.
is_set
():
sessions_copy
=
[
s
for
s
in
Session
.
sessions
.
values
()]
for
s
in
sessions_copy
:
# Session 没有正常关闭,
if
s
.
closed_unexpected
:
Session
.
remove_session
(
s
.
id
)
continue
if
s
.
date_end
is
None
:
self
.
remove_session
(
s
)
continue
delta
=
datetime
.
datetime
.
now
()
-
s
.
date_end
if
delta
>
datetime
.
timedelta
(
seconds
=
interval
*
5
):
self
.
remove_session
(
s
)
# Session已正常关闭
if
s
.
closed
:
Session
.
remove_session
(
s
)
else
:
check_session_idle_too_long
(
s
)
time
.
sleep
(
interval
)
thread
=
threading
.
Thread
(
target
=
func
)
thread
.
start
()
...
...
@@ -216,10 +174,10 @@ class Coco:
print
(
'Quit the server with CONTROL-C.'
)
try
:
if
self
.
config
[
"SSHD_PORT"
]
!=
0
:
if
config
[
"SSHD_PORT"
]
!=
0
:
self
.
run_sshd
()
if
self
.
config
[
'HTTPD_PORT'
]
!=
0
:
if
config
[
'HTTPD_PORT'
]
!=
0
:
self
.
run_httpd
()
signal
.
signal
(
signal
.
SIGTERM
,
lambda
x
,
y
:
self
.
shutdown
())
...
...
@@ -229,7 +187,6 @@ class Coco:
break
time
.
sleep
(
3
)
except
KeyboardInterrupt
:
self
.
stop_evt
.
set
()
self
.
shutdown
()
def
run_sshd
(
self
):
...
...
@@ -243,46 +200,11 @@ class Coco:
thread
.
start
()
def
shutdown
(
self
):
for
client
in
self
.
clients
:
self
.
remove_client
(
client
)
logger
.
info
(
"Grace shutdown the server"
)
for
connection
in
Connection
.
connections
.
values
():
connection
.
close
()
time
.
sleep
(
1
)
self
.
heartbeat
()
self
.
stop_evt
.
set
()
self
.
sshd
.
shutdown
()
self
.
httpd
.
shutdown
()
logger
.
info
(
"Grace shutdown the server"
)
def
add_client
(
self
,
client
):
with
self
.
lock
:
self
.
clients
.
append
(
client
)
logger
.
info
(
"New client {} join, total {} now"
.
format
(
client
,
len
(
self
.
clients
)
)
)
def
remove_client
(
self
,
client
):
with
self
.
lock
:
try
:
self
.
clients
.
remove
(
client
)
logger
.
info
(
"Client {} leave, total {} now"
.
format
(
client
,
len
(
self
.
clients
)
)
)
client
.
close
()
except
:
pass
def
add_session
(
self
,
session
):
with
self
.
lock
:
self
.
sessions
.
append
(
session
)
self
.
service
.
create_session
(
session
.
to_json
())
def
remove_session
(
self
,
session
):
with
self
.
lock
:
try
:
logger
.
info
(
"Remove session: {}"
.
format
(
session
))
self
.
sessions
.
remove
(
session
)
self
.
service
.
finish_session
(
session
.
to_json
())
except
ValueError
:
msg
=
"Remove session: {} fail, maybe already removed"
logger
.
warning
(
msg
.
format
(
session
))
coco/config.py
View file @
a07ede5b
...
...
@@ -17,10 +17,14 @@ import os
import
types
import
errno
import
json
import
socket
from
werkzeug.utils
import
import_string
BASE_DIR
=
os
.
path
.
dirname
(
os
.
path
.
dirname
(
__file__
))
class
ConfigAttribute
(
object
):
"""Makes an attribute forward to the config"""
...
...
@@ -233,7 +237,7 @@ class Config(dict):
The resulting dictionary `image_store_config` would look like::
{
'type': 'fs',
'type
s
': 'fs',
'path': '/var/app/images',
'base_url': 'http://img.website.com'
}
...
...
@@ -266,4 +270,40 @@ class Config(dict):
return
'<
%
s
%
s>'
%
(
self
.
__class__
.
__name__
,
dict
.
__repr__
(
self
))
default_config
=
{
'NAME'
:
socket
.
gethostname
(),
'CORE_HOST'
:
'http://127.0.0.1:8080'
,
'DEBUG'
:
True
,
'BIND_HOST'
:
'0.0.0.0'
,
'SSHD_PORT'
:
2222
,
'HTTPD_PORT'
:
5000
,
'ACCESS_KEY'
:
''
,
'DB_FILE'
:
os
.
path
.
join
(
BASE_DIR
,
'sqlite3.db'
),
'ACCESS_KEY_ENV'
:
'COCO_ACCESS_KEY'
,
'ACCESS_KEY_FILE'
:
os
.
path
.
join
(
BASE_DIR
,
'keys'
,
'.access_key'
),
'SECRET_KEY'
:
'SDK29K03
%
MM0ksf'
,
'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
,
'SSH_TIMEOUT'
:
10
,
'ALLOW_SSH_USER'
:
[
'admin'
],
'BLOCK_SSH_USER'
:
[],
'HEARTBEAT_INTERVAL'
:
5
,
'MAX_CONNECTIONS'
:
500
,
'ADMINS'
:
''
,
'COMMAND_STORAGE'
:
{
'TYPE'
:
'server'
},
# server
'REPLAY_STORAGE'
:
{
'TYPE'
:
'server'
},
'LANGUAGE_CODE'
:
'zh'
,
'SECURITY_MAX_IDLE_TIME'
:
60
,
}
root_path
=
os
.
environ
.
get
(
"COCO_PATH"
)
if
not
root_path
:
root_path
=
BASE_DIR
config
=
Config
(
root_path
,
default_config
)
config
[
'ROOT_PATH'
]
=
root_path
config
.
from_pyfile
(
'conf.py'
)
coco/connection.py
View file @
a07ede5b
...
...
@@ -10,8 +10,9 @@ import telnetlib
import
paramiko
from
paramiko.ssh_exception
import
SSHException
from
.ctx
import
app_service
,
current_app
from
.utils
import
get_logger
,
get_private_key_fingerprint
,
net_input
from
.ctx
import
app_service
from
.config
import
config
from
.utils
import
get_logger
,
get_private_key_fingerprint
logger
=
get_logger
(
__file__
)
...
...
@@ -46,10 +47,12 @@ class SSHConnection:
ssh
.
connect
(
asset
.
ip
,
port
=
asset
.
port
,
username
=
system_user
.
username
,
password
=
system_user
.
password
,
pkey
=
system_user
.
private_key
,
timeout
=
c
urrent_app
.
c
onfig
[
'SSH_TIMEOUT'
],
compress
=
True
,
auth_timeout
=
c
urrent_app
.
c
onfig
[
'SSH_TIMEOUT'
],
timeout
=
config
[
'SSH_TIMEOUT'
],
compress
=
True
,
auth_timeout
=
config
[
'SSH_TIMEOUT'
],
look_for_keys
=
False
,
sock
=
sock
)
transport
=
ssh
.
get_transport
()
transport
.
set_keepalive
(
300
)
except
(
paramiko
.
AuthenticationException
,
paramiko
.
BadAuthenticationType
,
SSHException
)
as
e
:
...
...
@@ -112,7 +115,7 @@ class SSHConnection:
username
=
gateway
.
username
,
password
=
gateway
.
password
,
pkey
=
gateway
.
private_key_obj
,
timeout
=
c
urrent_app
.
c
onfig
[
'SSH_TIMEOUT'
])
timeout
=
config
[
'SSH_TIMEOUT'
])
except
(
paramiko
.
AuthenticationException
,
paramiko
.
BadAuthenticationType
,
SSHException
):
...
...
@@ -143,7 +146,7 @@ class SSHConnection:
proxy_command
.
insert
(
0
,
"sshpass -p {}"
.
format
(
gateway
.
password
))
if
gateway
.
private_key
:
gateway
.
set_key_dir
(
os
.
path
.
join
(
self
.
app
.
root_path
,
'keys'
))
gateway
.
set_key_dir
(
os
.
path
.
join
(
config
[
'ROOT_PATH'
]
,
'keys'
))
proxy_command
.
append
(
"-i {}"
.
format
(
gateway
.
private_key_file
))
proxy_command
=
' '
.
join
(
proxy_command
)
...
...
coco/ctx.py
View file @
a07ede5b
...
...
@@ -4,8 +4,11 @@
from
werkzeug.local
import
LocalProxy
from
functools
import
partial
from
.config
import
config
from
jms.service
import
AppService
stack
=
{}
__db_sessions
=
[]
def
_find
(
name
):
...
...
@@ -15,8 +18,6 @@ def _find(name):
raise
ValueError
(
"Not found in stack: {}"
.
format
(
name
))
current_app
=
LocalProxy
(
partial
(
_find
,
'app'
))
app_service
=
LocalProxy
(
partial
(
_find
,
'service'
))
# current_app = []
# current_service = []
app_service
=
AppService
(
config
)
current_app
=
LocalProxy
(
partial
(
_find
,
'current_app'
))
# app_service = LocalProxy(partial(_find, 'app_service'))
coco/httpd.py
View file @
a07ede5b
This diff is collapsed.
Click to expand it.
coco/interactive.py
View file @
a07ede5b
...
...
@@ -7,11 +7,12 @@ import threading
import
os
from
.
import
char
from
.config
import
config
from
.utils
import
wrap_with_line_feed
as
wr
,
wrap_with_title
as
title
,
\
wrap_with_warning
as
warning
,
is_obj_attr_has
,
is_obj_attr_eq
,
\
sort_assets
,
ugettext
as
_
,
get_logger
,
net_input
,
format_with_zh
,
\
item_max_length
,
size_of_str_with_zh
from
.ctx
import
current_app
,
app_service
item_max_length
,
size_of_str_with_zh
,
switch_lang
from
.ctx
import
app_service
from
.proxy
import
ProxyServer
logger
=
get_logger
(
__file__
)
...
...
@@ -22,8 +23,8 @@ class InteractiveServer:
def
__init__
(
self
,
client
):
self
.
client
=
client
self
.
request
=
client
.
request
self
.
assets
=
None
self
.
closed
=
False
self
.
_search_result
=
None
self
.
nodes
=
None
self
.
get_user_assets_async
()
...
...
@@ -44,28 +45,39 @@ class InteractiveServer:
value
=
self
.
filter_system_users
(
value
)
self
.
_search_result
=
value
def
display_logo
(
self
):
logo_path
=
os
.
path
.
join
(
config
[
'ROOT_PATH'
],
"logo.txt"
)
if
not
os
.
path
.
isfile
(
logo_path
):
return
with
open
(
logo_path
,
'rb'
)
as
f
:
for
i
in
f
:
if
i
.
decode
(
'utf-8'
)
.
startswith
(
'#'
):
continue
self
.
client
.
send
(
i
.
decode
(
'utf-8'
)
.
replace
(
'
\n
'
,
'
\r\n
'
))
def
display_banner
(
self
):
self
.
client
.
send
(
char
.
CLEAR_CHAR
)
logo_path
=
os
.
path
.
join
(
current_app
.
root_path
,
"logo.txt"
)
if
os
.
path
.
isfile
(
logo_path
):
with
open
(
logo_path
,
'rb'
)
as
f
:
for
i
in
f
:
if
i
.
decode
(
'utf-8'
)
.
startswith
(
'#'
):
continue
self
.
client
.
send
(
i
.
decode
(
'utf-8'
)
.
replace
(
'
\n
'
,
'
\r\n
'
))
banner
=
_
(
"""
\n
{title} {user}, 欢迎使用Jumpserver开源跳板机系统 {end}
\r\n\r
1) 输入 {green}ID{end} 直接登录 或 输入{green}部分 IP,主机名,备注{end} 进行搜索登录(如果唯一).
\r
2) 输入 {green}/{end} + {green}IP, 主机名{end} or {green}备注 {end}搜索. 如: /ip
\r
3) 输入 {green}p{end} 显示您有权限的主机.
\r
4) 输入 {green}g{end} 显示您有权限的节点
\r
5) 输入 {green}g{end} + {green}组ID{end} 显示节点下主机. 如: g1
\r
6) 输入 {green}h{end} 帮助.
\r
0) 输入 {green}q{end} 退出.
\r\n
"""
)
.
format
(
title
=
"
\033
[1;32m"
,
green
=
"
\033
[32m"
,
end
=
"
\033
[0m"
,
user
=
self
.
client
.
user
)
self
.
client
.
send
(
banner
)
self
.
display_logo
()
header
=
_
(
"
\n
{T}{T}{title} {user}, Welcome to use Jumpserver open source fortress system {end}{R}{R}"
)
menus
=
[
_
(
"{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, Comment{end} to search login(if unique).{R}"
),
_
(
"{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} search, such as: /ip.{R}"
),
_
(
"{T}3) Enter {green}p{end} to display the host you have permission.{R}"
),
_
(
"{T}4) Enter {green}g{end} to display the node that you have permission.{R}"
),
_
(
"{T}5) Enter {green}g{end} + {green}Group ID{end} to display the host under the node, such as g1.{R}"
),
_
(
"{T}6) Enter {green}s{end} Chinese-english switch.{R}"
),
_
(
"{T}7) Enter {green}h{end} help.{R}"
),
_
(
"{T}0) Enter {green}q{end} exit.{R}"
)
]
self
.
client
.
send
(
header
.
format
(
title
=
"
\033
[1;32m"
,
user
=
self
.
client
.
user
,
end
=
"
\033
[0m"
,
T
=
'
\t
'
,
R
=
'
\r\n\r
'
))
for
menu
in
menus
:
self
.
client
.
send
(
menu
.
format
(
green
=
"
\033
[32m"
,
end
=
"
\033
[0m"
,
T
=
'
\t
'
,
R
=
'
\r\n\r
'
))
def
dispatch
(
self
,
opt
):
if
opt
is
None
:
...
...
@@ -80,6 +92,9 @@ class InteractiveServer:
self
.
display_node_assets
(
int
(
opt
.
lstrip
(
"g"
)))
elif
opt
in
[
'q'
,
'Q'
,
'exit'
,
'quit'
]:
return
self
.
_sentinel
elif
opt
in
[
's'
,
'S'
]:
switch_lang
()
self
.
display_banner
()
elif
opt
in
[
'h'
,
'H'
]:
self
.
display_banner
()
else
:
...
...
@@ -124,7 +139,7 @@ class InteractiveServer:
self
.
get_user_nodes
()
if
len
(
self
.
nodes
)
==
0
:
self
.
client
.
send
(
warning
(
_
(
"
无
"
)))
self
.
client
.
send
(
warning
(
_
(
"
No
"
)))
return
id_length
=
max
(
len
(
str
(
len
(
self
.
nodes
))),
5
)
...
...
@@ -137,11 +152,12 @@ class InteractiveServer:
for
index
,
node
in
enumerate
(
self
.
nodes
,
1
):
data
=
[
index
,
node
.
name
,
node
.
assets_amount
]
self
.
client
.
send
(
wr
(
format_with_zh
(
size_list
,
*
data
)))
self
.
client
.
send
(
wr
(
_
(
"
总共
: {}"
)
.
format
(
len
(
self
.
nodes
)),
before
=
1
))
self
.
client
.
send
(
wr
(
_
(
"
Total
: {}"
)
.
format
(
len
(
self
.
nodes
)),
before
=
1
))
def
display_node_assets
(
self
,
_id
):
if
_id
>
len
(
self
.
nodes
)
or
_id
<=
0
:
self
.
client
.
send
(
wr
(
warning
(
"没有匹配分组,请重新输入"
)))
msg
=
wr
(
warning
(
_
(
"There is no matched node, please re-enter"
)))
self
.
client
.
send
(
msg
)
self
.
display_nodes
()
return
...
...
@@ -149,7 +165,7 @@ class InteractiveServer:
self
.
display_search_result
()
def
display_search_result
(
self
):
sort_by
=
c
urrent_app
.
c
onfig
[
"ASSET_LIST_SORT_BY"
]
sort_by
=
config
[
"ASSET_LIST_SORT_BY"
]
self
.
search_result
=
sort_assets
(
self
.
search_result
,
sort_by
)
fake_data
=
[
_
(
"ID"
),
_
(
"Hostname"
),
_
(
"IP"
),
_
(
"LoginAs"
)]
id_length
=
max
(
len
(
str
(
len
(
self
.
search_result
))),
4
)
...
...
@@ -160,7 +176,7 @@ class InteractiveServer:
size_list
=
[
id_length
,
hostname_length
,
16
,
sysuser_length
]
header_without_comment
=
format_with_zh
(
size_list
,
*
fake_data
)
comment_length
=
max
(
self
.
request
.
meta
[
"width"
]
-
self
.
client
.
request
.
meta
[
"width"
]
-
size_of_str_with_zh
(
header_without_comment
)
-
1
,
2
)
...
...
@@ -173,7 +189,7 @@ class InteractiveServer:
asset
.
system_users_name_list
,
asset
.
comment
]
self
.
client
.
send
(
wr
(
format_with_zh
(
size_list
,
*
data
)))
self
.
client
.
send
(
wr
(
_
(
"
总共: {} 匹配
: {}"
)
.
format
(
self
.
client
.
send
(
wr
(
_
(
"
Total: {} Match
: {}"
)
.
format
(
len
(
self
.
assets
),
len
(
self
.
search_result
)),
before
=
1
)
)
...
...
@@ -216,7 +232,7 @@ class InteractiveServer:
return
None
while
True
:
self
.
client
.
send
(
wr
(
_
(
"
选择一个登录
: "
),
after
=
1
))
self
.
client
.
send
(
wr
(
_
(
"
Select a login:
: "
),
after
=
1
))
self
.
display_system_users
(
system_users
)
opt
=
net_input
(
self
.
client
,
prompt
=
"ID> "
)
if
opt
.
isdigit
()
and
len
(
system_users
)
>
int
(
opt
):
...
...
@@ -239,7 +255,8 @@ class InteractiveServer:
self
.
search_result
=
None
if
asset
.
platform
==
"Windows"
:
self
.
client
.
send
(
warning
(
_
(
"终端不支持登录windows, 请使用web terminal访问"
))
_
(
"Terminal does not support login Windows, "
"please use web terminal to access"
))
)
return
self
.
proxy
(
asset
)
...
...
@@ -249,20 +266,21 @@ class InteractiveServer:
def
proxy
(
self
,
asset
):
system_user
=
self
.
choose_system_user
(
asset
.
system_users_granted
)
if
system_user
is
None
:
self
.
client
.
send
(
_
(
"
没有系统用户
"
))
self
.
client
.
send
(
_
(
"
No system user
"
))
return
forwarder
=
ProxyServer
(
self
.
client
,
login_from
=
'ST'
)
forwarder
.
proxy
(
asset
,
system_user
)
forwarder
=
ProxyServer
(
self
.
client
,
asset
,
system_user
)
forwarder
.
proxy
()
def
interact
(
self
):
self
.
display_banner
()
while
True
:
while
not
self
.
closed
:
try
:
opt
=
net_input
(
self
.
client
,
prompt
=
'Opt> '
,
before
=
1
)
rv
=
self
.
dispatch
(
opt
)
if
rv
is
self
.
_sentinel
:
break
except
socket
.
error
:
except
socket
.
error
as
e
:
logger
.
debug
(
"Socket error: {}"
.
format
(
e
))
break
self
.
close
()
...
...
@@ -272,7 +290,9 @@ class InteractiveServer:
thread
.
start
()
def
close
(
self
):
current_app
.
remove_client
(
self
.
client
)
logger
.
debug
(
"Interactive server server close: {}"
.
format
(
self
))
self
.
closed
=
True
# current_app.remove_client(self.client)
# def __del__(self):
# print("GC: Interactive class been gc")
coco/interface.py
View file @
a07ede5b
...
...
@@ -4,9 +4,11 @@
import
paramiko
import
threading
from
collections
import
Iterable
from
.utils
import
get_logger
from
.ctx
import
current_app
,
app_service
from
.config
import
config
from
.ctx
import
app_service
logger
=
get_logger
(
__file__
)
...
...
@@ -19,12 +21,13 @@ class SSHInterface(paramiko.ServerInterface):
https://github.com/paramiko/paramiko/blob/master/demos/demo_server.py
"""
def
__init__
(
self
,
request
):
self
.
request
=
request
def
__init__
(
self
,
connection
):
self
.
connection
=
connection
self
.
event
=
threading
.
Event
()
self
.
auth_valid
=
False
self
.
otp_auth
=
False
self
.
info
=
None
self
.
user
=
None
def
check_auth_interactive
(
self
,
username
,
submethods
):
logger
.
info
(
"Check auth interactive:
%
s
%
s"
%
(
username
,
submethods
))
...
...
@@ -58,9 +61,9 @@ class SSHInterface(paramiko.ServerInterface):
supported
=
[]
if
self
.
otp_auth
:
return
'keyboard-interactive'
if
c
urrent_app
.
c
onfig
[
"PASSWORD_AUTH"
]:
if
config
[
"PASSWORD_AUTH"
]:
supported
.
append
(
"password"
)
if
c
urrent_app
.
c
onfig
[
"PUBLIC_KEY_AUTH"
]:
if
config
[
"PUBLIC_KEY_AUTH"
]:
supported
.
append
(
"publickey"
)
return
","
.
join
(
supported
)
...
...
@@ -69,6 +72,7 @@ class SSHInterface(paramiko.ServerInterface):
def
check_auth_password
(
self
,
username
,
password
):
user
=
self
.
validate_auth
(
username
,
password
=
password
)
if
not
user
:
logger
.
warning
(
"Password and public key auth <
%
s> failed, reject it"
%
username
)
return
paramiko
.
AUTH_FAILED
...
...
@@ -81,6 +85,7 @@ class SSHInterface(paramiko.ServerInterface):
def
check_auth_publickey
(
self
,
username
,
key
):
key
=
key
.
get_base64
()
user
=
self
.
validate_auth
(
username
,
public_key
=
key
)
if
not
user
:
logger
.
debug
(
"Public key auth <
%
s> failed, try to password"
%
username
)
return
paramiko
.
AUTH_FAILED
...
...
@@ -90,113 +95,150 @@ class SSHInterface(paramiko.ServerInterface):
return
paramiko
.
AUTH_PARTIALLY_SUCCESSFUL
return
paramiko
.
AUTH_SUCCESSFUL
@staticmethod
def
check_block_ssh_user
(
username
):
block_ssh_user
=
config
[
'BLOCK_SSH_USER'
]
if
not
block_ssh_user
or
not
isinstance
(
block_ssh_user
,
Iterable
):
return
False
if
username
in
block_ssh_user
:
return
True
else
:
return
False
@staticmethod
def
check_allow_ssh_user
(
username
):
allow_ssh_user
=
config
[
"ALLOW_SSH_USER"
]
if
not
allow_ssh_user
or
not
isinstance
(
allow_ssh_user
,
Iterable
):
return
True
if
username
in
allow_ssh_user
:
return
True
else
:
return
False
def
validate_auth
(
self
,
username
,
password
=
""
,
public_key
=
""
):
if
self
.
check_block_ssh_user
(
username
)
or
\
not
self
.
check_allow_ssh_user
(
username
):
logger
.
warn
(
"User in black list or not allowed: {}"
.
format
(
username
))
return
None
info
=
app_service
.
authenticate
(
username
,
password
=
password
,
public_key
=
public_key
,
remote_addr
=
self
.
request
.
remote_ip
remote_addr
=
self
.
connection
.
addr
[
0
]
)
user
=
info
.
get
(
'user'
,
None
)
if
user
:
self
.
request
.
user
=
user
self
.
connection
.
user
=
user
self
.
info
=
info
seed
=
info
.
get
(
'seed'
,
None
)
token
=
info
.
get
(
'token'
,
None
)
if
seed
and
not
token
:
self
.
connection
.
otp_auth
=
True
self
.
otp_auth
=
True
return
user
def
check_channel_direct_tcpip_request
(
self
,
chanid
,
origin
,
destination
):
def
check_channel_direct_tcpip_request
(
self
,
chan
_
id
,
origin
,
destination
):
logger
.
debug
(
"Check channel direct tcpip request:
%
d
%
s
%
s"
%
(
chanid
,
origin
,
destination
))
self
.
request
.
type
.
append
(
'direct-tcpip'
)
self
.
request
.
meta
.
update
({
'chanid'
:
chanid
,
'origin'
:
origin
,
'destination'
:
destination
,
(
chan_id
,
origin
,
destination
))
client
=
self
.
connection
.
new_client
(
chan_id
)
client
.
request
.
kind
=
'direct-tcpip'
client
.
request
.
type
=
'direct-tcpip'
client
.
request
.
meta
.
update
({
'origin'
:
origin
,
'destination'
:
destination
})
self
.
event
.
set
()
return
0
def
check_port_forward_request
(
self
,
address
,
port
):
logger
.
info
(
"Check channel port forward request:
%
s
%
s"
%
(
address
,
port
)
)
self
.
event
.
set
()
return
False
def
check_channel_request
(
self
,
kind
,
chan_id
):
logger
.
info
(
"Check channel request:
%
s
%
d"
%
(
kind
,
chan_id
))
client
=
self
.
connection
.
new_client
(
chan_id
)
client
.
request
.
kind
=
kind
return
paramiko
.
OPEN_SUCCEEDED
def
check_channel_env_request
(
self
,
channel
,
name
,
value
):
logger
.
debug
(
"Check channel env request:
%
s,
%
s,
%
s"
%
(
channel
,
name
,
value
))
self
.
request
.
type
.
append
(
'env'
)
(
channel
.
get_id
(),
name
,
value
))
client
=
self
.
connection
.
get_client
(
channel
)
client
.
request
.
meta
[
'env'
][
name
]
=
value
return
False
def
check_channel_exec_request
(
self
,
channel
,
command
):
logger
.
debug
(
"Check channel exec request: `
%
s`"
%
command
)
self
.
request
.
type
.
append
(
'exec'
)
self
.
request
.
meta
.
update
({
'channel'
:
channel
.
get_id
(),
'command'
:
command
})
client
=
self
.
connection
.
get_client
(
channel
)
client
.
request
.
type
=
'exec'
client
.
request
.
meta
.
update
({
'command'
:
command
})
self
.
event
.
set
()
return
False
def
check_channel_forward_agent_request
(
self
,
channel
):
logger
.
debug
(
"Check channel forward agent request:
%
s"
%
channel
)
self
.
request
.
type
.
append
(
"forward-agent"
)
self
.
request
.
meta
.
update
({
'channel'
:
channel
.
get_id
()})
client
=
self
.
connection
.
get_client
(
channel
)
client
.
request
.
meta
[
'forward-agent'
]
=
True
self
.
event
.
set
()
return
Fals
e
return
Tru
e
def
check_channel_pty_request
(
self
,
channel
,
term
,
width
,
height
,
pixelwidth
,
pixelheight
,
modes
):
logger
.
info
(
"Check channel pty request:
%
s
%
s
%
s
%
s
%
s"
%
(
term
,
width
,
height
,
pixelwidth
,
pixelheight
))
self
.
request
.
type
.
append
(
'pty'
)
self
.
request
.
meta
.
update
({
'channel'
:
channel
,
'term'
:
term
,
'width'
:
width
,
(
term
,
width
,
height
,
pixelwidth
,
pixelheight
))
client
=
self
.
connection
.
get_client
(
channel
)
client
.
request
.
type
=
'pty'
client
.
request
.
meta
.
update
({
'term'
:
term
,
'width'
:
width
,
'height'
:
height
,
'pixelwidth'
:
pixelwidth
,
'pixelheight'
:
pixelheight
,
})
self
.
event
.
set
()
return
True
def
check_channel_request
(
self
,
kind
,
chanid
):
logger
.
info
(
"Check channel request:
%
s
%
d"
%
(
kind
,
chanid
))
return
paramiko
.
OPEN_SUCCEEDED
def
check_channel_shell_request
(
self
,
channel
):
logger
.
info
(
"Check channel shell request:
%
s"
%
channel
.
get_id
())
self
.
event
.
set
()
client
=
self
.
connection
.
get_client
(
channel
)
client
.
request
.
meta
[
'shell'
]
=
True
return
True
def
check_channel_subsystem_request
(
self
,
channel
,
name
):
logger
.
info
(
"Check channel subsystem request:
%
s
%
s"
%
(
channel
,
name
))
self
.
request
.
type
.
append
(
'subsystem'
)
self
.
request
.
meta
.
update
({
'channel'
:
channel
.
get_id
(),
'name'
:
name
})
logger
.
info
(
"Check channel subsystem request:
%
s"
%
name
)
client
=
self
.
connection
.
get_client
(
channel
)
client
.
request
.
type
=
'subsystem'
client
.
request
.
meta
[
'subsystem'
]
=
name
self
.
event
.
set
()
return
super
()
.
check_channel_subsystem_request
(
channel
,
name
)
def
check_channel_window_change_request
(
self
,
channel
,
width
,
height
,
pixelwidth
,
pixelheight
):
self
.
request
.
meta
.
update
({
client
=
self
.
connection
.
get_client
(
channel
)
client
.
request
.
meta
.
update
({
'width'
:
width
,
'height'
:
height
,
'pixelwidth'
:
pixelwidth
,
'pixelheight'
:
pixelheight
,
})
self
.
request
.
change_size_even
t
.
set
()
client
.
change_size_ev
t
.
set
()
return
True
def
check_channel_x11_request
(
self
,
channel
,
single_connection
,
auth_protocol
,
auth_cookie
,
screen_number
):
logger
.
info
(
"Check channel x11 request
%
s
%
s
%
s
%
s
%
s
"
%
(
channel
,
single_connection
,
auth_protocol
,
logger
.
info
(
"Check channel x11 request
%
s
%
s
%
s
%
s"
%
(
single_connection
,
auth_protocol
,
auth_cookie
,
screen_number
))
self
.
request
.
type
.
append
(
'x11'
)
self
.
request
.
meta
.
update
({
'channel'
:
channel
.
get_id
(),
'single_connection'
:
single_connection
,
'auth_protocol'
:
auth_protocol
,
'auth_cookie'
:
auth_cookie
,
client
=
self
.
connection
.
get_client
(
channel
)
# client.request_x11_event.set()
client
.
request
.
meta
.
update
({
'single_connection'
:
single_connection
,
'auth_protocol'
:
auth_protocol
,
'auth_cookie'
:
auth_cookie
,
'screen_number'
:
screen_number
,
})
self
.
event
.
set
()
return
False
def
check_port_forward_request
(
self
,
address
,
port
):
logger
.
info
(
"Check channel port forward request:
%
s
%
s"
%
(
address
,
port
))
self
.
request
.
type
.
append
(
'port-forward'
)
self
.
request
.
meta
.
update
({
'address'
:
address
,
'port'
:
port
})
self
.
event
.
set
()
return
False
def
get_banner
(
self
):
...
...
coco/models.py
View file @
a07ede5b
This diff is collapsed.
Click to expand it.
coco/proxy.py
View file @
a07ede5b
...
...
@@ -5,14 +5,13 @@
import
threading
import
time
from
paramiko.ssh_exception
import
SSHException
from
.session
import
Session
from
.models
import
Server
,
TelnetServer
from
.connection
import
SSHConnection
,
TelnetConnection
from
.ctx
import
current_app
,
app_service
from
.ctx
import
app_service
from
.config
import
config
from
.utils
import
wrap_with_line_feed
as
wr
,
wrap_with_warning
as
warning
,
\
get_logger
,
net_input
get_logger
,
net_input
,
ugettext
as
_
logger
=
get_logger
(
__file__
)
...
...
@@ -22,140 +21,116 @@ AUTO_LOGIN = 'auto'
class
ProxyServer
:
def
__init__
(
self
,
client
,
login_from
):
def
__init__
(
self
,
client
,
asset
,
system_user
):
self
.
client
=
client
self
.
asset
=
asset
self
.
system_user
=
system_user
self
.
server
=
None
self
.
login_from
=
login_from
self
.
connecting
=
True
self
.
stop_event
=
threading
.
Event
()
def
get_system_user_auth
(
self
,
system_user
):
def
get_system_user_auth
_or_manual_set
(
self
):
"""
获取系统用户的认证信息,密码或秘钥
:return: system user have full info
"""
password
,
private_key
=
\
app_service
.
get_system_user_auth_info
(
system_user
)
if
system_user
.
login_mode
==
MANUAL_LOGIN
or
(
not
password
and
not
private_key
):
prompt
=
"{}'s password: "
.
format
(
system_user
.
username
)
app_service
.
get_system_user_auth_info
(
self
.
system_user
)
if
self
.
system_user
.
login_mode
==
MANUAL_LOGIN
\
or
(
not
password
and
not
private_key
):
prompt
=
"{}'s password: "
.
format
(
self
.
system_user
.
username
)
password
=
net_input
(
self
.
client
,
prompt
=
prompt
,
sensitive
=
True
)
system_user
.
password
=
password
system_user
.
private_key
=
private_key
s
elf
.
s
ystem_user
.
password
=
password
s
elf
.
s
ystem_user
.
private_key
=
private_key
def
proxy
(
self
,
asset
,
system_user
):
if
asset
.
protocol
!=
system_user
.
protocol
:
def
check_protocol
(
self
):
if
self
.
asset
.
protocol
!=
self
.
system_user
.
protocol
:
msg
=
'System user <{}> and asset <{}> protocol are inconsistent.'
.
format
(
s
ystem_user
.
name
,
asset
.
hostname
s
elf
.
system_user
.
name
,
self
.
asset
.
hostname
)
self
.
client
.
send
(
warning
(
wr
(
msg
,
before
=
1
,
after
=
0
)))
return
False
return
True
def
manual_set_system_user_username_if_need
(
self
):
if
self
.
system_user
.
login_mode
==
MANUAL_LOGIN
and
\
not
self
.
system_user
.
username
:
username
=
net_input
(
self
.
client
,
prompt
=
'username: '
,
before
=
1
)
self
.
system_user
.
username
=
username
return
True
return
False
def
proxy
(
self
):
if
not
self
.
check_protocol
():
return
if
system_user
.
login_mode
==
MANUAL_LOGIN
and
not
system_user
.
username
:
system_user_name
=
net_input
(
self
.
client
,
prompt
=
'username: '
,
before
=
1
)
system_user
.
username
=
system_user_name
self
.
get_system_user_auth
(
system_user
)
self
.
send_connecting_message
(
asset
,
system_user
)
self
.
server
=
self
.
get_server_conn
(
asset
,
system_user
)
self
.
manual_set_system_user_username_if_need
()
self
.
get_system_user_auth_or_manual_set
()
self
.
server
=
self
.
get_server_conn
()
if
self
.
server
is
None
:
return
command_recorder
=
current_app
.
new_command_recorder
()
replay_recorder
=
current_app
.
new_replay_recorder
()
session
=
Session
(
self
.
client
,
self
.
server
,
self
.
login_from
,
command_recorder
=
command_recorder
,
replay_recorder
=
replay_recorder
,
)
current_app
.
add_session
(
session
)
self
.
watch_win_size_change_async
()
session
=
Session
.
new_session
(
self
.
client
,
self
.
server
)
session
.
bridge
()
self
.
stop_event
.
set
()
self
.
end_watch_win_size_change
()
current_app
.
remove_session
(
session
)
Session
.
remove_session
(
session
.
id
)
self
.
server
.
close
()
def
validate_permission
(
self
,
asset
,
system_user
):
def
validate_permission
(
self
):
"""
验证用户是否有连接改资产的权限
:return: True or False
"""
return
app_service
.
validate_user_asset_permission
(
self
.
client
.
user
.
id
,
asset
.
id
,
system_user
.
id
self
.
client
.
user
.
id
,
self
.
asset
.
id
,
self
.
system_user
.
id
)
def
get_server_conn
(
self
,
asset
,
system_user
):
logger
.
info
(
"Connect to {}"
.
format
(
asset
.
hostname
))
if
not
self
.
validate_permission
(
asset
,
system_user
):
self
.
client
.
send
(
warning
(
'No permission'
))
return
None
if
system_user
.
protocol
==
asset
.
protocol
==
'telnet'
:
server
=
self
.
get_telnet_server_conn
(
asset
,
system_user
)
elif
system_user
.
protocol
==
asset
.
protocol
==
'ssh'
:
server
=
self
.
get_ssh_server_conn
(
asset
,
system_user
)
def
get_server_conn
(
self
):
logger
.
info
(
"Connect to {}"
.
format
(
self
.
asset
.
hostname
))
self
.
send_connecting_message
()
if
not
self
.
validate_permission
():
self
.
client
.
send
(
warning
(
_
(
'No permission'
)))
server
=
None
elif
self
.
system_user
.
protocol
==
self
.
asset
.
protocol
==
'telnet'
:
server
=
self
.
get_telnet_server_conn
()
elif
self
.
system_user
.
protocol
==
self
.
asset
.
protocol
==
'ssh'
:
server
=
self
.
get_ssh_server_conn
()
else
:
server
=
None
self
.
connecting
=
False
self
.
client
.
send
(
b
'
\r\n
'
)
return
server
# Todo: Support telnet
def
get_telnet_server_conn
(
self
,
asset
,
system_user
):
telnet
=
TelnetConnection
(
asset
,
system_user
,
self
.
client
)
def
get_telnet_server_conn
(
self
):
telnet
=
TelnetConnection
(
self
.
asset
,
self
.
system_user
,
self
.
client
)
sock
,
msg
=
telnet
.
get_socket
()
if
not
sock
:
self
.
client
.
send
(
warning
(
wr
(
msg
,
before
=
1
,
after
=
0
)))
server
=
None
else
:
server
=
TelnetServer
(
sock
,
asset
,
system_user
)
# self.client.send(b'\r\n')
self
.
connecting
=
False
server
=
TelnetServer
(
sock
,
self
.
asset
,
self
.
system_user
)
return
server
def
get_ssh_server_conn
(
self
,
asset
,
system_user
):
def
get_ssh_server_conn
(
self
):
request
=
self
.
client
.
request
term
=
request
.
meta
.
get
(
'term'
,
'xterm'
)
width
=
request
.
meta
.
get
(
'width'
,
80
)
height
=
request
.
meta
.
get
(
'height'
,
24
)
ssh
=
SSHConnection
()
chan
,
sock
,
msg
=
ssh
.
get_channel
(
asset
,
system_user
,
term
=
term
,
width
=
width
,
height
=
height
self
.
asset
,
self
.
system_user
,
term
=
term
,
width
=
width
,
height
=
height
)
if
not
chan
:
self
.
client
.
send
(
warning
(
wr
(
msg
,
before
=
1
,
after
=
0
)))
server
=
None
else
:
server
=
Server
(
chan
,
sock
,
asset
,
system_user
)
self
.
connecting
=
False
self
.
client
.
send
(
b
'
\r\n
'
)
server
=
Server
(
chan
,
sock
,
self
.
asset
,
self
.
system_user
)
return
server
def
watch_win_size_change
(
self
):
while
self
.
client
.
request
.
change_size_event
.
wait
():
if
self
.
stop_event
.
is_set
():
break
self
.
client
.
request
.
change_size_event
.
clear
()
width
=
self
.
client
.
request
.
meta
.
get
(
'width'
,
80
)
height
=
self
.
client
.
request
.
meta
.
get
(
'height'
,
24
)
logger
.
debug
(
"Change win size:
%
s -
%
s"
%
(
width
,
height
))
try
:
self
.
server
.
chan
.
resize_pty
(
width
=
width
,
height
=
height
)
except
SSHException
:
break
def
watch_win_size_change_async
(
self
):
if
not
isinstance
(
self
.
server
,
Server
):
return
thread
=
threading
.
Thread
(
target
=
self
.
watch_win_size_change
)
thread
.
daemon
=
True
thread
.
start
()
def
end_watch_win_size_change
(
self
):
self
.
client
.
request
.
change_size_event
.
set
()
def
send_connecting_message
(
self
,
asset
,
system_user
):
def
send_connecting_message
(
self
):
def
func
():
delay
=
0.0
self
.
client
.
send
(
'Connecting to {}@{} {:.1f}'
.
format
(
s
ystem_user
,
asset
,
delay
)
self
.
client
.
send
(
_
(
'Connecting to {}@{} {:.1f}'
)
.
format
(
s
elf
.
system_user
,
self
.
asset
,
delay
)
)
while
self
.
connecting
and
delay
<
c
urrent_app
.
c
onfig
[
'SSH_TIMEOUT'
]:
while
self
.
connecting
and
delay
<
config
[
'SSH_TIMEOUT'
]:
if
0
<=
delay
<
10
:
self
.
client
.
send
(
'
\x08\x08\x08
{:.1f}'
.
format
(
delay
)
.
encode
())
else
:
...
...
coco/recorder.py
View file @
a07ede5b
...
...
@@ -12,9 +12,10 @@ from copy import deepcopy
import
jms_storage
from
.config
import
config
from
.utils
import
get_logger
,
Singleton
from
.
alignmen
t
import
MemoryQueue
from
.ctx
import
current_app
,
app_service
from
.
struc
t
import
MemoryQueue
from
.ctx
import
app_service
logger
=
get_logger
(
__file__
)
BUF_SIZE
=
1024
...
...
@@ -48,7 +49,7 @@ class ReplayRecorder(metaclass=abc.ABCMeta):
def
session_start
(
self
,
session_id
):
self
.
time_start
=
time
.
time
()
filename
=
session_id
+
'.replay.gz'
self
.
file_path
=
os
.
path
.
join
(
c
urrent_app
.
c
onfig
[
'LOG_DIR'
],
filename
)
self
.
file_path
=
os
.
path
.
join
(
config
[
'LOG_DIR'
],
filename
)
self
.
file
=
gzip
.
open
(
self
.
file_path
,
'at'
)
self
.
file
.
write
(
'{'
)
...
...
@@ -58,9 +59,9 @@ class ReplayRecorder(metaclass=abc.ABCMeta):
self
.
upload_replay
(
session_id
)
def
get_storage
(
self
):
conf
ig
=
deepcopy
(
current_app
.
config
[
"REPLAY_STORAGE"
])
conf
ig
[
"SERVICE"
]
=
app_service
self
.
storage
=
jms_storage
.
get_object_storage
(
conf
ig
)
conf
=
deepcopy
(
config
[
"REPLAY_STORAGE"
])
conf
[
"SERVICE"
]
=
app_service
self
.
storage
=
jms_storage
.
get_object_storage
(
conf
)
def
upload_replay
(
self
,
session_id
,
times
=
3
):
if
times
<
1
:
...
...
@@ -130,9 +131,9 @@ class CommandRecorder(metaclass=Singleton):
self
.
queue
.
put
(
data
)
def
get_storage
(
self
):
conf
ig
=
deepcopy
(
current_app
.
config
[
"COMMAND_STORAGE"
])
conf
ig
[
'SERVICE'
]
=
app_service
self
.
storage
=
jms_storage
.
get_log_storage
(
conf
ig
)
conf
=
deepcopy
(
config
[
"COMMAND_STORAGE"
])
conf
[
'SERVICE'
]
=
app_service
self
.
storage
=
jms_storage
.
get_log_storage
(
conf
)
def
push_to_server_async
(
self
):
def
func
():
...
...
@@ -153,9 +154,19 @@ class CommandRecorder(metaclass=Singleton):
thread
.
start
()
def
session_start
(
self
,
session_id
):
print
(
"Session start: {}"
.
format
(
session_id
))
pass
def
session_end
(
self
,
session_id
):
print
(
"Session end: {}"
.
format
(
session_id
))
pass
def
get_command_recorder
():
return
CommandRecorder
()
def
get_replay_recorder
():
return
ReplayRecorder
()
def
get_recorder
():
return
get_command_recorder
(),
get_replay_recorder
()
\ No newline at end of file
coco/service.py
0 → 100644
View file @
a07ede5b
# -*- coding: utf-8 -*-
#
from
.ctx
import
stack
def
init_app
(
app
):
stack
[
'current_app'
]
=
app
coco/session.py
View file @
a07ede5b
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import
threading
import
uuid
import
datetime
import
selectors
import
time
from
.utils
import
get_logger
from
.utils
import
get_logger
,
wrap_with_warning
as
warn
,
\
wrap_with_line_feed
as
wr
,
ugettext
as
_
,
ignore_error
from
.ctx
import
app_service
from
.struct
import
SelectEvent
from
.recorder
import
get_recorder
BUF_SIZE
=
1024
logger
=
get_logger
(
__file__
)
class
Session
:
def
__init__
(
self
,
client
,
server
,
login_from
,
command_recorder
=
None
,
replay_recorder
=
None
):
sessions
=
{}
def
__init__
(
self
,
client
,
server
):
self
.
id
=
str
(
uuid
.
uuid4
())
self
.
client
=
client
# Master of the session, it's a client sock
self
.
server
=
server
# Server channel
self
.
login_from
=
login_from
# Login from
self
.
_watchers
=
[]
# Only watch session
self
.
_sharers
=
[]
# Join to the session, read and write
self
.
replaying
=
True
self
.
date_
created
=
datetime
.
datetime
.
utcnow
()
self
.
date_
start
=
datetime
.
datetime
.
utcnow
()
self
.
date_end
=
None
self
.
stop_evt
=
threading
.
Event
()
self
.
is_finished
=
False
self
.
closed
=
False
self
.
sel
=
selectors
.
DefaultSelector
()
self
.
_command_recorder
=
command_recorder
self
.
_replay_recorder
=
replay_recorder
self
.
_command_recorder
=
None
self
.
_replay_recorder
=
None
self
.
stop_evt
=
SelectEvent
()
self
.
server
.
set_session
(
self
)
self
.
date_last_active
=
datetime
.
datetime
.
utcnow
()
@classmethod
def
new_session
(
cls
,
client
,
server
):
session
=
cls
(
client
,
server
)
command_recorder
,
replay_recorder
=
get_recorder
()
session
.
set_command_recorder
(
command_recorder
)
session
.
set_replay_recorder
(
replay_recorder
)
cls
.
sessions
[
session
.
id
]
=
session
app_service
.
create_session
(
session
.
to_json
())
return
session
@classmethod
def
get_session
(
cls
,
sid
):
return
cls
.
sessions
.
get
(
sid
)
@classmethod
def
remove_session
(
cls
,
sid
):
session
=
cls
.
get_session
(
sid
)
if
session
:
session
.
close
()
app_service
.
finish_session
(
session
.
to_json
())
app_service
.
finish_replay
(
sid
)
del
cls
.
sessions
[
sid
]
def
add_watcher
(
self
,
watcher
,
silent
=
False
):
"""
Add a watcher, and will be transport server side msg to it.
...
...
@@ -64,6 +93,10 @@ class Session:
self
.
sel
.
register
(
sharer
,
selectors
.
EVENT_READ
)
self
.
_sharers
.
append
(
sharer
)
@property
def
closed_unexpected
(
self
):
return
not
self
.
is_finished
and
(
self
.
client
.
closed
or
self
.
server
.
closed
)
def
remove_sharer
(
self
,
sharer
):
logger
.
info
(
"Session
%
s remove sharer
%
s"
%
(
self
.
id
,
sharer
))
sharer
.
send
(
"Leave session {} at {}"
...
...
@@ -79,8 +112,6 @@ class Session:
self
.
_replay_recorder
=
recorder
def
put_command
(
self
,
_input
,
_output
):
if
not
_input
:
return
self
.
_command_recorder
.
record
({
"session"
:
self
.
id
,
"org_id"
:
self
.
server
.
asset
.
org_id
,
...
...
@@ -107,13 +138,14 @@ class Session:
self
.
_replay_recorder
.
session_end
(
self
.
id
)
self
.
_command_recorder
.
session_end
(
self
.
id
)
def
terminate
(
self
):
msg
=
b
"Terminate by administrator
\r\n
"
def
terminate
(
self
,
msg
=
None
):
if
not
msg
:
msg
=
_
(
"Terminated by administrator"
)
try
:
self
.
client
.
send
(
msg
)
self
.
client
.
send
(
wr
(
warn
(
msg
),
before
=
1
)
)
except
OSError
:
pass
self
.
close
()
self
.
stop_evt
.
set
()
def
bridge
(
self
):
"""
...
...
@@ -124,16 +156,17 @@ class Session:
self
.
pre_bridge
()
self
.
sel
.
register
(
self
.
client
,
selectors
.
EVENT_READ
)
self
.
sel
.
register
(
self
.
server
,
selectors
.
EVENT_READ
)
while
not
self
.
stop_evt
.
is_set
():
events
=
self
.
sel
.
select
()
self
.
sel
.
register
(
self
.
stop_evt
,
selectors
.
EVENT_READ
)
self
.
sel
.
register
(
self
.
client
.
change_size_evt
,
selectors
.
EVENT_READ
)
while
not
self
.
is_finished
:
events
=
self
.
sel
.
select
(
timeout
=
60
)
for
sock
in
[
key
.
fileobj
for
key
,
_
in
events
]:
data
=
sock
.
recv
(
BUF_SIZE
)
# self.put_replay(data)
if
sock
==
self
.
server
:
if
len
(
data
)
==
0
:
msg
=
"Server close the connection"
logger
.
info
(
msg
)
self
.
close
()
self
.
is_finished
=
True
break
self
.
date_last_active
=
datetime
.
datetime
.
utcnow
()
...
...
@@ -145,30 +178,32 @@ class Session:
logger
.
info
(
msg
)
for
watcher
in
self
.
_watchers
+
self
.
_sharers
:
watcher
.
send
(
msg
.
encode
(
"utf-8"
))
self
.
close
()
self
.
is_finished
=
True
break
self
.
server
.
send
(
data
)
elif
sock
in
self
.
_sharers
:
if
len
(
data
)
==
0
:
logger
.
info
(
"Sharer {} leave the session {}"
.
format
(
sock
,
self
.
id
))
self
.
remove_sharer
(
sock
)
self
.
server
.
send
(
data
)
elif
sock
in
self
.
_watchers
:
if
len
(
data
)
==
0
:
self
.
_watchers
.
remove
(
sock
)
logger
.
info
(
"Watcher {} leave the session {}"
.
format
(
sock
,
self
.
id
))
elif
sock
==
self
.
stop_evt
:
self
.
is_finished
=
True
break
elif
sock
==
self
.
client
.
change_size_evt
:
self
.
resize_win_size
()
logger
.
info
(
"Session stop event set: {}"
.
format
(
self
.
id
))
def
set_size
(
self
,
width
,
height
):
def
resize_win_size
(
self
):
width
,
height
=
self
.
client
.
request
.
meta
[
'width'
],
\
self
.
client
.
request
.
meta
[
'height'
]
logger
.
debug
(
"Resize server chan size {}*{}"
.
format
(
width
,
height
))
self
.
server
.
resize_pty
(
width
=
width
,
height
=
height
)
@ignore_error
def
close
(
self
):
if
self
.
closed
:
logger
.
info
(
"Session has been closed: {} "
.
format
(
self
.
id
))
return
logger
.
info
(
"Close the session: {} "
.
format
(
self
.
id
))
self
.
stop_evt
.
set
()
self
.
is_finished
=
True
self
.
closed
=
True
self
.
post_bridge
()
self
.
date_end
=
datetime
.
datetime
.
utcnow
()
self
.
server
.
close
()
def
to_json
(
self
):
return
{
...
...
@@ -177,11 +212,10 @@ class Session:
"asset"
:
self
.
server
.
asset
.
hostname
,
"org_id"
:
self
.
server
.
asset
.
org_id
,
"system_user"
:
self
.
server
.
system_user
.
username
,
"login_from"
:
self
.
login_from
,
"login_from"
:
self
.
client
.
login_from
,
"remote_addr"
:
self
.
client
.
addr
[
0
],
"is_finished"
:
True
if
self
.
stop_evt
.
is_set
()
else
False
,
"date_last_active"
:
self
.
date_last_active
.
strftime
(
"
%
Y-
%
m-
%
d
%
H:
%
M:
%
S"
)
+
" +0000"
,
"date_start"
:
self
.
date_created
.
strftime
(
"
%
Y-
%
m-
%
d
%
H:
%
M:
%
S"
)
+
" +0000"
,
"is_finished"
:
self
.
is_finished
,
"date_start"
:
self
.
date_start
.
strftime
(
"
%
Y-
%
m-
%
d
%
H:
%
M:
%
S"
)
+
" +0000"
,
"date_end"
:
self
.
date_end
.
strftime
(
"
%
Y-
%
m-
%
d
%
H:
%
M:
%
S"
)
+
" +0000"
if
self
.
date_end
else
None
}
...
...
coco/sftp.py
View file @
a07ede5b
...
...
@@ -2,9 +2,9 @@ import os
import
tempfile
import
paramiko
import
time
from
.ctx
import
app_service
from
datetime
import
datetime
from
.ctx
import
app_service
from
.connection
import
SSHConnection
...
...
@@ -53,7 +53,7 @@ class SFTPServer(paramiko.SFTPServerInterface):
def
get_perm_hosts
(
self
):
hosts
=
{}
assets
=
app_service
.
get_user_assets
(
self
.
server
.
request
.
user
self
.
server
.
connection
.
user
)
for
asset
in
assets
:
key
=
asset
.
hostname
...
...
coco/sshd.py
View file @
a07ede5b
...
...
@@ -5,14 +5,15 @@
import
os
import
socket
import
threading
import
paramiko
from
.utils
import
ssh_key_gen
,
get_logger
from
.interface
import
SSHInterface
from
.interactive
import
InteractiveServer
from
.models
import
C
lient
,
Request
from
.models
import
C
onnection
from
.sftp
import
SFTPServer
from
.c
tx
import
current_app
from
.c
onfig
import
config
logger
=
get_logger
(
__file__
)
BACKLOG
=
5
...
...
@@ -24,10 +25,11 @@ class SSHServer:
self
.
stop_evt
=
threading
.
Event
()
self
.
workers
=
[]
self
.
pipe
=
None
self
.
connections
=
[]
@property
def
host_key
(
self
):
host_key_path
=
os
.
path
.
join
(
c
urrent_app
.
root_path
,
'keys'
,
'host_rsa_key'
)
host_key_path
=
os
.
path
.
join
(
c
onfig
[
'ROOT_PATH'
]
,
'keys'
,
'host_rsa_key'
)
if
not
os
.
path
.
isfile
(
host_key_path
):
self
.
gen_host_key
(
host_key_path
)
return
paramiko
.
RSAKey
(
filename
=
host_key_path
)
...
...
@@ -39,24 +41,27 @@ class SSHServer:
f
.
write
(
ssh_key
)
def
run
(
self
):
host
=
c
urrent_app
.
c
onfig
[
"BIND_HOST"
]
port
=
c
urrent_app
.
c
onfig
[
"SSHD_PORT"
]
host
=
config
[
"BIND_HOST"
]
port
=
config
[
"SSHD_PORT"
]
print
(
'Starting ssh server at {}:{}'
.
format
(
host
,
port
))
sock
=
socket
.
socket
(
socket
.
AF_INET
,
socket
.
SOCK_STREAM
)
sock
.
setsockopt
(
socket
.
SOL_SOCKET
,
socket
.
SO_REUSEADDR
,
1
)
sock
.
setsockopt
(
socket
.
SOL_SOCKET
,
socket
.
SO_REUSEADDR
,
True
)
sock
.
bind
((
host
,
port
))
sock
.
listen
(
BACKLOG
)
while
not
self
.
stop_evt
.
is_set
():
try
:
client
,
addr
=
sock
.
accept
()
logger
.
info
(
"Get ssh request from {}: {}"
.
format
(
*
addr
))
thread
=
threading
.
Thread
(
target
=
self
.
handle_connection
,
args
=
(
client
,
addr
))
thread
.
daemon
=
True
thread
.
start
()
t
=
threading
.
Thread
(
target
=
self
.
handle_connection
,
args
=
(
client
,
addr
))
t
.
daemon
=
True
t
.
start
()
except
IndexError
as
e
:
logger
.
error
(
"Start SSH server error: {}"
.
format
(
e
))
def
new_connection
(
self
,
addr
,
sock
):
connection
=
Connection
.
new_connection
(
addr
=
addr
,
sock
=
sock
)
self
.
connections
.
append
(
connection
)
return
connection
def
handle_connection
(
self
,
sock
,
addr
):
transport
=
paramiko
.
Transport
(
sock
,
gss_kex
=
False
)
try
:
...
...
@@ -68,52 +73,52 @@ class SSHServer:
transport
.
set_subsystem_handler
(
'sftp'
,
paramiko
.
SFTPServer
,
SFTPServer
)
request
=
Request
(
addr
)
server
=
SSHInterface
(
request
)
connection
=
self
.
new_connection
(
addr
,
sock
=
sock
)
server
=
SSHInterface
(
connection
)
try
:
transport
.
start_server
(
server
=
server
)
except
paramiko
.
SSHException
:
logger
.
warning
(
"SSH negotiation failed"
)
return
except
EOFError
:
logger
.
warning
(
"Handle EOF Error
"
)
except
EOFError
as
e
:
logger
.
warning
(
"Handle EOF Error
: {}"
.
format
(
e
)
)
return
while
True
:
if
not
transport
.
is_active
():
transport
.
close
()
sock
.
close
()
break
while
transport
.
is_active
():
chan
=
transport
.
accept
()
server
.
event
.
wait
(
5
)
if
chan
is
None
:
continue
if
not
server
.
event
.
is_set
():
logger
.
warning
(
"Client not request a valid request, exiting"
)
sock
.
close
()
return
else
:
server
.
event
.
clear
()
t
=
threading
.
Thread
(
target
=
self
.
handle_chan
,
args
=
(
chan
,
request
))
client
=
connection
.
clients
.
get
(
chan
.
get_id
())
client
.
chan
=
chan
t
=
threading
.
Thread
(
target
=
self
.
dispatch
,
args
=
(
client
,))
t
.
daemon
=
True
t
.
start
()
Connection
.
remove_connection
(
connection
.
id
)
def
handle_chan
(
self
,
chan
,
request
):
client
=
Client
(
chan
,
request
)
current_app
.
add_client
(
client
)
self
.
dispatch
(
client
)
def
dispatch
(
self
,
client
):
@staticmethod
def
dispatch
(
client
):
supported
=
{
'pty'
,
'x11'
,
'forward-agent'
}
request_type
=
set
(
client
.
request
.
type
)
if
supported
&
request_type
:
logger
.
info
(
"Request type `pty`, dispatch to interactive mode"
)
chan_type
=
client
.
request
.
type
kind
=
client
.
request
.
kind
if
kind
==
'session'
and
chan_type
in
supported
:
logger
.
info
(
"Request type `{}:{}`, dispatch to interactive mode"
.
format
(
kind
,
chan_type
))
InteractiveServer
(
client
)
.
interact
()
elif
'subsystem'
in
request_type
:
connection
=
Connection
.
get_connection
(
client
.
connection_id
)
connection
.
remove_client
(
client
.
id
)
elif
chan_type
==
'subsystem'
:
pass
else
:
logger
.
info
(
"Request type `{}`"
.
format
(
request_type
))
client
.
send
(
"Not support request type:
%
s"
%
request_type
)
msg
=
"Request type `{}:{}` not support now"
.
format
(
kind
,
chan_type
)
logger
.
info
(
msg
)
client
.
send
(
msg
)
def
shutdown
(
self
):
self
.
stop_evt
.
set
()
coco/
alignmen
t.py
→
coco/
struc
t.py
View file @
a07ede5b
...
...
@@ -3,6 +3,7 @@
#
import
queue
import
socket
class
MultiQueueMixin
:
...
...
@@ -24,16 +25,31 @@ class MemoryQueue(MultiQueueMixin, queue.Queue):
pass
def
get_queue
(
config
):
queue_engine
=
config
[
'QUEUE_ENGINE'
]
queue_size
=
config
[
'QUEUE_MAX_SIZE'
]
class
SizedList
(
list
):
def
__init__
(
self
,
maxsize
=
0
):
self
.
maxsize
=
maxsize
self
.
size
=
0
super
()
.
__init__
()
if
queue_engine
==
"server"
:
replay_queue
=
MemoryQueue
(
queue_size
)
command_queue
=
MemoryQueue
(
queue_size
)
else
:
replay_queue
=
MemoryQueue
(
queue_size
)
command_queue
=
MemoryQueue
(
queue_size
)
def
append
(
self
,
b
):
if
self
.
maxsize
==
0
or
self
.
size
<
self
.
maxsize
:
super
()
.
append
(
b
)
self
.
size
+=
len
(
b
)
return
replay_queue
,
command_queue
def
clean
(
self
):
self
.
size
=
0
del
self
[:]
class
SelectEvent
:
def
__init__
(
self
):
self
.
p1
,
self
.
p2
=
socket
.
socketpair
()
def
set
(
self
):
self
.
p2
.
send
(
b
'0'
)
def
fileno
(
self
):
return
self
.
p1
.
fileno
()
def
__getattr__
(
self
,
item
):
return
getattr
(
self
.
p1
,
item
)
coco/tasks.py
View file @
a07ede5b
...
...
@@ -2,8 +2,9 @@
# -*- coding: utf-8 -*-
#
from
.ctx
import
current_app
,
app_service
from
.ctx
import
app_service
from
.utils
import
get_logger
from
.session
import
Session
logger
=
get_logger
(
__file__
)
...
...
@@ -18,12 +19,7 @@ class TaskHandler:
def
handle_kill_session
(
task
):
logger
.
info
(
"Handle kill session task: {}"
.
format
(
task
.
args
))
session_id
=
task
.
args
session
=
None
for
s
in
current_app
.
sessions
:
if
s
.
id
==
session_id
:
session
=
s
break
session
=
Session
.
sessions
.
get
(
session_id
)
if
session
:
session
.
terminate
()
app_service
.
finish_task
(
task
.
id
)
...
...
coco/utils.py
View file @
a07ede5b
...
...
@@ -10,15 +10,21 @@ import os
import
gettext
from
io
import
StringIO
from
binascii
import
hexlify
from
werkzeug.local
import
Local
,
LocalProxy
from
functools
import
partial
,
wraps
import
builtins
import
paramiko
import
pyte
from
.
import
char
from
.c
tx
import
stack
from
.c
onfig
import
config
BASE_DIR
=
os
.
path
.
abspath
(
os
.
path
.
dirname
(
os
.
path
.
dirname
(
__file__
)))
APP_NAME
=
"coco"
LOCALE_DIR
=
os
.
path
.
join
(
BASE_DIR
,
'locale'
)
class
Singleton
(
type
):
def
__init__
(
cls
,
*
args
,
**
kwargs
):
...
...
@@ -270,12 +276,6 @@ def sort_assets(assets, order_by='hostname'):
return
assets
def
_gettext
():
gettext
.
bindtextdomain
(
"coco"
,
os
.
path
.
join
(
BASE_DIR
,
"locale"
))
gettext
.
textdomain
(
"coco"
)
return
gettext
.
gettext
def
get_private_key_fingerprint
(
key
):
line
=
hexlify
(
key
.
get_fingerprint
())
return
b
':'
.
join
([
line
[
i
:
i
+
2
]
for
i
in
range
(
0
,
len
(
line
),
2
)])
...
...
@@ -342,7 +342,7 @@ def net_input(client, prompt='Opt> ', sensitive=False, before=0, after=0):
input_data
.
append
(
data
[:
-
1
])
multi_char_with_enter
=
True
# If user type ENTER we should get user input
# If user type
s
ENTER we should get user input
if
data
in
char
.
ENTER_CHAR
or
multi_char_with_enter
:
client
.
send
(
wrap_with_line_feed
(
b
''
,
after
=
2
))
option
=
parser
.
parse_input
(
input_data
)
...
...
@@ -356,14 +356,6 @@ def net_input(client, prompt='Opt> ', sensitive=False, before=0, after=0):
input_data
.
append
(
data
)
def
register_app
(
app
):
stack
[
'app'
]
=
app
def
register_service
(
service
):
stack
[
'service'
]
=
service
zh_pattern
=
re
.
compile
(
r'[\u4e00-\u9fa5]'
)
...
...
@@ -419,5 +411,62 @@ def int_length(i):
return
len
(
str
(
i
))
ugettext
=
_gettext
()
def
_get_trans
():
gettext
.
install
(
APP_NAME
,
LOCALE_DIR
)
zh
=
gettext
.
translation
(
APP_NAME
,
LOCALE_DIR
,
[
"zh_CN"
])
en
=
gettext
.
translation
(
APP_NAME
,
LOCALE_DIR
,
[
"en"
])
return
zh
,
en
trans_zh
,
trans_en
=
_get_trans
()
_thread_locals
=
Local
()
def
set_current_lang
(
lang
):
setattr
(
_thread_locals
,
'LANGUAGE_CODE'
,
lang
)
def
get_current_lang
(
attr
):
return
getattr
(
_thread_locals
,
attr
,
None
)
def
_gettext
(
lang
):
if
lang
==
'en'
:
trans_en
.
install
()
else
:
trans_zh
.
install
()
return
builtins
.
__dict__
[
'_'
]
def
_find
(
attr
):
lang
=
get_current_lang
(
attr
)
if
lang
is
None
:
lang
=
config
[
'LANGUAGE_CODE'
]
set_current_lang
(
lang
)
return
_gettext
(
lang
)
def
switch_lang
():
lang
=
get_current_lang
(
'LANGUAGE_CODE'
)
if
lang
==
'zh'
:
set_current_lang
(
'en'
)
elif
lang
==
'en'
:
set_current_lang
(
'zh'
)
logger
=
get_logger
(
__file__
)
def
ignore_error
(
func
):
@wraps
(
func
)
def
wrapper
(
*
args
,
**
kwargs
):
try
:
resp
=
func
(
*
args
,
**
kwargs
)
return
resp
except
Exception
as
e
:
logger
.
error
(
"Error occur: {} {}"
.
format
(
func
.
__name__
,
e
))
raise
e
return
wrapper
ugettext
=
LocalProxy
(
partial
(
_find
,
'LANGUAGE_CODE'
))
conf_example.py
View file @
a07ede5b
...
...
@@ -53,6 +53,12 @@ class Config:
# 登录是否支持秘钥认证
# PUBLIC_KEY_AUTH = True
# SSH白名单
# ALLOW_SSH_USER = 'all' # ['test', 'test2']
# SSH黑名单, 如果用户同时在白名单和黑名单,黑名单优先生效
# BLOCK_SSH_USER = []
# 和Jumpserver 保持心跳时间间隔
# HEARTBEAT_INTERVAL = 5
...
...
@@ -66,8 +72,11 @@ class Config:
"TYPE"
:
"server"
}
# SSH
connection timeout
(default 15 seconds)
# SSH
连接超时时间
(default 15 seconds)
# SSH_TIMEOUT = 15
# 语言 = en
LANGUAGE_CODE
=
'zh'
config
=
Config
()
locale/en/LC_MESSAGES/coco.mo
0 → 100644
View file @
a07ede5b
File added
locale/en/LC_MESSAGES/coco.po
0 → 100644
View file @
a07ede5b
# 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 ""
locale/en/LC_MESSAGES/coco.po~
0 → 100644
View file @
a07ede5b
# 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 ""
locale/zh_CN/LC_MESSAGES/coco.mo
0 → 100644
View file @
a07ede5b
File added
locale/zh_CN/LC_MESSAGES/coco.po
0 → 100644
View file @
a07ede5b
# 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 "被管理员中断"
locale/zh_CN/LC_MESSAGES/coco.po~
0 → 100644
View file @
a07ede5b
# 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 "被管理员中断"
utils/messages.sh
0 → 100755
View file @
a07ede5b
#!/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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment