Merge remote-tracking branch 'remotes/origin/dev'
commit
76518ddbfb
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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.
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
@ -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__
|
||||
|
|
|
@ -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:])
|
|
@ -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
|
|
@ -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()
|
|
@ -0,0 +1,5 @@
|
|||
# QR error correct levels
|
||||
ERROR_CORRECT_L = 1
|
||||
ERROR_CORRECT_M = 0
|
||||
ERROR_CORRECT_Q = 3
|
||||
ERROR_CORRECT_H = 2
|
|
@ -0,0 +1,2 @@
|
|||
class DataOverflowError(Exception):
|
||||
pass
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'
|
|
@ -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
|
|
@ -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)
|
|
@ -0,0 +1,8 @@
|
|||
import string
|
||||
import qrcode
|
||||
|
||||
qr = qrcode.QRCode()
|
||||
|
||||
qr.add_data(string.letters*13)
|
||||
qr.make()
|
||||
print(qr.version)
|
|
@ -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)
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -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__']
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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, '验证失败!')
|
||||
|
|
|
@ -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'] # 提取表单中‘name’为‘file’的文件元数据
|
||||
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)
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .base import TPBaseUserAuthHandler
|
||||
|
||||
|
||||
class IndexHandler(TPBaseUserAuthHandler):
|
||||
def get(self):
|
||||
self.render('pwd/index.mako')
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 |
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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>';
|
||||
|
|
|
@ -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> ');
|
||||
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> ');
|
||||
}
|
||||
} 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> ');
|
||||
|
|
|
@ -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('网路故障,无法连接到服务器!');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -689,6 +689,11 @@
|
|||
|
||||
.popover {
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
|
||||
.popover-content {
|
||||
padding: 10px 10px 20px 10px;
|
||||
}
|
||||
|
||||
&.bottom > .arrow:after {
|
||||
top: 1px;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">连接 · 尽在指掌中</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} | ©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">连接 · 尽在指掌中</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} | ©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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 ');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> iOS(Apple 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> Android(Google 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> iOS(Apple 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>
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue