支持记录SFTP操作日志了,能记录文件打开、删除,目录创建、删除,改名以及创建符号链接等操作。

pull/32/head
Apex Liu 2017-05-27 23:51:20 +08:00
parent 428dc323f3
commit ef23397be8
5 changed files with 1558 additions and 1306 deletions

View File

@ -409,7 +409,7 @@ TS_SSH_CHANNEL_INFO *SshSession::_get_srv_channel(ssh_channel cli_channel) {
return it->second; return it->second;
} }
void SshSession::_process_command(int from, const ex_u8* data, int len) void SshSession::_process_ssh_command(int from, const ex_u8* data, int len)
{ {
if (TS_SSH_DATA_FROM_CLIENT == from) if (TS_SSH_DATA_FROM_CLIENT == from)
{ {
@ -683,6 +683,100 @@ void SshSession::_process_command(int from, const ex_u8* data, int len)
return; return;
} }
void SshSession::_process_sftp_command(const ex_u8* data, int len) {
// SFTP protocol: https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13
//EXLOG_BIN(data, len, "[sftp] client channel data");
if (len < 9)
return;
int pkg_len = (int)((data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]);
if (pkg_len + 4 != len)
return;
ex_u8 sftp_cmd = data[4];
if (sftp_cmd == 0x01) {
// 0x01 = 1 = SSH_FXP_INIT
m_rec.record_command("SFTP INITIALIZE\r\n");
return;
}
// 需要的数据至少14字节
// uint32 + byte + uint32 + (uint32 + char + ...)
// pkg_len + cmd + req_id + string( length + content...)
if (len < 14)
return;
ex_u8* str1_ptr = (ex_u8*)data + 9;
int str1_len = (int)((str1_ptr[0] << 24) | (str1_ptr[1] << 16) | (str1_ptr[2] << 8) | str1_ptr[3]);
// if (str1_len + 9 != pkg_len)
// return;
ex_u8* str2_ptr = NULL;// (ex_u8*)data + 13;
int str2_len = 0;// (int)((data[9] << 24) | (data[10] << 16) | (data[11] << 8) | data[12]);
char* act = NULL;
switch (sftp_cmd) {
case 0x03:
// 0x03 = 3 = SSH_FXP_OPEN
act = "open file";
break;
// case 0x0b:
// // 0x0b = 11 = SSH_FXP_OPENDIR
// act = "open dir";
// break;
case 0x0d:
// 0x0d = 13 = SSH_FXP_REMOVE
act = "remove file";
break;
case 0x0e:
// 0x0e = 14 = SSH_FXP_MKDIR
act = "create dir";
break;
case 0x0f:
// 0x0f = 15 = SSH_FXP_RMDIR
act = "remove dir";
break;
case 0x12:
// 0x12 = 18 = SSH_FXP_RENAME
// rename操作数据中包含两个字符串
act = "rename";
str2_ptr = str1_ptr + str1_len + 4;
str2_len = (int)((str2_ptr[0] << 24) | (str2_ptr[1] << 16) | (str2_ptr[2] << 8) | str2_ptr[3]);
break;
case 0x15:
// 0x15 = 21 = SSH_FXP_LINK
// link操作数据中包含两个字符串前者是新的链接文件名后者是现有被链接的文件名
act = "create link";
str2_ptr = str1_ptr + str1_len + 4;
str2_len = (int)((str2_ptr[0] << 24) | (str2_ptr[1] << 16) | (str2_ptr[2] << 8) | str2_ptr[3]);
break;
default:
return;
}
int total_len = 5 + str1_len + 4;
if (str2_len > 0)
total_len += str2_len + 4;
if (total_len > pkg_len)
return;
char msg[2048] = { 0 };
if (str2_len == 0) {
ex_astr str1((char*)((ex_u8*)data + 13), str1_len);
ex_strformat(msg, 2048, "%d:%s:%s:\r\n", sftp_cmd, act, str1.c_str());
}
else {
ex_astr str1((char*)(str1_ptr + 4), str1_len);
ex_astr str2((char*)(str2_ptr + 4), str2_len);
ex_strformat(msg, 2048, "%d:%s:%s:%s\r\n", sftp_cmd, act, str1.c_str(), str2.c_str());
}
m_rec.record_command(msg);
}
int SshSession::_on_client_pty_request(ssh_session session, ssh_channel channel, const char *term, int x, int y, int px, int SshSession::_on_client_pty_request(ssh_session session, ssh_channel channel, const char *term, int x, int y, int px,
int py, void *userdata) { int py, void *userdata) {
SshSession *_this = (SshSession *)userdata; SshSession *_this = (SshSession *)userdata;
@ -809,12 +903,16 @@ int SshSession::_on_client_channel_data(ssh_session session, ssh_channel channel
{ {
try try
{ {
_this->_process_command(TS_SSH_DATA_FROM_CLIENT, (ex_u8*)data, len); _this->_process_ssh_command(TS_SSH_DATA_FROM_CLIENT, (ex_u8*)data, len);
} }
catch (...) catch (...)
{ {
} }
} }
else
{
_this->_process_sftp_command((ex_u8*)data, len);
}
int ret = 0; int ret = 0;
if (is_stderr) if (is_stderr)
@ -908,7 +1006,7 @@ int SshSession::_on_server_channel_data(ssh_session session, ssh_channel channel
{ {
try try
{ {
_this->_process_command(TS_SSH_DATA_FROM_SERVER, (ex_u8*)data, len); _this->_process_ssh_command(TS_SSH_DATA_FROM_SERVER, (ex_u8*)data, len);
_this->m_rec.record(TS_RECORD_TYPE_SSH_DATA, (unsigned char *)data, len); _this->m_rec.record(TS_RECORD_TYPE_SSH_DATA, (unsigned char *)data, len);
} }
catch (...) catch (...)

View File

@ -62,7 +62,8 @@ protected:
void _thread_loop(void); void _thread_loop(void);
void _set_stop_flag(void); void _set_stop_flag(void);
void _process_command(int from, const ex_u8* data, int len); void _process_ssh_command(int from, const ex_u8* data, int len);
void _process_sftp_command(const ex_u8* data, int len);
private: private:
void _run(void); void _run(void);

View File

@ -80,10 +80,20 @@ class ReplayStaticFileHandler(tornado.web.StaticFileHandler):
class ComandLogHandler(TPBaseAdminAuthHandler): class ComandLogHandler(TPBaseAdminAuthHandler):
def get(self, protocol, record_id): def get(self, protocol, record_id):
header = record.read_record_head(record_id)
if header is None:
return self.write_json(-3, '操作失败')
# ret = dict()
# ret['header'] = header
# return self.write_json(0, data=ret)
param = dict() param = dict()
param['header'] = header
param['count'] = 0 param['count'] = 0
param['op'] = list() param['op'] = list()
cmd_type = 0 # 0 = ssh, 1 = sftp
protocol = int(protocol) protocol = int(protocol)
if protocol == 1: if protocol == 1:
pass pass
@ -94,12 +104,26 @@ class ComandLogHandler(TPBaseAdminAuthHandler):
file = open(file_info, 'r') file = open(file_info, 'r')
data = file.readlines() data = file.readlines()
for i in range(len(data)): for i in range(len(data)):
if 0 == i:
cmd = data[i][22:-1]
if 'SFTP INITIALIZE' == cmd:
cmd_type = 1
continue
if cmd_type == 0:
param['op'].append({'t': data[i][1:20], 'c': data[i][22:-1]}) param['op'].append({'t': data[i][1:20], 'c': data[i][22:-1]})
else:
cmd_info = data[i][22:-1].split(':')
if len(cmd_info) != 4:
continue
param['op'].append({'t': data[i][1:20], 'c': cmd_info[0], 'p1': cmd_info[2], 'p2': cmd_info[3]})
except: except:
pass pass
param['count'] = len(param['op']) param['count'] = len(param['op'])
if cmd_type == 0:
self.render('log/record-ssh-cmd.mako', page_param=json.dumps(param)) self.render('log/record-ssh-cmd.mako', page_param=json.dumps(param))
else:
self.render('log/record-sftp-cmd.mako', page_param=json.dumps(param))
class RecordGetHeader(TPBaseAdminAuthJsonHandler): class RecordGetHeader(TPBaseAdminAuthJsonHandler):

View File

@ -0,0 +1,125 @@
<%!
page_title_ = 'SFTP操作记录'
%>
<%inherit file="../page_no_sidebar_base.mako"/>
<%block name="extend_js">
</%block>
<%block name="breadcrumb">
<ol class="breadcrumb">
<li><i class="fa fa-file-text-o"></i> ${self.attr.page_title_}</li>
<li><span id="recorder-info"></span></li>
</ol>
</%block>
<%block name="extend_css">
<style type="text/css">
#no-op-msg {
display: none;
padding: 20px;
margin: 50px;
background-color: #fffed5;
border-radius: 5px;
font-size: 120%;
}
#op-list {
display: none;
padding: 20px;
margin: 20px 10px 20px 10px;
background-color: #ffffff;
font-size: 14px;
border-radius: 5px;
}
.op-item {
margin-bottom: 3px;
}
.time, .cmd, .path {
font-family: Consolas, Lucida Console, Monaco, Courier, 'Courier New', monospace;
font-size:13px;
line-height: 15px;
padding: 0 5px;
border-radius: 3px;
}
.time {
margin-right: 15px;
background-color: #d8d8d8;
}
.path {
margin:0 5px 0 5px;
}
.cmd-danger {
background-color: #ffbba6;
font-weight: bold;
}
.cmd-info {
background-color: #b4fdb1;
}
</style>
</%block>
<div class="page-content">
<div id="no-op-msg">
他悄悄地来,又悄悄地走,挥一挥衣袖,没有留下任何操作~~~~
</div>
<div id="op-list"></div>
</div>
<%block name="embed_js">
<script type="text/javascript">
ywl.add_page_options(${page_param});
ywl.on_init = function (cb_stack, cb_args) {
if (ywl.page_options.count === 0) {
$('#no-op-msg').show();
} else {
var header = ywl.page_options.header;
$('#recorder-info').html(header.account + ' 于 ' + format_datetime(header.start) + ' 访问 ' + header.user_name + '@' + header.ip + ':' + header.port);
var dom_op_list = $('#op-list');
var html = [];
for (var i = 0; i < ywl.page_options.count; i++) {
html.push('<div class="op-item"><span class="time">' + ywl.page_options.op[i].t + '</span> ');
if (ywl.page_options.op[i].c === '3') {
html.push('<span class="cmd">打开文件</span>');
html.push('<span class="path">' + ywl.page_options.op[i].p1 + '</span>');
} else if (ywl.page_options.op[i].c === '13') {
html.push('<span class="cmd cmd-danger">删除文件</span>');
html.push('<span class="path cmd-danger">' + ywl.page_options.op[i].p1 + '</span>');
} else if (ywl.page_options.op[i].c === '14') {
html.push('<span class="cmd">创建目录</span>');
html.push('<span class="path">' + ywl.page_options.op[i].p1 + '</span>');
} else if (ywl.page_options.op[i].c === '15') {
html.push('<span class="cmd cmd-danger">删除目录</span>');
html.push('<span class="path cmd-danger">' + ywl.page_options.op[i].p1 + '</span>');
} else if (ywl.page_options.op[i].c === '18') {
html.push('<span class="cmd cmd-info">更改名称</span>');
html.push('<span class="path cmd-info">' + ywl.page_options.op[i].p1 + '</span>');
html.push('<i class="fa fa-arrow-circle-right"></i>');
html.push('<span class="path cmd-info">' + ywl.page_options.op[i].p2 + '</span>');
} else if (ywl.page_options.op[i].c === '21') {
html.push('<span class="cmd">创建链接</span>');
html.push('<span class="path">' + ywl.page_options.op[i].p2 + '</span>');
html.push('<i class="fa fa-arrow-right"></i>');
html.push('<span class="path">' + ywl.page_options.op[i].p1 + '</span>');
}
html.push('</div>');
}
dom_op_list.append(html.join(''));
dom_op_list.show();
}
cb_stack.exec();
};
</script>
</%block>

View File

@ -1,5 +1,5 @@
<%! <%!
page_title_ = '操作记录' page_title_ = 'SSH操作记录'
%> %>
<%inherit file="../page_no_sidebar_base.mako"/> <%inherit file="../page_no_sidebar_base.mako"/>
@ -8,7 +8,8 @@
<%block name="breadcrumb"> <%block name="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><i class="fa fa-server"></i> ${self.attr.page_title_}</li> <li><i class="fa fa-file-text-o"></i> ${self.attr.page_title_}</li>
<li><span id="recorder-info"></span></li>
</ol> </ol>
</%block> </%block>
@ -75,9 +76,12 @@
var info_cmd = ['exit']; var info_cmd = ['exit'];
ywl.on_init = function (cb_stack, cb_args) { ywl.on_init = function (cb_stack, cb_args) {
if (ywl.page_options.count == 0) { if (ywl.page_options.count === 0) {
$('#no-op-msg').show(); $('#no-op-msg').show();
} else { } else {
var header = ywl.page_options.header;
$('#recorder-info').html(header.account + ' 于 ' + format_datetime(header.start) + ' 访问 ' + header.user_name + '@' + header.ip + ':' + header.port);
var dom_op_list = $('#op-list'); var dom_op_list = $('#op-list');
var html = []; var html = [];
for (var i = 0; i < ywl.page_options.count; i++) { for (var i = 0; i < ywl.page_options.count; i++) {
@ -90,7 +94,7 @@
cmd_class = ' cmd-info'; cmd_class = ' cmd-info';
} }
html.push('<div class="op-item"><span class="time">' + ywl.page_options.op[i].t + '</span> <span class="cmd' + cmd_class + '">' + ywl.page_options.op[i].c + '</span></li></div>'); html.push('<div class="op-item"><span class="time">' + ywl.page_options.op[i].t + '</span> <span class="cmd' + cmd_class + '">' + ywl.page_options.op[i].c + '</span></div>');
} }
dom_op_list.append(html.join('')); dom_op_list.append(html.join(''));
dom_op_list.show(); dom_op_list.show();