Merge remote-tracking branch 'remotes/origin/dev'

pull/32/merge
apexliu 2017-06-04 23:29:34 +08:00
commit 76518ddbfb
74 changed files with 4852 additions and 1779 deletions

View File

@ -1,3 +1,3 @@
# -*- coding: utf8 -*-
VER_TELEPORT_SERVER = "2.2.8.1"
VER_TELEPORT_SERVER = "2.2.9.3"
VER_TELEPORT_ASSIST = "2.2.6.1"

View File

@ -16,6 +16,7 @@
<file url="file://$PROJECT_DIR$/tp_core/common/ts_memstream.h" charset="GBK" />
<file url="file://$PROJECT_DIR$/tp_core/core/main.cpp" charset="GBK" />
<file url="file://$PROJECT_DIR$/tp_core/core/ts_env.cpp" charset="GBK" />
<file url="file://$PROJECT_DIR$/tp_core/core/ts_http_rpc.cpp" charset="GBK" />
<file url="file://$PROJECT_DIR$/tp_core/core/ts_http_rpc.h" charset="GBK" />
<file url="file://$PROJECT_DIR$/tp_core/core/ts_main.cpp" charset="GBK" />
<file url="file://$PROJECT_DIR$/tp_core/protocol/rdp/rdp_recorder.h" charset="GBK" />

View File

@ -63,6 +63,8 @@
#define SESS_STAT_ERR_UNSUPPORT_PROTOCOL 5 // 会话结束,因为协议不支持(RDP)
#define SESS_STAT_ERR_BAD_PKG 6 // 会话结束,因为收到错误的报文
#define SESS_STAT_ERR_RESET 7 // 会话结束因为teleport核心服务重置了
#define SESS_STAT_ERR_IO 8 // 会话结束,因为网络中断
#define SESS_STAT_ERR_SESSION 9 // 会话结束因为无效的会话ID
#endif // __TS_ERRNO_H__

Binary file not shown.

View File

@ -49,24 +49,6 @@ bool TsEnv::init(bool load_config)
}
else // not in development mode
{
// #ifdef EX_OS_WIN32
// base_path = m_exec_path;
// ex_path_join(base_path, true, L"..", NULL);
// m_etc_path = base_path;
// ex_path_join(m_etc_path, false, L"etc", NULL);
// conf_file = m_etc_path;
// ex_path_join(conf_file, false, L"core.ini", NULL);
// m_replay_path = base_path;
// ex_path_join(m_replay_path, false, L"data", L"replay", NULL);
// log_path = base_path;
// ex_path_join(log_path, false, L"log", NULL);
// #else
// m_etc_path = L"/etc/teleport";
// conf_file = L"/etc/teleport/core.ini";
// m_replay_path = L"/var/lib/teleport/replay";
// log_path = L"/var/log/teleport";
// #endif
base_path = m_exec_path;
ex_path_join(base_path, true, L"..", L"data", NULL);
m_etc_path = base_path;
@ -79,8 +61,6 @@ bool TsEnv::init(bool load_config)
ex_path_join(log_path, false, L"log", NULL);
}
//EXLOGW(L"[core] load config file: %ls.\n", conf_file.c_str());
if (!m_ini.LoadFromFile(conf_file))
{
EXLOGE(L"[core] can not load %ls.\n", conf_file.c_str());

View File

@ -55,7 +55,7 @@ TsHttpRpc::~TsHttpRpc()
void TsHttpRpc::_thread_loop(void)
{
EXLOGV("[core] rpc TeleportServer-HTTP-RPC ready on %s:%d\n", m_host_ip.c_str(), m_host_port);
EXLOGV("[core] TeleportServer-RPC ready on %s:%d\n", m_host_ip.c_str(), m_host_port);
while(!m_stop_flag)
{
@ -127,7 +127,7 @@ void TsHttpRpc::_mg_event_handler(struct mg_connection *nc, int ev, void *ev_dat
ex_astr uri;
uri.assign(hm->uri.p, hm->uri.len);
EXLOGD("[core] rpc got request: %s\n", uri.c_str());
//EXLOGD("[core] rpc got request: %s\n", uri.c_str());
if (uri == "/rpc")
{
@ -151,9 +151,7 @@ void TsHttpRpc::_mg_event_handler(struct mg_connection *nc, int ev, void *ev_dat
EXLOGE("[core] rpc got invalid request: not `rpc` uri.\n");
_this->_create_json_ret(ret_buf, TSR_INVALID_REQUEST, "not a `rpc` request.");
}
mg_printf(nc, "HTTP/1.0 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: %d\r\nContent-Type: application/json\r\n\r\n%s", (int)ret_buf.size() - 1, &ret_buf[0]);
nc->flags |= MG_F_SEND_AND_CLOSE;
}

View File

@ -1,6 +1,6 @@
#ifndef __TS_SERVER_VER_H__
#define __TS_SERVER_VER_H__
#define TP_SERVER_VER L"2.2.8.1"
#define TP_SERVER_VER L"2.2.9.3"
#endif // __TS_SERVER_VER_H__

View File

@ -29,7 +29,6 @@ bool SshProxy::init(void)
m_host_ip = g_ssh_env.bind_ip;
m_host_port = g_ssh_env.bind_port;
m_bind = ssh_bind_new();
if (NULL == m_bind)
{

View File

@ -192,7 +192,7 @@ void SshSession::_run(void) {
return;
}
EXLOGW("[ssh] Authenticated and got a channel.\n");
EXLOGW("[ssh] authenticated and got a channel.\n");
// 现在双方的连接已经建立好了,开始转发
ssh_event_add_session(event_loop, m_srv_session);
@ -202,15 +202,12 @@ void SshSession::_run(void) {
if (0 != ssh_get_error_code(m_cli_session))
{
EXLOGE("[ssh] ssh_event_dopoll() [cli] %s\n", ssh_get_error(m_cli_session));
//ssh_disconnect(m_cli_session);
}
else if (0 != ssh_get_error_code(m_srv_session))
{
EXLOGE("[ssh] ssh_event_dopoll() [srv] %s\n", ssh_get_error(m_srv_session));
//ssh_disconnect(m_srv_session);
}
// EXLOGE("[ssh] ssh_event_dopoll() [cli] %s, [srv] %s\n", ssh_get_error(m_cli_session), ssh_get_error(m_srv_session));
_close_channels();
}
} while (m_channel_cli_srv.size() > 0);
@ -229,7 +226,6 @@ int SshSession::_on_auth_password_request(ssh_session session, const char *user,
_this->m_sid = user;
EXLOGV("[ssh] authenticating, session-id: %s\n", _this->m_sid.c_str());
//bool bRet = true;
int protocol = 0;
TPP_SESSION_INFO* sess_info = g_ssh_env.take_session(_this->m_sid.c_str());
@ -249,13 +245,6 @@ int SshSession::_on_auth_password_request(ssh_session session, const char *user,
_this->m_auth_mode = sftp_info.auth_mode;
_this->m_user_name = sftp_info.user_name;
_this->m_user_auth = sftp_info.user_auth;
// sess_info.host_ip = sftp_info.host_ip;
// sess_info.host_port = sftp_info.host_port;
// sess_info.auth_mode = sftp_info.auth_mode;
// sess_info.user_name = sftp_info.user_name;
// sess_info.user_auth = sftp_info.user_auth;
// sess_info.protocol = TS_PROXY_PROTOCOL_SSH;
protocol = TS_PROXY_PROTOCOL_SSH;
// 因为是从sftp会话得来的登录数据因此限制本会话只能用于sftp不允许再使用shell了。
@ -269,7 +258,6 @@ int SshSession::_on_auth_password_request(ssh_session session, const char *user,
protocol = sess_info->protocol;
}
//EXLOGE("[ssh---------1] auth info [password:%s:%s:%d]\n", _this->m_user_name.c_str(),_this->m_user_auth.c_str(), _this->m_auth_mode);
if (protocol != TS_PROXY_PROTOCOL_SSH) {
g_ssh_env.free_session(sess_info);
EXLOGE("[ssh] session '%s' is not for SSH.\n", _this->m_sid.c_str());
@ -293,51 +281,118 @@ int SshSession::_on_auth_password_request(ssh_session session, const char *user,
EXLOGV("[ssh] try to connect to real SSH server %s:%d\n", _this->m_server_ip.c_str(), _this->m_server_port);
_this->m_srv_session = ssh_new();
ssh_options_set(_this->m_srv_session, SSH_OPTIONS_HOST, _this->m_server_ip.c_str());
ssh_options_set(_this->m_srv_session, SSH_OPTIONS_PORT, &_this->m_server_port);
int port = (int)_this->m_server_port;
ssh_options_set(_this->m_srv_session, SSH_OPTIONS_PORT, &port);
#ifdef EX_DEBUG
// int flag = SSH_LOG_FUNCTIONS;
// ssh_options_set(_this->m_srv_session, SSH_OPTIONS_LOG_VERBOSITY, &flag);
#endif
if (_this->m_auth_mode != TS_AUTH_MODE_NONE)
ssh_options_set(_this->m_srv_session, SSH_OPTIONS_USER, _this->m_user_name.c_str());
int _timeout_us = 30000000; // 30 sec.
#ifdef EX_DEBUG
// int _timeout_us = 500000000; // 5 sec.
// ssh_options_set(_this->m_srv_session, SSH_OPTIONS_TIMEOUT_USEC, &_timeout_us);
#else
int _timeout_us = 10000000; // 10 sec.
ssh_options_set(_this->m_srv_session, SSH_OPTIONS_TIMEOUT_USEC, &_timeout_us);
#endif
int rc = 0;
rc = ssh_connect(_this->m_srv_session);
if (rc != SSH_OK) {
EXLOGE("[ssh] can not connect to real SSH server %s:%d.\n", _this->m_server_ip.c_str(), _this->m_server_port);
EXLOGE("[ssh] can not connect to real SSH server %s:%d. [%d]%s\n", _this->m_server_ip.c_str(), _this->m_server_port, rc, ssh_get_error(_this->m_srv_session));
_this->m_have_error = true;
_this->m_retcode = SESS_STAT_ERR_CONNECT;
return SSH_AUTH_DENIED;
return SSH_AUTH_ERROR;
}
// 检查服务端支持的认证协议
ssh_userauth_none(_this->m_srv_session, NULL);
int auth_methods = ssh_userauth_list(_this->m_srv_session, NULL);
if (_this->m_auth_mode == TS_AUTH_MODE_PASSWORD) {
rc = ssh_userauth_password(_this->m_srv_session, NULL, _this->m_user_auth.c_str());
if (rc != SSH_OK) {
EXLOGE("[ssh] can not use user/name login to real SSH server %s:%d.\n", _this->m_server_ip.c_str(), _this->m_server_port);
if (auth_methods & SSH_AUTH_METHOD_PASSWORD) {
rc = ssh_userauth_password(_this->m_srv_session, NULL, _this->m_user_auth.c_str());
if (rc != SSH_AUTH_SUCCESS) {
EXLOGE("[ssh] invalid password for password mode to login to real SSH server %s:%d.\n", _this->m_server_ip.c_str(), _this->m_server_port);
_this->m_have_error = true;
_this->m_retcode = SESS_STAT_ERR_AUTH_DENIED;
return SSH_AUTH_DENIED;
}
}
else if (auth_methods & SSH_AUTH_METHOD_INTERACTIVE) {
bool is_login = false;
for (;;) {
rc = ssh_userauth_kbdint(_this->m_srv_session, NULL, NULL);
if (rc != SSH_AUTH_INFO)
break;
if(ssh_userauth_kbdint_getnprompts(_this->m_srv_session) != 1)
break;
rc = ssh_userauth_kbdint_setanswer(_this->m_srv_session, 0, _this->m_user_auth.c_str());
if (rc < 0)
break;
// 有时候服务端会再发一个空的提示来完成交互
rc = ssh_userauth_kbdint(_this->m_srv_session, NULL, NULL);
if (rc == SSH_AUTH_INFO) {
if (ssh_userauth_kbdint_getnprompts(_this->m_srv_session) != 0)
break;
rc = ssh_userauth_kbdint(_this->m_srv_session, NULL, NULL);
if (rc < 0)
break;
}
if(rc == SSH_AUTH_SUCCESS)
is_login = true;
break;
}
if (!is_login) {
EXLOGE("[ssh] invalid password for keyboard-interactive mode to login to real SSH server %s:%d.\n", _this->m_server_ip.c_str(), _this->m_server_port);
_this->m_have_error = true;
_this->m_retcode = SESS_STAT_ERR_AUTH_DENIED;
return SSH_AUTH_DENIED;
}
}
else {
EXLOGE("[ssh] real SSH server [%s:%d] does not support password or keyboard-interactive login.\n", _this->m_server_ip.c_str(), _this->m_server_port);
_this->m_have_error = true;
_this->m_retcode = SESS_STAT_ERR_AUTH_DENIED;
return SSH_AUTH_DENIED;
}
}
else if (_this->m_auth_mode == TS_AUTH_MODE_PRIVATE_KEY) {
ssh_key key = NULL;
if (SSH_OK != ssh_pki_import_privkey_base64(_this->m_user_auth.c_str(), NULL, NULL, NULL, &key)) {
EXLOGE("[ssh] can not import private-key for auth.\n");
_this->m_have_error = true;
_this->m_retcode = SESS_STAT_ERR_BAD_SSH_KEY;
return SSH_AUTH_DENIED;
}
if (auth_methods & SSH_AUTH_METHOD_PUBLICKEY) {
ssh_key key = NULL;
if (SSH_OK != ssh_pki_import_privkey_base64(_this->m_user_auth.c_str(), NULL, NULL, NULL, &key)) {
EXLOGE("[ssh] can not import private-key for auth.\n");
_this->m_have_error = true;
_this->m_retcode = SESS_STAT_ERR_BAD_SSH_KEY;
return SSH_AUTH_DENIED;
}
rc = ssh_userauth_publickey(_this->m_srv_session, NULL, key);
if (rc != SSH_OK) {
ssh_key_free(key);
EXLOGE("[ssh] invalid private-key for login to real SSH server %s:%d.\n", _this->m_server_ip.c_str(), _this->m_server_port);
_this->m_have_error = true;
_this->m_retcode = SESS_STAT_ERR_AUTH_DENIED;
return SSH_AUTH_DENIED;
}
rc = ssh_userauth_publickey(_this->m_srv_session, NULL, key);
if (rc != SSH_OK) {
ssh_key_free(key);
EXLOGE("[ssh] can not use private-key login to real SSH server %s:%d.\n", _this->m_server_ip.c_str(), _this->m_server_port);
}
else {
EXLOGE("[ssh] real SSH server [%s:%d] does not support public key login.\n", _this->m_server_ip.c_str(), _this->m_server_port);
_this->m_have_error = true;
_this->m_retcode = SESS_STAT_ERR_AUTH_DENIED;
return SSH_AUTH_DENIED;
}
ssh_key_free(key);
}
else if (_this->m_auth_mode == TS_AUTH_MODE_NONE)
{
@ -409,7 +464,7 @@ TS_SSH_CHANNEL_INFO *SshSession::_get_srv_channel(ssh_channel cli_channel) {
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)
{
@ -498,10 +553,11 @@ void SshSession::_process_command(int from, const ex_u8* data, int len)
if (m_cmd_char_list.size() > 0)
{
m_cmd_char_list.push_back(0x0d);
m_cmd_char_list.push_back(0x0a);
ex_astr str(m_cmd_char_list.begin(), m_cmd_char_list.end());
// EXLOGD("[ssh] save cmd: %s", str.c_str());
ex_replace_all(str, "\r", "");
ex_replace_all(str, "\n", "");
//EXLOGD("[ssh] save cmd: [%s]", str.c_str());
str += "\r\n";
m_rec.record_command(str);
}
m_cmd_char_list.clear();
@ -683,13 +739,106 @@ void SshSession::_process_command(int from, const ex_u8* data, int len)
return;
}
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) {
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]);
const 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 py, void *userdata) {
SshSession *_this = (SshSession *)userdata;
if (_this->m_is_sftp) {
EXLOGE("[ssh] try to request pty on a sftp-session.\n");
return SSH_FATAL;
return SSH_ERROR;
}
EXLOGD("[ssh] client request terminal: %s, (%d, %d) / (%d, %d)\n", term, x, y, px, py);
@ -697,7 +846,7 @@ int SshSession::_on_client_pty_request(ssh_session session, ssh_channel channel,
TS_SSH_CHANNEL_INFO *info = _this->_get_srv_channel(channel);
if (NULL == info || NULL == info->channel) {
EXLOGE("[ssh] when client request pty, not found server channel.\n");
return SSH_FATAL;
return SSH_ERROR;
}
return ssh_channel_request_pty_size(info->channel, term, x, y);
@ -705,40 +854,37 @@ int SshSession::_on_client_pty_request(ssh_session session, ssh_channel channel,
int SshSession::_on_client_shell_request(ssh_session session, ssh_channel channel, void *userdata) {
SshSession *_this = (SshSession *)userdata;
char buf[2048] = { 0 };
if (_this->m_is_sftp) {
EXLOGE("[ssh] try to request shell on a sftp-session.\n");
snprintf(buf, sizeof(buf),
"\r\n\r\n"\
"!! ERROR !!\r\n"\
"Session-ID '%s' has been used for SFTP.\r\n"\
"\r\n", _this->m_sid.c_str()
);
ssh_channel_write(channel, buf, strlen(buf));
return SSH_FATAL;
// char buf[2048] = { 0 };
// snprintf(buf, sizeof(buf),
// "\r\n\r\n"\
// "!! ERROR !!\r\n"\
// "Session-ID '%s' has been used for SFTP.\r\n"\
// "\r\n", _this->m_sid.c_str()
// );
// ssh_channel_write(channel, buf, strlen(buf));
//
return 1;
}
EXLOGD("[ssh] client request shell\n");
TS_SSH_CHANNEL_INFO *srv_info = _this->_get_srv_channel(channel);
if (NULL == srv_info || NULL == srv_info->channel) {
EXLOGE("[ssh] when client request shell, not found server channel.\n");
return SSH_FATAL;
return 1;
}
srv_info->type = TS_SSH_CHANNEL_TYPE_SHELL;
TS_SSH_CHANNEL_INFO *cli_info = _this->_get_cli_channel(srv_info->channel);
if (NULL == cli_info || NULL == cli_info->channel) {
EXLOGE("[ssh] when client request shell, not found client channel.\n");
return SSH_FATAL;
return 1;
}
cli_info->type = TS_SSH_CHANNEL_TYPE_SHELL;
return ssh_channel_request_shell(srv_info->channel);
}
@ -788,19 +934,20 @@ void SshSession::_on_client_channel_close(ssh_session session, ssh_channel chann
int SshSession::_on_client_channel_data(ssh_session session, ssh_channel channel, void *data, unsigned int len, int is_stderr, void *userdata)
{
//EXLOG_BIN((ex_u8*)data, len, "on_client_channel_data [is_stderr=%d]:", is_stderr);
SshSession *_this = (SshSession *)userdata;
// 当前线程正在接收服务端返回的数据,因此我们直接返回,这样紧跟着会重新再发送此数据的
if (_this->m_recving_from_srv)
return 0;
if (_this->m_recving_from_cli)
return 0;
TS_SSH_CHANNEL_INFO *info = _this->_get_srv_channel(channel);
if (NULL == info || NULL == info->channel) {
EXLOGE("[ssh] when receive client channel data, not found server channel.\n");
return SSH_FATAL;
return SSH_ERROR;
}
_this->m_recving_from_cli = true;
@ -809,12 +956,16 @@ int SshSession::_on_client_channel_data(ssh_session session, ssh_channel channel
{
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 (...)
{
}
}
else
{
_this->_process_sftp_command((ex_u8*)data, len);
}
int ret = 0;
if (is_stderr)
@ -826,16 +977,16 @@ int SshSession::_on_client_channel_data(ssh_session session, ssh_channel channel
_this->m_recving_from_cli = false;
return ret;
return len;
}
int SshSession::_on_client_pty_win_change(ssh_session session, ssh_channel channel, int width, int height, int pxwidth, int pwheight, void *userdata) {
int SshSession::_on_client_pty_win_change(ssh_session session, ssh_channel channel, int width, int height, int pxwidth, int pwheight, void *userdata) {
EXLOGD("[ssh] client pty win size change to: (%d, %d)\n", width, height);
SshSession *_this = (SshSession *)userdata;
TS_SSH_CHANNEL_INFO *info = _this->_get_srv_channel(channel);
if (NULL == info || NULL == info->channel) {
EXLOGE("[ssh] when client pty win change, not found server channel.\n");
return SSH_FATAL;
return SSH_ERROR;
}
_this->m_rec.record_win_size_change(width, height);
@ -857,14 +1008,14 @@ int SshSession::_on_client_channel_subsystem_request(ssh_session session, ssh_ch
TS_SSH_CHANNEL_INFO *srv_info = _this->_get_srv_channel(channel);
if (NULL == srv_info || NULL == srv_info->channel) {
EXLOGE("[ssh] when receive client channel subsystem request, not found server channel.\n");
return SSH_FATAL;
return SSH_ERROR;
}
srv_info->type = TS_SSH_CHANNEL_TYPE_SFTP;
TS_SSH_CHANNEL_INFO *cli_info = _this->_get_cli_channel(srv_info->channel);
if (NULL == cli_info || NULL == cli_info->channel) {
EXLOGE("[ssh] when client request shell, not found client channel.\n");
return SSH_FATAL;
return SSH_ERROR;
}
cli_info->type = TS_SSH_CHANNEL_TYPE_SFTP;
@ -888,6 +1039,7 @@ int SshSession::_on_client_channel_exec_request(ssh_session session, ssh_channel
int SshSession::_on_server_channel_data(ssh_session session, ssh_channel channel, void *data, unsigned int len, int is_stderr, void *userdata)
{
//EXLOG_BIN((ex_u8*)data, len, "on_server_channel_data [is_stderr=%d]:", is_stderr);
SshSession *_this = (SshSession *)userdata;
if (_this->m_recving_from_cli)
@ -899,24 +1051,40 @@ int SshSession::_on_server_channel_data(ssh_session session, ssh_channel channel
if (NULL == info || NULL == info->channel) {
EXLOGE("[ssh] when receive server channel data, not found client channel.\n");
_this->m_retcode = SESS_STAT_ERR_INTERNAL;
return SSH_FATAL;
return SSH_ERROR;
}
#ifdef EX_OS_WIN32
// TODO: hard code not good... :(
// 偶尔某次操作会导致ssh_session->session_state为SSH_SESSION_STATE_ERROR
// 但是将其强制改为SSH_SESSION_STATE_AUTHENTICATED后续操作仍然能成功主要在向客户端发送第一包数据时
ex_u8* _t = (ex_u8*)(ssh_channel_get_session(info->channel));
if (_t[1116] == 9) // SSH_SESSION_STATE_AUTHENTICATED = 8, SSH_SESSION_STATE_ERROR = 9
{
EXLOGW(" --- [ssh] hard code to fix client connect session error state.\n");
_t[1116] = 8;
}
#endif
_this->m_recving_from_srv = true;
if (info->type == TS_SSH_CHANNEL_TYPE_SHELL)
if (info->type == TS_SSH_CHANNEL_TYPE_SHELL && !is_stderr)
{
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);
}
catch (...)
{
EXLOGE("[ssh] process ssh command got exception.\n");
}
}
int ret = 0;
// 收到第一包服务端返回的数据时,在输出数据之前显示一些自定义的信息
#if 1
if (!is_stderr && _this->m_is_first_server_data)
{
_this->m_is_first_server_data = false;
@ -940,88 +1108,44 @@ int SshSession::_on_server_channel_data(ssh_session session, ssh_channel channel
" - teleport to %s:%d\r\n"\
" - authroized by %s\r\n"\
"=============================================\r\n"\
"\r\n"\
"\033]0;tpssh://%s\007",
"\r\n",
// \
// "\033]0;tpssh://%s\007\r\n",
_this->m_server_ip.c_str(),
_this->m_server_port, auth_mode,
_this->m_server_ip.c_str()
_this->m_server_port, auth_mode
// ,
// _this->m_server_ip.c_str()
);
int buf_len = strlen(buf);
ex_bin _data;
_data.resize(buf_len + len);
memcpy(&_data[0], buf, buf_len);
memcpy(&_data[buf_len], data, len);
// 注意,这里虽然可以改变窗口(或者标签页)的标题,但是因为这是服务端发回的第一个包,后面服务端可能还会发类似的包(仅一次)来改变标题
// 导致窗口标题又被改变,因此理论上应该解析服务端发回的包,如果包含上述格式的,需要替换一次。
//_write(info->channel, buf, strlen(buf));
ret = ssh_channel_write(info->channel, &_data[0], _data.size());
//EXLOGD("--- first send to client : %d %d %d\n", _data.size(), ret, len);
ssh_channel_write(info->channel, buf, strlen(buf));
_this->m_recving_from_srv = false;
return len;
}
}
#endif
int ret = 0;
if (is_stderr)
{
if(is_stderr)
ret = ssh_channel_write_stderr(info->channel, data, len);
}
else if(info->type != TS_SSH_CHANNEL_TYPE_SHELL)
{
ret = ssh_channel_write(info->channel, data, len);
}
else
{
// if (len > 5 && len < 256)
// {
// const ex_u8* _begin = ex_memmem((const ex_u8*)data, len, (const ex_u8*)"\033]0;", 4);
// if (NULL != _begin)
// {
// size_t len_before = _begin - (const ex_u8*)data;
// const ex_u8* _end = ex_memmem(_begin + 4, len - len_before, (const ex_u8*)"\007", 1);
// if (NULL != _end)
// {
// _end++;
//
// // 这个包中含有改变标题的数据,将标题换为我们想要的
// size_t len_end = len - (_end - (const ex_u8*)data);
// MemBuffer mbuf;
//
// if (len_before > 0)
// mbuf.append((ex_u8*)data, len_before);
//
// mbuf.append((ex_u8*)"\033]0;tpssh://", 13);
// mbuf.append((ex_u8*)_this->m_server_ip.c_str(), _this->m_server_ip.length());
// mbuf.append((ex_u8*)"\007", 1);
//
// if (len_end > 0)
// mbuf.append((ex_u8*)_end, len_end);
//
// if(mbuf.size() > 0)
// {
// ret = ssh_channel_write(info->channel, mbuf.data(), mbuf.size());
// if (ret <= 0)
// EXLOGE("[ssh] send to client failed (1).\n");
// else
// ret = len;
// }
// else
// {
// ret = ssh_channel_write(info->channel, data, len);
// }
// }
// else
// {
// ret = ssh_channel_write(info->channel, data, len);
// }
// }
// else
// {
// ret = ssh_channel_write(info->channel, data, len);
// }
// }
// else
{
ret = ssh_channel_write(info->channel, data, len);
}
ret = ssh_channel_write(info->channel, data, len);
if (ret == SSH_ERROR) {
EXLOGE("[ssh] send data(%dB) to client failed (2). [%d][%s][%s]\n", len, ret, ssh_get_error(_this->m_cli_session), ssh_get_error(_this->m_cli_session));
}
_this->m_recving_from_srv = false;
if (ret <= 0)
EXLOGE("[ssh] send to client failed (2).\n");
//EXLOGD("--- send to client: %d %d\n", ret, len);
_this->m_recving_from_srv = false;
return ret;
}

View File

@ -1,133 +1,134 @@
#ifndef __SSH_SESSION_H__
#define __SSH_SESSION_H__
#include "ssh_recorder.h"
#include <ex.h>
#include <libssh/libssh.h>
#include <libssh/server.h>
#include <libssh/callbacks.h>
#include <libssh/sftp.h>
#include <vector>
#include <list>
#define TS_SSH_CHANNEL_TYPE_UNKNOWN 0
#define TS_SSH_CHANNEL_TYPE_SHELL 1
#define TS_SSH_CHANNEL_TYPE_SFTP 2
#define TS_SSH_DATA_FROM_CLIENT 1
#define TS_SSH_DATA_FROM_SERVER 2
typedef struct TS_SSH_CHANNEL_INFO
{
int type; // TS_SSH_CHANNEL_TYPE_SHELL or TS_SSH_CHANNEL_TYPE_SFTP
ssh_channel channel;
TS_SSH_CHANNEL_INFO()
{
type = TS_SSH_CHANNEL_TYPE_UNKNOWN;
channel = NULL;
}
}TS_SSH_CHANNEL_INFO;
typedef std::map<ssh_channel, TS_SSH_CHANNEL_INFO*> ts_ssh_channel_map;
class SshProxy;
class SshSession : public ExThreadBase
{
public:
SshSession(SshProxy* proxy, ssh_session sess_client);
virtual ~SshSession();
SshProxy* get_proxy(void) { return m_proxy; }
TS_SSH_CHANNEL_INFO* _get_cli_channel(ssh_channel srv_channel);
TS_SSH_CHANNEL_INFO* _get_srv_channel(ssh_channel cli_channel);
void client_ip(const char* ip) { m_client_ip = ip; }
const char* client_ip(void) const { return m_client_ip.c_str(); }
void client_port(ex_u16 port) { m_client_port = port; }
ex_u16 client_port(void) const { return m_client_port; }
protected:
// 继承自 TppSessionBase
bool _on_session_begin(const TPP_SESSION_INFO* info);
bool _on_session_end(void);
void _thread_loop(void);
void _set_stop_flag(void);
void _process_command(int from, const ex_u8* data, int len);
private:
void _run(void);
void _close_channels(void);
void _enter_sftp_mode(void);
static int _on_auth_password_request(ssh_session session, const char *user, const char *password, void *userdata);
static ssh_channel _on_new_channel_request(ssh_session session, void *userdata);
static int _on_client_pty_request(ssh_session session, ssh_channel channel, const char *term, int x, int y, int px, int py, void *userdata);
static int _on_client_shell_request(ssh_session session, ssh_channel channel, void *userdata);
static void _on_client_channel_close(ssh_session session, ssh_channel channel, void* userdata);
static int _on_client_channel_data(ssh_session session, ssh_channel channel, void *data, unsigned int len, int is_stderr, void *userdata);
static int _on_client_pty_win_change(ssh_session session, ssh_channel channel, int width, int height, int pxwidth, int pwheight, void *userdata);
static int _on_client_channel_subsystem_request(ssh_session session, ssh_channel channel, const char *subsystem, void *userdata);
static int _on_client_channel_exec_request(ssh_session session, ssh_channel channel, const char *command, void *userdata);
static int _on_server_channel_data(ssh_session session, ssh_channel channel, void *data, unsigned int len, int is_stderr, void *userdata);
static void _on_server_channel_close(ssh_session session, ssh_channel channel, void* userdata);
private:
int m_retcode;
int m_db_id;
TppSshRec m_rec;
SshProxy* m_proxy;
ssh_session m_cli_session;
ssh_session m_srv_session;
ExThreadLock m_lock;
ex_astr m_client_ip;
ex_u16 m_client_port;
ex_astr m_sid;
ex_astr m_server_ip;
ex_u16 m_server_port;
ex_astr m_user_name;
ex_astr m_user_auth;
int m_auth_mode;
bool m_is_first_server_data;
bool m_is_sftp;
bool m_is_logon;
// 一个ssh_session中可以打开多个ssh_channel
ts_ssh_channel_map m_channel_cli_srv; // 通过客户端通道查找服务端通道
ts_ssh_channel_map m_channel_srv_cli; // 通过服务端通道查找客户端通道
bool m_have_error;
bool m_recving_from_srv; // 是否正在从服务器接收数据?
bool m_recving_from_cli; // 是否正在从客户端接收数据?
struct ssh_server_callbacks_struct m_srv_cb;
struct ssh_channel_callbacks_struct m_cli_channel_cb;
struct ssh_channel_callbacks_struct m_srv_channel_cb;
int m_command_flag;
std::list<char> m_cmd_char_list;
std::list<char>::iterator m_cmd_char_pos;
};
#endif // __SSH_SESSION_H__
#ifndef __SSH_SESSION_H__
#define __SSH_SESSION_H__
#include "ssh_recorder.h"
#include <ex.h>
#include <libssh/libssh.h>
#include <libssh/server.h>
#include <libssh/callbacks.h>
#include <libssh/sftp.h>
#include <vector>
#include <list>
#define TS_SSH_CHANNEL_TYPE_UNKNOWN 0
#define TS_SSH_CHANNEL_TYPE_SHELL 1
#define TS_SSH_CHANNEL_TYPE_SFTP 2
#define TS_SSH_DATA_FROM_CLIENT 1
#define TS_SSH_DATA_FROM_SERVER 2
typedef struct TS_SSH_CHANNEL_INFO
{
int type; // TS_SSH_CHANNEL_TYPE_SHELL or TS_SSH_CHANNEL_TYPE_SFTP
ssh_channel channel;
TS_SSH_CHANNEL_INFO()
{
type = TS_SSH_CHANNEL_TYPE_UNKNOWN;
channel = NULL;
}
}TS_SSH_CHANNEL_INFO;
typedef std::map<ssh_channel, TS_SSH_CHANNEL_INFO*> ts_ssh_channel_map;
class SshProxy;
class SshSession : public ExThreadBase
{
public:
SshSession(SshProxy* proxy, ssh_session sess_client);
virtual ~SshSession();
SshProxy* get_proxy(void) { return m_proxy; }
TS_SSH_CHANNEL_INFO* _get_cli_channel(ssh_channel srv_channel);
TS_SSH_CHANNEL_INFO* _get_srv_channel(ssh_channel cli_channel);
void client_ip(const char* ip) { m_client_ip = ip; }
const char* client_ip(void) const { return m_client_ip.c_str(); }
void client_port(ex_u16 port) { m_client_port = port; }
ex_u16 client_port(void) const { return m_client_port; }
protected:
// 继承自 TppSessionBase
bool _on_session_begin(const TPP_SESSION_INFO* info);
bool _on_session_end(void);
void _thread_loop(void);
void _set_stop_flag(void);
void _process_ssh_command(int from, const ex_u8* data, int len);
void _process_sftp_command(const ex_u8* data, int len);
private:
void _run(void);
void _close_channels(void);
void _enter_sftp_mode(void);
static int _on_auth_password_request(ssh_session session, const char *user, const char *password, void *userdata);
static ssh_channel _on_new_channel_request(ssh_session session, void *userdata);
static int _on_client_pty_request(ssh_session session, ssh_channel channel, const char *term, int x, int y, int px, int py, void *userdata);
static int _on_client_shell_request(ssh_session session, ssh_channel channel, void *userdata);
static void _on_client_channel_close(ssh_session session, ssh_channel channel, void* userdata);
static int _on_client_channel_data(ssh_session session, ssh_channel channel, void *data, unsigned int len, int is_stderr, void *userdata);
static int _on_client_pty_win_change(ssh_session session, ssh_channel channel, int width, int height, int pxwidth, int pwheight, void *userdata);
static int _on_client_channel_subsystem_request(ssh_session session, ssh_channel channel, const char *subsystem, void *userdata);
static int _on_client_channel_exec_request(ssh_session session, ssh_channel channel, const char *command, void *userdata);
static int _on_server_channel_data(ssh_session session, ssh_channel channel, void *data, unsigned int len, int is_stderr, void *userdata);
static void _on_server_channel_close(ssh_session session, ssh_channel channel, void* userdata);
private:
int m_retcode;
int m_db_id;
TppSshRec m_rec;
SshProxy* m_proxy;
ssh_session m_cli_session;
ssh_session m_srv_session;
ExThreadLock m_lock;
ex_astr m_client_ip;
ex_u16 m_client_port;
ex_astr m_sid;
ex_astr m_server_ip;
ex_u16 m_server_port;
ex_astr m_user_name;
ex_astr m_user_auth;
int m_auth_mode;
bool m_is_first_server_data;
bool m_is_sftp;
bool m_is_logon;
// 一个ssh_session中可以打开多个ssh_channel
ts_ssh_channel_map m_channel_cli_srv; // 通过客户端通道查找服务端通道
ts_ssh_channel_map m_channel_srv_cli; // 通过服务端通道查找客户端通道
bool m_have_error;
bool m_recving_from_srv; // 是否正在从服务器接收数据?
bool m_recving_from_cli; // 是否正在从客户端接收数据?
struct ssh_server_callbacks_struct m_srv_cb;
struct ssh_channel_callbacks_struct m_cli_channel_cb;
struct ssh_channel_callbacks_struct m_srv_channel_cb;
int m_command_flag;
std::list<char> m_cmd_char_list;
std::list<char>::iterator m_cmd_char_pos;
};
#endif // __SSH_SESSION_H__

View File

@ -8,33 +8,26 @@ TppSshEnv::TppSshEnv()
TppSshEnv::~TppSshEnv()
{}
bool TppSshEnv::_on_init(TPP_INIT_ARGS* args)
{
bool TppSshEnv::_on_init(TPP_INIT_ARGS* args) {
ex_path_join(replay_path, false, L"ssh", NULL);
ExIniSection* ps = args->cfg->GetSection(L"protocol-ssh");
if (NULL == ps)
{
if (NULL == ps) {
EXLOGE("[ssh] invalid config(2).\n");
return false;
}
ex_wstr tmp;
if (!ps->GetStr(L"bind-ip", tmp))
{
if (!ps->GetStr(L"bind-ip", tmp)) {
bind_ip = TS_SSH_PROXY_HOST;
}
else
{
else {
ex_wstr2astr(tmp, bind_ip);
}
EXLOGW("[ssh] bind-ip: %s\n", bind_ip.c_str());
if (!ps->GetInt(L"bind-port", bind_port))
{
if (!ps->GetInt(L"bind-port", bind_port)) {
bind_port = TS_SSH_PROXY_PORT;
}
EXLOGW(L"[ssh] bind-port: %d\n", bind_port);
return true;
}

View File

@ -6,6 +6,8 @@ TPP_API ex_rv tpp_init(TPP_INIT_ARGS* init_args)
#ifdef EX_OS_UNIX
ssh_threads_set_callbacks(ssh_threads_get_pthread());
ssh_init();
#else
ssh_init();
#endif
if (!g_ssh_env.init(init_args))

Binary file not shown.

View File

@ -1,6 +1,6 @@
#ifndef __TS_SERVER_VER_H__
#define __TS_SERVER_VER_H__
#define TP_SERVER_VER L"2.2.8.1"
#define TP_SERVER_VER L"2.2.9.3"
#endif // __TS_SERVER_VER_H__

View File

@ -0,0 +1,25 @@
from qrcode.main import QRCode
from qrcode.main import make # noqa
from qrcode.constants import ( # noqa
ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_Q, ERROR_CORRECT_H)
from qrcode import image # noqa
def run_example(data="http://www.lincolnloop.com", *args, **kwargs):
"""
Build an example QR Code and display it.
There's an even easier way than the code here though: just use the ``make``
shortcut.
"""
qr = QRCode(*args, **kwargs)
qr.add_data(data)
im = qr.make_image()
im.show()
if __name__ == '__main__': # pragma: no cover
import sys
run_example(*sys.argv[1:])

View File

@ -0,0 +1,361 @@
from qrcode import constants
EXP_TABLE = list(range(256))
LOG_TABLE = list(range(256))
for i in range(8):
EXP_TABLE[i] = 1 << i
for i in range(8, 256):
EXP_TABLE[i] = (
EXP_TABLE[i - 4] ^ EXP_TABLE[i - 5] ^ EXP_TABLE[i - 6] ^
EXP_TABLE[i - 8])
for i in range(255):
LOG_TABLE[EXP_TABLE[i]] = i
RS_BLOCK_OFFSET = {
constants.ERROR_CORRECT_L: 0,
constants.ERROR_CORRECT_M: 1,
constants.ERROR_CORRECT_Q: 2,
constants.ERROR_CORRECT_H: 3,
}
RS_BLOCK_TABLE = [
# L
# M
# Q
# H
# 1
[1, 26, 19],
[1, 26, 16],
[1, 26, 13],
[1, 26, 9],
# 2
[1, 44, 34],
[1, 44, 28],
[1, 44, 22],
[1, 44, 16],
# 3
[1, 70, 55],
[1, 70, 44],
[2, 35, 17],
[2, 35, 13],
# 4
[1, 100, 80],
[2, 50, 32],
[2, 50, 24],
[4, 25, 9],
# 5
[1, 134, 108],
[2, 67, 43],
[2, 33, 15, 2, 34, 16],
[2, 33, 11, 2, 34, 12],
# 6
[2, 86, 68],
[4, 43, 27],
[4, 43, 19],
[4, 43, 15],
# 7
[2, 98, 78],
[4, 49, 31],
[2, 32, 14, 4, 33, 15],
[4, 39, 13, 1, 40, 14],
# 8
[2, 121, 97],
[2, 60, 38, 2, 61, 39],
[4, 40, 18, 2, 41, 19],
[4, 40, 14, 2, 41, 15],
# 9
[2, 146, 116],
[3, 58, 36, 2, 59, 37],
[4, 36, 16, 4, 37, 17],
[4, 36, 12, 4, 37, 13],
# 10
[2, 86, 68, 2, 87, 69],
[4, 69, 43, 1, 70, 44],
[6, 43, 19, 2, 44, 20],
[6, 43, 15, 2, 44, 16],
# 11
[4, 101, 81],
[1, 80, 50, 4, 81, 51],
[4, 50, 22, 4, 51, 23],
[3, 36, 12, 8, 37, 13],
# 12
[2, 116, 92, 2, 117, 93],
[6, 58, 36, 2, 59, 37],
[4, 46, 20, 6, 47, 21],
[7, 42, 14, 4, 43, 15],
# 13
[4, 133, 107],
[8, 59, 37, 1, 60, 38],
[8, 44, 20, 4, 45, 21],
[12, 33, 11, 4, 34, 12],
# 14
[3, 145, 115, 1, 146, 116],
[4, 64, 40, 5, 65, 41],
[11, 36, 16, 5, 37, 17],
[11, 36, 12, 5, 37, 13],
# 15
[5, 109, 87, 1, 110, 88],
[5, 65, 41, 5, 66, 42],
[5, 54, 24, 7, 55, 25],
[11, 36, 12, 7, 37, 13],
# 16
[5, 122, 98, 1, 123, 99],
[7, 73, 45, 3, 74, 46],
[15, 43, 19, 2, 44, 20],
[3, 45, 15, 13, 46, 16],
# 17
[1, 135, 107, 5, 136, 108],
[10, 74, 46, 1, 75, 47],
[1, 50, 22, 15, 51, 23],
[2, 42, 14, 17, 43, 15],
# 18
[5, 150, 120, 1, 151, 121],
[9, 69, 43, 4, 70, 44],
[17, 50, 22, 1, 51, 23],
[2, 42, 14, 19, 43, 15],
# 19
[3, 141, 113, 4, 142, 114],
[3, 70, 44, 11, 71, 45],
[17, 47, 21, 4, 48, 22],
[9, 39, 13, 16, 40, 14],
# 20
[3, 135, 107, 5, 136, 108],
[3, 67, 41, 13, 68, 42],
[15, 54, 24, 5, 55, 25],
[15, 43, 15, 10, 44, 16],
# 21
[4, 144, 116, 4, 145, 117],
[17, 68, 42],
[17, 50, 22, 6, 51, 23],
[19, 46, 16, 6, 47, 17],
# 22
[2, 139, 111, 7, 140, 112],
[17, 74, 46],
[7, 54, 24, 16, 55, 25],
[34, 37, 13],
# 23
[4, 151, 121, 5, 152, 122],
[4, 75, 47, 14, 76, 48],
[11, 54, 24, 14, 55, 25],
[16, 45, 15, 14, 46, 16],
# 24
[6, 147, 117, 4, 148, 118],
[6, 73, 45, 14, 74, 46],
[11, 54, 24, 16, 55, 25],
[30, 46, 16, 2, 47, 17],
# 25
[8, 132, 106, 4, 133, 107],
[8, 75, 47, 13, 76, 48],
[7, 54, 24, 22, 55, 25],
[22, 45, 15, 13, 46, 16],
# 26
[10, 142, 114, 2, 143, 115],
[19, 74, 46, 4, 75, 47],
[28, 50, 22, 6, 51, 23],
[33, 46, 16, 4, 47, 17],
# 27
[8, 152, 122, 4, 153, 123],
[22, 73, 45, 3, 74, 46],
[8, 53, 23, 26, 54, 24],
[12, 45, 15, 28, 46, 16],
# 28
[3, 147, 117, 10, 148, 118],
[3, 73, 45, 23, 74, 46],
[4, 54, 24, 31, 55, 25],
[11, 45, 15, 31, 46, 16],
# 29
[7, 146, 116, 7, 147, 117],
[21, 73, 45, 7, 74, 46],
[1, 53, 23, 37, 54, 24],
[19, 45, 15, 26, 46, 16],
# 30
[5, 145, 115, 10, 146, 116],
[19, 75, 47, 10, 76, 48],
[15, 54, 24, 25, 55, 25],
[23, 45, 15, 25, 46, 16],
# 31
[13, 145, 115, 3, 146, 116],
[2, 74, 46, 29, 75, 47],
[42, 54, 24, 1, 55, 25],
[23, 45, 15, 28, 46, 16],
# 32
[17, 145, 115],
[10, 74, 46, 23, 75, 47],
[10, 54, 24, 35, 55, 25],
[19, 45, 15, 35, 46, 16],
# 33
[17, 145, 115, 1, 146, 116],
[14, 74, 46, 21, 75, 47],
[29, 54, 24, 19, 55, 25],
[11, 45, 15, 46, 46, 16],
# 34
[13, 145, 115, 6, 146, 116],
[14, 74, 46, 23, 75, 47],
[44, 54, 24, 7, 55, 25],
[59, 46, 16, 1, 47, 17],
# 35
[12, 151, 121, 7, 152, 122],
[12, 75, 47, 26, 76, 48],
[39, 54, 24, 14, 55, 25],
[22, 45, 15, 41, 46, 16],
# 36
[6, 151, 121, 14, 152, 122],
[6, 75, 47, 34, 76, 48],
[46, 54, 24, 10, 55, 25],
[2, 45, 15, 64, 46, 16],
# 37
[17, 152, 122, 4, 153, 123],
[29, 74, 46, 14, 75, 47],
[49, 54, 24, 10, 55, 25],
[24, 45, 15, 46, 46, 16],
# 38
[4, 152, 122, 18, 153, 123],
[13, 74, 46, 32, 75, 47],
[48, 54, 24, 14, 55, 25],
[42, 45, 15, 32, 46, 16],
# 39
[20, 147, 117, 4, 148, 118],
[40, 75, 47, 7, 76, 48],
[43, 54, 24, 22, 55, 25],
[10, 45, 15, 67, 46, 16],
# 40
[19, 148, 118, 6, 149, 119],
[18, 75, 47, 31, 76, 48],
[34, 54, 24, 34, 55, 25],
[20, 45, 15, 61, 46, 16]
]
def glog(n):
if n < 1: # pragma: no cover
raise ValueError("glog(%s)" % n)
return LOG_TABLE[n]
def gexp(n):
return EXP_TABLE[n % 255]
class Polynomial:
def __init__(self, num, shift):
if not num: # pragma: no cover
raise Exception("%s/%s" % (len(num), shift))
offset = 0
for item in num:
if item != 0:
break
offset += 1
self.num = [0] * (len(num) - offset + shift)
for i in range(len(num) - offset):
self.num[i] = num[i + offset]
def __getitem__(self, index):
return self.num[index]
def __iter__(self):
return iter(self.num)
def __len__(self):
return len(self.num)
def __mul__(self, other):
num = [0] * (len(self) + len(other) - 1)
for i, item in enumerate(self):
for j, other_item in enumerate(other):
num[i + j] ^= gexp(glog(item) + glog(other_item))
return Polynomial(num, 0)
def __mod__(self, other):
difference = len(self) - len(other)
if difference < 0:
return self
ratio = glog(self[0]) - glog(other[0])
num = self[:]
num = [
item ^ gexp(glog(other_item) + ratio)
for item, other_item in zip(self, other)]
if difference:
num.extend(self[-difference:])
# recursive call
return Polynomial(num, 0) % other
class RSBlock:
def __init__(self, total_count, data_count):
self.total_count = total_count
self.data_count = data_count
def rs_blocks(version, error_correction):
if error_correction not in RS_BLOCK_OFFSET: # pragma: no cover
raise Exception(
"bad rs block @ version: %s / error_correction: %s" %
(version, error_correction))
offset = RS_BLOCK_OFFSET[error_correction]
rs_block = RS_BLOCK_TABLE[(version - 1) * 4 + offset]
blocks = []
for i in range(0, len(rs_block), 3):
count, total_count, data_count = rs_block[i:i + 3]
for j in range(count):
blocks.append(RSBlock(total_count, data_count))
return blocks

View File

@ -0,0 +1,96 @@
#!/usr/bin/env python
"""
qr - Convert stdin (or the first argument) to a QR Code.
When stdout is a tty the QR Code is printed to the terminal and when stdout is
a pipe to a file an image is written. The default image format is PNG.
"""
import sys
import optparse
import os
import qrcode
# The next block is added to get the terminal to display properly on MS platforms
if sys.platform.startswith(('win', 'cygwin')):
import colorama
colorama.init()
default_factories = {
'pil': 'qrcode.image.pil.PilImage',
'pymaging': 'qrcode.image.pure.PymagingImage',
'svg': 'qrcode.image.svg.SvgImage',
'svg-fragment': 'qrcode.image.svg.SvgFragmentImage',
'svg-path': 'qrcode.image.svg.SvgPathImage',
}
error_correction = {
'L': qrcode.ERROR_CORRECT_L,
'M': qrcode.ERROR_CORRECT_M,
'Q': qrcode.ERROR_CORRECT_Q,
'H': qrcode.ERROR_CORRECT_H,
}
def main(args=sys.argv[1:]):
parser = optparse.OptionParser(usage=__doc__.strip())
parser.add_option(
"--factory", help="Full python path to the image factory class to "
"create the image with. You can use the following shortcuts to the "
"built-in image factory classes: {0}.".format(
", ".join(sorted(default_factories.keys()))))
parser.add_option(
"--optimize", type=int, help="Optimize the data by looking for chunks "
"of at least this many characters that could use a more efficient "
"encoding method. Use 0 to turn off chunk optimization.")
parser.add_option(
"--error-correction", type='choice',
choices=sorted(error_correction.keys()), default='M',
help="The error correction level to use. Choices are L (7%), "
"M (15%, default), Q (25%), and H (30%).")
opts, args = parser.parse_args(args)
qr = qrcode.QRCode(
error_correction=error_correction[opts.error_correction])
if opts.factory:
module = default_factories.get(opts.factory, opts.factory)
if '.' not in module:
parser.error("The image factory is not a full python path")
module, name = module.rsplit('.', 1)
imp = __import__(module, {}, [], [name])
image_factory = getattr(imp, name)
else:
image_factory = None
if args:
data = args[0]
else:
# Use sys.stdin.buffer if available (Python 3) avoiding
# UnicodeDecodeErrors.
stdin_buffer = getattr(sys.stdin, 'buffer', sys.stdin)
data = stdin_buffer.read()
if opts.optimize is None:
qr.add_data(data)
else:
qr.add_data(data, optimize=opts.optimize)
if image_factory is None and os.isatty(sys.stdout.fileno()):
qr.print_ascii(tty=True)
return
img = qr.make_image(image_factory=image_factory)
sys.stdout.flush()
# Use sys.stdout.buffer if available (Python 3), avoiding
# UnicodeDecodeErrors.
stdout_buffer = getattr(sys.stdout, 'buffer', None)
if not stdout_buffer:
if sys.platform == 'win32': # pragma: no cover
import msvcrt
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
stdout_buffer = sys.stdout
img.save(stdout_buffer)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,5 @@
# QR error correct levels
ERROR_CORRECT_L = 1
ERROR_CORRECT_M = 0
ERROR_CORRECT_Q = 3
ERROR_CORRECT_H = 2

View File

@ -0,0 +1,2 @@
class DataOverflowError(Exception):
pass

View File

@ -0,0 +1,62 @@
class BaseImage(object):
"""
Base QRCode image output class.
"""
kind = None
allowed_kinds = None
def __init__(self, border, width, box_size, *args, **kwargs):
self.border = border
self.width = width
self.box_size = box_size
self.pixel_size = (self.width + self.border*2) * self.box_size
self._img = self.new_image(**kwargs)
def drawrect(self, row, col):
"""
Draw a single rectangle of the QR code.
"""
raise NotImplementedError("BaseImage.drawrect")
def save(self, stream, kind=None):
"""
Save the image file.
"""
raise NotImplementedError("BaseImage.save")
def pixel_box(self, row, col):
"""
A helper method for pixel-based image generators that specifies the
four pixel coordinates for a single rect.
"""
x = (col + self.border) * self.box_size
y = (row + self.border) * self.box_size
return [(x, y), (x + self.box_size - 1, y + self.box_size - 1)]
def new_image(self, **kwargs): # pragma: no cover
"""
Build the image class. Subclasses should return the class created.
"""
return None
def get_image(self, **kwargs):
"""
Return the image class for further processing.
"""
return self._img
def check_kind(self, kind, transform=None):
"""
Get the image type.
"""
if kind is None:
kind = self.kind
allowed = not self.allowed_kinds or kind in self.allowed_kinds
if transform:
kind = transform(kind)
if not allowed:
allowed = kind in self.allowed_kinds
if not allowed:
raise ValueError(
"Cannot set %s type to %s" % (type(self).__name__, kind))
return kind

View File

@ -0,0 +1,50 @@
# Needed on case-insensitive filesystems
from __future__ import absolute_import
# Try to import PIL in either of the two ways it can be installed.
try:
from PIL import Image, ImageDraw
except ImportError: # pragma: no cover
import Image
import ImageDraw
import qrcode.image.base
class PilImage(qrcode.image.base.BaseImage):
"""
PIL image builder, default format is PNG.
"""
kind = "PNG"
def new_image(self, **kwargs):
back_color = kwargs.get("fill_color", "white")
fill_color = kwargs.get("back_color", "black")
if fill_color.lower() != "black" or back_color.lower() != "white":
if back_color.lower() == "transparent":
mode = "RGBA"
back_color = None
else:
mode = "RGB"
else:
mode = "1"
img = Image.new(mode, (self.pixel_size, self.pixel_size), back_color)
self.fill_color = fill_color
self._idr = ImageDraw.Draw(img)
return img
def drawrect(self, row, col):
box = self.pixel_box(row, col)
self._idr.rectangle(box, fill=self.fill_color)
def save(self, stream, format=None, **kwargs):
if format is None:
format = kwargs.get("kind", self.kind)
if "kind" in kwargs:
del kwargs["kind"]
self._img.save(stream, format=format, **kwargs)
def __getattr__(self, name):
return getattr(self._img, name)

View File

@ -0,0 +1,49 @@
from pymaging import Image
from pymaging.colors import RGB
from pymaging.formats import registry
from pymaging.shapes import Line
from pymaging.webcolors import Black, White
from pymaging_png.png import PNG
import qrcode.image.base
class PymagingImage(qrcode.image.base.BaseImage):
"""
pymaging image builder, default format is PNG.
"""
kind = "PNG"
allowed_kinds = ("PNG",)
def __init__(self, *args, **kwargs):
"""
Register PNG with pymaging.
"""
registry.formats = []
registry.names = {}
registry._populate()
registry.register(PNG)
super(PymagingImage, self).__init__(*args, **kwargs)
def new_image(self, **kwargs):
return Image.new(RGB, self.pixel_size, self.pixel_size, White)
def drawrect(self, row, col):
(x, y), (x2, y2) = self.pixel_box(row, col)
for r in range(self.box_size):
line_y = y + r
line = Line(x, line_y, x2, line_y)
self._img.draw(line, Black)
def save(self, stream, kind=None):
self._img.save(stream, self.check_kind(kind))
def check_kind(self, kind, transform=None, **kwargs):
"""
pymaging (pymaging_png at least) uses lower case for the type.
"""
if transform is None:
transform = lambda x: x.lower()
return super(PymagingImage, self).check_kind(
kind, transform=transform, **kwargs)

View File

@ -0,0 +1,159 @@
from decimal import Decimal
# On Python 2.6 must install lxml since the older xml.etree.ElementTree
# version can not be used to create SVG images.
try:
import lxml.etree as ET
except ImportError:
import xml.etree.ElementTree as ET
import qrcode.image.base
class SvgFragmentImage(qrcode.image.base.BaseImage):
"""
SVG image builder
Creates a QR-code image as a SVG document fragment.
"""
_SVG_namespace = "http://www.w3.org/2000/svg"
kind = "SVG"
allowed_kinds = ("SVG",)
def __init__(self, *args, **kwargs):
ET.register_namespace("svg", self._SVG_namespace)
super(SvgFragmentImage, self).__init__(*args, **kwargs)
# Save the unit size, for example the default box_size of 10 is '1mm'.
self.unit_size = self.units(self.box_size)
def drawrect(self, row, col):
self._img.append(self._rect(row, col))
def units(self, pixels, text=True):
"""
A box_size of 10 (default) equals 1mm.
"""
units = Decimal(pixels) / 10
if not text:
return units
return '%smm' % units
def save(self, stream, kind=None):
self.check_kind(kind=kind)
self._write(stream)
def new_image(self, **kwargs):
return self._svg()
def _svg(self, tag=None, version='1.1', **kwargs):
if tag is None:
tag = ET.QName(self._SVG_namespace, "svg")
dimension = self.units(self.pixel_size)
return ET.Element(
tag, width=dimension, height=dimension, version=version,
**kwargs)
def _rect(self, row, col, tag=None):
if tag is None:
tag = ET.QName(self._SVG_namespace, "rect")
x, y = self.pixel_box(row, col)[0]
return ET.Element(
tag, x=self.units(x), y=self.units(y),
width=self.unit_size, height=self.unit_size)
def _write(self, stream):
ET.ElementTree(self._img).write(stream, xml_declaration=False)
class SvgImage(SvgFragmentImage):
"""
Standalone SVG image builder
Creates a QR-code image as a standalone SVG document.
"""
background = None
def _svg(self, tag='svg', **kwargs):
svg = super(SvgImage, self)._svg(tag=tag, **kwargs)
svg.set("xmlns", self._SVG_namespace)
if self.background:
svg.append(
ET.Element(
'rect', fill=self.background, x='0', y='0', width='100%',
height='100%'))
return svg
def _rect(self, row, col):
return super(SvgImage, self)._rect(row, col, tag="rect")
def _write(self, stream):
ET.ElementTree(self._img).write(stream, encoding="UTF-8",
xml_declaration=True)
class SvgPathImage(SvgImage):
"""
SVG image builder with one single <path> element (removes white spaces
between individual QR points).
"""
QR_PATH_STYLE = 'fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none'
def __init__(self, *args, **kwargs):
self._points = set()
super(SvgPathImage, self).__init__(*args, **kwargs)
def _svg(self, viewBox=None, **kwargs):
if viewBox is None:
dimension = self.units(self.pixel_size, text=False)
viewBox = '0 0 %(d)s %(d)s' % {'d': dimension}
return super(SvgPathImage, self)._svg(viewBox=viewBox, **kwargs)
def drawrect(self, row, col):
# (x, y)
self._points.add((col, row))
def _generate_subpaths(self):
"""Generates individual QR points as subpaths"""
rect_size = self.units(self.box_size, text=False)
for point in self._points:
x_base = self.units(
(point[0]+self.border)*self.box_size, text=False)
y_base = self.units(
(point[1]+self.border)*self.box_size, text=False)
yield (
'M %(x0)s %(y0)s L %(x0)s %(y1)s L %(x1)s %(y1)s L %(x1)s '
'%(y0)s z' % dict(
x0=x_base, y0=y_base,
x1=x_base+rect_size, y1=y_base+rect_size,
))
def make_path(self):
subpaths = self._generate_subpaths()
return ET.Element(
ET.QName("path"),
style=self.QR_PATH_STYLE,
d=' '.join(subpaths),
id="qr-path"
)
def _write(self, stream):
self._img.append(self.make_path())
super(SvgPathImage, self)._write(stream)
class SvgFillImage(SvgImage):
"""
An SvgImage that fills the background to white.
"""
background = 'white'
class SvgPathFillImage(SvgPathImage):
"""
An SvgPathImage that fills the background to white.
"""
background = 'white'

View File

@ -0,0 +1,422 @@
from qrcode import constants, exceptions, util
from qrcode.image.base import BaseImage
import six
from bisect import bisect_left
def make(data=None, **kwargs):
qr = QRCode(**kwargs)
qr.add_data(data)
return qr.make_image()
def _check_version(version):
if version < 1 or version > 40:
raise ValueError(
"Invalid version (was %s, expected 1 to 40)" % version)
def _check_box_size(size):
if int(size) <= 0:
raise ValueError(
"Invalid box size (was %s, expected larger than 0)" % size)
class QRCode:
def __init__(self, version=None,
error_correction=constants.ERROR_CORRECT_M,
box_size=10, border=4,
image_factory=None):
_check_box_size(box_size)
self.version = version and int(version)
self.error_correction = int(error_correction)
self.box_size = int(box_size)
# Spec says border should be at least four boxes wide, but allow for
# any (e.g. for producing printable QR codes).
self.border = int(border)
self.image_factory = image_factory
if image_factory is not None:
assert issubclass(image_factory, BaseImage)
self.clear()
def clear(self):
"""
Reset the internal data.
"""
self.modules = None
self.modules_count = 0
self.data_cache = None
self.data_list = []
def add_data(self, data, optimize=20):
"""
Add data to this QR Code.
:param optimize: Data will be split into multiple chunks to optimize
the QR size by finding to more compressed modes of at least this
length. Set to ``0`` to avoid optimizing at all.
"""
if isinstance(data, util.QRData):
self.data_list.append(data)
else:
if optimize:
self.data_list.extend(util.optimal_data_chunks(data))
else:
self.data_list.append(util.QRData(data))
self.data_cache = None
def make(self, fit=True):
"""
Compile the data into a QR Code array.
:param fit: If ``True`` (or if a size has not been provided), find the
best fit for the data to avoid data overflow errors.
"""
if fit or (self.version is None):
self.best_fit(start=self.version)
self.makeImpl(False, self.best_mask_pattern())
def makeImpl(self, test, mask_pattern):
_check_version(self.version)
self.modules_count = self.version * 4 + 17
self.modules = [None] * self.modules_count
for row in range(self.modules_count):
self.modules[row] = [None] * self.modules_count
for col in range(self.modules_count):
self.modules[row][col] = None # (col + row) % 3
self.setup_position_probe_pattern(0, 0)
self.setup_position_probe_pattern(self.modules_count - 7, 0)
self.setup_position_probe_pattern(0, self.modules_count - 7)
self.setup_position_adjust_pattern()
self.setup_timing_pattern()
self.setup_type_info(test, mask_pattern)
if self.version >= 7:
self.setup_type_number(test)
if self.data_cache is None:
self.data_cache = util.create_data(
self.version, self.error_correction, self.data_list)
self.map_data(self.data_cache, mask_pattern)
def setup_position_probe_pattern(self, row, col):
for r in range(-1, 8):
if row + r <= -1 or self.modules_count <= row + r:
continue
for c in range(-1, 8):
if col + c <= -1 or self.modules_count <= col + c:
continue
if (0 <= r and r <= 6 and (c == 0 or c == 6)
or (0 <= c and c <= 6 and (r == 0 or r == 6))
or (2 <= r and r <= 4 and 2 <= c and c <= 4)):
self.modules[row + r][col + c] = True
else:
self.modules[row + r][col + c] = False
def best_fit(self, start=None):
"""
Find the minimum size required to fit in the data.
"""
if start is None:
start = 1
_check_version(start)
# Corresponds to the code in util.create_data, except we don't yet know
# version, so optimistically assume start and check later
mode_sizes = util.mode_sizes_for_version(start)
buffer = util.BitBuffer()
for data in self.data_list:
buffer.put(data.mode, 4)
buffer.put(len(data), mode_sizes[data.mode])
data.write(buffer)
needed_bits = len(buffer)
self.version = bisect_left(util.BIT_LIMIT_TABLE[self.error_correction],
needed_bits, start)
if self.version == 41:
raise exceptions.DataOverflowError()
# Now check whether we need more bits for the mode sizes, recursing if
# our guess was too low
if mode_sizes is not util.mode_sizes_for_version(self.version):
self.best_fit(start=self.version)
return self.version
def best_mask_pattern(self):
"""
Find the most efficient mask pattern.
"""
min_lost_point = 0
pattern = 0
for i in range(8):
self.makeImpl(True, i)
lost_point = util.lost_point(self.modules)
if i == 0 or min_lost_point > lost_point:
min_lost_point = lost_point
pattern = i
return pattern
def print_tty(self, out=None):
"""
Output the QR Code only using TTY colors.
If the data has not been compiled yet, make it first.
"""
if out is None:
import sys
out = sys.stdout
if not out.isatty():
raise OSError("Not a tty")
if self.data_cache is None:
self.make()
modcount = self.modules_count
out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n")
for r in range(modcount):
out.write("\x1b[1;47m \x1b[40m")
for c in range(modcount):
if self.modules[r][c]:
out.write(" ")
else:
out.write("\x1b[1;47m \x1b[40m")
out.write("\x1b[1;47m \x1b[0m\n")
out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n")
out.flush()
def print_ascii(self, out=None, tty=False, invert=False):
"""
Output the QR Code using ASCII characters.
:param tty: use fixed TTY color codes (forces invert=True)
:param invert: invert the ASCII characters (solid <-> transparent)
"""
if out is None:
import sys
if sys.version_info < (2, 7):
# On Python versions 2.6 and earlier, stdout tries to encode
# strings using ASCII rather than stdout.encoding, so use this
# workaround.
import codecs
out = codecs.getwriter(sys.stdout.encoding)(sys.stdout)
else:
out = sys.stdout
if tty and not out.isatty():
raise OSError("Not a tty")
if self.data_cache is None:
self.make()
modcount = self.modules_count
codes = [six.int2byte(code).decode('cp437')
for code in (255, 223, 220, 219)]
if tty:
invert = True
if invert:
codes.reverse()
def get_module(x, y):
if (invert and self.border and
max(x, y) >= modcount+self.border):
return 1
if min(x, y) < 0 or max(x, y) >= modcount:
return 0
return self.modules[x][y]
for r in range(-self.border, modcount+self.border, 2):
if tty:
if not invert or r < modcount+self.border-1:
out.write('\x1b[48;5;232m') # Background black
out.write('\x1b[38;5;255m') # Foreground white
for c in range(-self.border, modcount+self.border):
pos = get_module(r, c) + (get_module(r+1, c) << 1)
out.write(codes[pos])
if tty:
out.write('\x1b[0m')
out.write('\n')
out.flush()
def make_image(self, image_factory=None, **kwargs):
"""
Make an image from the QR Code data.
If the data has not been compiled yet, make it first.
"""
_check_box_size(self.box_size)
if self.data_cache is None:
self.make()
if image_factory is not None:
assert issubclass(image_factory, BaseImage)
else:
image_factory = self.image_factory
if image_factory is None:
# Use PIL by default
from qrcode.image.pil import PilImage
image_factory = PilImage
im = image_factory(
self.border, self.modules_count, self.box_size, **kwargs)
for r in range(self.modules_count):
for c in range(self.modules_count):
if self.modules[r][c]:
im.drawrect(r, c)
return im
def setup_timing_pattern(self):
for r in range(8, self.modules_count - 8):
if self.modules[r][6] is not None:
continue
self.modules[r][6] = (r % 2 == 0)
for c in range(8, self.modules_count - 8):
if self.modules[6][c] is not None:
continue
self.modules[6][c] = (c % 2 == 0)
def setup_position_adjust_pattern(self):
pos = util.pattern_position(self.version)
for i in range(len(pos)):
for j in range(len(pos)):
row = pos[i]
col = pos[j]
if self.modules[row][col] is not None:
continue
for r in range(-2, 3):
for c in range(-2, 3):
if (r == -2 or r == 2 or c == -2 or c == 2 or
(r == 0 and c == 0)):
self.modules[row + r][col + c] = True
else:
self.modules[row + r][col + c] = False
def setup_type_number(self, test):
bits = util.BCH_type_number(self.version)
for i in range(18):
mod = (not test and ((bits >> i) & 1) == 1)
self.modules[i // 3][i % 3 + self.modules_count - 8 - 3] = mod
for i in range(18):
mod = (not test and ((bits >> i) & 1) == 1)
self.modules[i % 3 + self.modules_count - 8 - 3][i // 3] = mod
def setup_type_info(self, test, mask_pattern):
data = (self.error_correction << 3) | mask_pattern
bits = util.BCH_type_info(data)
# vertical
for i in range(15):
mod = (not test and ((bits >> i) & 1) == 1)
if i < 6:
self.modules[i][8] = mod
elif i < 8:
self.modules[i + 1][8] = mod
else:
self.modules[self.modules_count - 15 + i][8] = mod
# horizontal
for i in range(15):
mod = (not test and ((bits >> i) & 1) == 1)
if i < 8:
self.modules[8][self.modules_count - i - 1] = mod
elif i < 9:
self.modules[8][15 - i - 1 + 1] = mod
else:
self.modules[8][15 - i - 1] = mod
# fixed module
self.modules[self.modules_count - 8][8] = (not test)
def map_data(self, data, mask_pattern):
inc = -1
row = self.modules_count - 1
bitIndex = 7
byteIndex = 0
mask_func = util.mask_func(mask_pattern)
data_len = len(data)
for col in six.moves.xrange(self.modules_count - 1, 0, -2):
if col <= 6:
col -= 1
col_range = (col, col-1)
while True:
for c in col_range:
if self.modules[row][c] is None:
dark = False
if byteIndex < data_len:
dark = (((data[byteIndex] >> bitIndex) & 1) == 1)
if mask_func(row, c):
dark = not dark
self.modules[row][c] = dark
bitIndex -= 1
if bitIndex == -1:
byteIndex += 1
bitIndex = 7
row += inc
if row < 0 or self.modules_count <= row:
row -= inc
inc = -inc
break
def get_matrix(self):
"""
Return the QR Code as a multidimensonal array, including the border.
To return the array without a border, set ``self.border`` to 0 first.
"""
if self.data_cache is None:
self.make()
if not self.border:
return self.modules
width = len(self.modules) + self.border*2
code = [[False]*width] * self.border
x_border = [False]*self.border
for module in self.modules:
code.append(x_border + module + x_border)
code += [[False]*width] * self.border
return code

View File

@ -0,0 +1,33 @@
import six
# {'code': 'N', 'label': 'Name', 'required': True, 'multipart': [
# 'Last Name', 'First Name']},
PROPERTIES = {
'NICKNAME': {'label': 'Nickname'},
'BDAY': {'label': 'Birthday', 'date': True},
'TEL': {'label': 'Phone'},
'EMAIL': {'label': 'E-mail'},
'ADR': {'label': 'Address', 'multipart': [
'PO Box', 'Room Number', 'House Number', 'City', 'Prefecture',
'Zip Code', 'Country']},
'URL': {'label': 'URL'},
'MEMO': {'label': 'Note'},
}
def build_code(data):
notation = []
name = data['N']
if not isinstance(name, six.text_type):
name = ','.join(name)
notation.append('N', name)
for prop in PROPERTIES:
value = data.get(prop['code'])
if not value:
continue
if prop['date']:
value = value.strftime('%Y%m%d')
elif prop['multipart']:
value = ','.join(value)

View File

@ -0,0 +1,8 @@
import string
import qrcode
qr = qrcode.QRCode()
qr.add_data(string.letters*13)
qr.make()
print(qr.version)

View File

@ -0,0 +1,556 @@
import re
import math
import six
from six.moves import xrange
from qrcode import base, exceptions
# QR encoding modes.
MODE_NUMBER = 1 << 0
MODE_ALPHA_NUM = 1 << 1
MODE_8BIT_BYTE = 1 << 2
MODE_KANJI = 1 << 3
# Encoding mode sizes.
MODE_SIZE_SMALL = {
MODE_NUMBER: 10,
MODE_ALPHA_NUM: 9,
MODE_8BIT_BYTE: 8,
MODE_KANJI: 8,
}
MODE_SIZE_MEDIUM = {
MODE_NUMBER: 12,
MODE_ALPHA_NUM: 11,
MODE_8BIT_BYTE: 16,
MODE_KANJI: 10,
}
MODE_SIZE_LARGE = {
MODE_NUMBER: 14,
MODE_ALPHA_NUM: 13,
MODE_8BIT_BYTE: 16,
MODE_KANJI: 12,
}
ALPHA_NUM = six.b('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:')
RE_ALPHA_NUM = re.compile(six.b('^[') + re.escape(ALPHA_NUM) + six.b(']*\Z'))
# The number of bits for numeric delimited data lengths.
NUMBER_LENGTH = {3: 10, 2: 7, 1: 4}
PATTERN_POSITION_TABLE = [
[],
[6, 18],
[6, 22],
[6, 26],
[6, 30],
[6, 34],
[6, 22, 38],
[6, 24, 42],
[6, 26, 46],
[6, 28, 50],
[6, 30, 54],
[6, 32, 58],
[6, 34, 62],
[6, 26, 46, 66],
[6, 26, 48, 70],
[6, 26, 50, 74],
[6, 30, 54, 78],
[6, 30, 56, 82],
[6, 30, 58, 86],
[6, 34, 62, 90],
[6, 28, 50, 72, 94],
[6, 26, 50, 74, 98],
[6, 30, 54, 78, 102],
[6, 28, 54, 80, 106],
[6, 32, 58, 84, 110],
[6, 30, 58, 86, 114],
[6, 34, 62, 90, 118],
[6, 26, 50, 74, 98, 122],
[6, 30, 54, 78, 102, 126],
[6, 26, 52, 78, 104, 130],
[6, 30, 56, 82, 108, 134],
[6, 34, 60, 86, 112, 138],
[6, 30, 58, 86, 114, 142],
[6, 34, 62, 90, 118, 146],
[6, 30, 54, 78, 102, 126, 150],
[6, 24, 50, 76, 102, 128, 154],
[6, 28, 54, 80, 106, 132, 158],
[6, 32, 58, 84, 110, 136, 162],
[6, 26, 54, 82, 110, 138, 166],
[6, 30, 58, 86, 114, 142, 170]
]
G15 = (
(1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) |
(1 << 0))
G18 = (
(1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) |
(1 << 2) | (1 << 0))
G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1)
PAD0 = 0xEC
PAD1 = 0x11
# Precompute bit count limits, indexed by error correction level and code size
_data_count = lambda block: block.data_count
BIT_LIMIT_TABLE = [
[0] + [8*sum(map(_data_count, base.rs_blocks(version, error_correction)))
for version in xrange(1, 41)]
for error_correction in xrange(4)
]
def BCH_type_info(data):
d = data << 10
while BCH_digit(d) - BCH_digit(G15) >= 0:
d ^= (G15 << (BCH_digit(d) - BCH_digit(G15)))
return ((data << 10) | d) ^ G15_MASK
def BCH_type_number(data):
d = data << 12
while BCH_digit(d) - BCH_digit(G18) >= 0:
d ^= (G18 << (BCH_digit(d) - BCH_digit(G18)))
return (data << 12) | d
def BCH_digit(data):
digit = 0
while data != 0:
digit += 1
data >>= 1
return digit
def pattern_position(version):
return PATTERN_POSITION_TABLE[version - 1]
def mask_func(pattern):
"""
Return the mask function for the given mask pattern.
"""
if pattern == 0: # 000
return lambda i, j: (i + j) % 2 == 0
if pattern == 1: # 001
return lambda i, j: i % 2 == 0
if pattern == 2: # 010
return lambda i, j: j % 3 == 0
if pattern == 3: # 011
return lambda i, j: (i + j) % 3 == 0
if pattern == 4: # 100
return lambda i, j: (math.floor(i / 2) + math.floor(j / 3)) % 2 == 0
if pattern == 5: # 101
return lambda i, j: (i * j) % 2 + (i * j) % 3 == 0
if pattern == 6: # 110
return lambda i, j: ((i * j) % 2 + (i * j) % 3) % 2 == 0
if pattern == 7: # 111
return lambda i, j: ((i * j) % 3 + (i + j) % 2) % 2 == 0
raise TypeError("Bad mask pattern: " + pattern) # pragma: no cover
def mode_sizes_for_version(version):
if version < 10:
return MODE_SIZE_SMALL
elif version < 27:
return MODE_SIZE_MEDIUM
else:
return MODE_SIZE_LARGE
def length_in_bits(mode, version):
if mode not in (
MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE, MODE_KANJI):
raise TypeError("Invalid mode (%s)" % mode) # pragma: no cover
if version < 1 or version > 40: # pragma: no cover
raise ValueError(
"Invalid version (was %s, expected 1 to 40)" % version)
return mode_sizes_for_version(version)[mode]
def lost_point(modules):
modules_count = len(modules)
lost_point = 0
lost_point = _lost_point_level1(modules, modules_count)
lost_point += _lost_point_level2(modules, modules_count)
lost_point += _lost_point_level3(modules, modules_count)
lost_point += _lost_point_level4(modules, modules_count)
return lost_point
def _lost_point_level1(modules, modules_count):
lost_point = 0
modules_range = xrange(modules_count)
row_range_first = (0, 1)
row_range_last = (-1, 0)
row_range_standard = (-1, 0, 1)
col_range_first = ((0, 1), (1,))
col_range_last = ((-1, 0), (-1,))
col_range_standard = ((-1, 0, 1), (-1, 1))
for row in modules_range:
if row == 0:
row_range = row_range_first
elif row == modules_count-1:
row_range = row_range_last
else:
row_range = row_range_standard
for col in modules_range:
sameCount = 0
dark = modules[row][col]
if col == 0:
col_range = col_range_first
elif col == modules_count-1:
col_range = col_range_last
else:
col_range = col_range_standard
for r in row_range:
row_offset = row + r
if r != 0:
col_idx = 0
else:
col_idx = 1
for c in col_range[col_idx]:
if dark == modules[row_offset][col + c]:
sameCount += 1
if sameCount > 5:
lost_point += (3 + sameCount - 5)
return lost_point
def _lost_point_level2(modules, modules_count):
lost_point = 0
modules_range = xrange(modules_count - 1)
for row in modules_range:
this_row = modules[row]
next_row = modules[row+1]
for col in modules_range:
count = 0
if this_row[col]:
count += 1
if next_row[col]:
count += 1
if this_row[col + 1]:
count += 1
if next_row[col + 1]:
count += 1
if count == 0 or count == 4:
lost_point += 3
return lost_point
def _lost_point_level3(modules, modules_count):
modules_range_short = xrange(modules_count-6)
lost_point = 0
for row in xrange(modules_count):
this_row = modules[row]
for col in modules_range_short:
if (this_row[col]
and not this_row[col + 1]
and this_row[col + 2]
and this_row[col + 3]
and this_row[col + 4]
and not this_row[col + 5]
and this_row[col + 6]):
lost_point += 40
for col in xrange(modules_count):
for row in modules_range_short:
if (modules[row][col]
and not modules[row + 1][col]
and modules[row + 2][col]
and modules[row + 3][col]
and modules[row + 4][col]
and not modules[row + 5][col]
and modules[row + 6][col]):
lost_point += 40
return lost_point
def _lost_point_level4(modules, modules_count):
modules_range = xrange(modules_count)
dark_count = 0
for row in modules_range:
this_row = modules[row]
for col in modules_range:
if this_row[col]:
dark_count += 1
ratio = abs(100 * dark_count / modules_count / modules_count - 50) / 5
return ratio * 10
def optimal_data_chunks(data, minimum=4):
"""
An iterator returning QRData chunks optimized to the data content.
:param minimum: The minimum number of bytes in a row to split as a chunk.
"""
data = to_bytestring(data)
re_repeat = (
six.b('{') + six.text_type(minimum).encode('ascii') + six.b(',}'))
num_pattern = re.compile(six.b('\d') + re_repeat)
num_bits = _optimal_split(data, num_pattern)
alpha_pattern = re.compile(
six.b('[') + re.escape(ALPHA_NUM) + six.b(']') + re_repeat)
for is_num, chunk in num_bits:
if is_num:
yield QRData(chunk, mode=MODE_NUMBER, check_data=False)
else:
for is_alpha, sub_chunk in _optimal_split(chunk, alpha_pattern):
if is_alpha:
mode = MODE_ALPHA_NUM
else:
mode = MODE_8BIT_BYTE
yield QRData(sub_chunk, mode=mode, check_data=False)
def _optimal_split(data, pattern):
while data:
match = re.search(pattern, data)
if not match:
break
start, end = match.start(), match.end()
if start:
yield False, data[:start]
yield True, data[start:end]
data = data[end:]
if data:
yield False, data
def to_bytestring(data):
"""
Convert data to a (utf-8 encoded) byte-string if it isn't a byte-string
already.
"""
if not isinstance(data, six.binary_type):
data = six.text_type(data).encode('utf-8')
return data
def optimal_mode(data):
"""
Calculate the optimal mode for this chunk of data.
"""
if data.isdigit():
return MODE_NUMBER
if RE_ALPHA_NUM.match(data):
return MODE_ALPHA_NUM
return MODE_8BIT_BYTE
class QRData:
"""
Data held in a QR compatible format.
Doesn't currently handle KANJI.
"""
def __init__(self, data, mode=None, check_data=True):
"""
If ``mode`` isn't provided, the most compact QR data type possible is
chosen.
"""
if check_data:
data = to_bytestring(data)
if mode is None:
self.mode = optimal_mode(data)
else:
self.mode = mode
if mode not in (MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE):
raise TypeError("Invalid mode (%s)" % mode) # pragma: no cover
if check_data and mode < optimal_mode(data): # pragma: no cover
raise ValueError(
"Provided data can not be represented in mode "
"{0}".format(mode))
self.data = data
def __len__(self):
return len(self.data)
def write(self, buffer):
if self.mode == MODE_NUMBER:
for i in xrange(0, len(self.data), 3):
chars = self.data[i:i + 3]
bit_length = NUMBER_LENGTH[len(chars)]
buffer.put(int(chars), bit_length)
elif self.mode == MODE_ALPHA_NUM:
for i in xrange(0, len(self.data), 2):
chars = self.data[i:i + 2]
if len(chars) > 1:
buffer.put(
ALPHA_NUM.find(chars[0]) * 45 +
ALPHA_NUM.find(chars[1]), 11)
else:
buffer.put(ALPHA_NUM.find(chars), 6)
else:
if six.PY3:
# Iterating a bytestring in Python 3 returns an integer,
# no need to ord().
data = self.data
else:
data = [ord(c) for c in self.data]
for c in data:
buffer.put(c, 8)
def __repr__(self):
return repr(self.data)
class BitBuffer:
def __init__(self):
self.buffer = []
self.length = 0
def __repr__(self):
return ".".join([str(n) for n in self.buffer])
def get(self, index):
buf_index = math.floor(index / 8)
return ((self.buffer[buf_index] >> (7 - index % 8)) & 1) == 1
def put(self, num, length):
for i in range(length):
self.put_bit(((num >> (length - i - 1)) & 1) == 1)
def __len__(self):
return self.length
def put_bit(self, bit):
buf_index = self.length // 8
if len(self.buffer) <= buf_index:
self.buffer.append(0)
if bit:
self.buffer[buf_index] |= (0x80 >> (self.length % 8))
self.length += 1
def create_bytes(buffer, rs_blocks):
offset = 0
maxDcCount = 0
maxEcCount = 0
dcdata = [0] * len(rs_blocks)
ecdata = [0] * len(rs_blocks)
for r in range(len(rs_blocks)):
dcCount = rs_blocks[r].data_count
ecCount = rs_blocks[r].total_count - dcCount
maxDcCount = max(maxDcCount, dcCount)
maxEcCount = max(maxEcCount, ecCount)
dcdata[r] = [0] * dcCount
for i in range(len(dcdata[r])):
dcdata[r][i] = 0xff & buffer.buffer[i + offset]
offset += dcCount
# Get error correction polynomial.
rsPoly = base.Polynomial([1], 0)
for i in range(ecCount):
rsPoly = rsPoly * base.Polynomial([1, base.gexp(i)], 0)
rawPoly = base.Polynomial(dcdata[r], len(rsPoly) - 1)
modPoly = rawPoly % rsPoly
ecdata[r] = [0] * (len(rsPoly) - 1)
for i in range(len(ecdata[r])):
modIndex = i + len(modPoly) - len(ecdata[r])
if (modIndex >= 0):
ecdata[r][i] = modPoly[modIndex]
else:
ecdata[r][i] = 0
totalCodeCount = 0
for rs_block in rs_blocks:
totalCodeCount += rs_block.total_count
data = [None] * totalCodeCount
index = 0
for i in range(maxDcCount):
for r in range(len(rs_blocks)):
if i < len(dcdata[r]):
data[index] = dcdata[r][i]
index += 1
for i in range(maxEcCount):
for r in range(len(rs_blocks)):
if i < len(ecdata[r]):
data[index] = ecdata[r][i]
index += 1
return data
def create_data(version, error_correction, data_list):
buffer = BitBuffer()
for data in data_list:
buffer.put(data.mode, 4)
buffer.put(len(data), length_in_bits(data.mode, version))
data.write(buffer)
# Calculate the maximum number of bits for the given version.
rs_blocks = base.rs_blocks(version, error_correction)
bit_limit = 0
for block in rs_blocks:
bit_limit += block.data_count * 8
if len(buffer) > bit_limit:
raise exceptions.DataOverflowError(
"Code length overflow. Data size (%s) > size available (%s)" %
(len(buffer), bit_limit))
# Terminate the bits (add up to four 0s).
for i in range(min(bit_limit - len(buffer), 4)):
buffer.put_bit(False)
# Delimit the string into 8-bit words, padding with 0s if necessary.
delimit = len(buffer) % 8
if delimit:
for i in range(8 - delimit):
buffer.put_bit(False)
# Add special alternating padding bitstrings until buffer is full.
bytes_to_fill = (bit_limit - len(buffer)) // 8
for i in range(bytes_to_fill):
if i % 2 == 0:
buffer.put(PAD0, 8)
else:
buffer.put(PAD1, 8)
return create_bytes(buffer, rs_blocks)

View File

@ -156,11 +156,12 @@ class BaseAppConfig(dict):
self._on_init()
def __getattr__(self, name):
if name in self['_kvs']:
return self['_kvs'][name]
_name = name.replace('-', '_')
if _name in self['_kvs']:
return self['_kvs'][_name]
else:
if name in self['_kvs']['_']:
return self['_kvs']['_'][name]
if _name in self['_kvs']['_']:
return self['_kvs']['_'][_name]
else:
return AttrDict()
@ -168,7 +169,7 @@ class BaseAppConfig(dict):
x = key.split('::')
if 1 == len(x):
_sec = '_'
_key = x[0]
_key = x[0].replace('-', '_')
elif 2 == len(x):
_sec = x[0].replace('-', '_')
_key = x[1].replace('-', '_')
@ -199,7 +200,7 @@ class BaseAppConfig(dict):
x = key.split('::')
if 1 == len(x):
_sec = '_'
_key = x[0]
_key = x[0].replace('-', '_')
elif 2 == len(x):
_sec = x[0].replace('-', '_')
_key = x[1].replace('-', '_')
@ -215,7 +216,7 @@ class BaseAppConfig(dict):
x = key.split('::')
if 1 == len(x):
_sec = '_'
_key = x[0]
_key = x[0].replace('-', '_')
elif 2 == len(x):
_sec = x[0].replace('-', '_')
_key = x[1].replace('-', '_')
@ -311,10 +312,10 @@ class BaseAppConfig(dict):
x = key.split('::')
if 1 == len(x):
_sec = '_'
_key = x[0]
_key = x[0].replace('-', '_')
elif 2 == len(x):
_sec = x[0]
_key = x[1]
_sec = x[0].replace('-', '_')
_key = x[1].replace('-', '_')
else:
return def_value, False
@ -322,16 +323,20 @@ class BaseAppConfig(dict):
return def_value, False
if _key not in self['_kvs'][_sec]:
return def_value, False
if self['_kvs'][_sec][_key] is None:
return def_value, False
return str(self['_kvs'][_sec][_key]), True
def get_int(self, key, def_value=-1):
x = key.split('::')
if 1 == len(x):
_sec = '_'
_key = x[0]
_key = x[0].replace('-', '_')
elif 2 == len(x):
_sec = x[0]
_key = x[1]
_sec = x[0].replace('-', '_')
_key = x[1].replace('-', '_')
else:
return def_value, False
@ -340,6 +345,9 @@ class BaseAppConfig(dict):
if _key not in self['_kvs'][_sec]:
return def_value, False
if self['_kvs'][_sec][_key] is None:
return def_value, False
try:
return int(self['_kvs'][_sec][_key]), True
except ValueError as e:
@ -350,10 +358,10 @@ class BaseAppConfig(dict):
x = key.split('::')
if 1 == len(x):
_sec = '_'
_key = x[0]
_key = x[0].replace('-', '_')
elif 2 == len(x):
_sec = x[0]
_key = x[1]
_sec = x[0].replace('-', '_')
_key = x[1].replace('-', '_')
else:
return def_value, False
@ -362,6 +370,9 @@ class BaseAppConfig(dict):
if _key not in self['_kvs'][_sec]:
return def_value, False
if self['_kvs'][_sec][_key] is None:
return def_value, False
tmp = str(self['_kvs'][_sec][_key]).lower()
if tmp in ['yes', 'true', '1']:
@ -415,14 +426,6 @@ class AppConfig(BaseAppConfig):
self.set_default('database::sqlite-file', None,
'sqlite-file=/var/lib/teleport/data/ts_db.db'
)
# self.set_default('database::mysql-host', None,
# 'mysql-host=127.0.0.1\n'
# 'mysql-port=3306\n'
# 'mysql-db=teleport\n'
# 'mysql-prefix=tp_\n'
# 'mysql-user=teleport\n'
# 'mysql-password=password'
# )
self.set_default('database::mysql-host', '127.0.0.1', 'mysql-host=127.0.0.1')
self.set_default('database::mysql-port', 3306, 'mysql-port=3306')
self.set_default('database::mysql-db', 'teleport', 'mysql-db=teleport')
@ -507,7 +510,7 @@ class AppConfig(BaseAppConfig):
self.set_kv('database::mysql-password', _tmp_str)
_log_file, ok = self.get_str('common::log-file')
if ok:
if ok and _log_file:
self.log_path = os.path.abspath(os.path.dirname(_log_file))
else:
_log_file = os.path.join(self.log_path, 'tpweb.log')

View File

@ -101,7 +101,8 @@ class WebServerCore:
'static_hash_cache': False,
}
from eom_app.controller import controllers
from eom_app.controller import controllers, fix_controller
fix_controller()
web_app = tornado.web.Application(controllers, **settings)
server = tornado.httpserver.HTTPServer(web_app)
@ -118,9 +119,12 @@ class WebServerCore:
# 启动session超时管理
web_session().start()
# 启动数据库定时事务例如MySQL防丢失连接
get_db().start_keep_alive()
tornado.ioloop.IOLoop.instance().start()
get_db().stop_keep_alive()
web_session().stop()
return 0

View File

@ -24,12 +24,13 @@ def create_and_init(db, step_begin, step_end):
`account_pwd` varchar(128) DEFAULT NULL,
`account_status` int(11) DEFAULT 0,
`account_lock` int(11) DEFAULT 0,
`account_desc` varchar(255)
`account_desc` varchar(255),
`oath_secret` varchar(64),
);""".format(db.table_prefix, db.auto_increment))
_db_exec(db, step_begin, step_end, '创建表 auth', """CREATE TABLE `{}auth`(
`auth_id` INTEGER PRIMARY KEY {},
`account_name` varchar(256),
`account_name` varchar(255),
`host_id` INTEGER,
`host_auth_id` int(11) NOT NULL
);""".format(db.table_prefix, db.auto_increment))
@ -38,22 +39,22 @@ def create_and_init(db, step_begin, step_end):
# 这也是升级到数据库版本5的标志
_db_exec(db, step_begin, step_end, '创建表 key', """CREATE TABLE `{}key` (
`cert_id` integer PRIMARY KEY {},
`cert_name` varchar(256),
`cert_name` varchar(255),
`cert_pub` varchar(2048) DEFAULT '',
`cert_pri` varchar(4096) DEFAULT '',
`cert_desc` varchar(256)
`cert_desc` varchar(255)
);
""".format(db.table_prefix, db.auto_increment))
_db_exec(db, step_begin, step_end, '创建表 config', """CREATE TABLE `{}config` (
`name` varchar(256) NOT NULL,
`value` varchar(256),
`name` varchar(128) NOT NULL,
`value` varchar(255),
PRIMARY KEY (`name` ASC)
);""".format(db.table_prefix))
_db_exec(db, step_begin, step_end, '创建表 group', """CREATE TABLE `{}group` (
`group_id` integer PRIMARY KEY {},
`group_name` varchar(255) DEFAULT''
`group_name` varchar(255) DEFAULT ''
);""".format(db.table_prefix, db.auto_increment))
_db_exec(db, step_begin, step_end, '创建表 host_info', """CREATE TABLE `{}host_info`(
@ -64,16 +65,16 @@ PRIMARY KEY (`name` ASC)
`host_port` int(11) DEFAULT 0,
`protocol` int(11) DEFAULT 0,
`host_lock` int(11) DEFAULT 0,
`host_desc` varchar(256) DEFAULT ''
`host_desc` varchar(255) DEFAULT ''
);""".format(db.table_prefix, db.auto_increment))
_db_exec(db, step_begin, step_end, '创建表 auth_info', """CREATE TABLE `{}auth_info`(
`id` INTEGER PRIMARY KEY {},
`host_id` INTEGER,
`auth_mode` INTEGER,
`user_name` varchar(256),
`user_pswd` varchar(256),
`user_param` varchar(256),
`user_name` varchar(255),
`user_pswd` varchar(255),
`user_param` varchar(255),
`cert_id` INTEGER,
`encrypt` INTEGER,
`log_time` varchar(60)
@ -99,7 +100,7 @@ PRIMARY KEY (`name` ASC)
_db_exec(db, step_begin, step_end,
'建立管理员账号',
'INSERT INTO `{}account` VALUES (1, 100, "admin", "{}", 0, 0, "超级管理员");'.format(db.table_prefix, _admin_sec_password)
'INSERT INTO `{}account` VALUES (1, 100, "admin", "{}", 0, 0, "超级管理员", "");'.format(db.table_prefix, _admin_sec_password)
)
_db_exec(db, step_begin, step_end,

View File

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
import time
from eom_app.app.util import sec_generate_password
from eom_common.eomcore.logger import log
def _db_exec(db, step_begin, step_end, msg, sql):
_step = step_begin(msg)
ret = db.exec(sql)
if not ret:
step_end(_step, -1)
raise RuntimeError('[FAILED] {}'.format(sql))
else:
step_end(_step, 0)
def _export_table(db, table_name, fields):
ret = ['', '-- table: {}'.format(table_name), '-- fields: {}'.format(', '.join(fields)), 'TRUNCATE TABLE `{}`;'.format(table_name)]
fields_str = '`,`'.join(fields)
sql = 'SELECT `{}` FROM `{}{}`'.format(fields_str, db.table_prefix, table_name)
d = db.query(sql)
if not d or len(d) == 0:
ret.append('-- table is empty.')
else:
fields_count = len(fields)
for i in range(len(d)):
x = []
for j in range(fields_count):
x.append(d[i][j].__str__())
val = "','".join(x).replace('\n', '\\n')
sql = "INSERT INTO `{}` VALUES ('{}');".format(table_name, val)
ret.append(sql)
return '\r\n'.join(ret)
def export_database(db):
ret = []
now = time.localtime(time.time())
ret.append('-- {:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d} '.format(now.tm_year, now.tm_mon, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec))
if db.db_type == db.DB_TYPE_SQLITE:
ret.append('-- export from SQLite Database')
elif db.db_type == db.DB_TYPE_MYSQL:
ret.append('-- export from MySQL Database')
else:
return '未知的数据库类型', False
db_ret = db.query('SELECT `value` FROM `{}config` WHERE `name`="db_ver";'.format(db.table_prefix))
if db_ret is None or 0 == len(db_ret):
return '无法获取数据库版本', False
else:
ret.append('-- DATABASE VERSION {}'.format(db_ret[0][0]))
_fields = ['account_id', 'account_type', 'account_name', 'account_pwd', 'account_status', 'account_lock', 'account_desc', 'oath_secret']
ret.append(_export_table(db, 'account', _fields))
_fields = ['auth_id', 'account_name', 'host_id', 'host_auth_id']
ret.append(_export_table(db, 'auth', _fields))
_fields = ['cert_id', 'cert_name', 'cert_pub', 'cert_pri', 'cert_desc']
ret.append(_export_table(db, 'key', _fields))
_fields = ['name', 'value']
ret.append(_export_table(db, 'config', _fields))
_fields = ['group_id', 'group_name']
ret.append(_export_table(db, 'group', _fields))
_fields = ['host_id', 'group_id', 'host_sys_type', 'host_ip', 'host_port', 'protocol', 'host_lock', 'host_desc']
ret.append(_export_table(db, 'host_info', _fields))
_fields = ['id', 'host_id', 'auth_mode', 'user_name', 'user_pswd', 'user_param', 'cert_id', 'encrypt', 'log_time']
ret.append(_export_table(db, 'auth_info', _fields))
_fields = ['id', 'session_id', 'account_name', 'host_ip', 'host_port', 'sys_type', 'auth_type', 'protocol', 'user_name', 'ret_code', 'begin_time', 'end_time', 'log_time']
ret.append(_export_table(db, 'log', _fields))
return '\r\n'.join(ret), True

View File

@ -149,7 +149,7 @@ class DatabaseUpgrade:
`group_id` int(11) DEFAULT 0,
`host_sys_type` int(11) DEFAULT 1,
`host_ip` varchar(32) DEFAULT '',
`pro_port` varchar(256) NULL,
`pro_port` varchar(255) NULL,
`host_lock` int(11) DEFAULT 0,
`host_desc` varchar(128) DEFAULT ''
);""".format(self.db.table_prefix, self.db.auto_increment)):
@ -161,8 +161,8 @@ class DatabaseUpgrade:
`host_id` INTEGER,
`pro_type` INTEGER,
`auth_mode` INTEGER,
`user_name` varchar(256),
`user_pswd` varchar(256),
`user_name` varchar(255),
`user_pswd` varchar(255),
`cert_id` INTEGER,
`encrypt` INTEGER,
`log_time` varchar(60)
@ -381,7 +381,7 @@ class DatabaseUpgrade:
# 先创建三个临时表
if not self.db.exec("""CREATE TABLE `{}auth_tmp` (
`auth_id` INTEGER PRIMARY KEY {},
`account_name` varchar(256),
`account_name` varchar(255),
`host_id` INTEGER,
`host_auth_id` int(11) NOT NULL
);""".format(self.db.table_prefix, self.db.auto_increment)):
@ -405,9 +405,9 @@ class DatabaseUpgrade:
`id` INTEGER PRIMARY KEY {},
`host_id` INTEGER,
`auth_mode` INTEGER,
`user_name` varchar(256),
`user_pswd` varchar(256),
`user_param` varchar(256),
`user_name` varchar(255),
`user_pswd` varchar(255),
`user_param` varchar(255),
`cert_id` INTEGER,
`encrypt` INTEGER,
`log_time` varchar(60)
@ -492,8 +492,8 @@ class DatabaseUpgrade:
if not self.db.is_table_exists('{}config'.format(self.db.table_prefix)):
if not self.db.exec("""CREATE TABLE `{}config` (
`name` varchar(256) NOT NULL,
`value` varchar(256),
`name` varchar(128) NOT NULL,
`value` varchar(255),
PRIMARY KEY (`name` ASC)
);""".format(self.db.table_prefix)):
self.step_end(_step, -1, 'config表不存在且无法创建')
@ -526,3 +526,37 @@ class DatabaseUpgrade:
log.e('failed.\n')
self.step_end(_step, -1)
return False
def _upgrade_to_v6(self):
_step = self.step_begin('检查数据库版本v6...')
# 服务端升级到版本2.2.9时为增加双因子认证为account表增加oath_secret字段
db_ret = self.db.is_field_exists('{}account'.format(self.db.table_prefix), 'oath_secret')
if db_ret is None:
self.step_end(_step, -1, '无法连接到数据库')
return False
if db_ret:
self.step_end(_step, 0, '跳过 v5 到 v6 的升级操作')
return True
self.step_end(_step, 0, '需要升级到v6')
try:
_step = self.step_begin(' - 在account表中加入oath_secret字段')
if not self.db.exec('ALTER TABLE {}account ADD oath_secret VARCHAR(64) DEFAULT ""'.format(self.db.table_prefix)):
self.step_end(_step, -1, '失败')
return False
_step = self.step_begin(' - 更新数据库版本号')
if not self.db.exec('UPDATE `{}config` SET `value`="6" WHERE `name`="db_ver";'.format(self.db.table_prefix)):
self.step_end(_step, -1, '无法更新数据库版本号')
return False
else:
self.step_end(_step, 0)
return True
except:
log.e('failed.\n')
self.step_end(_step, -1)
return False

View File

@ -12,13 +12,14 @@ from eom_common.eomcore import utils
from eom_common.eomcore.logger import log
from .database.create import create_and_init
from .database.upgrade import DatabaseUpgrade
from .database.export import export_database
__all__ = ['get_db', 'DbItem']
class TPDatabase:
# 注意,每次调整数据库结构,必须增加版本号,并且在升级接口中编写对应的升级操作
DB_VERSION = 5
DB_VERSION = 6
DB_TYPE_UNKNOWN = 0
DB_TYPE_SQLITE = 1
@ -45,6 +46,10 @@ class TPDatabase:
self._table_prefix = ''
self._conn_pool = None
self._stop_flag = False
self._thread_keep_alive_handle = None
self._thread_keep_alive_cond = threading.Condition()
@property
def table_prefix(self):
return self._table_prefix
@ -90,6 +95,30 @@ class TPDatabase:
return True
def start_keep_alive(self):
self._thread_keep_alive_handle = threading.Thread(target=self._thread_keep_alive)
self._thread_keep_alive_handle.start()
def stop_keep_alive(self):
self._stop_flag = True
self._thread_keep_alive_cond.acquire()
self._thread_keep_alive_cond.notify()
self._thread_keep_alive_cond.release()
if self._thread_keep_alive_handle is not None:
self._thread_keep_alive_handle.join()
log.v('database-keep-alive-thread stopped.\n')
def _thread_keep_alive(self):
while True:
self._thread_keep_alive_cond.acquire()
# 每一小时醒来执行一次查询,避免连接丢失
self._thread_keep_alive_cond.wait(3600)
self._thread_keep_alive_cond.release()
if self._stop_flag:
break
self.query('SELECT `value` FROM `{}config` WHERE `name`="db_ver";'.format(self._table_prefix))
def _init_sqlite(self, db_file):
self.db_type = self.DB_TYPE_SQLITE
self.auto_increment = 'AUTOINCREMENT'
@ -120,49 +149,6 @@ class TPDatabase:
return True
# def init__(self, db_source):
# self.db_source = db_source
#
# if db_source['type'] == self.DB_TYPE_MYSQL:
# log.e('MySQL not supported yet.')
# return False
# elif db_source['type'] == self.DB_TYPE_SQLITE:
# self._table_prefix = 'ts_'
# self._conn_pool = TPSqlitePool(db_source['file'])
#
# if not os.path.exists(db_source['file']):
# log.w('database need create.\n')
# self.need_create = True
# return True
# else:
# log.e('Unknown database type: {}'.format(db_source['type']))
# return False
#
# # 看看数据库中是否存在指定的数据表(如果不存在,可能是一个空数据库文件),则可能是一个新安装的系统
# # ret = self.query('SELECT COUNT(*) FROM `sqlite_master` WHERE `type`="table" AND `name`="{}account";'.format(self._table_prefix))
# ret = self.is_table_exists('{}group'.format(self._table_prefix))
# if ret is None or not ret:
# log.w('database need create.\n')
# self.need_create = True
# return True
#
# # 尝试从配置表中读取当前数据库版本号(如果不存在,说明是比较旧的版本了)
# ret = self.query('SELECT `value` FROM `{}config` WHERE `name`="db_ver";'.format(self._table_prefix))
# if ret is None or 0 == len(ret):
# self.current_ver = 1
# else:
# self.current_ver = int(ret[0][0])
#
# if self.current_ver < self.DB_VERSION:
# log.w('database need upgrade.\n')
# self.need_upgrade = True
# return True
#
# # DO TEST
# # self.alter_table('ts_account', [['account_id', 'id'], ['account_type', 'type']])
#
# return True
def is_table_exists(self, table_name):
"""
判断指定的表是否存在
@ -179,12 +165,34 @@ class TPDatabase:
return False
return True
elif self.db_type == self.DB_TYPE_MYSQL:
# select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA='dbname' and TABLE_NAME='tablename' ;
ret = self.query('SELECT TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA="{}" and TABLE_NAME="{}";'.format(self.mysql_db, table_name))
if ret is None:
return None
if len(ret) == 0:
return False
else:
return True
else:
log.e('Unknown database type.\n')
return None
def is_field_exists(self, table_name, field_name):
if self.db_type == self.DB_TYPE_SQLITE:
ret = self.query('PRAGMA table_info(`{}`);'.format(table_name))
print(ret)
if ret is None:
return None
if len(ret) == 0:
return False
else:
return True
elif self.db_type == self.DB_TYPE_MYSQL:
ret = self.query('DESC `{}` `{}`;'.format(table_name, field_name))
print(ret)
if ret is None:
return None
if len(ret) == 0:
return False
else:
return True
else:
@ -207,6 +215,14 @@ class TPDatabase:
# log.d('[db] cost {} seconds.\n'.format(_end - _start))
return ret
def transaction(self, sql_list):
# _start = datetime.datetime.utcnow().timestamp()
ret = self._conn_pool.transaction(sql_list)
# _end = datetime.datetime.utcnow().timestamp()
# log.d('[db] transaction\n')
# log.d('[db] cost {} seconds.\n'.format(_end - _start))
return ret
def last_insert_id(self):
return self._conn_pool.last_insert_id()
@ -302,6 +318,9 @@ class TPDatabase:
log.e('Unknown database type.\n')
return False
def export_to_sql(self):
return export_database(self)
class TPDatabasePool:
def __init__(self):
@ -320,6 +339,12 @@ class TPDatabasePool:
return False
return self._do_exec(_conn, sql)
def transaction(self, sql_list):
_conn = self._get_connect()
if _conn is None:
return False
return self._do_transaction(_conn, sql_list)
def last_insert_id(self):
_conn = self._get_connect()
if _conn is None:
@ -347,9 +372,13 @@ class TPDatabasePool:
def _do_exec(self, conn, sql):
return None
def _do_transaction(self, conn, sql_list):
return False
def _last_insert_id(self, conn):
return -1
class TPSqlitePool(TPDatabasePool):
def __init__(self, db_file):
super().__init__()
@ -372,26 +401,32 @@ class TPSqlitePool(TPDatabasePool):
cursor.execute(sql)
db_ret = cursor.fetchall()
return db_ret
# except sqlite3.OperationalError:
# # log.e('_do_query() error.\n')
# return None
except Exception as e:
log.e('[sqlite] _do_query() failed: {}\n'.format(e.__str__()))
log.e('[sqlite] SQL={}'.format(sql))
finally:
cursor.close()
def _do_exec(self, conn, sql):
cursor = conn.cursor()
try:
cursor.execute(sql)
conn.commit()
with conn:
conn.execute(sql)
return True
# except sqlite3.OperationalError as e:
except Exception as e:
log.e('[sqlite] _do_exec() failed: {}\n'.format(e.__str__()))
log.e('[sqlite] SQL={}'.format(sql))
return False
def _do_transaction(self, conn, sql_list):
try:
# 使用context manager发生异常时会自动rollback正常执行完毕后会自动commit
with conn:
for sql in sql_list:
conn.execute(sql)
return True
except Exception as e:
log.e('[sqlite] _do_transaction() failed: {}\n'.format(e.__str__()))
return False
finally:
cursor.close()
def _last_insert_id(self, conn):
cursor = conn.cursor()
@ -422,6 +457,7 @@ class TPMysqlPool(TPDatabasePool):
passwd=self._password,
db=self._db_name,
port=self._port,
autocommit=False,
connect_timeout=3.0,
charset='utf8')
except Exception as e:
@ -436,6 +472,7 @@ class TPMysqlPool(TPDatabasePool):
conn.commit()
return db_ret
except Exception as e:
log.v('[mysql] SQL={}\n'.format(sql))
log.e('[mysql] _do_query() failed: {}\n'.format(e.__str__()))
return None
finally:
@ -449,10 +486,26 @@ class TPMysqlPool(TPDatabasePool):
return True
except Exception as e:
log.e('[mysql] _do_exec() failed: {}\n'.format(e.__str__()))
log.e('[mysql] SQL={}'.format(sql))
return None
finally:
cursor.close()
def _do_transaction(self, conn, sql_list):
cursor = conn.cursor()
try:
conn.begin()
for sql in sql_list:
cursor.execute(sql)
conn.commit()
return True
except Exception as e:
conn.rollback()
log.e('[mysql] _do_transaction() failed: {}\n'.format(e.__str__()))
return False
finally:
cursor.close()
def _last_insert_id(self, conn):
cursor = conn.cursor()
try:
@ -461,7 +514,7 @@ class TPMysqlPool(TPDatabasePool):
conn.commit()
return db_ret[0][0]
except Exception as e:
log.e('[sqlite] _last_insert_id() failed: {}\n'.format(e.__str__()))
log.e('[mysql] _last_insert_id() failed: {}\n'.format(e.__str__()))
return -1
finally:
cursor.close()

View File

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
import os
import io
import qrcode
import base64
import binascii
import hmac
import time
import hashlib
import struct
__all__ = ['gen_oath_secret', 'verify_oath_code', 'gen_oath_qrcode']
def gen_oath_secret():
return _convert_secret_to_base32(binascii.b2a_hex(os.urandom(16))).replace('=', '')
def _convert_secret_to_base32(secret):
return base64.b32encode(base64.b16decode(secret.upper())).decode()
def get_totp_token(secret, factor=None):
# 通过 secret 得到 原始密钥 key
# 需要对padding符进行处理
_len = len(secret)
_pad = 8 - (_len % 8)
if _pad > 0:
secret += '=' * _pad
key = base64.b32decode(secret)
if factor is None:
factor = int(time.time()) // 30 # input 为次数, 30为默认密码刷新间隔值
msg = struct.pack(">Q", factor)
# 然后使用 HMAC-SHA1算法计算hash
hsh = hmac.new(key, msg, hashlib.sha1).digest()
# 将hsh转换成数字(默认为6位)
i = hsh[-1] & 0xf # 以最后一个字节的后4个bits为数字作为接下来的索引
f = hsh[i:i + 4] # 以i为索引, 取hsh中的4个字节
n = struct.unpack('>I', f)[0] & 0x7fffffff # 将4个字节按big-endian转换为无符号整数, 转换时去掉最高位的符号位
# 等价于 n = ((f[0] & 0x7f) << 24) | ((f[1] & 0xff) << 16) | ((f[2] & 0xff) << 8) | (f[3] & 0xff)
# 将 n % 1000000 得到6位数字, 不足补零
r = '%06d' % (n % 1000000) # r 即为 生成的动态密码
return r
def verify_oath_code(secret, code):
# secret = '6OHEKKJPLMUBJ4EHCT5ZT5YLUQ'
cur_input = int(time.time()) // 30
window = 3
for i in range(cur_input - (window - 1) // 2, cur_input + window // 2 + 1): # [cur_input-(window-1)//2, cur_input + window//2]
if get_totp_token(secret, i) == code:
return True
return False
def gen_oath_qrcode(username, secret):
msg = 'otpauth://totp/{}?secret={}&issuer=teleport'.format(username, secret)
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=4,
border=4,
)
qr.add_data(msg)
qr.make(fit=True)
img = qr.make_image()
out = io.BytesIO()
img.save(out, "jpeg", quality=100)
return out.getvalue()

View File

@ -1,112 +1,122 @@
# -*- coding: utf-8 -*-
# import pickle
import time
import datetime
import threading
# from pymemcache.client.base import Client as mem_client
from .configs import app_cfg
from eom_common.eomcore.logger import log
cfg = app_cfg()
SESSION_EXPIRE = 3600 # 60*60 默认超时时间为1小时
class WebSession(threading.Thread):
"""
:type _mem_client: pymemcache.client.base.Client
"""
def __init__(self):
super().__init__(name='session-manager-thread')
import builtins
if '__web_session__' in builtins.__dict__:
raise RuntimeError('WebSession object exists, you can not create more than one instance.')
# session表session_id为索引每个项为一个字典包括 v(Value), t(Timestamp when add or modify), e(Expire seconds)
self._session_dict = dict()
self._lock = threading.RLock()
self._stop_flag = False
def init(self):
return True
def stop(self):
self._stop_flag = True
self.join()
log.v('{} stopped.'.format(self.name))
def run(self):
while True:
_now = int(datetime.datetime.utcnow().timestamp())
with self._lock:
_keys = [k for k in self._session_dict]
for k in _keys:
if self._session_dict[k]['e'] == 0:
continue
if _now - self._session_dict[k]['t'] > self._session_dict[k]['e']:
del self._session_dict[k]
# 每隔一分钟检查一次超时的会话
for i in range(60):
if not self._stop_flag:
time.sleep(1)
def set(self, s_id, value, expire=SESSION_EXPIRE):
"""
设置一个会话数据如果expire为负数则立即删除已经存在的名为s_id的会话如果expire为0则此会话数据永不过期expire的单位为秒
@param s_id: string
@param value: string
@param expire: integer
@return: None
"""
if expire < 0:
with self._lock:
if s_id in self._session_dict:
del self._session_dict[s_id]
else:
self._session_dict[s_id] = {'v': value, 't': int(datetime.datetime.utcnow().timestamp()), 'e': expire}
def get(self, s_id, _default=None):
# 从session中获取一个数据读取并更新最后访问时间
with self._lock:
if s_id in self._session_dict:
if self._session_dict[s_id]['e'] == 0:
return self._session_dict[s_id]['v']
else:
if int(datetime.datetime.utcnow().timestamp()) - self._session_dict[s_id]['t'] > self._session_dict[s_id]['e']:
del self._session_dict[s_id]
return _default
else:
self._session_dict[s_id]['t'] = int(datetime.datetime.utcnow().timestamp())
return self._session_dict[s_id]['v']
else:
return _default
def taken(self, s_id, _default=None):
# 从session中取走一个数据读取并删除
with self._lock:
if s_id in self._session_dict:
ret = self._session_dict[s_id]['v']
del self._session_dict[s_id]
return ret
else:
return _default
def web_session():
"""
取得Session管理器的唯一实例
:rtype : WebSession
"""
import builtins
if '__web_session__' not in builtins.__dict__:
builtins.__dict__['__web_session__'] = WebSession()
return builtins.__dict__['__web_session__']
# -*- coding: utf-8 -*-
# import pickle
import time
import datetime
import threading
# from pymemcache.client.base import Client as mem_client
from .configs import app_cfg
from eom_common.eomcore.logger import log
cfg = app_cfg()
SESSION_EXPIRE = 3600 # 60*60 默认超时时间为1小时
class WebSession(threading.Thread):
"""
:type _mem_client: pymemcache.client.base.Client
"""
def __init__(self):
super().__init__(name='session-manager-thread')
import builtins
if '__web_session__' in builtins.__dict__:
raise RuntimeError('WebSession object exists, you can not create more than one instance.')
# session表session_id为索引每个项为一个字典包括 v(Value), t(Timestamp when add or modify), e(Expire seconds)
self._session_dict = dict()
self._lock = threading.RLock()
self._stop_flag = False
self._timer_cond = threading.Condition()
def init(self):
return True
def stop(self):
self._stop_flag = True
self._timer_cond.acquire()
self._timer_cond.notify()
self._timer_cond.release()
self.join()
log.v('{} stopped.\n'.format(self.name))
def run(self):
while True:
self._timer_cond.acquire()
# 每隔一分钟醒来检查一次超时的会话
self._timer_cond.wait(60)
self._timer_cond.release()
if self._stop_flag:
break
_now = int(datetime.datetime.utcnow().timestamp())
with self._lock:
_keys = [k for k in self._session_dict]
for k in _keys:
if self._session_dict[k]['e'] == 0:
continue
if _now - self._session_dict[k]['t'] > self._session_dict[k]['e']:
del self._session_dict[k]
# for i in range(60):
# if not self._stop_flag:
# time.sleep(1)
def set(self, s_id, value, expire=SESSION_EXPIRE):
"""
设置一个会话数据如果expire为负数则立即删除已经存在的名为s_id的会话如果expire为0则此会话数据永不过期expire的单位为秒
@param s_id: string
@param value: string
@param expire: integer
@return: None
"""
if expire < 0:
with self._lock:
if s_id in self._session_dict:
del self._session_dict[s_id]
else:
self._session_dict[s_id] = {'v': value, 't': int(datetime.datetime.utcnow().timestamp()), 'e': expire}
def get(self, s_id, _default=None):
# 从session中获取一个数据读取并更新最后访问时间
with self._lock:
if s_id in self._session_dict:
if self._session_dict[s_id]['e'] == 0:
return self._session_dict[s_id]['v']
else:
if int(datetime.datetime.utcnow().timestamp()) - self._session_dict[s_id]['t'] > self._session_dict[s_id]['e']:
del self._session_dict[s_id]
return _default
else:
self._session_dict[s_id]['t'] = int(datetime.datetime.utcnow().timestamp())
return self._session_dict[s_id]['v']
else:
return _default
def taken(self, s_id, _default=None):
# 从session中取走一个数据读取并删除
with self._lock:
if s_id in self._session_dict:
ret = self._session_dict[s_id]['v']
del self._session_dict[s_id]
return ret
else:
return _default
def web_session():
"""
取得Session管理器的唯一实例
:rtype : WebSession
"""
import builtins
if '__web_session__' not in builtins.__dict__:
builtins.__dict__['__web_session__'] = WebSession()
return builtins.__dict__['__web_session__']

View File

@ -7,24 +7,21 @@ from . import auth
from . import host
from . import cert
from . import user
from . import pwd
from . import config
from . import group
from . import index
from . import record
from . import maintenance
# import tornado.web
from eom_app.app.configs import app_cfg
cfg = app_cfg()
__all__ = ['controllers']
__all__ = ['controllers', 'fix_controller']
controllers = [
(r'/dashboard', dashboard.IndexHandler),
(r'/', index.IndexHandler),
(r'/dashboard', dashboard.IndexHandler),
(r'/maintenance/install', maintenance.InstallHandler),
(r'/maintenance/upgrade', maintenance.UpgradeHandler),
@ -40,6 +37,10 @@ controllers = [
(r'/auth/get-captcha', auth.GetCaptchaHandler),
(r'/auth/verify-captcha', auth.VerifyCaptchaHandler),
(r'/auth/modify-pwd', auth.ModifyPwd),
(r'/auth/oath-verify', auth.OathVerifyHandler),
(r'/auth/oath-secret-qrcode', auth.OathSecretQrCodeHandler),
(r'/auth/oath-secret-reset', auth.OathSecretResetHandler),
(r'/auth/oath-update-secret', auth.OathUpdateSecretHandler),
(r'/group/list', group.GetListHandler),
(r'/group/', group.IndexHandler),
@ -49,9 +50,9 @@ controllers = [
(r'/cert/', cert.IndexHandler),
(r'/cert', cert.IndexHandler),
(r'/pwd', pwd.IndexHandler),
(r'/user', user.IndexHandler),
(r'/user/list', user.GetListHandler),
(r'/user/personal', user.PersonalHandler),
#(r"/log/replay/(.*)", tornado.web.StaticFileHandler, {"path": os.path.join(cfg.data_path, 'replay')}),
(r"/log/replay/(.*)", record.ReplayStaticFileHandler, {"path": os.path.join(cfg.data_path, 'replay')}),
@ -66,8 +67,6 @@ controllers = [
(r'/log/', record.LogHandler),
(r'/log', record.LogHandler),
(r'/exit', auth.LogoutHandler),
(r'/user/delete-user', user.DeleteUser),
(r'/user/modify-user', user.ModifyUser),
(r'/user/add-user', user.AddUser),
@ -109,15 +108,17 @@ controllers = [
(r'/host/sys-user/update', host.SysUserUpdate),
(r'/host/sys-user/delete', host.SysUserDelete),
# (r'/set/update-config', set.UpdateConfig),
# (r'/set/os-operator', set.OsOperator),
# (r'/set/info', config.InfoHandler),
# (r'/set/db', config.DatabaseHandler),
(r'/config/export-database', config.ExportDatabaseHandler),
(r'/config/import-database', config.ImportDatabaseHandler),
(r'/config/', config.IndexHandler),
(r'/config', config.IndexHandler),
(r'/uidesign', index.UIDesignHandler),
(r'/uidesign/without-sidebar', index.UIDesignWithoutSidebarHandler),
(r'/uidesign/table', index.UIDesignTableHandler)
]
def fix_controller():
dbg_mode, _ = cfg.get_bool('common::debug-mode', False)
if dbg_mode:
controllers.append((r'/exit/9E37CBAEE2294D9D9965112025CEE87F', index.ExitHandler))
controllers.append((r'/uidesign', index.UIDesignHandler))
controllers.append((r'/uidesign/without-sidebar', index.UIDesignWithoutSidebarHandler))
controllers.append((r'/uidesign/table', index.UIDesignTableHandler))

View File

@ -8,6 +8,7 @@ from eom_app.module import user
from eom_common.eomcore.logger import *
from .base import TPBaseHandler, TPBaseUserAuthHandler, TPBaseJsonHandler, TPBaseUserAuthJsonHandler
from eom_app.app.util import gen_captcha
from eom_app.app.oath import gen_oath_secret, gen_oath_qrcode, verify_oath_code
class LoginHandler(TPBaseHandler):
@ -33,27 +34,40 @@ class LoginHandler(TPBaseHandler):
class VerifyUser(TPBaseJsonHandler):
def post(self):
code = self.get_session('captcha')
if code is None:
return self.write_json(-1, '验证码已失效')
self.del_session('captcha')
# code = self.get_session('captcha')
# if code is None:
# return self.write_json(-1, '验证码已失效')
#
# self.del_session('captcha')
args = self.get_argument('args', None)
if args is not None:
args = json.loads(args)
captcha = args['captcha']
username = args['username']
userpwd = args['userpwd']
login_type = args['type'].strip()
captcha = args['captcha'].strip()
username = args['username'].strip()
password = args['password'].strip()
oath = args['oath'].strip()
remember = args['remember']
else:
return self.write_json(-1, '参数错误')
if code.lower() != captcha.lower():
return self.write_json(-1, '验证码错误')
if login_type == 'password':
oath = None
code = self.get_session('captcha')
if code is None:
return self.write_json(-1, '验证码已失效')
self.del_session('captcha')
if code.lower() != captcha.lower():
return self.write_json(-1, '验证码错误')
elif login_type == 'oath':
if len(oath) == 0:
return self.write_json(-1, '身份验证器动态验证码错误')
self.del_session('captcha')
try:
user_id, account_type, nickname, locked = user.verify_user(username, userpwd)
user_id, account_type, nickname, locked = user.verify_user(username, password, oath)
if locked == 1:
return self.write_json(-1, '账号被锁定,请联系管理员!')
if user_id == 0:
@ -81,7 +95,7 @@ class VerifyUser(TPBaseJsonHandler):
_user['type'] = account_type
if remember:
self.set_session('user', _user, 12*60*60)
self.set_session('user', _user, 12 * 60 * 60)
else:
self.set_session('user', _user)
return self.write_json(0)
@ -150,3 +164,104 @@ class ModifyPwd(TPBaseUserAuthJsonHandler):
except:
log.e('modify password failed.')
return self.write_json(-4, '发生异常')
class OathVerifyHandler(TPBaseUserAuthJsonHandler):
def post(self):
args = self.get_argument('args', None)
if args is not None:
try:
args = json.loads(args)
code = args['code']
except:
return self.write_json(-2, '参数错误')
else:
return self.write_json(-1, '参数错误')
# secret = self.get_session('tmp_oath_secret', None)
# if secret is None:
# return self.write_json(-1, '内部错误!')
# self.del_session('tmp_oath_secret')
user_info = self.get_current_user()
if not user.verify_oath(user_info['id'], code):
return self.write_json(-3, '验证失败!')
else:
return self.write_json(0)
class OathSecretQrCodeHandler(TPBaseUserAuthJsonHandler):
def get(self):
secret = self.get_session('tmp_oath_secret', None)
print('tmp-oath-secret:', secret)
user_info = self.get_current_user()
img_data = gen_oath_qrcode(user_info['name'], secret)
# secret = '6OHEKKJPLMUBJ4EHCT5ZT5YLUQ'
#
# print('TOPT should be:', get_totp_token(secret))
# # cur_input = int(time.time()) // 30
# # print('cur-input', cur_input, int(time.time()))
# # window = 10
# # for i in range(cur_input - (window - 1) // 2, cur_input + window // 2 + 1): # [cur_input-(window-1)//2, cur_input + window//2]
# # print(get_totp_token(secret, i))
#
# msg = 'otpauth://totp/Admin?secret={}&issuer=teleport'.format(secret)
# qr = qrcode.QRCode(
# version=1,
# error_correction=qrcode.constants.ERROR_CORRECT_L,
# box_size=4,
# border=4,
# )
# qr.add_data(msg)
# qr.make(fit=True)
# img = qr.make_image()
#
# # img = qrcode.make(msg)
# out = io.BytesIO()
# img.save(out, "jpeg", quality=100)
# # web.header('Content-Type','image/jpeg')
# # img.save('test.png')
self.set_header('Content-Type', 'image/jpeg')
self.write(img_data)
class OathSecretResetHandler(TPBaseUserAuthJsonHandler):
def post(self):
oath_secret = gen_oath_secret()
self.set_session('tmp_oath_secret', oath_secret)
return self.write_json(0, data={"tmp_oath_secret": oath_secret})
class OathUpdateSecretHandler(TPBaseUserAuthJsonHandler):
def post(self):
args = self.get_argument('args', None)
if args is not None:
try:
args = json.loads(args)
code = args['code']
except:
return self.write_json(-2, '参数错误')
else:
return self.write_json(-1, '参数错误')
secret = self.get_session('tmp_oath_secret', None)
if secret is None:
return self.write_json(-1, '内部错误!')
self.del_session('tmp_oath_secret')
if verify_oath_code(secret, code):
user_info = self.get_current_user()
try:
ret = user.update_oath_secret(user_info['id'], secret)
if 0 != ret:
return self.write_json(ret)
except:
log.e('update user oath-secret failed.')
return self.write_json(-2, '发生异常')
# self.set_session('oath_secret', secret)
return self.write_json(0)
else:
return self.write_json(-3, '验证失败!')

View File

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
import os
import time
import json
import tornado.gen
import tornado.httpclient
@ -8,6 +10,7 @@ from eom_ver import *
from eom_app.app.db import get_db
from eom_app.app.configs import app_cfg
from eom_app.app.util import *
from eom_common.eomcore.logger import log
from .base import TPBaseAdminAuthHandler, TPBaseAdminAuthJsonHandler
cfg = app_cfg()
@ -24,7 +27,6 @@ class IndexHandler(TPBaseAdminAuthHandler):
if 'code' in return_data:
_code = return_data['code']
if _code == 0:
# core['detected'] = True
cfg.update_core(return_data['data'])
core_detected = True
@ -52,138 +54,128 @@ class IndexHandler(TPBaseAdminAuthHandler):
}
self.render('config/index.mako', page_param=json.dumps(param))
# class InfoHandler(TPBaseAdminAuthHandler):
# @tornado.gen.coroutine
# def get(self):
# core_detected = False
# req = {'method': 'get_config', 'param': []}
# _yr = async_post_http(req)
# return_data = yield _yr
# if return_data is not None:
# if 'code' in return_data:
# _code = return_data['code']
# if _code == 0:
# # core['detected'] = True
# cfg.update_core(return_data['data'])
# core_detected = True
#
# if not core_detected:
# cfg.update_core(None)
#
# _db = get_db()
# database = '未知'
# if _db.db_source['type'] == _db.DB_TYPE_SQLITE:
# database = 'SQLite{}'.format(_db.db_source['file'])
# elif _db.db_source['type'] == _db.DB_TYPE_MYSQL:
# database = 'MySQL'
#
# param = {
# 'core': cfg.core,
# 'web': {
# 'version': TS_VER,
# 'core_server_rpc': cfg['core_server_rpc'],
# 'database': database
# }
# }
# self.render('set/info.mako', page_param=json.dumps(param))
class ExportDatabaseHandler(TPBaseAdminAuthHandler):
def get(self):
sql, ret = get_db().export_to_sql()
if ret:
now = time.localtime(time.time())
dt = '{:04d}{:02d}{:02d}-{:02d}{:02d}{:02d}'.format(now.tm_year, now.tm_mon, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec)
self.set_header('Content-Type', 'application/octet-stream')
self.set_header('Content-Disposition', 'attachment; filename=teleport-database-export-{}.sql'.format(dt))
self.write(sql)
else:
self.write('<h1>错误</h1>导出数据时发生错误:{}'.format(sql))
self.finish()
# class DatabaseHandler(TPBaseAdminAuthHandler):
# def get(self):
# _db = get_db()
# # database = '未知'
# # if _db.db_source['type'] == _db.DB_TYPE_SQLITE:
# # database = 'SQLite{}'.format(_db.db_source['file'])
# # elif _db.db_source['type'] == _db.DB_TYPE_MYSQL:
# # database = 'MySQL'
#
# param = {'db': _db.db_source}
# self.render('set/database.mako', page_param=json.dumps(param))
class ImportDatabaseHandler(TPBaseAdminAuthHandler):
# TODO: 导入操作可能会比较耗时,应该分离导入和获取导入状态两个过程,在页面上可以呈现导入进度,并列出导出成功/失败的项
# def _restart_func():
# time.sleep(1)
#
# PLATFORM = platform.system().lower()
#
# if PLATFORM == 'windows':
# sf = os.path.join(cfg.app_path, 'tools', 'restart.bat')
# os.system('cmd.exe /c "{}"'.format(sf))
# else:
# # sf = os.path.join(cfg.app_path, 'tools', 'restart.sh')
# # os.system(sf)
# os.system('service eom_ts restart')
#
# # os.system(sf)
@tornado.gen.coroutine
def post(self):
"""
sql导入规则
以事务方式执行sql语句
"""
ret = dict()
ret['code'] = 0
ret['message'] = ''
sql_filename = ''
# def restart_service():
# # todo: 使用eom_ts.exe运行脚本的方式新进程来重启服务避免正在运行的本服务未退出的影响
#
# t = threading.Thread(target=_restart_func)
# t.start()
#
try:
upload_path = os.path.join(cfg.data_path, 'tmp') # 文件的暂存路径
if not os.path.exists(upload_path):
os.mkdir(upload_path)
file_metas = self.request.files['sqlfile'] # 提取表单中namefile的文件元数据
for meta in file_metas:
now = time.localtime(time.time())
tmp_name = 'upload-{:04d}{:02d}{:02d}{:02d}{:02d}{:02d}.sql'.format(now.tm_year, now.tm_mon, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec)
sql_filename = os.path.join(upload_path, tmp_name)
with open(sql_filename, 'wb') as f:
f.write(meta['body'])
# class UpdateConfig(TPBaseAdminAuthJsonHandler):
# def post(self):
# args = self.get_argument('args', None)
# if args is not None:
# args = json.loads(args)
# else:
# self.write_json(-1)
# return
#
# change_list = args['cfg']
# reboot = args['reboot']
#
# try:
# ret = set.set_config(change_list)
# if ret:
# for i in range(len(change_list)):
# if change_list[i]['name'] == 'ts_server_ip':
# # static_path = cfg.static_path
# var_js = os.path.join(cfg.static_path, 'js', 'var.js')
# f = None
# try:
# f = open(var_js, 'w')
# # config_list = host.get_config_list()
# # ts_server = dict()
# # ts_server['ip'] = config_list['ts_server_ip']
# # ts_server['ssh_port'] = config_list['ts_server_ssh_port']
# # ts_server['rdp_port'] = config_list['ts_server_rdp_port']
# # f.write("\"use strict\";\nvar teleport_ip = \"{}\";\n".format(ts_server['ip']))
# f.write("\"use strict\";\nvar teleport_ip = \"{}\";\n".format(change_list[i]['value']))
# break
# except Exception:
# return self.write(-1)
# finally:
# if f is not None:
# f.close()
#
# if reboot:
# restart_service()
#
# self.write_json(0)
# else:
# self.write_json(-1)
# except:
# self.write_json(-2)
# file encode maybe utf8 or gbk... check it out.
file_encode = None
with open(sql_filename, encoding='utf8') as f:
try:
f.readlines()
file_encode = 'utf8'
except:
pass
# class OsOperator(TPBaseUserAuthJsonHandler):
# def post(self):
# args = self.get_argument('args', None)
# if args is not None:
# args = json.loads(args)
# else:
# self.write_json(-1)
# return
# _OP = int(args['OP'])
# try:
# if _OP == 1:
# os.system('reboot')
# else:
# os.system('shutdown -h now')
# # 重新启动
# self.write_json(0)
# except:
# self.write_json(-2)
#
if file_encode is None:
os.remove(sql_filename)
log.e('file `{}` unknown encode, neither GBK nor UTF8.\n'.format(sql_filename))
ret['code'] = -2
ret['message'] = 'upload sql file is not utf8 encode.'
return self.write(json.dumps(ret).encode('utf8'))
db_ver_checked = False
with open(sql_filename, encoding=file_encode) as f:
db = get_db()
sql = []
lines = f.readlines()
for line in lines:
line = line.strip('\r\n')
if line.startswith('-- DATABASE VERSION '):
x = line.split(' ')
if len(x) != 4:
ret['code'] = -1
ret['message'] = 'SQL文件格式错误无法解析数据库版本'
return self.write(json.dumps(ret).encode('utf8'))
db_ver_sql = int(x[3].strip())
if db.DB_VERSION != db_ver_sql:
ret['code'] = -1
ret['message'] = 'SQL文件数据库版本为 {},当前数据版本为 {},不允许导入!'.format(db_ver_sql, db.DB_VERSION)
return self.write(json.dumps(ret).encode('utf8'))
db_ver_checked = True
continue
if not db_ver_checked:
continue
if line .startswith('TRUNCATE TABLE '):
x = line.split(' ', 2)
_table_name = '`{}{}`'.format(db.table_prefix, x[2][1:-2])
if db.db_type == db.DB_TYPE_MYSQL:
x[2] = _table_name
line = ' '.join(x)
line += ';'
sql.append(line)
elif db.db_type == db.DB_TYPE_SQLITE:
sql.append('DELETE FROM {};'.format(_table_name))
sql.append('UPDATE `sqlite_sequence` SET `seq`=0 WHERE `name`="{}";'.format(_table_name[1:-1]))
if line.startswith('INSERT INTO '):
x = line.split(' ', 3)
_table_name = '`{}{}`'.format(db.table_prefix, x[2][1:-1])
x[2] = _table_name
line = ' '.join(x)
sql.append(line)
if not db_ver_checked:
ret['code'] = -1
ret['message'] = 'SQL文件格式错误未能确定数据库版本'
return self.write(json.dumps(ret).encode('utf8'))
db_ret = db.transaction(sql)
if not db_ret:
ret['code'] = -1
ret['message'] = 'SQL语句执行出错'
return self.write(json.dumps(ret).encode('utf8'))
ret['code'] = 0
return self.write(json.dumps(ret).encode('utf8'))
except:
log.e('error\n')
ret['code'] = -6
ret['message'] = '发生异常.'
return self.write(json.dumps(ret).encode('utf8'))
finally:
if os.path.exists(sql_filename):
os.remove(sql_filename)

View File

@ -27,4 +27,11 @@ class UIDesignWithoutSidebarHandler(TPBaseHandler):
class UIDesignTableHandler(TPBaseHandler):
def get(self):
# from hashlib import sha1
# import hmac
# my_sign = hmac.new('key', 'msg', sha1).digest()
# # my_sign = base64.b64encode(my_sign)
# # print my_sign
self.render('uidesign/table.mako')

View File

@ -1,8 +0,0 @@
# -*- coding: utf-8 -*-
from .base import TPBaseUserAuthHandler
class IndexHandler(TPBaseUserAuthHandler):
def get(self):
self.render('pwd/index.mako')

View File

@ -80,10 +80,20 @@ class ReplayStaticFileHandler(tornado.web.StaticFileHandler):
class ComandLogHandler(TPBaseAdminAuthHandler):
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['header'] = header
param['count'] = 0
param['op'] = list()
cmd_type = 0 # 0 = ssh, 1 = sftp
protocol = int(protocol)
if protocol == 1:
pass
@ -94,12 +104,26 @@ class ComandLogHandler(TPBaseAdminAuthHandler):
file = open(file_info, 'r')
data = file.readlines()
for i in range(len(data)):
param['op'].append({'t': data[i][1:20], 'c': data[i][22:-1]})
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]})
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:
pass
param['count'] = len(param['op'])
self.render('log/record-ssh-cmd.mako', page_param=json.dumps(param))
if cmd_type == 0:
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):

View File

@ -16,6 +16,12 @@ class IndexHandler(TPBaseAdminAuthHandler):
self.render('user/index.mako')
class PersonalHandler(TPBaseAdminAuthHandler):
def get(self):
user_info = self.get_current_user()
self.render('user/personal.mako', user=user_info)
class AuthHandler(TPBaseAdminAuthHandler):
def get(self, user_name):
group_list = host.get_group_list()

View File

@ -730,7 +730,7 @@ def get_auth_info(auth_id):
elif db_item.c_auth_mode == 2:
cert_id = db_item.c_cert_id
sql = 'SELECT `cert_pri` FROM `{}key` WHERE `cert_id`={}'.format(int(cert_id))
sql = 'SELECT `cert_pri` FROM `{}key` WHERE `cert_id`={}'.format(db.table_prefix, int(cert_id))
db_ret = db.query(sql)
if db_ret is None or len(db_ret) > 1:
return None

View File

@ -6,20 +6,21 @@ from eom_app.app.configs import app_cfg
from eom_app.app.const import *
from eom_app.app.db import get_db, DbItem
from eom_app.app.util import sec_generate_password, sec_verify_password
from eom_app.app.oath import verify_oath_code
def verify_user(name, password):
def verify_user(name, password, oath_code):
cfg = app_cfg()
db = get_db()
sql = 'SELECT `account_id`, `account_type`, `account_name`, `account_pwd`, `account_lock` FROM `{}account` WHERE `account_name`="{}";'.format(db.table_prefix, name)
sql = 'SELECT `account_id`, `account_type`, `account_desc`, `account_pwd`, `account_lock` FROM `{}account` WHERE `account_name`="{}";'.format(db.table_prefix, name)
db_ret = db.query(sql)
if db_ret is None:
# 特别地,如果无法取得数据库连接,有可能是新安装的系统,尚未建立数据库,此时应该处于维护模式
# 因此可以特别地处理用户验证用户名admin密码admin可以登录为管理员
if cfg.app_mode == APP_MODE_MAINTENANCE:
if name == 'admin' and password == 'admin':
return 1, 100, 'admin', 0
return 1, 100, '系统管理员', 0
return 0, 0, '', 0
if len(db_ret) != 1:
@ -27,7 +28,7 @@ def verify_user(name, password):
user_id = db_ret[0][0]
account_type = db_ret[0][1]
name = db_ret[0][2]
desc = db_ret[0][2]
locked = db_ret[0][4]
if locked == 1:
return 0, 0, '', locked
@ -42,7 +43,27 @@ def verify_user(name, password):
sql = 'UPDATE `{}account` SET `account_pwd`="{}" WHERE `account_id`={}'.format(db.table_prefix, _new_sec_password, int(user_id))
db.exec(sql)
return user_id, account_type, name, locked
if oath_code is not None:
if not verify_oath(user_id, oath_code):
return 0, 0, '', 0
return user_id, account_type, desc, locked
def verify_oath(user_id, oath_code):
db = get_db()
sql = 'SELECT `oath_secret` FROM `{}account` WHERE `account_id`={};'.format(db.table_prefix, user_id)
db_ret = db.query(sql)
if db_ret is None:
return False
if len(db_ret) != 1:
return False
oath_secret = db_ret[0][0]
return verify_oath_code(oath_secret, oath_code)
def modify_pwd(old_pwd, new_pwd, user_id):
@ -66,6 +87,16 @@ def modify_pwd(old_pwd, new_pwd, user_id):
return -102
def update_oath_secret(user_id, oath_secret):
db = get_db()
sql = 'UPDATE `{}account` SET `oath_secret`="{}" WHERE `account_id`={}'.format(db.table_prefix, oath_secret, int(user_id))
db_ret = db.exec(sql)
if db_ret:
return 0
else:
return -102
def get_user_list(with_admin=False):
db = get_db()
ret = list()

View File

@ -1,4 +1,4 @@
# -*- coding: utf8 -*-
TS_VER = "2.2.8.1"
TS_VER = "2.2.9.3"
TP_ASSIST_LAST_VER = "2.2.6.1"
TP_ASSIST_REQUIRE = "2.0.0.1"

View File

@ -1 +1 @@
@charset "utf-8";body{padding-top:70px;padding-bottom:24px;background-color:#ececed}#head nav.navbar{height:70px;line-height:70px;background-color:#333;color:#fff}#head .logo .desc{display:block;float:right;color:#ccc;margin-top:10px;font-size:18px}#foot nav.navbar{min-height:24px;height:24px;line-height:24px;background-color:#ddd;color:#fff;font-size:12px;border-top:1px solid #ccc}#foot nav.navbar .container{height:24px}#foot nav.navbar p{margin:0 auto;text-align:center;color:#333}#content{margin:10px 0 50px 0}.auth-box{margin-top:30px;min-height:120px;border:1px solid #ccc;border-radius:8px;background-color:rgba(255,255,255,0.6)}.auth-box .header{min-height:50px;height:50px;border:none;box-shadow:none;border-bottom:1px solid #ccc}.auth-box .header .title{display:inline-block;float:left;margin-left:60px;height:24px;margin-top:25px;line-height:16px;font-size:20px;color:#999}.auth-box .header .selected{border-bottom:1px solid #69c;color:#555}.auth-box .header .title:hover{border-bottom:1px solid #999}.auth-box .inputarea{margin:30px}.auth-box .inputarea .input-group-addon{padding:0 5px 0 5px}.auth-box .inputarea p.input-addon-desc{text-align:right;padding:0 5px 0 5px;color:#999}#leftside{width:560px;height:560px;padding-top:60px;background:url(../img/login/side-001.jpg) 0 0 no-repeat}@media screen and (max-width:990px){#leftside{display:none}}#leftside h1{font-size:24px;color:#888}#leftside p{font-size:18px;color:#888;padding-left:24px}.auth-box .inputbox{margin-bottom:10px}.auth-box-lg .inputbox{margin-bottom:20px}.auth-box .op_box{display:block;padding:5px;border-radius:3px;text-align:center;margin:5px 20px 10px 20px}.auth-box .op_error{background:#fbb}.auth-box .op_wait{background:#ccc}.auth-box .quick-area{padding:80px 0 80px 0}.auth-box .quick-area .quick-disc{text-align:center;margin-bottom:20px}.auth-box .quick-area .quick-no{padding-top:80px;padding-bottom:100px}.auth-box .quick-area .quick-yes{text-align:center}.auth-box .quick-area .quick-yes .quick-account{display:inline-block;margin:auto;margin-bottom:20px}.auth-box .quick-area .quick-yes .quick-account:hover .quick-image{box-shadow:0 0 8px #00c2f6}.auth-box .quick-area .quick-yes .quick-image{display:block;width:82px;height:82px;line-height:80px;font-size:64px;margin:auto;border:1px solid #a4cdf6;box-shadow:0 0 6px #a7d1fb}.auth-box .quick-area .quick-yes .quick-name{display:block;margin-top:5px}
@charset "utf-8";body{padding-top:70px;padding-bottom:24px;background-color:#ececed}#head nav.navbar{height:70px;line-height:70px;background-color:#333;color:#fff}#head .logo .desc{display:block;float:right;color:#ccc;margin-top:10px;font-size:18px}#foot nav.navbar{min-height:24px;height:24px;line-height:24px;background-color:#ddd;color:#fff;font-size:12px;border-top:1px solid #ccc}#foot nav.navbar .container{height:24px}#foot nav.navbar p{margin:0 auto;text-align:center;color:#333}#content{margin:10px 0 50px 0}.auth-box{margin-top:30px;min-height:120px;border:1px solid #ccc;border-radius:8px;background-color:rgba(255,255,255,0.6)}.auth-box .header{min-height:50px;height:50px;border:none;box-shadow:none;border-bottom:1px solid #ccc}.auth-box .header .title{display:inline-block;float:left;margin-left:60px;height:24px;margin-top:25px;line-height:16px;font-size:20px;color:#999}.auth-box .header .selected{border-bottom:2px solid #4882cc;color:#555}.auth-box .header .title:hover{border-bottom:2px solid #5396eb}.auth-box .inputarea{margin:30px}.auth-box .inputarea .input-group-addon{padding:0 5px 0 5px}.auth-box .inputarea p.input-addon-desc{text-align:right;padding:0 5px 0 5px;color:#999}#leftside{width:560px;height:560px;padding-top:60px;background:url(../img/login/side-001.jpg) 0 0 no-repeat}@media screen and (max-width:990px){#leftside{display:none}}#leftside h1{font-size:24px;color:#888}#leftside p{font-size:18px;color:#888;padding-left:24px}.auth-box .inputbox{margin-bottom:10px}.auth-box-lg .inputbox{margin-bottom:20px}.auth-box .op_box{display:block;padding:5px;border-radius:3px;text-align:center;margin:5px 20px 10px 20px}.auth-box .op_error{background:#fbb}.auth-box .op_wait{background:#ccc}.auth-box .quick-area{padding:80px 0 80px 0}.auth-box .quick-area .quick-disc{text-align:center;margin-bottom:20px}.auth-box .quick-area .quick-no{padding-top:80px;padding-bottom:100px}.auth-box .quick-area .quick-yes{text-align:center}.auth-box .quick-area .quick-yes .quick-account{display:inline-block;margin:auto;margin-bottom:20px}.auth-box .quick-area .quick-yes .quick-account:hover .quick-image{box-shadow:0 0 8px #00c2f6}.auth-box .quick-area .quick-yes .quick-image{display:block;width:82px;height:82px;line-height:80px;font-size:64px;margin:auto;border:1px solid #a4cdf6;box-shadow:0 0 6px #a7d1fb}.auth-box .quick-area .quick-yes .quick-name{display:block;margin-top:5px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,151 +1,218 @@
"use strict";
ywl.on_init = function (cb_stack, cb_args) {
if (ywl.page_options.user_name.length > 0) {
$('#username_account').val(ywl.page_options.user_name);
}
$('#captcha_image').attr('src', '/auth/get-captcha?' + Math.random());
ywl.app = ywl.create_app();
cb_stack
.add(ywl.app.init)
.exec();
};
ywl.create_app = function () {
var _app = {};
_app.dom_login_account = null;
_app.init = function (cb_stack, cb_args) {
_app.dom_login_account = $('#login-type-account');
$('#btn-login-account').click(_app.login_account);
$('#captcha_image').click(function () {
$(this).attr('src', '/auth/get-captcha?' + Math.random());
$('#captcha').focus().val('');
});
$('#username_account').keydown(function (event) {
$('[data-toggle="popover"]').popover('hide');
if (event.which === 13) {
$('#password_account').focus();
}
});
$('#password_account').keydown(function (event) {
$('[data-toggle="popover"]').popover('hide');
if (event.which === 13) {
$('#captcha').focus();
}
});
$('#captcha').keydown(function (event) {
$('[data-toggle="popover"]').popover('hide');
if (event.which === 13) {
_app.login_account();
}
});
cb_stack.exec();
};
_app.login_account = function () {
var str_username = '';
var str_password = '';
var str_captcha = '';
var is_remember = false;
var dom_username = $('#username_account');
var dom_password = $('#password_account');
var dom_captcha = $('#captcha');
var dom_remember = $('#remember-me');
str_username = dom_username.val();
str_password = dom_password.val();
str_captcha = dom_captcha.val();
is_remember = dom_remember.is(':checked');
if (str_username.length === 0) {
show_op_box('error', '缺少账号!');
dom_username.attr('data-content', "请填写您的账号!").popover('show');
dom_username.focus();
return;
}
if (str_password.length === 0) {
show_op_box('error', '缺少密码!');
dom_password.attr('data-content', "请填写密码!").popover('show');
dom_password.focus();
return;
}
if (str_captcha.length !== 4) {
show_op_box('error', '验证码错误!');
dom_captcha.attr('data-content', "验证码为4位数字和字母的组合请重新填写").popover('show').focus();
return;
}
$('#btn_login').attr('disabled', 'disabled');
show_op_box('wait', '<i class="fa fa-circle-o-notch fa-spin"></i> 正在进行身份认证,请稍候...');
// 先判断一下captcha是否正确如果不正确拒绝登录
ywl.ajax_post_json('/auth/verify-captcha', {captcha: str_captcha},
function (ret) {
if (ret.code === TPE_OK) {
// 验证成功
hide_op_box();
show_op_box('wait', '<i class="fa fa-circle-o-notch fa-spin"></i> 正在登录TELEPORT请稍候...');
_app.do_account_login(str_username, str_password, str_captcha, is_remember);
}
else {
hide_op_box();
show_op_box('error', '验证码错误!');
$('#captcha_image').attr('src', '/auth/get-captcha?' + Math.random());
$('#captcha').focus().val('');
}
$('#btn_login').removeAttr('disabled');
},
function () {
hide_op_box();
show_op_box('error', '很抱歉,无法连接服务器!请稍后再试一次!');
$('#btn_login').removeAttr('disabled');
}
);
};
_app.do_account_login = function (username, userpwd, captcha, is_remember) {
ywl.ajax_post_json('/auth/verify-user', {username: username, userpwd: userpwd, captcha: captcha, remember: is_remember},
function (ret) {
if (ret.code === TPE_OK) {
window.location.href = ywl.page_options.ref;
} else {
hide_op_box();
show_op_box('error', '无法登录TELEPORT' + ret.message);
console.log(ret);
}
$('#btn_login').removeAttr('disabled');
},
function () {
hide_op_box();
show_op_box('error', '很抱歉,无法连接服务器!请稍后再试一次!');
$('#btn_login').removeAttr('disabled');
}
);
};
return _app;
};
function hide_op_box() {
$('#login_message').hide();
}
function show_op_box(op_type, op_msg) {
var obj_box = $('#login_message');
obj_box.html(op_msg);
obj_box.removeClass().addClass('op_box op_' + op_type);
obj_box.show();
}
"use strict";
var LOGIN_TYPE_PASSWORD = 1; // 使用用户名密码登录(额外需要验证码)
var LOGIN_TYPE_OATH = 2; // 使用用户名密码登录(额外需要身份验证器的动态验证码)
ywl.on_init = function (cb_stack, cb_args) {
if (ywl.page_options.user_name.length > 0) {
$('#username-account').val(ywl.page_options.user_name);
}
$('#captcha-image').attr('src', '/auth/get-captcha?' + Math.random());
ywl.app = ywl.create_app();
cb_stack
.add(ywl.app.init)
.exec();
};
ywl.create_app = function () {
var _app = {};
_app.login_type = LOGIN_TYPE_PASSWORD;
_app.dom = {
btn_login_type_password: $('#login-type-password'),
btn_login_type_oath: $('#login-type-oath'),
area_captcha: $('#login-area-captcha'),
area_oath: $('#login-area-oath'),
captcha_image: $('#captcha-image'),
input_username: $('#username-account'),
input_password: $('#password-account'),
input_captcha: $('#captcha'),
input_oath: $('#oath-code'),
remember: $('#remember-me'),
btn_login: $('#btn-login'),
message: $('#message')
};
// _app.dom_login_account = null;
// _app.dom_login_google = null;
_app.init = function (cb_stack, cb_args) {
// _app.dom_login_account = $('#login-type-account');
// _app.dom_login_google = $('#login-type-google');
_app.dom.btn_login_type_password.click(function () {
_app.login_type = LOGIN_TYPE_PASSWORD;
_app.dom.btn_login_type_oath.removeClass('selected');
$(this).addClass('selected');
_app.dom.area_oath.slideUp(100);
_app.dom.area_captcha.slideDown(100);
});
_app.dom.btn_login_type_oath.click(function () {
_app.login_type = LOGIN_TYPE_OATH;
_app.dom.btn_login_type_password.removeClass('selected');
$(this).addClass('selected');
_app.dom.area_oath.slideDown(100);
_app.dom.area_captcha.slideUp(100);
});
_app.dom.btn_login.click(_app.login_account);
_app.dom.captcha_image.click(function () {
$(this).attr('src', '/auth/get-captcha?' + Math.random());
_app.dom.input_captcha.focus().val('');
});
_app.dom.input_username.keydown(function (event) {
$('[data-toggle="popover"]').popover('hide');
if (event.which === 13) {
_app.dom.input_password.focus();
}
});
_app.dom.input_password.keydown(function (event) {
$('[data-toggle="popover"]').popover('hide');
if (event.which === 13) {
if(_app.login_type === LOGIN_TYPE_PASSWORD)
_app.dom.input_captcha.focus();
else if(_app.login_type === LOGIN_TYPE_OATH)
_app.dom.input_oath.focus();
}
});
_app.dom.input_captcha.keydown(function (event) {
$('[data-toggle="popover"]').popover('hide');
if (event.which === 13) {
_app.login_account();
}
});
cb_stack.exec();
};
_app.login_account = function () {
// var str_username = '';
// var str_password = '';
// var str_captcha = '';
// var str_gcode = '';
// var is_remember = false;
//
// var dom_username = $('#username-account');
// var dom_password = $('#password-account');
// var dom_captcha = $('#captcha');
// var dom_gcode = $('#oath-code');
// var dom_remember = $('#remember-me');
var str_username = _app.dom.input_username.val();
var str_password = _app.dom.input_password.val();
var str_captcha = _app.dom.input_captcha.val();
var str_oath = _app.dom.input_oath.val();
var is_remember = _app.dom.remember.is(':checked');
if (str_username.length === 0) {
show_op_box('error', '缺少账号!');
_app.dom.input_username.attr('data-content', "请填写您的账号!").popover('show');
_app.dom.input_username.focus();
return;
}
if (str_password.length === 0) {
show_op_box('error', '缺少密码!');
_app.dom.input_password.attr('data-content', "请填写密码!").popover('show');
_app.dom.input_password.focus();
return;
}
if (_app.login_type === 'account') {
if (str_captcha.length !== 4) {
show_op_box('error', '验证码错误!');
_app.dom.input_captcha.attr('data-content', "验证码为4位数字和字母的组合请重新填写").popover('show').focus();
return;
}
} else if (_app.login_type === 'google') {
if (str_oath.length !== 6) {
show_op_box('error', '身份验证器动态验证码错误!');
_app.dom.input_oath.attr('data-content', "身份验证器动态验证码为6位数字请重新填写").popover('show').focus();
return;
}
}
_app.dom.btn_login.attr('disabled', 'disabled');
_app.show_op_box('wait', '<i class="fa fa-circle-o-notch fa-spin"></i> 正在进行身份认证,请稍候...');
// 先判断一下captcha是否正确如果不正确拒绝登录
if (_app.login_type === LOGIN_TYPE_PASSWORD) {
ywl.ajax_post_json('/auth/verify-captcha', {captcha: str_captcha},
function (ret) {
if (ret.code === TPE_OK) {
// 验证成功
_app.hide_op_box();
_app.show_op_box('wait', '<i class="fa fa-circle-o-notch fa-spin"></i> 正在登录TELEPORT请稍候...');
_app.do_account_login(str_username, str_password, str_captcha, str_oath, is_remember);
}
else {
_app.hide_op_box();
_app.show_op_box('error', '验证码错误!');
_app.dom.captcha_image.attr('src', '/auth/get-captcha?' + Math.random());
_app.dom.input_captcha.focus().val('');
}
_app.dom.btn_login.removeAttr('disabled');
},
function () {
_app.hide_op_box();
_app.show_op_box('error', '很抱歉,无法连接服务器!请稍后再试一次!');
_app.dom.btn_login.removeAttr('disabled');
}
);
} else {
_app.do_account_login(str_username, str_password, str_captcha, str_oath, is_remember);
}
};
_app.do_account_login = function (username, password, captcha, oath, is_remember) {
var login_type = '';
if(_app.login_type === LOGIN_TYPE_PASSWORD) {
login_type = 'password';
} else if(_app.login_type === LOGIN_TYPE_OATH) {
login_type = 'oath';
}
var args = {type:login_type, username: username, password: password, captcha: captcha, oath: oath, remember: is_remember};
console.log(args);
ywl.ajax_post_json('/auth/verify-user', args,
function (ret) {
if (ret.code === TPE_OK) {
window.location.href = ywl.page_options.ref;
} else {
_app.hide_op_box();
_app.show_op_box('error', '无法登录TELEPORT' + ret.message);
console.log(ret);
}
_app.dom.btn_login.removeAttr('disabled');
},
function () {
_app.hide_op_box();
_app.show_op_box('error', '很抱歉,无法连接服务器!请稍后再试一次!');
_app.dom.btn_login.removeAttr('disabled');
}
);
};
_app.hide_op_box = function() {
_app.dom.message.hide();
};
_app.show_op_box = function(op_type, op_msg) {
_app.dom.message.html(op_msg);
_app.dom.message.removeClass().addClass('op_box op_' + op_type);
_app.dom.message.show();
};
return _app;
};

View File

@ -251,15 +251,17 @@ ywl.create_dlg_modify_host_desc = function (tbl, row_id, host_id, host_ip, host_
self._make_dialog_box = function () {
var _html = [
'<div class="popover-inline-edit" id="' + self.dlg_id + '">',
' <div class="popover fade bottom in" role="tooltip" ywl-dlg="modify-host-desc">',
' <div class="arrow" style="left:50px;"></div>',
' <h3 class="popover-title">为主机 ' + self.host_ip + ' 添加备注,以便识别</h3>',
' <div class="popover fade bottom in" ywl-dlg="modify-host-desc">',
' <div class="arrow" style="left:70px;"></div>',
' <h3 class="popover-title">编辑备注</h3>',
' <div class="popover-content">',
' <div>为主机 ' + self.host_ip + ' 设置备注,以便识别:</div>',
' <div style="display:inline-block;float:right;">',
' <a href="javascript:;" class="btn btn-success btn-sm" ywl-btn="ok"><i class="glyphicon glyphicon-ok"></i></a>',
' <a href="javascript:;" class="btn btn-danger btn-sm" ywl-btn="cancel"><i class="glyphicon glyphicon-remove"></i></a>',
// ' <a href="javascript:;" class="btn btn-success btn-sm" ywl-btn="ok"><i class="glyphicon glyphicon-ok"></i></a>',
' <a href="javascript:;" class="btn btn-success btn-sm" ywl-btn="ok"><i class="fa fa-check"></i> 确定</a>',
' <a href="javascript:;" class="btn btn-danger btn-sm" ywl-btn="cancel"><i class="fa fa-close"></i> 取消</a>',
' </div>',
' <div style="padding-right:80px;">',
' <div style="padding-right:120px;">',
' <input type="text" ywl-input="desc" class="form-control" value="' + self.host_desc + '">',
' </div>',
' </div>',
@ -277,9 +279,9 @@ ywl.create_dlg_modify_host_desc = function (tbl, row_id, host_id, host_ip, host_
});
// 绑定“修改主机描述” 对话框中的输入框的回车事件
$('#' + self.dlg_id + " [ywl-input='desc']").keydown(function (event) {
if (event.which == 13) {
if (event.which === 13) {
self._save();
} else if (event.which == 27) {
} else if (event.which === 27) {
self._destroy();
}
});

View File

@ -4,8 +4,10 @@ ywl.on_init = function (cb_stack, cb_args) {
console.log(ywl.page_options);
var dom = {
info: $('#info-kv')
info: $('#info-kv'),
// , btn_maintance: $('#btn_maintenance')
btn_db_export: $('#btn-db-export'),
btn_db_import: $('#btn-db-import'),
};
var html = [];
@ -66,13 +68,64 @@ ywl.on_init = function (cb_stack, cb_args) {
// });
// });
//
dom.btn_db_export.click(function () {
window.location.href = '/config/export-database'
});
dom.btn_db_import.click(function () {
var _fn_sure = function (cb_stack, cb_args) {
var html = '<input id="upload-file" type="file" name="sqlfile" class="hidden" value="" style="display: none;"/>';
dom.btn_db_import.after($(html));
var update_file = $("#upload-file");
update_file.change(function () {
var file_path = $(this).val();
if (file_path === null || file_path === undefined || file_path === '') {
return;
}
ywl.do_upload_sql_file();
});
update_file.trigger('click');
};
var cb_stack = CALLBACK_STACK.create();
ywl.dlg_confirm(cb_stack, {
msg: '<p><strong>注意:操作不可恢复!!</strong></p><p>您确定要清除所有现有数据然后导入sql文件吗</p>',
fn_yes: _fn_sure
});
});
cb_stack.exec();
};
ywl.do_upload_sql_file = function () {
var param = {};
$.ajaxFileUpload({
url: "/config/import-database",// 需要链接到服务器地址
secureuri: false,
fileElementId: "upload-file", // 文件选择框的id属性
dataType: 'text', // 服务器返回的格式可以是json
data: param,
success: function (data) {
$('#upload-file').remove();
var ret = JSON.parse(data);
if (ret.code === TPE_OK) {
ywl.notify_success('导入sql成功');
} else {
ywl.notify_error('导入sql失败<br/>[' + ret.code+'] '+ret.message);
}
},
error: function () {
$('#upload-file').remove();
ywl.notify_error('网络故障导入sql失败');
}
});
};
ywl._make_protocol_info = function (name, p) {
if (_.isUndefined(p))
return ywl._make_info(name, '未能检测到');
// <tr><td class="key">RDP 端口:</td><td class="value">52089</td></tr>
var val = p.port;
if (!p.enable) {
val = '<span class="disabled">' + val + '(未启用)</span>';

View File

@ -8,7 +8,7 @@ ywl.on_init = function (cb_stack, cb_args) {
//===================================
// 表格数据
var disk_rate = 0;
if(0 == ywl.page_options.total_size) {
if(0 === ywl.page_options.total_size) {
$('#disk-status').text('未能获取到录像文件所在磁盘空间信息');
} else {
disk_rate = parseInt(ywl.page_options.free_size * 100 / ywl.page_options.total_size);
@ -238,10 +238,16 @@ ywl.on_host_table_created = function (tbl) {
msg = '协议不支持';
break;
case 6:
msg = '通讯错误';
msg = '数据格式错误';
break;
case 7:
msg = '错误重置';
msg = '核心服务重置';
break;
case 8:
msg = '网络通讯故障';
break;
case 9:
msg = '无效会话';
break;
default:
msg = fields.ret_code;
@ -286,7 +292,9 @@ ywl.on_host_table_created = function (tbl) {
render.make_action_btn = function (row_id, fields) {
var ret = [];
if (fields.protocol === PROTOCOL_TYPE_RDP) {
ret.push('<a href="javascript:;" class="btn btn-sm btn-primary" protocol=' + fields.protocol + ' ywl-btn-record="' + fields.ID + '">录像查看</a>&nbsp');
if(fields.ret_code === 9999) {
ret.push('<a href="javascript:;" class="btn btn-sm btn-primary" protocol=' + fields.protocol + ' ywl-btn-record="' + fields.ID + '">录像查看</a>&nbsp');
}
} else if (fields.protocol === PROTOCOL_TYPE_SSH) {
if (fields.ret_code === 9999 && fields.cost_time > 0) {
ret.push('<a href="javascript:;" class="btn btn-sm btn-primary" protocol=' + fields.protocol + ' ywl-btn-record="' + fields.ID + '">录像查看</a>&nbsp');

View File

@ -0,0 +1,145 @@
"use strict";
ywl.on_init = function (cb_stack, cb_args) {
ywl.dom = {
btn_reset_oath_code: $('#btn-reset-oath-code'),
btn_verify_oath_code: $('#btn-verify-oath-code'),
btn_verify_oath_code_and_save: $('#btn-verify-oath-and-save'),
btn_modify_password: $('#btn-modify-password'),
btn_toggle_oath_download: $('#toggle-oath-download'),
oath_app_download_box: $('#oath-app-download-box'),
input_current_password: $('#current-password'),
input_new_password: $('#new-password-1'),
input_new_password_confirm: $('#new-password-2'),
input_oath_code: $('#oath-code'),
input_oath_code_verify: $('#oath-code-verify'),
dlg_reset_oath_code: $('#dialog-reset-oath-code'),
oath_secret_image: $('#oath-secret-qrcode'),
tmp_oath_secret: $('#tmp-oath-secret'),
};
// ywl.dom.tmp_oath_secret.text(ywl.page_options.tmp_oath_secret);
ywl.clear_password_input = function () {
ywl.dom.input_current_password.val('');
ywl.dom.input_new_password.val('');
ywl.dom.input_new_password_confirm.val('');
};
ywl.dom.btn_modify_password.click(function () {
var old_pwd = ywl.dom.input_current_password.val();
var new_pwd_1 = ywl.dom.input_new_password.val();
var new_pwd_2 = ywl.dom.input_new_password_confirm.val();
if (old_pwd.length === 0) {
ywl.notify_error('请输入当前密码!');
ywl.dom.input_current_password.focus();
return;
}
if (new_pwd_1.length === 0) {
ywl.notify_error('请设置新密码!');
ywl.dom.input_new_password.focus();
return;
}
if (new_pwd_1 !== new_pwd_2) {
ywl.notify_error('两次密码输入不一致!');
ywl.dom.input_new_password_confirm.focus();
return;
}
ywl.ajax_post_json('/auth/modify-pwd', {o_pwd: old_pwd, n_pwd: new_pwd_1, callback: 1},
function (ret) {
if (ret.code === TPE_OK) {
ywl.notify_success('密码修改成功!');
ywl.clear_password_input();
} else if (ret.code === -101) {
ywl.notify_error('密码错误!');
} else {
ywl.notify_error('密码修改失败:' + ret.message);
}
},
function () {
ywl.notify_error('密码修改失败!');
}
);
});
ywl.dom.btn_toggle_oath_download.click(function () {
if (ywl.dom.oath_app_download_box.is(':hidden')) {
ywl.dom.oath_app_download_box.slideDown('fast', function () {
ywl.dom.btn_toggle_oath_download.html('收起 <i class="fa fa-angle-up"></i>');
});
} else {
ywl.dom.oath_app_download_box.slideUp('fast', function () {
ywl.dom.btn_toggle_oath_download.html('显示下载地址 <i class="fa fa-angle-down"></i>');
});
}
});
ywl.dom.btn_verify_oath_code.click(function () {
var code = ywl.dom.input_oath_code.val().trim();
if (code.length !== 6) {
ywl.notify_error('动态验证码错误应该是6位数字');
ywl.dom.input_oath_code_verify.focus();
return;
}
ywl.ajax_post_json('/auth/oath-verify', {code: code},
function (ret) {
if (ret.code === TPE_OK) {
ywl.notify_success('动态验证码验证成功!');
} else if (ret.code === -3) {
ywl.notify_error('动态验证码验证失败!');
} else {
ywl.notify_error('发生内部错误!' + ret.code + ret.message);
}
},
function () {
ywl.notify_error('网路故障,无法连接到服务器!');
}
);
});
ywl.dom.btn_reset_oath_code.click(function () {
ywl.ajax_post_json('/auth/oath-secret-reset', {},
function (ret) {
if (ret.code === TPE_OK) {
ywl.dom.oath_secret_image.attr('src', '/auth/oath-secret-qrcode?' + Math.random());
ywl.dom.tmp_oath_secret.text(ret.data.tmp_oath_secret);
ywl.dom.dlg_reset_oath_code.modal({backdrop: 'static'});
} else {
ywl.notify_error('发生内部错误!');
}
},
function () {
ywl.notify_error('网路故障,无法连接到服务器!');
}
);
});
ywl.dom.btn_verify_oath_code_and_save.click(function () {
var code = ywl.dom.input_oath_code_verify.val().trim();
if (code.length !== 6) {
ywl.notify_error('动态验证码错误应该是6位数字');
ywl.dom.input_oath_code_verify.focus();
return;
}
ywl.ajax_post_json('/auth/oath-update-secret', {code: code},
function (ret) {
if (ret.code === TPE_OK) {
ywl.notify_success('身份验证器绑定成功!您可以用此身份验证器登录系统了!');
ywl.dom.dlg_reset_oath_code.modal('hide');
} else {
ywl.notify_error('发生内部错误!');
}
},
function () {
ywl.notify_error('网路故障,无法连接到服务器!');
}
);
});
};

View File

@ -5,515 +5,535 @@
@font-family-mono: Consolas, Lucida Console, Monaco, Courier, 'Courier New', monospace;
body {
font-family: @font-family-normal;
//font-family: "微软雅黑", "Microsoft YaHei", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
background-color: @page-bg;
color: @page-color;
font-family: @font-family-normal;
//font-family: "微软雅黑", "Microsoft YaHei", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
background-color: @page-bg;
color: @page-color;
}
html, body {
height: 100%;
height: 100%;
}
#page-container {
min-width: 1260px;
min-width: 1260px;
}
a {
text-decoration: none;
text-decoration: none;
}
a:link {
text-decoration: none;
text-decoration: none;
}
a:hover {
text-decoration: none;
text-decoration: none;
}
a:active {
text-decoration: none;
text-decoration: none;
}
a:visited {
text-decoration: none;
text-decoration: none;
}
select {
outline: none;
outline: none;
}
label {
font-weight: normal;
font-weight: normal;
}
.clear-float {
clear: both;
clear: both;
}
.bigger {
font-size: 120%;
font-size: 120%;
}
.normal-text {
font-size: 13px;
color: @page-color;
font-size: 13px;
color: @page-color;
}
.mono {
font-family:@font-family-mono;
font-family: @font-family-mono;
}
hr.hr-sm {
margin-top: 5px;
margin-bottom: 5px;
margin-top: 5px;
margin-bottom: 5px;
}
//==============================================
// 重载bootstrap的样式
//==============================================
.btn-group-sm > .btn, .btn-sm {
padding: 2px 5px;
//margin-bottom: 2px;
padding: 2px 5px;
//margin-bottom: 2px;
}
.btn.btn-sm {
padding: 3px 8px;
padding: 3px 8px;
}
.btn.btn-icon {
padding: 3px 6px;
&.btn-sm {
//padding:1px 3px;
padding: 0;
font-size: 14px;
height: 24px;
width: 24px;
line-height: 24px;
border-radius: 0;
}
padding: 3px 6px;
&.btn-sm {
//padding:1px 3px;
padding: 0;
font-size: 14px;
height: 24px;
width: 24px;
line-height: 24px;
border-radius: 0;
}
}
.form-group-sm .input-group .input-group-btn > .btn {
height: 30px;
padding: 0 8px;
height: 30px;
padding: 0 8px;
}
.pop-menu-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1040;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1040;
}
//.modal {
.form-group {
margin-bottom: 5px;
margin-bottom: 5px;
}
//}
.badge {
display: inline-block;
display: inline-block;
min-width: 8px;
padding: 5px 10px;
border-radius: 10px;
min-width: 8px;
padding: 5px 10px;
border-radius: 10px;
text-align: center;
white-space: nowrap;
vertical-align: middle;
text-align: center;
white-space: nowrap;
vertical-align: middle;
font-size: 13px;
font-weight: 400;
line-height: 1em;
font-size: 13px;
font-weight: 400;
line-height: 1em;
background-color: @color-bg-default;
color: @color-text-on-dark-bg;
text-shadow: 1px 1px 0 @color-text-shadow-on-dark-bg;
background-color: @color-bg-default;
color: @color-text-on-dark-bg;
text-shadow: 1px 1px 0 @color-text-shadow-on-dark-bg;
&.badge-plain {
text-shadow: none;
}
&.badge-plain {
text-shadow: none;
}
&.badge-sm {
font-size: 11px;
padding: 3px 6px;
margin-top: 0;
border-radius: 8px;
text-shadow: none;
}
&.badge-sm {
font-size: 11px;
padding: 3px 6px;
margin-top: 0;
border-radius: 8px;
text-shadow: none;
}
&.badge-sup {
margin-left: -6px;
margin-top: -16px;
}
&.badge-sup {
margin-left: -6px;
margin-top: -16px;
}
&.badge-ignore {
background-color: @color-bg-ignore;
color: @color-text-ignore;
text-shadow: none;
}
&.badge-info {
background-color: @color-bg-info;
}
&.badge-primary {
background-color: @color-bg-primary;
}
&.badge-ignore {
background-color: @color-bg-ignore;
color: @color-text-ignore;
text-shadow: none;
}
&.badge-info {
background-color: @color-bg-info;
}
&.badge-primary {
background-color: @color-bg-primary;
}
&.badge-success {
background-color: @color-bg-success;
}
&.badge-warning {
background-color: @color-bg-warning;
}
&.badge-danger {
background-color: @color-bg-danger;
}
&.badge-success {
background-color: @color-bg-success;
}
&.badge-warning {
background-color: @color-bg-warning;
}
&.badge-danger {
background-color: @color-bg-danger;
}
}
.label {
display: inline-block;
display: inline-block;
min-width: 8px;
padding: 5px 10px;
border-radius: 5px;
min-width: 8px;
padding: 5px 10px;
border-radius: 5px;
text-align: center;
white-space: nowrap;
vertical-align: middle;
text-align: center;
white-space: nowrap;
vertical-align: middle;
font-size: 13px;
font-weight: 400;
line-height: 1em;
font-size: 13px;
font-weight: 400;
line-height: 1em;
background-color: @color-bg-default;
color: @color-text-on-dark-bg;
text-shadow: 1px 1px 0 @color-text-shadow-on-dark-bg;
background-color: @color-bg-default;
color: @color-text-on-dark-bg;
text-shadow: 1px 1px 0 @color-text-shadow-on-dark-bg;
&.label-plain {
text-shadow: none;
}
&.label-plain {
text-shadow: none;
}
&.label-sm {
font-size: 11px;
padding: 3px 8px;
margin-top: 0;
border-radius: 5px;
text-shadow: none;
}
&.label-sm {
font-size: 11px;
padding: 3px 8px;
margin-top: 0;
border-radius: 5px;
text-shadow: none;
}
&.label-ignore {
background-color: @color-bg-ignore;
color: @color-text-ignore;
text-shadow: none;
}
&.label-info {
background-color: @color-bg-info;
}
&.label-primary {
background-color: @color-bg-primary;
}
&.label-ignore {
background-color: @color-bg-ignore;
color: @color-text-ignore;
text-shadow: none;
}
&.label-info {
background-color: @color-bg-info;
}
&.label-primary {
background-color: @color-bg-primary;
}
&.label-success {
background-color: @color-bg-success;
}
&.label-warning {
background-color: @color-bg-warning;
}
&.label-danger {
background-color: @color-bg-danger;
}
&.label-success {
background-color: @color-bg-success;
}
&.label-warning {
background-color: @color-bg-warning;
}
&.label-danger {
background-color: @color-bg-danger;
}
}
// 表格页面中的一些小部件
.progress.progress-sm {
height: 18px;
margin-bottom: 2px;
background-color: #aaa;
&.button {
cursor: pointer;
}
height: 18px;
margin-bottom: 2px;
background-color: #aaa;
&.button {
cursor: pointer;
}
.progress-bar {
display: block;
font-size: 11px;
float: none;
}
.progress-bar {
display: block;
font-size: 11px;
float: none;
}
}
.alert-sm {
padding: 5px;
margin-bottom: 10px;
padding: 5px;
margin-bottom: 10px;
}
.modal-dialog-sm {
.modal-header {
padding: 10px;
}
.modal-body {
padding: 10px;
}
.modal-footer {
padding: 10px;
}
.modal-header {
padding: 10px;
}
.modal-body {
padding: 10px;
}
.modal-footer {
padding: 10px;
}
.form-horizontal .form-group {
margin-right: -5px;
margin-left: -5px;
}
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11 {
padding-right: 5px;
padding-left: 5px;
}
.form-horizontal .form-group {
margin-right: -5px;
margin-left: -5px;
}
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11 {
padding-right: 5px;
padding-left: 5px;
}
}
.btn-single-line {
white-space: nowrap;
.btn {
&:first-child {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
&:last-child {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
}
white-space: nowrap;
.btn {
&:first-child {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
&:last-child {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
}
}
.remote-action-group {
margin-bottom: 3px;
height: 28px;
min-width: 390px;
margin-bottom: 3px;
height: 28px;
min-width: 390px;
ul {
display: block;
height: 28px;
margin: 0;
padding: 0;
ul {
display: block;
height: 28px;
margin: 0;
padding: 0;
li {
float: left;
position: relative;
display: block;
height: 28px;
padding: 4px 5px;
li {
float: left;
position: relative;
display: block;
height: 28px;
padding: 4px 5px;
background-color: #eee;
border-top: 1px solid #ccc;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
background-color: #eee;
border-top: 1px solid #ccc;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
&.remote-action-btn {
background: none;
padding: 0;
border: none;
}
&.remote-action-btn {
background: none;
padding: 0;
border: none;
}
&.remote-action-input {
background: none;
padding: 4px 0;
&.remote-action-input {
background: none;
padding: 4px 0;
select {
border: none;
}
}
select {
border: none;
}
}
&.remote-action-chk-protocol {
width: 86px;
}
&.remote-action-chk-protocol {
width: 86px;
}
&.remote-action-username, &.remote-action-name, &.remote-action-protocol {
width: 96px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.remote-action-username {
font-size: 90%;
color: #999;
}
&.remote-action-name, &.remote-action-protocol, &.remote-action-chk-protocol {
color: #000;
}
&.remote-action-name, &.remote-action-chk-protocol {
font-weight:bold;
}
&.remote-action-username, &.remote-action-name, &.remote-action-protocol {
width: 96px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.remote-action-username {
font-size: 90%;
color: #999;
}
&.remote-action-name, &.remote-action-protocol, &.remote-action-chk-protocol {
color: #000;
}
&.remote-action-name, &.remote-action-chk-protocol {
font-weight: bold;
}
&.remote-action-password, &.remote-action-sshkey, &.remote-action-noauth {
text-align: center;
padding: 4px 8px;
width:45px;
}
&.remote-action-password {
background-color: #e3ffe3;
color: #999;
}
&.remote-action-sshkey {
background-color: #fbe9c8;
color: #666;
}
&.remote-action-noauth {
background-color: #e0e0e0;
color: #666;
}
&.remote-action-password, &.remote-action-sshkey, &.remote-action-noauth {
text-align: center;
padding: 4px 8px;
width: 45px;
}
&.remote-action-password {
background-color: #e3ffe3;
color: #999;
}
&.remote-action-sshkey {
background-color: #fbe9c8;
color: #666;
}
&.remote-action-noauth {
background-color: #e0e0e0;
color: #666;
}
.btn {
line-height: 1.5;
margin: 0;
padding: 4px 8px;
font-size: 12px;
border-radius: 0;
}
.btn {
line-height: 1.5;
margin: 0;
padding: 4px 8px;
font-size: 12px;
border-radius: 0;
}
label {
padding: 0;
display: block;
float: left;
margin-top: 1px;
cursor: pointer;
}
input[type=checkbox] {
display: block;
float: left;
margin: 3px 5px 0 0;
}
select {
margin-top: -3px;
}
label {
padding: 0;
display: block;
float: left;
margin-top: 1px;
cursor: pointer;
}
input[type=checkbox] {
display: block;
float: left;
margin: 3px 5px 0 0;
}
select {
margin-top: -3px;
}
&:first-child {
border-left: 1px solid #ccc;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
&:first-child {
border-left: 1px solid #ccc;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
.btn {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
}
.btn {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
}
&:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
&:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
.btn {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
}
.btn {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
}
}
hr.small {
margin: 5px 0;
margin: 5px 0;
}
.dlg-protocol-group {
margin-bottom: 3px;
margin-bottom: 3px;
ul {
display: block;
height: 28px;
margin: 0;
padding: 0;
ul {
display: block;
height: 28px;
margin: 0;
padding: 0;
li {
float: left;
position: relative;
display: block;
height: 28px;
padding: 4px 5px;
li {
float: left;
position: relative;
display: block;
height: 28px;
padding: 4px 5px;
background-color: #eee;
border-top: 1px solid #ccc;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
background-color: #eee;
border-top: 1px solid #ccc;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
&.item-name {
width: 120px;
//font-size: 90%;
//color: #999;
//text-align: center;
//white-space: nowrap;
//overflow: hidden;
//text-overflow: ellipsis;
}
&.item-name {
width: 120px;
//font-size: 90%;
//color: #999;
//text-align: center;
//white-space: nowrap;
//overflow: hidden;
//text-overflow: ellipsis;
}
&.item-btn {
background: none;
padding: 0;
border: none;
}
&.item-btn {
background: none;
padding: 0;
border: none;
}
&.item-input {
background: none;
border: none;
padding: 0;
}
&.item-input {
background: none;
border: none;
padding: 0;
}
.form-control {
line-height: 1.5;
margin: 0;
padding: 4px 5px;
font-size: 12px;
height: 28px;
border-radius: 0;
border-left: none;
width: 100px;
}
.form-control {
line-height: 1.5;
margin: 0;
padding: 4px 5px;
font-size: 12px;
height: 28px;
border-radius: 0;
border-left: none;
width: 100px;
}
label {
padding: 0;
display: block;
float: left;
margin-top: 1px;
cursor: pointer;
}
input[type=checkbox] {
display: block;
float: left;
margin: 3px 5px 0 0;
}
//select {
// margin-top: -3px;
//}
label {
padding: 0;
display: block;
float: left;
margin-top: 1px;
cursor: pointer;
}
input[type=checkbox] {
display: block;
float: left;
margin: 3px 5px 0 0;
}
//select {
// margin-top: -3px;
//}
&:first-child {
border-left: 1px solid #ccc;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
&:first-child {
border-left: 1px solid #ccc;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
.btn, .form-control {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
}
.btn, .form-control {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
}
&:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
&:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
.btn, .form-control {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
}
.btn, .form-control {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
}
}
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
padding-right: 5px;
padding-left: 5px;
padding-right: 5px;
padding-left: 5px;
}
.form-group-sm .form-control-static {
padding: 6px 0;
padding: 6px 0;
}
.input-group {
:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
color: @color-placeholder;
}
::-moz-placeholder { /* Mozilla Firefox 19+ */
color: @color-placeholder;
}
input:-ms-input-placeholder,
textarea:-ms-input-placeholder {
color: @color-placeholder;
}
input::-webkit-input-placeholder, textarea::-webkit-input-placeholder {
color: @color-placeholder;
}
}

View File

@ -27,6 +27,7 @@
@color-text-on-dark-bg: #fff;
@color-text-shadow-on-dark-bg: #525252;
@color-placeholder: #d2d2d2;
//.text-warning {
// color: @color-text-warning !important;

View File

@ -1,246 +1,246 @@
@charset "utf-8";
body {
padding-top: 70px;
padding-bottom: 24px;
background-color: #ececed;
}
#head nav.navbar {
height: 70px;
line-height: 70px;
background-color: #333;
color: #fff;
}
#head .logo .desc {
display: block;
float: right;
color: #ccc;
margin-top: 10px;
font-size: 18px;
}
#foot nav.navbar {
min-height: 24px;
height: 24px;
line-height: 24px;
background-color: #ddd;
color: #fff;
font-size: 12px;
border-top: 1px solid #ccc;
}
#foot nav.navbar .container {
height: 24px;
}
#foot nav.navbar p {
margin: 0 auto;
text-align: center;
color: #333;
}
#content {
margin: 10px 0 50px 0;
}
.auth-box {
margin-top: 30px;
min-height: 120px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: rgba(255, 255, 255, 0.6);
}
.auth-box .header {
min-height: 50px;
height: 50px;
border: none;
box-shadow: none;
border-bottom: 1px solid #ccc;
}
.auth-box .header .title {
display: inline-block;
float: left;
margin-left: 60px;
height: 24px;
margin-top: 25px;
line-height: 16px;
font-size: 20px;
color: #999;
}
.auth-box .header .selected {
border-bottom: 1px solid #6699cc;
color: #555;
}
.auth-box .header .title:hover {
border-bottom: 1px solid #999;
}
.auth-box .inputarea {
margin: 30px;
}
.auth-box .inputarea .input-group-addon {
padding: 0 5px 0 5px;
}
.auth-box .inputarea p.input-addon-desc {
text-align: right;
padding: 0 5px 0 5px;
color: #999;
}
/*.auth-box .inputbox {
border:1px solid #6699cc;
border-radius:2px;
height:38px;
margin-bottom:20px;
}
.auth-box .inputbox .input {
display: inline-block;
outline:none;
border-style:none;
position:relative;
width:360px;
padding:10px 10px 0 10px;
line-height: 18px;
font-family: Verdana, Tahoma, Ariall;
font-size:16px;
}
.auth-box .inputbox .clean {
display:inline-block;
position:relative;
top:5px;
right:0px;height:22px;width:22px;background:url(../img/login/input_right_clean.png) 0 0 no-repeat;
}
*/
#leftside {
width: 560px;
height: 560px;
padding-top: 60px;
background: url(../img/login/side-001.jpg) 0 0 no-repeat;
}
@media screen and (max-width: 990px) {
#leftside {
display: none;
}
}
#leftside h1 {
font-size: 24px;
color: #888;
}
#leftside p {
font-size: 18px;
color: #888;
padding-left: 24px;
}
.auth-box .inputbox {
margin-bottom: 10px;
}
.auth-box-lg .inputbox {
margin-bottom: 20px;
}
/*.auth-box .inputbox .clean {
display: block;
height:22px;width:22px;background:url(../img/login/input_right_clean.png) 0 0 no-repeat;
}*/
/*.auth-box .inputarea label {
font-size:12px;
font-weight:400;
color:#999;
cursor:pointer;
}*/
.auth-box .op_box {
display: block;
padding: 5px;
border-radius: 3px;
text-align: center;
//margin-top: 5px;
margin: 5px 20px 10px 20px;
}
.auth-box .op_error {
background: #fbb;
}
.auth-box .op_wait {
background: #ccc;
}
.auth-box {
.quick-area {
padding: 80px 0 80px 0;
.quick-disc {
//font-size:120%;
text-align: center;
//margin:auto;
margin-bottom: 20px;
}
.quick-no {
padding-top: 80px;
padding-bottom: 100px;
}
.quick-yes {
text-align: center;
//margin: auto;
.quick-account {
display: inline-block;
margin: auto;
margin-bottom: 20px;
//border:1px solid #888;
&:hover {
.quick-image {
//background-color: #00bcee;
box-shadow: 0 0 8px rgb(0, 194, 246);
}
}
}
.quick-image {
display: block;
width: 82px;
height: 82px;
line-height: 80px;
font-size: 64px;
margin: auto;
//border-radius: 5px;
//color:#fff;
//background-color: #00acda;
//padding:3px;
border: 1px solid #a4cdf6;
box-shadow: 0 0 6px rgba(167, 209, 251, 1);
}
.quick-name {
display: block;
margin-top: 5px;
}
}
}
}
@charset "utf-8";
body {
padding-top: 70px;
padding-bottom: 24px;
background-color: #ececed;
}
#head nav.navbar {
height: 70px;
line-height: 70px;
background-color: #333;
color: #fff;
}
#head .logo .desc {
display: block;
float: right;
color: #ccc;
margin-top: 10px;
font-size: 18px;
}
#foot nav.navbar {
min-height: 24px;
height: 24px;
line-height: 24px;
background-color: #ddd;
color: #fff;
font-size: 12px;
border-top: 1px solid #ccc;
}
#foot nav.navbar .container {
height: 24px;
}
#foot nav.navbar p {
margin: 0 auto;
text-align: center;
color: #333;
}
#content {
margin: 10px 0 50px 0;
}
.auth-box {
margin-top: 30px;
min-height: 120px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: rgba(255, 255, 255, 0.6);
}
.auth-box .header {
min-height: 50px;
height: 50px;
border: none;
box-shadow: none;
border-bottom: 1px solid #ccc;
}
.auth-box .header .title {
display: inline-block;
float: left;
margin-left: 60px;
height: 24px;
margin-top: 25px;
line-height: 16px;
font-size: 20px;
color: #999;
}
.auth-box .header .selected {
border-bottom: 2px solid #4882cc;
color: #555;
}
.auth-box .header .title:hover {
border-bottom: 2px solid #5396eb;
}
.auth-box .inputarea {
margin: 30px;
}
.auth-box .inputarea .input-group-addon {
padding: 0 5px 0 5px;
}
.auth-box .inputarea p.input-addon-desc {
text-align: right;
padding: 0 5px 0 5px;
color: #999;
}
/*.auth-box .inputbox {
border:1px solid #6699cc;
border-radius:2px;
height:38px;
margin-bottom:20px;
}
.auth-box .inputbox .input {
display: inline-block;
outline:none;
border-style:none;
position:relative;
width:360px;
padding:10px 10px 0 10px;
line-height: 18px;
font-family: Verdana, Tahoma, Ariall;
font-size:16px;
}
.auth-box .inputbox .clean {
display:inline-block;
position:relative;
top:5px;
right:0px;height:22px;width:22px;background:url(../img/login/input_right_clean.png) 0 0 no-repeat;
}
*/
#leftside {
width: 560px;
height: 560px;
padding-top: 60px;
background: url(../img/login/side-001.jpg) 0 0 no-repeat;
}
@media screen and (max-width: 990px) {
#leftside {
display: none;
}
}
#leftside h1 {
font-size: 24px;
color: #888;
}
#leftside p {
font-size: 18px;
color: #888;
padding-left: 24px;
}
.auth-box .inputbox {
margin-bottom: 10px;
}
.auth-box-lg .inputbox {
margin-bottom: 20px;
}
/*.auth-box .inputbox .clean {
display: block;
height:22px;width:22px;background:url(../img/login/input_right_clean.png) 0 0 no-repeat;
}*/
/*.auth-box .inputarea label {
font-size:12px;
font-weight:400;
color:#999;
cursor:pointer;
}*/
.auth-box .op_box {
display: block;
padding: 5px;
border-radius: 3px;
text-align: center;
//margin-top: 5px;
margin: 5px 20px 10px 20px;
}
.auth-box .op_error {
background: #fbb;
}
.auth-box .op_wait {
background: #ccc;
}
.auth-box {
.quick-area {
padding: 80px 0 80px 0;
.quick-disc {
//font-size:120%;
text-align: center;
//margin:auto;
margin-bottom: 20px;
}
.quick-no {
padding-top: 80px;
padding-bottom: 100px;
}
.quick-yes {
text-align: center;
//margin: auto;
.quick-account {
display: inline-block;
margin: auto;
margin-bottom: 20px;
//border:1px solid #888;
&:hover {
.quick-image {
//background-color: #00bcee;
box-shadow: 0 0 8px rgb(0, 194, 246);
}
}
}
.quick-image {
display: block;
width: 82px;
height: 82px;
line-height: 80px;
font-size: 64px;
margin: auto;
//border-radius: 5px;
//color:#fff;
//background-color: #00acda;
//padding:3px;
border: 1px solid #a4cdf6;
box-shadow: 0 0 6px rgba(167, 209, 251, 1);
}
.quick-name {
display: block;
margin-top: 5px;
}
}
}
}

View File

@ -689,6 +689,11 @@
.popover {
padding: 0;
max-width: 500px;
.popover-content {
padding: 10px 10px 20px 10px;
}
&.bottom > .arrow:after {
top: 1px;

View File

@ -1,78 +1,89 @@
<%!
# -*- coding: utf-8 -*-
page_title_ = '登录'
## page_menu_ = ['host']
## page_id_ = 'host'
%>
<%inherit file="page_base.mako"/>
<%block name="extend_js">
<script type="text/javascript" src="${ static_url('js/ui/auth/login.js') }"></script>
</%block>
<%block name="embed_js" >
<script type="text/javascript">
ywl.add_page_options(${ page_param });
</script>
</%block>
<div class="row">
<div class="col-md-6">
<div id="leftside">
</div>
</div>
<div class="col-md-5">
<div class="auth-box auth-box-lg">
<div class="header">
<div id="login-type-account" class="title selected">账号/密码 登录</div>
</div>
<div id="input-area-account" class="inputarea">
<div id="login_account" class="login-account">
<div class="inputbox">
<div class="input-group input-group-lg">
<span class="input-group-addon"><i class="fa fa-user fa-fw"></i></span>
<input id="username_account" type="text" class="form-control" placeholder="账号:邮箱地址或手机号" data-toggle="popover" data-trigger="manual" data-placement="top">
</div>
</div>
<div class="inputbox">
<div class="input-group input-group-lg">
<span class="input-group-addon"><i class="fa fa-key fa-fw"></i></span>
<input id="password_account" type="password" class="form-control" placeholder="密码" data-toggle="popover" data-trigger="manual" data-placement="top">
</div>
</div>
</div>
<div class="inputbox">
<div class="input-group input-group-lg">
<span class="input-group-addon"><i class="fa fa-check-square-o fa-fw"></i></span>
<input id="captcha" type="text" class="form-control" placeholder="验证码"
data-toggle="popover" data-trigger="manual" data-placement="top">
<span class="input-group-addon"><a href="javascript:;"><img id="captcha_image" src=""></a></span>
</div>
<p class="input-addon-desc">验证码,点击图片可更换</p>
</div>
<div class="inputbox">
<div class="checkbox">
<label><input id="remember-me" type="checkbox" value=""> 记住我12小时内无需重新登录。</label>
</div>
</div>
<div class="inputbox">
<button id="btn-login-account" class="btn btn-primary btn-lg btn-block">登 录</button>
</div>
</div>
<div>
<p id="login_message" class="op_box" style="display:none;"></p>
</div>
</div>
</div>
</div>
<%!
# -*- coding: utf-8 -*-
page_title_ = '登录'
## page_menu_ = ['host']
## page_id_ = 'host'
%>
<%inherit file="page_base.mako"/>
<%block name="extend_js">
<script type="text/javascript" src="${ static_url('js/ui/auth/login.js') }"></script>
</%block>
<%block name="embed_js" >
<script type="text/javascript">
ywl.add_page_options(${ page_param });
</script>
</%block>
<div class="row">
<div class="col-md-6">
<div id="leftside">
</div>
</div>
<div class="col-md-5">
<div class="auth-box auth-box-lg">
<div class="header">
<a id="login-type-password" class="title selected" href="javascript:;">账号/密码登录</a>
<a id="login-type-oath" class="title" href="javascript:;">身份验证器登录</a>
</div>
<div class="inputarea">
<div id="login-area-account" class="login-account">
<div class="inputbox">
<div class="input-group input-group-lg">
<span class="input-group-addon"><i class="fa fa-user fa-fw"></i></span>
<input id="username-account" type="text" class="form-control" placeholder="账号:邮箱地址或手机号" data-toggle="popover" data-trigger="manual" data-placement="top">
</div>
</div>
<div class="inputbox">
<div class="input-group input-group-lg">
<span class="input-group-addon"><i class="fa fa-key fa-fw"></i></span>
<input id="password-account" type="password" class="form-control" placeholder="密码" data-toggle="popover" data-trigger="manual" data-placement="top">
</div>
</div>
</div>
<div id="login-area-captcha" class="inputbox">
<div class="input-group input-group-lg">
<span class="input-group-addon"><i class="fa fa-check-square-o fa-fw"></i></span>
<input id="captcha" type="text" class="form-control" placeholder="验证码"
data-toggle="popover" data-trigger="manual" data-placement="top">
<span class="input-group-addon"><a href="javascript:;"><img id="captcha-image" src=""></a></span>
</div>
<p class="input-addon-desc">验证码,点击图片可更换</p>
</div>
<div id="login-area-oath" style="display:none;">
<div class="inputbox">
<div class="input-group input-group-lg">
<span class="input-group-addon"><i class="fa fa-clock-o fa-fw"></i></span>
<input id="oath-code" type="text" class="form-control" placeholder="6位数字身份验证器动态验证码">
</div>
</div>
</div>
<div class="inputbox">
<div class="checkbox">
<label><input id="remember-me" type="checkbox" value=""> 记住我12小时内无需重新登录。</label>
</div>
</div>
<div class="inputbox">
<button id="btn-login" class="btn btn-primary btn-lg btn-block">登 录</button>
</div>
</div>
<div>
<p id="message" class="op_box" style="display:none;"></p>
</div>
</div>
</div>
</div>

View File

@ -1,100 +1,100 @@
<!DOCTYPE html>
<%!
import eom_ver
page_title_ = ''
%>
<!--[if IE 8]><html lang="en" class="ie8"> <![endif]-->
<!--[if !IE]><!-->
<html lang="zh_CN">
<!--<![endif]-->
<head>
<meta charset="utf-8"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta content="yes" name="apple-mobile-web-app-capable">
<meta content="black-translucent" name="apple-mobile-web-app-status-bar-style">
<title>${self.attr.page_title_}::TELEPORT</title>
<link rel="shortcut icon" href="${ static_url('favicon.png') }">
<!-- CSS -->
<link href="${ static_url('plugins/bootstrap/css/bootstrap.min.css') }" rel="stylesheet" type="text/css"/>
<link href="${ static_url('plugins/font-awesome/css/font-awesome.min.css') }" rel="stylesheet">
<link href="${ static_url('css/main.css') }" rel="stylesheet" type="text/css"/>
<link href="${ static_url('css/auth.css') }" rel="stylesheet" type="text/css"/>
<%block name="extend_css"/>
</head>
<body>
<div id="head">
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<ul class="nav navbar-nav navbar-left">
<li>
<div class="logo">
<a href="/"><img src="${ static_url('img/site-logo.png') }" alt="TELEPORT触维软件旗下产品。"/></a>
<span class="desc">连接 &middot; 尽在指掌中</span>
</div>
</li>
</ul>
</div>
</nav>
</div>
<div id="content">
<div class="container">
${self.body()}
</div>
</div>
<div id="foot">
<nav class="navbar navbar-default navbar-fixed-bottom">
<div class="container">
<p>触维软件旗下产品 | TELEPORT v${eom_ver.TS_VER} | &copy;2015 - 2017 <a href="http://www.eomsoft.net/" target="_blank">触维软件</a>,保留所有权利。</p>
</div>
</nav>
</div>
<%block name="extend_content" />
<!-- JavaScript -->
<script type="text/javascript" src="${ static_url('plugins/underscore/underscore.js') }"></script>
<script type="text/javascript" src="${ static_url('plugins/jquery/jquery.min.js') }"></script>
<script type="text/javascript" src="${ static_url('plugins/bootstrap/js/bootstrap.min.js') }"></script>
<!--[if lt IE 9]>
<script src="${ static_url('plugins/html5shiv/html5shiv.min.js') }"></script>
<![endif]-->
<script type="text/javascript" src="${ static_url('js/json2.js') }"></script>
<script type="text/javascript" src="${ static_url('plugins/gritter/js/jquery.gritter.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ywl_const.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ywl_common.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ywl.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ywl_assist.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ui/common.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ui/controls.js') }"></script>
<%block name="extend_js"/>
<script type="text/javascript">
$(document).ready(function () {
// once page ready, init ywl object.
ywl.init();
});
</script>
<%block name="embed_js" />
</body>
<!DOCTYPE html>
<%!
import eom_ver
page_title_ = ''
%>
<!--[if IE 8]><html lang="en" class="ie8"> <![endif]-->
<!--[if !IE]><!-->
<html lang="zh_CN">
<!--<![endif]-->
<head>
<meta charset="utf-8"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta content="yes" name="apple-mobile-web-app-capable">
<meta content="black-translucent" name="apple-mobile-web-app-status-bar-style">
<title>${self.attr.page_title_}::TELEPORT</title>
<link rel="shortcut icon" href="${ static_url('favicon.png') }">
<!-- CSS -->
<link href="${ static_url('plugins/bootstrap/css/bootstrap.min.css') }" rel="stylesheet" type="text/css"/>
<link href="${ static_url('plugins/font-awesome/css/font-awesome.min.css') }" rel="stylesheet">
<link href="${ static_url('css/main.css') }" rel="stylesheet" type="text/css"/>
<link href="${ static_url('css/auth.css') }" rel="stylesheet" type="text/css"/>
<%block name="extend_css"/>
</head>
<body>
<div id="head">
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<ul class="nav navbar-nav navbar-left">
<li>
<div class="logo">
<a href="/"><img src="${ static_url('img/site-logo.png') }" alt="TELEPORT触维软件旗下产品。"/></a>
<span class="desc">连接 &middot; 尽在指掌中</span>
</div>
</li>
</ul>
</div>
</nav>
</div>
<div id="content">
<div class="container">
${self.body()}
</div>
</div>
<div id="foot">
<nav class="navbar navbar-default navbar-fixed-bottom">
<div class="container">
<p>触维软件旗下产品 | TELEPORT v${eom_ver.TS_VER} | &copy;2015 - 2017 <a href="http://teleport.eomsoft.net/" target="_blank">触维软件</a>,保留所有权利。</p>
</div>
</nav>
</div>
<%block name="extend_content" />
<!-- JavaScript -->
<script type="text/javascript" src="${ static_url('plugins/underscore/underscore.js') }"></script>
<script type="text/javascript" src="${ static_url('plugins/jquery/jquery.min.js') }"></script>
<script type="text/javascript" src="${ static_url('plugins/bootstrap/js/bootstrap.min.js') }"></script>
<!--[if lt IE 9]>
<script src="${ static_url('plugins/html5shiv/html5shiv.min.js') }"></script>
<![endif]-->
<script type="text/javascript" src="${ static_url('js/json2.js') }"></script>
<script type="text/javascript" src="${ static_url('plugins/gritter/js/jquery.gritter.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ywl_const.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ywl_common.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ywl.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ywl_assist.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ui/common.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ui/controls.js') }"></script>
<%block name="extend_js"/>
<script type="text/javascript">
$(document).ready(function () {
// once page ready, init ywl object.
ywl.init();
});
</script>
<%block name="embed_js" />
</body>
</html>

View File

@ -101,18 +101,19 @@
<div class="dropdown">
<a class="title" href="#" id="user-profile" data-target="#" data-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false">
<span class="name">${ current_user['nick_name'] }</span>
<span class="name">${ current_user['name'] }</span>
<span class="role">
%if current_user['type'] == 100:
平台管理员
系统管理员
%else:
普通用户
运维人员
%endif
<i class="fa fa-caret-right"></i></span>
</a>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="/pwd" id="btn-logout">修改密码</a></li>
<li><a href="/auth/logout" id="btn-logout">安全退出</a></li>
## <li><a href="/pwd" id="btn-logout">修改密码</a></li>
<li><a href="/user/personal">个人中心</a></li>
<li><a href="/auth/logout">安全退出</a></li>
</ul>
</div>

View File

@ -19,6 +19,24 @@
<%block name="extend_css">
<style type="text/css">
.box {
padding-left: 30px;
}
h2 {
margin-left:-20px;
font-size:160%;
}
h3 {
font-size: 120%;
font-weight: bold;
}
.table {
width: auto;
}
.table .key {
text-align: right;
}
@ -35,6 +53,19 @@
.table .value .disabled {
color: #ffa861;
}
.db-box {
margin-bottom: 20px;
}
.notice-box {
border: 1px solid #e2cab4;
background-color: #ffe4cb;
padding: 15px;
color: #000000;
margin-bottom: 10px;
}
</style>
</%block>
@ -46,14 +77,27 @@
<div class="box">
<div>
<h4><strong>服务器配置信息</strong></h4>
<h2><strong>服务器配置信息</strong></h2>
<table id="info-kv" class="table"></table>
</div>
## <div>
## <h4><strong>高级设置</strong></h4>
## <p><a href="javascript:;" id="btn_maintenance">进入维护模式</a></p>
## </div>
<div>
<hr/>
<h2><strong>数据库管理</strong></h2>
<div class="db-box">
<h3>导出</h3>
<p>将数据库中所有数据导出到sql文件可用作备份。</p>
<button type="button" id="btn-db-export" class="btn btn-sm btn-primary"><i class="fa fa-sign-out fa-fw"></i> 导出数据库</button>
</div>
<div class="db-box">
<h3>导入</h3>
<p>清空当前数据库中所有数据然后从sql文件中导入数据到数据库中。</p>
<p class="notice-box">注意!导入操作将导致现有数据被清除且无法恢复,请谨慎使用!</p>
<button type="button" id="btn-db-import" class="btn btn-sm btn-danger"><i class="fa fa-sign-in fa-fw"></i> 导入数据库</button>
</div>
</div>
</div>
<!-- end of box -->

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,101 +1,105 @@
<%!
page_title_ = '操作记录'
%>
<%inherit file="../page_no_sidebar_base.mako"/>
<%block name="extend_js">
</%block>
<%block name="breadcrumb">
<ol class="breadcrumb">
<li><i class="fa fa-server"></i> ${self.attr.page_title_}</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 {
font-family: Consolas, Lucida Console, Monaco, Courier, 'Courier New', monospace;
line-height: 15px;
padding: 0 5px;
border-radius: 3px;
}
.time {
margin-right: 15px;
background-color: #d8d8d8;
}
.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});
var danger_cmd = ['chmod', 'chown', 'kill', 'rm', 'su', 'sudo'];
var info_cmd = ['exit'];
ywl.on_init = function (cb_stack, cb_args) {
if (ywl.page_options.count == 0) {
$('#no-op-msg').show();
} else {
var dom_op_list = $('#op-list');
var html = [];
for (var i = 0; i < ywl.page_options.count; i++) {
var cmd_list = ywl.page_options.op[i].c.split(' ');
var cmd_class = '';
if (_.intersection(cmd_list, danger_cmd).length > 0) {
cmd_class = ' cmd-danger';
} else if (_.intersection(cmd_list, info_cmd).length > 0) {
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>');
}
dom_op_list.append(html.join(''));
dom_op_list.show();
}
cb_stack.exec();
};
</script>
<%!
page_title_ = 'SSH操作记录'
%>
<%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 {
font-family: Consolas, Lucida Console, Monaco, Courier, 'Courier New', monospace;
line-height: 15px;
padding: 0 5px;
border-radius: 3px;
}
.time {
margin-right: 15px;
background-color: #d8d8d8;
}
.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});
var danger_cmd = ['chmod', 'chown', 'kill', 'rm', 'su', 'sudo'];
var info_cmd = ['exit'];
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++) {
var cmd_list = ywl.page_options.op[i].c.split(' ');
var cmd_class = '';
if (_.intersection(cmd_list, danger_cmd).length > 0) {
cmd_class = ' cmd-danger';
} else if (_.intersection(cmd_list, info_cmd).length > 0) {
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></div>');
}
dom_op_list.append(html.join(''));
dom_op_list.show();
}
cb_stack.exec();
};
</script>
</%block>

View File

@ -93,7 +93,7 @@
ywl.ajax_post_json('/maintenance/rpc', {cmd: 'upgrade_db'},
function (ret) {
console.log('upgrade-db:', ret);
if (ret.code == 0) {
if (ret.code === 0) {
var cb_stack = CALLBACK_STACK.create();
cb_stack
@ -112,7 +112,7 @@
ywl.get_task_ret = function (cb_stack, cb_args) {
var task_id = cb_args.task_id || 0;
if (task_id == 0) {
if (task_id === 0) {
console.log('task-id', task_id);
return;
}
@ -120,7 +120,7 @@
ywl.ajax_post_json('/maintenance/rpc', {cmd: 'get_task_ret', 'tid': task_id},
function (ret) {
console.log('get_task_ret:', ret);
if (ret.code == 0) {
if (ret.code === 0) {
// show step progress.
var steps = ret.data.steps;
@ -130,14 +130,20 @@
var icon_class = '';
var err_class = '';
for(var i = 0; i < steps.length; ++i) {
if(steps[i].stat == 0)
if(steps[i].code !== 0) {
err_class = ' class="error"';
icon_class = 'fa-times-circle';
}
else {
err_class = '';
icon_class = 'fa-check';
}
if(steps[i].stat === 0)
;//icon_class = 'fa-check';
else
icon_class = 'fa-cog fa-spin';
if(steps[i].code != 0)
err_class = ' class="error"';
else
err_class = '';
html.push('<p');
html.push(err_class);
html.push('><i class="fa ');

View File

@ -1,7 +1,9 @@
<!DOCTYPE html>
<%!
import time
page_title_ = ''
page_menu_ = []
time_now = int(time.time())
%>
<!--[if IE 8]> <html lang="en" class="ie8"> <![endif]-->
<!--[if !IE]><!-->
@ -38,6 +40,9 @@
<div class="breadcrumb-container">
<%block name="breadcrumb" />
</div>
<div style="display:inline-block;float:right;padding-top:12px;">
系统时间:<span id="system-timer">111</span>
</div>
</div>
</div>
<!-- end #header -->
@ -81,8 +86,6 @@
</div>
<%block name="extend_content" />
## <script type="text/javascript" src="${ static_url('js/var.js') }"></script>
<script type="text/javascript" src="${ static_url('plugins/underscore/underscore.js') }"></script>
<script type="text/javascript" src="${ static_url('plugins/jquery/jquery.min.js') }"></script>
<script type="text/javascript" src="${ static_url('plugins/jquery/ajaxfileupload.js') }"></script>
@ -102,7 +105,6 @@
<%block name="extend_js"/>
<script type="text/javascript">
ywl.add_page_options({
## 有些参数由后台python脚本生成到模板中无法直接生成到js文件中所以必须通过这种方式传递参数到js脚本中。
@ -110,10 +112,15 @@
});
$(document).ready(function () {
// once page ready, init ywl object.
## var teleport_ip_info = "请核对您的堡垒机IP地址当前为 " + teleport_ip;
## $("#teleport-server-ip").text(teleport_ip_info);
ywl.init();
var g_time_now = ${time_now};
var g_dom_timer = $('#system-timer');
g_dom_timer.text(format_datetime(g_time_now));
setInterval(function(){
g_dom_timer.text(format_datetime(g_time_now));
g_time_now += 1;
}, 1000);
});
</script>

View File

@ -0,0 +1,222 @@
<%!
page_title_ = '个人中心'
page_menu_ = ['personal']
page_id_ = 'personal'
%>
<%inherit file="../page_base.mako"/>
<%block name="extend_js">
<script type="text/javascript" src="${ static_url('js/ui/teleport.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ui/user/personal.js') }"></script>
</%block>
<%block name="breadcrumb">
<ol class="breadcrumb">
<li><i class="fa fa-pencil-square-o fa-fw"></i> ${self.attr.page_title_}</li>
</ol>
</%block>
<%block name="extend_css">
<style type="text/css">
.table {
width: auto;
}
.table .key {
text-align: right;
}
.table .value {
text-align: left;
font-weight: bold;
}
.oath-code {
font-family: Consolas, Lucida Console, Monaco, Courier, 'Courier New', monospace;
font-size: 18px;
line-height: 26px;
font-weight: bold;
color: #559f47;
}
</style>
</%block>
## Begin Main Body.
<div class="page-content">
<div class="box">
<!-- Nav tabs -->
<ul class="nav nav-tabs">
<li class="active"><a href="#info" data-toggle="tab">个人信息</a></li>
<li><a href="#password" data-toggle="tab">修改密码</a></li>
<li><a href="#oath" data-toggle="tab">身份认证器</a></li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div class="tab-pane active" id="info">
<table class="table">
<tr>
<td class="key">登录名:</td>
<td class="value">${user['name']}</td>
</tr>
<tr>
<td class="key">姓名:</td>
<td class="value">${user['nick_name']}</td>
</tr>
<tr>
<td class="key">邮箱:</td>
<td class="value">-</td>
</tr>
<tr>
<td class="key">手机:</td>
<td class="value">-</td>
</tr>
<tr>
<td class="key">注册时间:</td>
<td class="value">-</td>
</tr>
<tr>
<td class="key">上次登录时间:</td>
<td class="value">-</td>
</tr>
</table>
</div>
<div class="tab-pane" id="password">
<div class="input-group input-group-sm" style="margin-top: 10px">
<span class="input-group-addon" style="width:90px;">当前密码:</span>
<input type="password" class="form-control" id="current-password">
</div>
<div class="input-group input-group-sm" style="margin-top: 10px">
<span class="input-group-addon" style="width:90px;">新密码:</span>
<input type="password" class="form-control" id="new-password-1">
</div>
<div class="input-group input-group-sm" style="margin-top: 10px">
<span class="input-group-addon" style="width:90px;">重复新密码:</span>
<input type="password" class="form-control" id="new-password-2">
</div>
<div style="margin-top:20px;">
<a href="javascript:;" id="btn-modify-password" class="btn btn-sm btn-primary"><i class="fa fa-check fa-fw"></i> 确认修改</a>
</div>
</div>
<div class="tab-pane" id="oath">
<p>请在你的手机上安装身份验证器,然后在验证器中添加你的登录账号。</p>
<div id="oath-app-download-box" style="display:none;">
<p>选择你喜欢的身份验证器,扫描二维码进行安装:</p>
<div class="row">
<div class="col-md-2">
<p style="text-align: center;">
<i class="fa fa-apple"></i> iOSApple Store<br/>
<img src="${ static_url('img/qrcode/google-oath-appstore.png') }"><br/>
Google身份验证器
</p>
</div>
<div class="col-md-2">
<p style="text-align: center;">
<i class="fa fa-android"></i> Android百度手机助手<br/>
<img src="${ static_url('img/qrcode/google-oath-baidu.png') }"><br/>
Google身份验证器
</p>
</div>
<div class="col-md-2">
<p style="text-align: center;">
<i class="fa fa-android"></i> AndroidGoogle Play<br/>
<img src="${ static_url('img/qrcode/google-oath-googleplay.png') }"><br/>
Google身份验证器
</p>
</div>
<div class="col-md-2">
<p style="text-align: center;">
<i class="fa fa-apple"></i> iOSApple Store<br/>
<img src="${ static_url('img/qrcode/xiaomi-oath-appstore.png') }"><br/>
小米安全令牌
</p>
</div>
<div class="col-md-2">
<p style="text-align: center;">
<i class="fa fa-android"></i> Android小米应用商店<br/>
<img src="${ static_url('img/qrcode/xiaomi-oath-xiaomi.png') }"><br/>
小米安全令牌
</p>
</div>
</div>
</div>
<p style="margin-top:5px;"><a href="javascript:;" id="toggle-oath-download">显示下载地址 <i class="fa fa-angle-down"></i></a></p>
<hr/>
<p>要验证已经绑定的身份验证器,可在下面的输入框中输入验证器器上显示的动态验证码,然后点击验证。</p>
<p>如果验证失败,请注意检查您的身份验证器的时间与服务器时间是否一致,如果两者时间偏差超过两分钟则无法验证通过!</p>
<div style="width:360px;">
<div class="input-group input-group-sm" style="margin-top: 10px">
<span class="input-group-addon">动态验证码6位数字</span>
<input type="text" class="form-control" id="oath-code">
<div class="input-group-btn">
<button id="btn-verify-oath-code" class="btn btn-primary"><i class="fa fa-check"></i> 验证</button>
</div>
</div>
</div>
<hr/>
<p>要将你的登录账号添加到身份验证器中,请点击下面的“绑定身份验证器”按钮。</p>
<p>
<button id="btn-reset-oath-code" class="btn btn-sm btn-success"><i class="fa fa-refresh"></i> 绑定身份验证器</button>
</p>
</div>
</div>
</div>
</div>
<%block name="extend_content">
<div class="modal fade" id="dialog-reset-oath-code" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">绑定身份验证器</h3>
</div>
<div class="modal-body">
<p>请在手机上打开身份验证器,点击增加账号按钮,然后选择“扫描条形码”并扫描下面的二维码来完成账号绑定。</p>
<p style="text-align: center;"><img id="oath-secret-qrcode" src=""></p>
<p>如果无法扫描二维码,则可以选择“手动输入验证码”,设置一个容易记忆的账号名称,并确保“基于时间”一项是选中的,然后在“密钥”一项中输入下列密钥:</p>
<div class="row">
<div class="col-sm-4" style="text-align:right;">
<span style="line-height:25px;">密钥:</span>
</div>
<div class="col-sm-8">
<span class="oath-code"><span id="tmp-oath-secret"></span></span>
</div>
</div>
<hr/>
<p>然后请在下面的动态验证码输入框中输入身份验证器提供的6位数字</p>
<div class="row">
<div class="col-sm-4" style="text-align:right;">
<span style="line-height:34px;">动态验证码:</span>
</div>
<div class="col-sm-4">
<input type="text" class="form-control" id="oath-code-verify">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-primary" id="btn-verify-oath-and-save"><i class="fa fa-check fa-fw"></i> 验证动态验证码并完成绑定</button>
<button type="button" class="btn btn-sm btn-default" data-dismiss="modal"><i class="fa fa-close fa-fw"></i> 取消</button>
</div>
</div>
</div>
</div>
</%block>

View File

@ -14,6 +14,6 @@ Build 构建号。构建号用于表明此版本发布之前进行了多少
TELEPORT_SERVER 2.2.8.1
TELEPORT_SERVER 2.2.9.3
TELEPORT_ASSIST 2.2.6.1
TELEPORT_ASSIST_REQUIRE 2.0.0.1