diff --git a/build/builder/core/ver.py b/build/builder/core/ver.py index b5ebd39..0180b99 100644 --- a/build/builder/core/ver.py +++ b/build/builder/core/ver.py @@ -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" diff --git a/server/.idea/encodings.xml b/server/.idea/encodings.xml index e2ddfa7..aae22e9 100644 --- a/server/.idea/encodings.xml +++ b/server/.idea/encodings.xml @@ -16,6 +16,7 @@ + diff --git a/server/tp_core/common/ts_const.h b/server/tp_core/common/ts_const.h index db4e21e..d1ffa72 100644 --- a/server/tp_core/common/ts_const.h +++ b/server/tp_core/common/ts_const.h @@ -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__ diff --git a/server/tp_core/core/tp_core.rc b/server/tp_core/core/tp_core.rc index 264df56..468f908 100644 Binary files a/server/tp_core/core/tp_core.rc and b/server/tp_core/core/tp_core.rc differ diff --git a/server/tp_core/core/ts_env.cpp b/server/tp_core/core/ts_env.cpp index 607cae1..82e069a 100644 --- a/server/tp_core/core/ts_env.cpp +++ b/server/tp_core/core/ts_env.cpp @@ -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()); diff --git a/server/tp_core/core/ts_http_rpc.cpp b/server/tp_core/core/ts_http_rpc.cpp index ad0257e..337a008 100644 --- a/server/tp_core/core/ts_http_rpc.cpp +++ b/server/tp_core/core/ts_http_rpc.cpp @@ -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; } diff --git a/server/tp_core/core/ts_ver.h b/server/tp_core/core/ts_ver.h index 214ab3c..4205ff6 100644 --- a/server/tp_core/core/ts_ver.h +++ b/server/tp_core/core/ts_ver.h @@ -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__ diff --git a/server/tp_core/protocol/ssh/ssh_proxy.cpp b/server/tp_core/protocol/ssh/ssh_proxy.cpp index fd28926..39b0792 100644 --- a/server/tp_core/protocol/ssh/ssh_proxy.cpp +++ b/server/tp_core/protocol/ssh/ssh_proxy.cpp @@ -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) { diff --git a/server/tp_core/protocol/ssh/ssh_session.cpp b/server/tp_core/protocol/ssh/ssh_session.cpp index a6763b5..3970d9d 100644 --- a/server/tp_core/protocol/ssh/ssh_session.cpp +++ b/server/tp_core/protocol/ssh/ssh_session.cpp @@ -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; } diff --git a/server/tp_core/protocol/ssh/ssh_session.h b/server/tp_core/protocol/ssh/ssh_session.h index c69f2a8..3ece32b 100644 --- a/server/tp_core/protocol/ssh/ssh_session.h +++ b/server/tp_core/protocol/ssh/ssh_session.h @@ -1,133 +1,134 @@ -#ifndef __SSH_SESSION_H__ -#define __SSH_SESSION_H__ - -#include "ssh_recorder.h" - -#include - -#include -#include -#include -#include - -#include -#include - -#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 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 m_cmd_char_list; - std::list::iterator m_cmd_char_pos; -}; - -#endif // __SSH_SESSION_H__ +#ifndef __SSH_SESSION_H__ +#define __SSH_SESSION_H__ + +#include "ssh_recorder.h" + +#include + +#include +#include +#include +#include + +#include +#include + +#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 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 m_cmd_char_list; + std::list::iterator m_cmd_char_pos; +}; + +#endif // __SSH_SESSION_H__ diff --git a/server/tp_core/protocol/ssh/tpp_env.cpp b/server/tp_core/protocol/ssh/tpp_env.cpp index a5594b3..7a03179 100644 --- a/server/tp_core/protocol/ssh/tpp_env.cpp +++ b/server/tp_core/protocol/ssh/tpp_env.cpp @@ -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; } diff --git a/server/tp_core/protocol/ssh/tpssh.cpp b/server/tp_core/protocol/ssh/tpssh.cpp index 3fc3fcb..76d3b2a 100644 --- a/server/tp_core/protocol/ssh/tpssh.cpp +++ b/server/tp_core/protocol/ssh/tpssh.cpp @@ -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)) diff --git a/server/tp_web/src/tp_web.rc b/server/tp_web/src/tp_web.rc index e78c1af..b6df1a4 100644 Binary files a/server/tp_web/src/tp_web.rc and b/server/tp_web/src/tp_web.rc differ diff --git a/server/tp_web/src/ts_ver.h b/server/tp_web/src/ts_ver.h index 214ab3c..4205ff6 100644 --- a/server/tp_web/src/ts_ver.h +++ b/server/tp_web/src/ts_ver.h @@ -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__ diff --git a/server/www/packages/packages-common/qrcode/__init__.py b/server/www/packages/packages-common/qrcode/__init__.py new file mode 100644 index 0000000..f6aa53f --- /dev/null +++ b/server/www/packages/packages-common/qrcode/__init__.py @@ -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:]) diff --git a/server/www/packages/packages-common/qrcode/base.py b/server/www/packages/packages-common/qrcode/base.py new file mode 100644 index 0000000..26c521f --- /dev/null +++ b/server/www/packages/packages-common/qrcode/base.py @@ -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 diff --git a/server/www/packages/packages-common/qrcode/console_scripts.py b/server/www/packages/packages-common/qrcode/console_scripts.py new file mode 100644 index 0000000..5d1a904 --- /dev/null +++ b/server/www/packages/packages-common/qrcode/console_scripts.py @@ -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() diff --git a/server/www/packages/packages-common/qrcode/constants.py b/server/www/packages/packages-common/qrcode/constants.py new file mode 100644 index 0000000..385dda0 --- /dev/null +++ b/server/www/packages/packages-common/qrcode/constants.py @@ -0,0 +1,5 @@ +# QR error correct levels +ERROR_CORRECT_L = 1 +ERROR_CORRECT_M = 0 +ERROR_CORRECT_Q = 3 +ERROR_CORRECT_H = 2 diff --git a/server/www/packages/packages-common/qrcode/exceptions.py b/server/www/packages/packages-common/qrcode/exceptions.py new file mode 100644 index 0000000..b37bd30 --- /dev/null +++ b/server/www/packages/packages-common/qrcode/exceptions.py @@ -0,0 +1,2 @@ +class DataOverflowError(Exception): + pass diff --git a/server/www/packages/packages-common/qrcode/image/__init__.py b/server/www/packages/packages-common/qrcode/image/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/www/packages/packages-common/qrcode/image/base.py b/server/www/packages/packages-common/qrcode/image/base.py new file mode 100644 index 0000000..d0bfd8f --- /dev/null +++ b/server/www/packages/packages-common/qrcode/image/base.py @@ -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 diff --git a/server/www/packages/packages-common/qrcode/image/pil.py b/server/www/packages/packages-common/qrcode/image/pil.py new file mode 100644 index 0000000..a56d2f9 --- /dev/null +++ b/server/www/packages/packages-common/qrcode/image/pil.py @@ -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) diff --git a/server/www/packages/packages-common/qrcode/image/pure.py b/server/www/packages/packages-common/qrcode/image/pure.py new file mode 100644 index 0000000..34f75fe --- /dev/null +++ b/server/www/packages/packages-common/qrcode/image/pure.py @@ -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) diff --git a/server/www/packages/packages-common/qrcode/image/svg.py b/server/www/packages/packages-common/qrcode/image/svg.py new file mode 100644 index 0000000..e99a66f --- /dev/null +++ b/server/www/packages/packages-common/qrcode/image/svg.py @@ -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 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' diff --git a/server/www/packages/packages-common/qrcode/main.py b/server/www/packages/packages-common/qrcode/main.py new file mode 100644 index 0000000..4fddd37 --- /dev/null +++ b/server/www/packages/packages-common/qrcode/main.py @@ -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 diff --git a/server/www/packages/packages-common/qrcode/mecard.py b/server/www/packages/packages-common/qrcode/mecard.py new file mode 100644 index 0000000..ce05582 --- /dev/null +++ b/server/www/packages/packages-common/qrcode/mecard.py @@ -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) diff --git a/server/www/packages/packages-common/qrcode/speedy.py b/server/www/packages/packages-common/qrcode/speedy.py new file mode 100644 index 0000000..11ec3aa --- /dev/null +++ b/server/www/packages/packages-common/qrcode/speedy.py @@ -0,0 +1,8 @@ +import string +import qrcode + +qr = qrcode.QRCode() + +qr.add_data(string.letters*13) +qr.make() +print(qr.version) diff --git a/server/www/packages/packages-common/qrcode/util.py b/server/www/packages/packages-common/qrcode/util.py new file mode 100644 index 0000000..89dcf09 --- /dev/null +++ b/server/www/packages/packages-common/qrcode/util.py @@ -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) diff --git a/server/www/teleport/app/eom_app/app/configs.py b/server/www/teleport/app/eom_app/app/configs.py index 3ebe894..eac6ef7 100644 --- a/server/www/teleport/app/eom_app/app/configs.py +++ b/server/www/teleport/app/eom_app/app/configs.py @@ -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') diff --git a/server/www/teleport/app/eom_app/app/core.py b/server/www/teleport/app/eom_app/app/core.py index eae0e79..fcb89ef 100644 --- a/server/www/teleport/app/eom_app/app/core.py +++ b/server/www/teleport/app/eom_app/app/core.py @@ -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 diff --git a/server/www/teleport/app/eom_app/app/database/create.py b/server/www/teleport/app/eom_app/app/database/create.py index 47f8c86..8d21d92 100644 --- a/server/www/teleport/app/eom_app/app/database/create.py +++ b/server/www/teleport/app/eom_app/app/database/create.py @@ -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, diff --git a/server/www/teleport/app/eom_app/app/database/export.py b/server/www/teleport/app/eom_app/app/database/export.py new file mode 100644 index 0000000..c17e040 --- /dev/null +++ b/server/www/teleport/app/eom_app/app/database/export.py @@ -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 diff --git a/server/www/teleport/app/eom_app/app/database/upgrade.py b/server/www/teleport/app/eom_app/app/database/upgrade.py index 7d7d393..8e050c8 100644 --- a/server/www/teleport/app/eom_app/app/database/upgrade.py +++ b/server/www/teleport/app/eom_app/app/database/upgrade.py @@ -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琛ㄥ鍔爋ath_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(' - 鍦╝ccount琛ㄤ腑鍔犲叆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 diff --git a/server/www/teleport/app/eom_app/app/db.py b/server/www/teleport/app/eom_app/app/db.py index e0b8f50..f6b0e8a 100644 --- a/server/www/teleport/app/eom_app/app/db.py +++ b/server/www/teleport/app/eom_app/app/db.py @@ -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锛屽彂鐢熷紓甯告椂浼氳嚜鍔╮ollback锛屾甯告墽琛屽畬姣曞悗浼氳嚜鍔╟ommit + 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() diff --git a/server/www/teleport/app/eom_app/app/oath.py b/server/www/teleport/app/eom_app/app/oath.py new file mode 100644 index 0000000..7f798bd --- /dev/null +++ b/server/www/teleport/app/eom_app/app/oath.py @@ -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() + + # 灏唄sh杞崲鎴愭暟瀛(榛樿涓6浣) + i = hsh[-1] & 0xf # 浠ユ渶鍚庝竴涓瓧鑺傜殑鍚4涓猙its涓烘暟瀛楋紝浣滀负鎺ヤ笅鏉ョ殑绱㈠紩 + f = hsh[i:i + 4] # 浠涓虹储寮, 鍙杊sh涓殑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() diff --git a/server/www/teleport/app/eom_app/app/session.py b/server/www/teleport/app/eom_app/app/session.py index 9304815..1c76e47 100644 --- a/server/www/teleport/app/eom_app/app/session.py +++ b/server/www/teleport/app/eom_app/app/session.py @@ -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锛屽垯姝や細璇濇暟鎹案涓嶈繃鏈熴俥xpire鐨勫崟浣嶄负绉掋 - @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): - # 浠巗ession涓幏鍙栦竴涓暟鎹紙璇诲彇骞舵洿鏂版渶鍚庤闂椂闂达級 - 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): - # 浠巗ession涓彇璧颁竴涓暟鎹紙璇诲彇骞跺垹闄わ級 - 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锛屽垯姝や細璇濇暟鎹案涓嶈繃鏈熴俥xpire鐨勫崟浣嶄负绉掋 + @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): + # 浠巗ession涓幏鍙栦竴涓暟鎹紙璇诲彇骞舵洿鏂版渶鍚庤闂椂闂达級 + 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): + # 浠巗ession涓彇璧颁竴涓暟鎹紙璇诲彇骞跺垹闄わ級 + 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__'] diff --git a/server/www/teleport/app/eom_app/controller/__init__.py b/server/www/teleport/app/eom_app/controller/__init__.py index 21bee29..dbc430a 100644 --- a/server/www/teleport/app/eom_app/controller/__init__.py +++ b/server/www/teleport/app/eom_app/controller/__init__.py @@ -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)) diff --git a/server/www/teleport/app/eom_app/controller/auth.py b/server/www/teleport/app/eom_app/controller/auth.py index 86e5df5..da10174 100644 --- a/server/www/teleport/app/eom_app/controller/auth.py +++ b/server/www/teleport/app/eom_app/controller/auth.py @@ -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, '楠岃瘉澶辫触锛') diff --git a/server/www/teleport/app/eom_app/controller/config.py b/server/www/teleport/app/eom_app/controller/config.py index 10f69fa..5b081e5 100644 --- a/server/www/teleport/app/eom_app/controller/config.py +++ b/server/www/teleport/app/eom_app/controller/config.py @@ -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('

閿欒

瀵煎嚭鏁版嵁鏃跺彂鐢熼敊璇細{}'.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瀵煎叆瑙勫垯锛 + 浠ヤ簨鍔℃柟寮忔墽琛宻ql璇彞 + """ + ret = dict() + ret['code'] = 0 + ret['message'] = '' + sql_filename = '' -# def restart_service(): -# # todo: 浣跨敤eom_ts.exe杩愯鑴氭湰鐨勬柟寮忥紙鏂拌繘绋嬶級鏉ラ噸鍚湇鍔★紝閬垮厤姝e湪杩愯鐨勬湰鏈嶅姟鏈鍑虹殑褰卞搷 -# -# 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'] # 鎻愬彇琛ㄥ崟涓榥ame鈥欎负鈥榝ile鈥欑殑鏂囦欢鍏冩暟鎹 + 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) diff --git a/server/www/teleport/app/eom_app/controller/index.py b/server/www/teleport/app/eom_app/controller/index.py index 65950cb..1710131 100644 --- a/server/www/teleport/app/eom_app/controller/index.py +++ b/server/www/teleport/app/eom_app/controller/index.py @@ -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') + diff --git a/server/www/teleport/app/eom_app/controller/pwd.py b/server/www/teleport/app/eom_app/controller/pwd.py deleted file mode 100644 index 79290ca..0000000 --- a/server/www/teleport/app/eom_app/controller/pwd.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- - -from .base import TPBaseUserAuthHandler - - -class IndexHandler(TPBaseUserAuthHandler): - def get(self): - self.render('pwd/index.mako') diff --git a/server/www/teleport/app/eom_app/controller/record.py b/server/www/teleport/app/eom_app/controller/record.py index df01e33..f995fbd 100644 --- a/server/www/teleport/app/eom_app/controller/record.py +++ b/server/www/teleport/app/eom_app/controller/record.py @@ -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): diff --git a/server/www/teleport/app/eom_app/controller/user.py b/server/www/teleport/app/eom_app/controller/user.py index a50bec1..d0c270b 100644 --- a/server/www/teleport/app/eom_app/controller/user.py +++ b/server/www/teleport/app/eom_app/controller/user.py @@ -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() diff --git a/server/www/teleport/app/eom_app/module/host.py b/server/www/teleport/app/eom_app/module/host.py index b276451..d6ec01b 100644 --- a/server/www/teleport/app/eom_app/module/host.py +++ b/server/www/teleport/app/eom_app/module/host.py @@ -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 diff --git a/server/www/teleport/app/eom_app/module/user.py b/server/www/teleport/app/eom_app/module/user.py index 337a23f..09fb3f3 100644 --- a/server/www/teleport/app/eom_app/module/user.py +++ b/server/www/teleport/app/eom_app/module/user.py @@ -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: # 鐗瑰埆鍦帮紝濡傛灉鏃犳硶鍙栧緱鏁版嵁搴撹繛鎺ワ紝鏈夊彲鑳芥槸鏂板畨瑁呯殑绯荤粺锛屽皻鏈缓绔嬫暟鎹簱锛屾鏃跺簲璇ュ浜庣淮鎶ゆā寮 # 鍥犳鍙互鐗瑰埆鍦板鐞嗙敤鎴烽獙璇侊細鐢ㄦ埛鍚峚dmin锛屽瘑鐮乤dmin鍙互鐧诲綍涓虹鐞嗗憳 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() diff --git a/server/www/teleport/app/eom_ver.py b/server/www/teleport/app/eom_ver.py index a502a9e..5559b90 100644 --- a/server/www/teleport/app/eom_ver.py +++ b/server/www/teleport/app/eom_ver.py @@ -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" diff --git a/server/www/teleport/static/css/auth.css b/server/www/teleport/static/css/auth.css index 882cdaa..7b7d0c6 100644 --- a/server/www/teleport/static/css/auth.css +++ b/server/www/teleport/static/css/auth.css @@ -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} \ No newline at end of file +@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} \ No newline at end of file diff --git a/server/www/teleport/static/css/doc.css b/server/www/teleport/static/css/doc.css index 69d39d3..eca947a 100644 --- a/server/www/teleport/static/css/doc.css +++ b/server/www/teleport/static/css/doc.css @@ -1 +1 @@ -@charset "utf-8";body{font-family:"Open Sans","Helvetica Neue","Microsoft YaHei","寰蒋闆呴粦",Helvetica,Arial,sans-serif;font-size:13px;background-color:#e9e9e9;color:#333}html,body{height:100%}#page-container{min-width:1260px}a{text-decoration:none}a:link{text-decoration:none}a:hover{text-decoration:none}a:active{text-decoration:none}a:visited{text-decoration:none}select{outline:none}label{font-weight:normal}.clear-float{clear:both}.bigger{font-size:120%}.normal-text{font-size:13px;color:#333}.mono{font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}hr.hr-sm{margin-top:5px;margin-bottom:5px}.btn-group-sm>.btn,.btn-sm{padding:2px 5px}.btn.btn-sm{padding:3px 8px}.btn.btn-icon{padding:3px 6px}.btn.btn-icon.btn-sm{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}.pop-menu-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040}.form-group{margin-bottom:5px}.badge{display:inline-block;min-width:8px;padding:5px 10px;border-radius:10px;text-align:center;white-space:nowrap;vertical-align:middle;font-size:13px;font-weight:400;line-height:1em;background-color:#888;color:#fff;text-shadow:1px 1px 0 #525252}.badge.badge-plain{text-shadow:none}.badge.badge-sm{font-size:11px;padding:3px 6px;margin-top:0;border-radius:8px;text-shadow:none}.badge.badge-sup{margin-left:-6px;margin-top:-16px}.badge.badge-ignore{background-color:#e5e5e5;color:#999;text-shadow:none}.badge.badge-info{background-color:#33b7d0}.badge.badge-primary{background-color:#348fe2}.badge.badge-success{background-color:#368142}.badge.badge-warning{background-color:#f57523}.badge.badge-danger{background-color:#d34242}.label{display:inline-block;min-width:8px;padding:5px 10px;border-radius:5px;text-align:center;white-space:nowrap;vertical-align:middle;font-size:13px;font-weight:400;line-height:1em;background-color:#888;color:#fff;text-shadow:1px 1px 0 #525252}.label.label-plain{text-shadow:none}.label.label-sm{font-size:11px;padding:3px 8px;margin-top:0;border-radius:5px;text-shadow:none}.label.label-ignore{background-color:#e5e5e5;color:#999;text-shadow:none}.label.label-info{background-color:#33b7d0}.label.label-primary{background-color:#348fe2}.label.label-success{background-color:#368142}.label.label-warning{background-color:#f57523}.label.label-danger{background-color:#d34242}.progress.progress-sm{height:18px;margin-bottom:2px;background-color:#aaa}.progress.progress-sm.button{cursor:pointer}.progress.progress-sm .progress-bar{display:block;font-size:11px;float:none}.alert-sm{padding:5px;margin-bottom:10px}.modal-dialog-sm .modal-header{padding:10px}.modal-dialog-sm .modal-body{padding:10px}.modal-dialog-sm .modal-footer{padding:10px}.modal-dialog-sm .form-horizontal .form-group{margin-right:-5px;margin-left:-5px}.modal-dialog-sm .col-sm-1,.modal-dialog-sm .col-sm-2,.modal-dialog-sm .col-sm-3,.modal-dialog-sm .col-sm-4,.modal-dialog-sm .col-sm-5,.modal-dialog-sm .col-sm-6,.modal-dialog-sm .col-sm-7,.modal-dialog-sm .col-sm-8,.modal-dialog-sm .col-sm-9,.modal-dialog-sm .col-sm-10,.modal-dialog-sm .col-sm-11{padding-right:5px;padding-left:5px}.btn-single-line{white-space:nowrap}.btn-single-line .btn:first-child{border-top-left-radius:3px;border-bottom-left-radius:3px}.btn-single-line .btn:last-child{border-top-right-radius:3px;border-bottom-right-radius:3px}.remote-action-group{margin-bottom:3px;height:28px;min-width:390px}.remote-action-group ul{display:block;height:28px;margin:0;padding:0}.remote-action-group ul 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}.remote-action-group ul li.remote-action-btn{background:none;padding:0;border:none}.remote-action-group ul li.remote-action-input{background:none;padding:4px 0}.remote-action-group ul li.remote-action-input select{border:none}.remote-action-group ul li.remote-action-chk-protocol{width:86px}.remote-action-group ul li.remote-action-username,.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-protocol{width:96px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.remote-action-group ul li.remote-action-username{font-size:90%;color:#999}.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-protocol,.remote-action-group ul li.remote-action-chk-protocol{color:#000}.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-chk-protocol{font-weight:bold}.remote-action-group ul li.remote-action-password,.remote-action-group ul li.remote-action-sshkey,.remote-action-group ul li.remote-action-noauth{text-align:center;padding:4px 8px;width:45px}.remote-action-group ul li.remote-action-password{background-color:#e3ffe3;color:#999}.remote-action-group ul li.remote-action-sshkey{background-color:#fbe9c8;color:#666}.remote-action-group ul li.remote-action-noauth{background-color:#e0e0e0;color:#666}.remote-action-group ul li .btn{line-height:1.5;margin:0;padding:4px 8px;font-size:12px;border-radius:0}.remote-action-group ul li label{padding:0;display:block;float:left;margin-top:1px;cursor:pointer}.remote-action-group ul li input[type=checkbox]{display:block;float:left;margin:3px 5px 0 0}.remote-action-group ul li select{margin-top:-3px}.remote-action-group ul li:first-child{border-left:1px solid #ccc;border-top-left-radius:4px;border-bottom-left-radius:4px}.remote-action-group ul li:first-child .btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.remote-action-group ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.remote-action-group ul li:last-child .btn{border-top-right-radius:4px;border-bottom-right-radius:4px}hr.small{margin:5px 0}.dlg-protocol-group{margin-bottom:3px}.dlg-protocol-group ul{display:block;height:28px;margin:0;padding:0}.dlg-protocol-group ul 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}.dlg-protocol-group ul li.item-name{width:120px}.dlg-protocol-group ul li.item-btn{background:none;padding:0;border:none}.dlg-protocol-group ul li.item-input{background:none;border:none;padding:0}.dlg-protocol-group ul li .form-control{line-height:1.5;margin:0;padding:4px 5px;font-size:12px;height:28px;border-radius:0;border-left:none;width:100px}.dlg-protocol-group ul li label{padding:0;display:block;float:left;margin-top:1px;cursor:pointer}.dlg-protocol-group ul li input[type=checkbox]{display:block;float:left;margin:3px 5px 0 0}.dlg-protocol-group ul li:first-child{border-left:1px solid #ccc;border-top-left-radius:4px;border-bottom-left-radius:4px}.dlg-protocol-group ul li:first-child .btn,.dlg-protocol-group ul li:first-child .form-control{border-top-left-radius:4px;border-bottom-left-radius:4px}.dlg-protocol-group ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.dlg-protocol-group ul li:last-child .btn,.dlg-protocol-group ul li:last-child .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}.form-group-sm .form-control-static{padding:6px 0}.page-header-fixed{padding-top:48px}.header{border:none;min-height:48px;height:48px;top:0;width:100%;position:fixed;z-index:999}.header .top-navbar{min-height:48px;height:48px;line-height:48px;background-color:#3a3a3a;color:#ccc}.header .top-navbar a{color:#d5d5d5}.header .top-navbar a:hover{color:#5a8fee}.header .top-navbar .brand{float:left;display:inline-block;padding:12px 0;margin:0}.header .top-navbar .brand .site-logo{display:block;width:86px;height:24px;background:url(../img/site-logo-small.png) no-repeat}.header .top-navbar .title-container{float:left;display:inline-block;margin:0;padding:0}.header .top-navbar .title-container .title{font-size:18px}.header .top-navbar .status-container{float:right}.footer{width:100%;height:36px;line-height:36px;background-color:#d5d5d5;border-top:1px solid #a2a2a2;border-bottom:1px solid #efefef;z-index:998;text-align:center}.footer.footer-fixed-bottom{bottom:0;position:fixed}.row-sm .col-sm-1,.row-sm .col-sm-2,.row-sm .col-sm-3,.row-sm .col-sm-4,.row-sm .col-sm-5,.row-sm .col-sm-6,.row-sm .col-sm-7,.row-sm .col-sm-8,.row-sm .col-sm-9,.row-sm .col-sm-10,.row-sm .col-sm-11{padding-right:5px;padding-left:5px}.sidebar{background-color:#fff;width:285px;position:fixed}.search-box{padding:10px;border-bottom:1px solid #eee;margin-bottom:10px}.tree-view{overflow-x:auto;overflow-y:auto;padding:0 10px}.content{margin-top:15px;margin-bottom:56px;min-height:360px;background-color:#fff;border-radius:5px;padding:10px;margin-left:300px}.jstree-ocl{cursor:default}.jstree-default .fa-folder:before{color:#f59c1a}.jstree-default .jstree-wholerow{cursor:default}.jstree-default>ul>li{padding:4px 0}.jstree-default .jstree-node{margin-left:12px}.jstree-default .jstree-open>.jstree-anchor>.fa-folder:before{content:'\f07c';color:#a26307}.jstree-default>.jstree-no-dots .jstree-closed>.jstree-ocl,.jstree-default>.jstree-no-dots .jstree-open>.jstree-ocl{background:none}.jstree-default>.jstree-no-dots .jstree-closed>.jstree-ocl:before,.jstree-default>.jstree-no-dots .jstree-open>.jstree-ocl:before{font-style:normal;font-family:"FontAwesome"}.jstree-default>.jstree-no-dots .jstree-closed>.jstree-ocl:before{content:'\f054';color:#ccc}.jstree-default>.jstree-no-dots .jstree-open>.jstree-ocl:before{content:'\f078';color:#666}.jstree-default>.jstree-no-dots .jstree-loading>.jstree-ocl{background:url("img/loading_01.gif") center center no-repeat}.jstree-default>.jstree-no-dots .jstree-loading>.jstree-ocl:before{content:''}#gritter-notice-wrapper{z-index:9999}.gritter-bottom,.gritter-item,.gritter-top{background:rgba(0,0,0,0.8) !important}.gritter-top{border-top-left-radius:3px;border-top-right-radius:3px}.gritter-bottom{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.gritter-close,.gritter-light .gritter-close{left:auto !important;right:5px !important;top:5px !important;width:16px !important;height:16px !important;line-height:16px !important;display:block !important;border-radius:50%}.gritter-close:before,.gritter-light .gritter-close:before{content:'\f00d' !important;font-family:FontAwesome !important;font-size:9px !important;width:16px !important;height:16px !important;line-height:16px !important;color:#fff !important;text-indent:0 !important;position:absolute !important;text-align:center !important;right:0 !important;top:0 !important}.gritter-title{font-size:13px !important;line-height:16px !important;padding-bottom:5px !important;font-weight:400 !important;color:#fff !important;text-shadow:none !important}.gritter-item{color:#aaa !important;font-size:13px !important;padding:2px 15px 5px !important}.gritter-error .gritter-bottom,.gritter-error .gritter-item,.gritter-error .gritter-top{background:rgba(123,32,32,0.9) !important}.gritter-error .gritter-title{color:#fff !important}.gritter-error .gritter-item{color:#ddd !important}.gritter-error .gritter-close{left:auto !important;right:5px !important;top:5px !important;width:16px !important;height:16px !important;line-height:16px !important;display:block !important;border-radius:50%;background:#e33b3b !important}.gritter-success .gritter-bottom,.gritter-success .gritter-item,.gritter-success .gritter-top{background:rgba(1,65,16,0.9) !important}.gritter-success .gritter-title{color:#ddd !important}.gritter-success .gritter-item{color:#ccc !important}.gritter-success .gritter-close{background:#0eb320 !important} \ No newline at end of file +@charset "utf-8";body{font-family:"Open Sans","Helvetica Neue","Microsoft YaHei","寰蒋闆呴粦",Helvetica,Arial,sans-serif;font-size:13px;background-color:#e9e9e9;color:#333}html,body{height:100%}#page-container{min-width:1260px}a{text-decoration:none}a:link{text-decoration:none}a:hover{text-decoration:none}a:active{text-decoration:none}a:visited{text-decoration:none}select{outline:none}label{font-weight:normal}.clear-float{clear:both}.bigger{font-size:120%}.normal-text{font-size:13px;color:#333}.mono{font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}hr.hr-sm{margin-top:5px;margin-bottom:5px}.btn-group-sm>.btn,.btn-sm{padding:2px 5px}.btn.btn-sm{padding:3px 8px}.btn.btn-icon{padding:3px 6px}.btn.btn-icon.btn-sm{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}.pop-menu-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040}.form-group{margin-bottom:5px}.badge{display:inline-block;min-width:8px;padding:5px 10px;border-radius:10px;text-align:center;white-space:nowrap;vertical-align:middle;font-size:13px;font-weight:400;line-height:1em;background-color:#888;color:#fff;text-shadow:1px 1px 0 #525252}.badge.badge-plain{text-shadow:none}.badge.badge-sm{font-size:11px;padding:3px 6px;margin-top:0;border-radius:8px;text-shadow:none}.badge.badge-sup{margin-left:-6px;margin-top:-16px}.badge.badge-ignore{background-color:#e5e5e5;color:#999;text-shadow:none}.badge.badge-info{background-color:#33b7d0}.badge.badge-primary{background-color:#348fe2}.badge.badge-success{background-color:#368142}.badge.badge-warning{background-color:#f57523}.badge.badge-danger{background-color:#d34242}.label{display:inline-block;min-width:8px;padding:5px 10px;border-radius:5px;text-align:center;white-space:nowrap;vertical-align:middle;font-size:13px;font-weight:400;line-height:1em;background-color:#888;color:#fff;text-shadow:1px 1px 0 #525252}.label.label-plain{text-shadow:none}.label.label-sm{font-size:11px;padding:3px 8px;margin-top:0;border-radius:5px;text-shadow:none}.label.label-ignore{background-color:#e5e5e5;color:#999;text-shadow:none}.label.label-info{background-color:#33b7d0}.label.label-primary{background-color:#348fe2}.label.label-success{background-color:#368142}.label.label-warning{background-color:#f57523}.label.label-danger{background-color:#d34242}.progress.progress-sm{height:18px;margin-bottom:2px;background-color:#aaa}.progress.progress-sm.button{cursor:pointer}.progress.progress-sm .progress-bar{display:block;font-size:11px;float:none}.alert-sm{padding:5px;margin-bottom:10px}.modal-dialog-sm .modal-header{padding:10px}.modal-dialog-sm .modal-body{padding:10px}.modal-dialog-sm .modal-footer{padding:10px}.modal-dialog-sm .form-horizontal .form-group{margin-right:-5px;margin-left:-5px}.modal-dialog-sm .col-sm-1,.modal-dialog-sm .col-sm-2,.modal-dialog-sm .col-sm-3,.modal-dialog-sm .col-sm-4,.modal-dialog-sm .col-sm-5,.modal-dialog-sm .col-sm-6,.modal-dialog-sm .col-sm-7,.modal-dialog-sm .col-sm-8,.modal-dialog-sm .col-sm-9,.modal-dialog-sm .col-sm-10,.modal-dialog-sm .col-sm-11{padding-right:5px;padding-left:5px}.btn-single-line{white-space:nowrap}.btn-single-line .btn:first-child{border-top-left-radius:3px;border-bottom-left-radius:3px}.btn-single-line .btn:last-child{border-top-right-radius:3px;border-bottom-right-radius:3px}.remote-action-group{margin-bottom:3px;height:28px;min-width:390px}.remote-action-group ul{display:block;height:28px;margin:0;padding:0}.remote-action-group ul 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}.remote-action-group ul li.remote-action-btn{background:none;padding:0;border:none}.remote-action-group ul li.remote-action-input{background:none;padding:4px 0}.remote-action-group ul li.remote-action-input select{border:none}.remote-action-group ul li.remote-action-chk-protocol{width:86px}.remote-action-group ul li.remote-action-username,.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-protocol{width:96px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.remote-action-group ul li.remote-action-username{font-size:90%;color:#999}.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-protocol,.remote-action-group ul li.remote-action-chk-protocol{color:#000}.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-chk-protocol{font-weight:bold}.remote-action-group ul li.remote-action-password,.remote-action-group ul li.remote-action-sshkey,.remote-action-group ul li.remote-action-noauth{text-align:center;padding:4px 8px;width:45px}.remote-action-group ul li.remote-action-password{background-color:#e3ffe3;color:#999}.remote-action-group ul li.remote-action-sshkey{background-color:#fbe9c8;color:#666}.remote-action-group ul li.remote-action-noauth{background-color:#e0e0e0;color:#666}.remote-action-group ul li .btn{line-height:1.5;margin:0;padding:4px 8px;font-size:12px;border-radius:0}.remote-action-group ul li label{padding:0;display:block;float:left;margin-top:1px;cursor:pointer}.remote-action-group ul li input[type=checkbox]{display:block;float:left;margin:3px 5px 0 0}.remote-action-group ul li select{margin-top:-3px}.remote-action-group ul li:first-child{border-left:1px solid #ccc;border-top-left-radius:4px;border-bottom-left-radius:4px}.remote-action-group ul li:first-child .btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.remote-action-group ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.remote-action-group ul li:last-child .btn{border-top-right-radius:4px;border-bottom-right-radius:4px}hr.small{margin:5px 0}.dlg-protocol-group{margin-bottom:3px}.dlg-protocol-group ul{display:block;height:28px;margin:0;padding:0}.dlg-protocol-group ul 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}.dlg-protocol-group ul li.item-name{width:120px}.dlg-protocol-group ul li.item-btn{background:none;padding:0;border:none}.dlg-protocol-group ul li.item-input{background:none;border:none;padding:0}.dlg-protocol-group ul li .form-control{line-height:1.5;margin:0;padding:4px 5px;font-size:12px;height:28px;border-radius:0;border-left:none;width:100px}.dlg-protocol-group ul li label{padding:0;display:block;float:left;margin-top:1px;cursor:pointer}.dlg-protocol-group ul li input[type=checkbox]{display:block;float:left;margin:3px 5px 0 0}.dlg-protocol-group ul li:first-child{border-left:1px solid #ccc;border-top-left-radius:4px;border-bottom-left-radius:4px}.dlg-protocol-group ul li:first-child .btn,.dlg-protocol-group ul li:first-child .form-control{border-top-left-radius:4px;border-bottom-left-radius:4px}.dlg-protocol-group ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.dlg-protocol-group ul li:last-child .btn,.dlg-protocol-group ul li:last-child .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}.form-group-sm .form-control-static{padding:6px 0}.input-group :-moz-placeholder{color:#d2d2d2}.input-group ::-moz-placeholder{color:#d2d2d2}.input-group input:-ms-input-placeholder,.input-group textarea:-ms-input-placeholder{color:#d2d2d2}.input-group input::-webkit-input-placeholder,.input-group textarea::-webkit-input-placeholder{color:#d2d2d2}.page-header-fixed{padding-top:48px}.header{border:none;min-height:48px;height:48px;top:0;width:100%;position:fixed;z-index:999}.header .top-navbar{min-height:48px;height:48px;line-height:48px;background-color:#3a3a3a;color:#ccc}.header .top-navbar a{color:#d5d5d5}.header .top-navbar a:hover{color:#5a8fee}.header .top-navbar .brand{float:left;display:inline-block;padding:12px 0;margin:0}.header .top-navbar .brand .site-logo{display:block;width:86px;height:24px;background:url(../img/site-logo-small.png) no-repeat}.header .top-navbar .title-container{float:left;display:inline-block;margin:0;padding:0}.header .top-navbar .title-container .title{font-size:18px}.header .top-navbar .status-container{float:right}.footer{width:100%;height:36px;line-height:36px;background-color:#d5d5d5;border-top:1px solid #a2a2a2;border-bottom:1px solid #efefef;z-index:998;text-align:center}.footer.footer-fixed-bottom{bottom:0;position:fixed}.row-sm .col-sm-1,.row-sm .col-sm-2,.row-sm .col-sm-3,.row-sm .col-sm-4,.row-sm .col-sm-5,.row-sm .col-sm-6,.row-sm .col-sm-7,.row-sm .col-sm-8,.row-sm .col-sm-9,.row-sm .col-sm-10,.row-sm .col-sm-11{padding-right:5px;padding-left:5px}.sidebar{background-color:#fff;width:285px;position:fixed}.search-box{padding:10px;border-bottom:1px solid #eee;margin-bottom:10px}.tree-view{overflow-x:auto;overflow-y:auto;padding:0 10px}.content{margin-top:15px;margin-bottom:56px;min-height:360px;background-color:#fff;border-radius:5px;padding:10px;margin-left:300px}.jstree-ocl{cursor:default}.jstree-default .fa-folder:before{color:#f59c1a}.jstree-default .jstree-wholerow{cursor:default}.jstree-default>ul>li{padding:4px 0}.jstree-default .jstree-node{margin-left:12px}.jstree-default .jstree-open>.jstree-anchor>.fa-folder:before{content:'\f07c';color:#a26307}.jstree-default>.jstree-no-dots .jstree-closed>.jstree-ocl,.jstree-default>.jstree-no-dots .jstree-open>.jstree-ocl{background:none}.jstree-default>.jstree-no-dots .jstree-closed>.jstree-ocl:before,.jstree-default>.jstree-no-dots .jstree-open>.jstree-ocl:before{font-style:normal;font-family:"FontAwesome"}.jstree-default>.jstree-no-dots .jstree-closed>.jstree-ocl:before{content:'\f054';color:#ccc}.jstree-default>.jstree-no-dots .jstree-open>.jstree-ocl:before{content:'\f078';color:#666}.jstree-default>.jstree-no-dots .jstree-loading>.jstree-ocl{background:url("img/loading_01.gif") center center no-repeat}.jstree-default>.jstree-no-dots .jstree-loading>.jstree-ocl:before{content:''}#gritter-notice-wrapper{z-index:9999}.gritter-bottom,.gritter-item,.gritter-top{background:rgba(0,0,0,0.8) !important}.gritter-top{border-top-left-radius:3px;border-top-right-radius:3px}.gritter-bottom{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.gritter-close,.gritter-light .gritter-close{left:auto !important;right:5px !important;top:5px !important;width:16px !important;height:16px !important;line-height:16px !important;display:block !important;border-radius:50%}.gritter-close:before,.gritter-light .gritter-close:before{content:'\f00d' !important;font-family:FontAwesome !important;font-size:9px !important;width:16px !important;height:16px !important;line-height:16px !important;color:#fff !important;text-indent:0 !important;position:absolute !important;text-align:center !important;right:0 !important;top:0 !important}.gritter-title{font-size:13px !important;line-height:16px !important;padding-bottom:5px !important;font-weight:400 !important;color:#fff !important;text-shadow:none !important}.gritter-item{color:#aaa !important;font-size:13px !important;padding:2px 15px 5px !important}.gritter-error .gritter-bottom,.gritter-error .gritter-item,.gritter-error .gritter-top{background:rgba(123,32,32,0.9) !important}.gritter-error .gritter-title{color:#fff !important}.gritter-error .gritter-item{color:#ddd !important}.gritter-error .gritter-close{left:auto !important;right:5px !important;top:5px !important;width:16px !important;height:16px !important;line-height:16px !important;display:block !important;border-radius:50%;background:#e33b3b !important}.gritter-success .gritter-bottom,.gritter-success .gritter-item,.gritter-success .gritter-top{background:rgba(1,65,16,0.9) !important}.gritter-success .gritter-title{color:#ddd !important}.gritter-success .gritter-item{color:#ccc !important}.gritter-success .gritter-close{background:#0eb320 !important} \ No newline at end of file diff --git a/server/www/teleport/static/css/main.css b/server/www/teleport/static/css/main.css index 0a232d3..270a3eb 100644 --- a/server/www/teleport/static/css/main.css +++ b/server/www/teleport/static/css/main.css @@ -1 +1 @@ -@charset "utf-8";body{font-family:"Open Sans","Helvetica Neue","Microsoft YaHei","寰蒋闆呴粦",Helvetica,Arial,sans-serif;font-size:13px;background-color:#e9e9e9;color:#333}html,body{height:100%}#page-container{min-width:1260px}a{text-decoration:none}a:link{text-decoration:none}a:hover{text-decoration:none}a:active{text-decoration:none}a:visited{text-decoration:none}select{outline:none}label{font-weight:normal}.clear-float{clear:both}.bigger{font-size:120%}.normal-text{font-size:13px;color:#333}.mono{font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}hr.hr-sm{margin-top:5px;margin-bottom:5px}.btn-group-sm>.btn,.btn-sm{padding:2px 5px}.btn.btn-sm{padding:3px 8px}.btn.btn-icon{padding:3px 6px}.btn.btn-icon.btn-sm{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}.pop-menu-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040}.form-group{margin-bottom:5px}.badge{display:inline-block;min-width:8px;padding:5px 10px;border-radius:10px;text-align:center;white-space:nowrap;vertical-align:middle;font-size:13px;font-weight:400;line-height:1em;background-color:#888;color:#fff;text-shadow:1px 1px 0 #525252}.badge.badge-plain{text-shadow:none}.badge.badge-sm{font-size:11px;padding:3px 6px;margin-top:0;border-radius:8px;text-shadow:none}.badge.badge-sup{margin-left:-6px;margin-top:-16px}.badge.badge-ignore{background-color:#e5e5e5;color:#999;text-shadow:none}.badge.badge-info{background-color:#33b7d0}.badge.badge-primary{background-color:#348fe2}.badge.badge-success{background-color:#368142}.badge.badge-warning{background-color:#f57523}.badge.badge-danger{background-color:#d34242}.label{display:inline-block;min-width:8px;padding:5px 10px;border-radius:5px;text-align:center;white-space:nowrap;vertical-align:middle;font-size:13px;font-weight:400;line-height:1em;background-color:#888;color:#fff;text-shadow:1px 1px 0 #525252}.label.label-plain{text-shadow:none}.label.label-sm{font-size:11px;padding:3px 8px;margin-top:0;border-radius:5px;text-shadow:none}.label.label-ignore{background-color:#e5e5e5;color:#999;text-shadow:none}.label.label-info{background-color:#33b7d0}.label.label-primary{background-color:#348fe2}.label.label-success{background-color:#368142}.label.label-warning{background-color:#f57523}.label.label-danger{background-color:#d34242}.progress.progress-sm{height:18px;margin-bottom:2px;background-color:#aaa}.progress.progress-sm.button{cursor:pointer}.progress.progress-sm .progress-bar{display:block;font-size:11px;float:none}.alert-sm{padding:5px;margin-bottom:10px}.modal-dialog-sm .modal-header{padding:10px}.modal-dialog-sm .modal-body{padding:10px}.modal-dialog-sm .modal-footer{padding:10px}.modal-dialog-sm .form-horizontal .form-group{margin-right:-5px;margin-left:-5px}.modal-dialog-sm .col-sm-1,.modal-dialog-sm .col-sm-2,.modal-dialog-sm .col-sm-3,.modal-dialog-sm .col-sm-4,.modal-dialog-sm .col-sm-5,.modal-dialog-sm .col-sm-6,.modal-dialog-sm .col-sm-7,.modal-dialog-sm .col-sm-8,.modal-dialog-sm .col-sm-9,.modal-dialog-sm .col-sm-10,.modal-dialog-sm .col-sm-11{padding-right:5px;padding-left:5px}.btn-single-line{white-space:nowrap}.btn-single-line .btn:first-child{border-top-left-radius:3px;border-bottom-left-radius:3px}.btn-single-line .btn:last-child{border-top-right-radius:3px;border-bottom-right-radius:3px}.remote-action-group{margin-bottom:3px;height:28px;min-width:390px}.remote-action-group ul{display:block;height:28px;margin:0;padding:0}.remote-action-group ul 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}.remote-action-group ul li.remote-action-btn{background:none;padding:0;border:none}.remote-action-group ul li.remote-action-input{background:none;padding:4px 0}.remote-action-group ul li.remote-action-input select{border:none}.remote-action-group ul li.remote-action-chk-protocol{width:86px}.remote-action-group ul li.remote-action-username,.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-protocol{width:96px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.remote-action-group ul li.remote-action-username{font-size:90%;color:#999}.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-protocol,.remote-action-group ul li.remote-action-chk-protocol{color:#000}.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-chk-protocol{font-weight:bold}.remote-action-group ul li.remote-action-password,.remote-action-group ul li.remote-action-sshkey,.remote-action-group ul li.remote-action-noauth{text-align:center;padding:4px 8px;width:45px}.remote-action-group ul li.remote-action-password{background-color:#e3ffe3;color:#999}.remote-action-group ul li.remote-action-sshkey{background-color:#fbe9c8;color:#666}.remote-action-group ul li.remote-action-noauth{background-color:#e0e0e0;color:#666}.remote-action-group ul li .btn{line-height:1.5;margin:0;padding:4px 8px;font-size:12px;border-radius:0}.remote-action-group ul li label{padding:0;display:block;float:left;margin-top:1px;cursor:pointer}.remote-action-group ul li input[type=checkbox]{display:block;float:left;margin:3px 5px 0 0}.remote-action-group ul li select{margin-top:-3px}.remote-action-group ul li:first-child{border-left:1px solid #ccc;border-top-left-radius:4px;border-bottom-left-radius:4px}.remote-action-group ul li:first-child .btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.remote-action-group ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.remote-action-group ul li:last-child .btn{border-top-right-radius:4px;border-bottom-right-radius:4px}hr.small{margin:5px 0}.dlg-protocol-group{margin-bottom:3px}.dlg-protocol-group ul{display:block;height:28px;margin:0;padding:0}.dlg-protocol-group ul 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}.dlg-protocol-group ul li.item-name{width:120px}.dlg-protocol-group ul li.item-btn{background:none;padding:0;border:none}.dlg-protocol-group ul li.item-input{background:none;border:none;padding:0}.dlg-protocol-group ul li .form-control{line-height:1.5;margin:0;padding:4px 5px;font-size:12px;height:28px;border-radius:0;border-left:none;width:100px}.dlg-protocol-group ul li label{padding:0;display:block;float:left;margin-top:1px;cursor:pointer}.dlg-protocol-group ul li input[type=checkbox]{display:block;float:left;margin:3px 5px 0 0}.dlg-protocol-group ul li:first-child{border-left:1px solid #ccc;border-top-left-radius:4px;border-bottom-left-radius:4px}.dlg-protocol-group ul li:first-child .btn,.dlg-protocol-group ul li:first-child .form-control{border-top-left-radius:4px;border-bottom-left-radius:4px}.dlg-protocol-group ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.dlg-protocol-group ul li:last-child .btn,.dlg-protocol-group ul li:last-child .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}.form-group-sm .form-control-static{padding:6px 0}.table{margin-bottom:10px}.table>thead>tr>th{padding:5px 5px;outline:none;white-space:nowrap;font-weight:normal;text-align:center;background-color:#ededed}.table>tbody>tr>td{padding:5px;text-align:center;vertical-align:middle}.table>tbody>tr>td .nowrap{white-space:nowrap}.table.table-data thead .sorting,.table.table-data thead .sorting_asc,.table.table-data thead .sorting_desc{cursor:pointer;position:relative}.table.table-data thead .sorting>span:after,.table.table-data thead .sorting_asc>span:after,.table.table-data thead .sorting_desc>span:after{bottom:4px;padding-left:5px;display:inline-block;font-family:'FontAwesome';opacity:.8}.table.table-data thead .sorting>span:after{opacity:.2;content:"\f0dc"}.table.table-data thead .sorting_asc>span:after{content:"\f0de"}.table.table-data thead .sorting_desc>span:after{content:"\f0dd"}.host-id{display:block;font-size:16px;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace;color:#333}.host-id.not-active{font-size:14px;font-weight:400;color:#999}.host-desc{font-size:12px;color:#999;display:inline-block;white-space:nowrap;width:160px;overflow:hidden;text-overflow:ellipsis}a.host-desc:hover:before{display:inline-block;padding-right:3px;line-height:12px;content:"\f040";font-family:'FontAwesome'}.td-ip-list{padding-right:20px;padding-left:5px}.td-ip-show-more{font-size:14px;width:12px;float:right;display:block}.td-ip-item{min-width:12em;width:12em;height:18px;padding:2px 4px;margin:1px 0;color:#333;text-align:center;white-space:nowrap;border-radius:9px;line-height:11px;font-size:11px;background:#dfdfdf !important}.td-ip-item span{display:inline-block;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace;font-size:11px;font-weight:400}.td-ip-item a{display:inline-block;width:14px;float:right;font-size:14px}.admin{background-color:#930;color:#fff;padding:5px 15px;border-radius:5px}.page-header-fixed{padding-top:48px}.header{border:none;box-shadow:0 0 3px rgba(0,0,0,0.5)}.header .container-fluid{padding-left:0}.header .breadcrumb-container{display:inline-block;padding-top:6px}.header .breadcrumb{background-color:transparent;padding-left:20px;font-size:16px}.header.navbar{min-height:48px;height:48px;margin:0}.header.navbar .brand{display:inline-block;float:left;width:180px;height:48px;padding:12px 0 0;text-align:center;margin:0 auto;background-color:#3a3a3a}.header.navbar .brand .navbar-logo{display:inline-block;width:93px;height:30px;background:url(../img/site-logo-small.png) no-repeat}.header.navbar .breadcrumb>li+li:before{font-size:18px;padding:0 5px;color:#ccc;content:"\f105";font-family:'FontAwesome'}.page-sidebar-fixed .sidebar{position:fixed}.sidebar{top:0;bottom:0;left:0;width:180px;padding-top:48px;z-index:1010;background-color:#3a3a3a}.sidebar .nav-menu>li>a{padding:8px 0 8px 20px;line-height:24px;font-size:13px;color:#c2c2c2;border-left:5px solid #3a3a3a}.sidebar .nav-menu>li>a:focus{background-color:#3a3a3a;border-left:5px solid #3a3a3a}.sidebar .nav-menu>li>a:hover{background-color:#2d2d2d;border-left:5px solid #005c74}.sidebar .nav-menu>li>a.active{color:#fff;background-color:#0084a7;border-left:5px solid #0084a7}.sidebar .nav-menu>li>a.active:hover{border-left:5px solid #00acda}.sidebar .nav-menu>li>a.active:after{content:"\e251";font-family:'Glyphicons Halflings';position:relative;top:1px;display:inline-block;font-style:normal;font-weight:400;float:right;color:#e9e9e9;font-size:20px;line-height:24px;margin-right:-6px}.sidebar .nav-menu li .menu-caret:after{display:inline-block;width:12px;height:12px;margin-left:5px;top:1px;position:relative;border:none;font-family:'FontAwesome';font-style:normal}.sidebar .nav-menu li .menu-caret:after{content:'\f0da'}.sidebar .nav-menu li.expand .menu-caret:after{content:'\f0d7'}.sidebar .nav-menu>li.super-admin>a:hover{background-color:#620;border-left:5px solid #4d1a00}.sidebar .nav-menu>li.super-admin>a.active{background-color:#930;border-left:5px solid #930}.sidebar .nav-menu>li.super-admin>a.active:hover{border-left:5px solid #c40}.sidebar .nav-menu>li>a>i.icon{float:left;margin-top:1px;margin-right:15px;text-align:center;line-height:24px;font-size:14px}.sidebar .sub-menu{padding:0;margin:0;background-color:#292929;position:relative;list-style-type:none;border-top:1px solid #202020;border-bottom:1px solid #464646}.sidebar .sub-menu>li>a{padding:8px 0 8px 40px;line-height:20px;font-size:13px;display:block;position:relative;color:#889097;border-left:5px solid #292929}.sidebar .sub-menu>li>a:before{display:inline-block;padding-right:8px;line-height:20px;content:"\f105";font-family:'FontAwesome'}.sidebar .sub-menu>li>a:hover{color:#fff;border-left:5px solid #005c74}.sidebar .sub-menu>li>a.active{color:#fff;background-color:#0084a7;border-left:5px solid #0084a7}.sidebar .sub-menu>li>a.active:hover{border-left:5px solid #00acda}.sidebar .sub-menu>li>a.active:after{content:"\e251";font-family:'Glyphicons Halflings';position:relative;top:-2px;display:inline-block;font-style:normal;font-weight:400;float:right;color:#e9e9e9;font-size:20px;line-height:24px;margin-right:-6px}.sidebar .nav-profile{padding:15px 10px;color:#ccc;background-color:#333;border-bottom:1px solid #464646}.sidebar .nav-profile a.title{color:#ccc}.sidebar .nav-profile a.title:hover{color:#fff;background-color:transparent}.sidebar .nav-profile a.title:focus{background-color:transparent}.sidebar .nav-profile .image{float:left;margin-top:3px;font-size:24px;color:#69f;width:36px;height:36px;border-radius:5px;background-color:#eee;text-align:center;margin-right:10px;overflow:hidden}.sidebar .nav-profile .image img{margin-top:-3px}.sidebar .nav-profile .name{display:block;font-size:16px}.sidebar .nav-profile .role{display:block;font-size:12px;color:#999}.sidebar .nav-profile .dropdown-menu{font-size:13px}.sidebar .nav-profile .dropdown-menu>li>a{padding:5px 20px}.sidebar .nav-profile .dropdown-menu>li>a:hover{background-color:#ccc}.sidebar .nav-profile .dropdown-menu .divider{margin:5px 0}.sidebar .badge{margin-top:-10px;margin-left:5px}.content{margin-left:180px}.page-content{padding:15px}.page-content-dashboard{padding:20px 25px}.widget{overflow:hidden;border-radius:3px;padding:15px;margin-bottom:20px;color:#fff}.widget.widget-stats{position:relative}.widget .stats-icon{font-size:52px;top:12px;right:21px;width:56px;height:56px;text-align:center;line-height:56px;margin-left:15px;color:#fff;position:absolute;opacity:.2}.widget .stats-title{color:#fff;color:rgba(255,255,255,0.6)}.widget .stats-split{height:2px;margin:0 -15px 10px;background:rgba(0,0,0,0.2)}.widget .stats-content{font-size:24px;font-weight:300;margin-bottom:10px}.widget .stats-desc{display:inline-block;color:#fff;color:rgba(255,255,255,0.6)}.widget .stats-action{display:inline-block;float:right}.widget a{color:#eee;color:rgba(255,255,255,0.7)}.widget a:hover{color:#fff}.widget.widget-info{background-color:#33b7d0}.widget.widget-primary{background-color:#348fe2}.widget.widget-success{background-color:#368142}.widget.widget-warning{background-color:#f57523}.widget.widget-danger{background-color:#d34242}.panel{border:none;box-shadow:none;border-radius:3px}.panel .panel-heading{padding:6px 15px;color:#fff}.panel .panel-heading .panel-title{font-size:14px}.panel .panel-heading .panel-heading-btn{float:right}.panel .panel-heading .panel-heading-btn .btn{display:inline-block;padding:0;border:none;text-align:center}.panel .panel-heading .panel-heading-btn .btn.btn-xs{width:18px;height:18px;line-height:18px;font-size:12px}.panel .panel-heading .panel-heading-btn .btn.btn-circle{border-radius:50%}.place-holder-h200{width:100%;height:300px;background-color:#eee;border:1px solid #ccc;line-height:200px;text-align:center}.dashboard-panel2-holder{width:100%;height:1150px;background-color:#eee;border:1px solid #ccc;line-height:200px;text-align:center}.dashboard-panel-time{color:#cecece}.box{border:none;box-shadow:none;border-radius:3px;background-color:#fff;padding:15px;margin-bottom:15px}.box-fluid{border:none;box-shadow:none;border-radius:3px;margin-bottom:15px}.box.box-sm,.box-fluid.box-sm{padding:5px 15px}.box .box-title,.box-fluid .box-title{margin-bottom:10px}.box .box-title .title,.box-fluid .box-title .title{display:inline-block;font-size:18px;color:#333;height:30px;line-height:30px}.box .box-title .btn-sm,.box-fluid .box-title .btn-sm{padding:3px 8px;margin-top:-5px}.box .nav-tabs,.box-fluid .nav-tabs{font-size:14px;font-weight:bold}.box .nav-tabs>li:first-child,.box-fluid .nav-tabs>li:first-child{margin-left:50px}.box .tab-content>.tab-pane,.box-fluid .tab-content>.tab-pane{background-color:#fff;padding:20px;border:1px solid #ddd;border-top:none;border-bottom-left-radius:3px;border-bottom-right-radius:3px}.box-license{line-height:30px}.box-license .breadcrumb{padding:0;margin:0;border-radius:0;background-color:transparent}.box-btn-bar{line-height:30px}.box-btn-bar a.btn{margin-right:20px}.page-nav{height:30px;line-height:30px}.page-nav .breadcrumb{padding:0;margin:0;border-radius:0;background-color:transparent}.page-nav .pagination{margin:0 0}.page-nav .btn{margin-top:-3px}.page-filter{height:36px;line-height:36px;margin-bottom:10px}.page-filter .form-control{margin-top:5px;margin-right:4px}.btn.btn-sm .dropdown-menu li a{font-size:11px}.invite{text-align:center;padding-bottom:20px}.invite .code{color:#2f3991;font-size:36px;font-weight:700;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}.invite .link{padding:5px;color:#2f3991;font-size:13px;font-weight:700;background-color:#eee;border-radius:5px}.invite-send-box{width:300px;margin:0 auto}.form-group .input-group{margin-bottom:5px}.op_box{display:block;padding:5px;border-radius:3px;text-align:center;margin-top:5px}.op_error{background:#fbb}.op_wait{background:#ccc}.table-data td.loading{text-align:left;padding:20px}.table-data .btn-group.open .dropdown-toggle{-webkit-box-shadow:none;box-shadow:none}.more-action{position:absolute !important}.more-action .dropdown-menu{background-color:rgba(60,60,60,0.9);color:#fff;font-size:13px}.more-action .dropdown-menu.dropdown-menu-left{margin-left:-120px}.more-action .dropdown-menu>li>a{padding:5px 20px;color:#fff}.more-action .dropdown-menu>li>a:hover,.more-action .dropdown-menu>li>a:active,.more-action .dropdown-menu>li>a:visited{background-color:#0084a7}.more-action .dropdown-menu .divider{margin:5px 0;background-color:#666}.popover-inline-edit input,.popover-inline-edit .btn{height:30px}.popover-inline-edit .popover-title{background-color:#ddd}.popover-inline-edit .popover-content{padding:20px 10px}.popover-inline-edit .popover{padding:0}.popover-inline-edit .popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#ddd}.user-info-table{font-size:14px}.user-info-table tbody>tr>td{padding:8px}.user-info-table .user-field{min-width:100px;width:100px;color:#999;text-align:right}.user-info-table .user-value{color:#333;font-weight:bold}.user-info-table .user-value a{font-weight:normal}.breadcrumb.breadcrumb-trans{background-color:transparent}.biz-box{display:inline-block;width:20%;max-width:20%}.biz-box .bb-inner{background-color:#368142;margin:3px;border-radius:4px}.biz-box .bb-name{color:#fff;padding:9px;margin-bottom:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center;padding-top:1px;padding-bottom:1px}.biz-box .bb-ver{font-size:11px;height:16px;text-align:center;padding:0 5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-bottom-left-radius:4px;border-bottom-right-radius:4px;color:rgba(255,255,255,0.85);background-color:rgba(0,0,0,0.3)}.biz-box.mp-disabled .mp-inner{background-color:#e5e5e5}.biz-box.mp-disabled .mp-name{color:#999}.biz-box.mp-success .mp-inner{background-color:#368142}.biz-box.mp-success .mp-name{color:#fff}.biz-box.mp-danger .mp-inner{background-color:#d34242}.biz-box.mp-danger .mp-name{color:#fff}.biz-box.mp-warning .mp-inner{background-color:#f57523}.biz-box.mp-warning .mp-name{color:#fff}textarea.textarea-resize-y{resize:vertical}textarea.textarea-resize-none{resize:none}textarea.textarea-code{font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}textarea.cert_pub{width:100%;height:64px;border:1px solid #e2e2e2;background-color:#e4ffe5}.icon{display:inline-block}.icon16{width:16px;height:16px;line-height:16px;margin-right:3px;margin-top:0 !important;margin-bottom:-2px !important;vertical-align:top;background-image:url("img/desktop/mimetype-16.png") !important}.icon16.icon-disk{background-position:0 0 !important}.icon16.icon-folder{background-position:-16px 0 !important}.icon16.icon-file{background-position:0 -16px !important}.icon16.icon-txt{background-position:-16px -16px !important}.icon16.icon-help{background-position:-32px -16px !important}.icon16.icon-sys{background-position:-48px -16px !important}.icon16.icon-exe{background-position:-64px -16px !important}.icon16.icon-office{background-position:0 -32px !important}.icon16.icon-word{background-position:-16px -32px !important}.icon16.icon-excel{background-position:-32px -32px !important}.icon16.icon-ppt{background-position:-48px -32px !important}.icon16.icon-access{background-position:-64px -32px !important}.icon16.icon-visio{background-position:-80px -32px !important}.icon16.icon-audio{background-position:0 -48px !important}.icon16.icon-video{background-position:-16px -48px !important}.icon16.icon-pic{background-position:-32px -48px !important}.icon16.icon-pdf{background-position:-48px -48px !important}.icon16.icon-font{background-position:-64px -48px !important}.icon16.icon-script{background-position:0 -64px !important}.icon16.icon-html{background-position:-16px -64px !important}.icon16.icon-py{background-position:-32px -64px !important}.icon16.icon-h{background-position:-48px -64px !important}.icon16.icon-c{background-position:-64px -64px !important}.icon16.icon-cpp{background-position:-80px -64px !important}.icon16.icon-cs{background-position:-96px -64px !important}.icon16.icon-php{background-position:-112px -64px !important}.icon16.icon-ruby{background-position:-128px -64px !important}.icon16.icon-java{background-position:-144px -64px !important}.icon16.icon-vs{background-position:-160px -64px !important}.icon16.icon-js{background-position:-176px -64px !important}.icon16.icon-archive{background-position:0 -80px !important}.icon16.icon-rar{background-position:-16px -80px !important}.icon16.icon-zip{background-position:-32px -80px !important}.icon16.icon-7z{background-position:-48px -80px !important}.icon16.icon-tar{background-position:-64px -80px !important}.icon16.icon-gz{background-position:-80px -80px !important}.icon16.icon-jar{background-position:-96px -80px !important}.icon16.icon-bz2{background-position:-112px -80px !important}.icon24{width:24px;height:24px;line-height:24px;margin-right:3px;margin-top:0 !important;margin-bottom:-2px !important;vertical-align:top;background-image:url("img/desktop/icons-tree-24x24.png") !important}.icon24.icon-disk{background-position:0 0 !important}.icon24.icon-folder{background-position:-24px 0 !important}.icon24.icon-folder-open{background-position:-48px 0 !important}.os-icon-windows:after{color:#00bcf6;content:"\f17a";font-size:18px;width:24px;height:24px;line-height:24px;display:inline-block;font-family:'FontAwesome'}.os-icon-linux:after{color:#fff;content:"\f17c";font-size:18px;width:24px;height:24px;line-height:24px;background-color:#333;border-radius:50%;display:inline-block;font-family:'FontAwesome'}.os-icon-macos:after{color:#a7a7a7;content:"\f179";font-size:20px;width:24px;height:24px;line-height:24px;display:inline-block;font-family:'FontAwesome'}.os-icon-ubuntu:after,.os-icon-debian:after,.os-icon-centos:after,.os-icon-redhat:after{content:" ";width:24px;height:24px;line-height:24px;display:inline-block}.os-icon-ubuntu:after{background:url(../img/os-icon/ubuntu-24x24.png) no-repeat}.os-icon-debian:after{background:url(../img/os-icon/debian-24x24.png) no-repeat}.os-icon-centos:after{background:url(../img/os-icon/centos-24x24.png) no-repeat}.os-icon-redhat:after{background:url(../img/os-icon/redhat-24x24.png) no-repeat}#gritter-notice-wrapper{z-index:9999}.gritter-bottom,.gritter-item,.gritter-top{background:rgba(0,0,0,0.8) !important}.gritter-top{border-top-left-radius:3px;border-top-right-radius:3px}.gritter-bottom{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.gritter-close,.gritter-light .gritter-close{left:auto !important;right:5px !important;top:5px !important;width:16px !important;height:16px !important;line-height:16px !important;display:block !important;border-radius:50%}.gritter-close:before,.gritter-light .gritter-close:before{content:'\f00d' !important;font-family:FontAwesome !important;font-size:9px !important;width:16px !important;height:16px !important;line-height:16px !important;color:#fff !important;text-indent:0 !important;position:absolute !important;text-align:center !important;right:0 !important;top:0 !important}.gritter-title{font-size:13px !important;line-height:16px !important;padding-bottom:5px !important;font-weight:400 !important;color:#fff !important;text-shadow:none !important}.gritter-item{color:#aaa !important;font-size:13px !important;padding:2px 15px 5px !important}.gritter-error .gritter-bottom,.gritter-error .gritter-item,.gritter-error .gritter-top{background:rgba(123,32,32,0.9) !important}.gritter-error .gritter-title{color:#fff !important}.gritter-error .gritter-item{color:#ddd !important}.gritter-error .gritter-close{left:auto !important;right:5px !important;top:5px !important;width:16px !important;height:16px !important;line-height:16px !important;display:block !important;border-radius:50%;background:#e33b3b !important}.gritter-success .gritter-bottom,.gritter-success .gritter-item,.gritter-success .gritter-top{background:rgba(1,65,16,0.9) !important}.gritter-success .gritter-title{color:#ddd !important}.gritter-success .gritter-item{color:#ccc !important}.gritter-success .gritter-close{background:#0eb320 !important} \ No newline at end of file +@charset "utf-8";body{font-family:"Open Sans","Helvetica Neue","Microsoft YaHei","寰蒋闆呴粦",Helvetica,Arial,sans-serif;font-size:13px;background-color:#e9e9e9;color:#333}html,body{height:100%}#page-container{min-width:1260px}a{text-decoration:none}a:link{text-decoration:none}a:hover{text-decoration:none}a:active{text-decoration:none}a:visited{text-decoration:none}select{outline:none}label{font-weight:normal}.clear-float{clear:both}.bigger{font-size:120%}.normal-text{font-size:13px;color:#333}.mono{font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}hr.hr-sm{margin-top:5px;margin-bottom:5px}.btn-group-sm>.btn,.btn-sm{padding:2px 5px}.btn.btn-sm{padding:3px 8px}.btn.btn-icon{padding:3px 6px}.btn.btn-icon.btn-sm{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}.pop-menu-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040}.form-group{margin-bottom:5px}.badge{display:inline-block;min-width:8px;padding:5px 10px;border-radius:10px;text-align:center;white-space:nowrap;vertical-align:middle;font-size:13px;font-weight:400;line-height:1em;background-color:#888;color:#fff;text-shadow:1px 1px 0 #525252}.badge.badge-plain{text-shadow:none}.badge.badge-sm{font-size:11px;padding:3px 6px;margin-top:0;border-radius:8px;text-shadow:none}.badge.badge-sup{margin-left:-6px;margin-top:-16px}.badge.badge-ignore{background-color:#e5e5e5;color:#999;text-shadow:none}.badge.badge-info{background-color:#33b7d0}.badge.badge-primary{background-color:#348fe2}.badge.badge-success{background-color:#368142}.badge.badge-warning{background-color:#f57523}.badge.badge-danger{background-color:#d34242}.label{display:inline-block;min-width:8px;padding:5px 10px;border-radius:5px;text-align:center;white-space:nowrap;vertical-align:middle;font-size:13px;font-weight:400;line-height:1em;background-color:#888;color:#fff;text-shadow:1px 1px 0 #525252}.label.label-plain{text-shadow:none}.label.label-sm{font-size:11px;padding:3px 8px;margin-top:0;border-radius:5px;text-shadow:none}.label.label-ignore{background-color:#e5e5e5;color:#999;text-shadow:none}.label.label-info{background-color:#33b7d0}.label.label-primary{background-color:#348fe2}.label.label-success{background-color:#368142}.label.label-warning{background-color:#f57523}.label.label-danger{background-color:#d34242}.progress.progress-sm{height:18px;margin-bottom:2px;background-color:#aaa}.progress.progress-sm.button{cursor:pointer}.progress.progress-sm .progress-bar{display:block;font-size:11px;float:none}.alert-sm{padding:5px;margin-bottom:10px}.modal-dialog-sm .modal-header{padding:10px}.modal-dialog-sm .modal-body{padding:10px}.modal-dialog-sm .modal-footer{padding:10px}.modal-dialog-sm .form-horizontal .form-group{margin-right:-5px;margin-left:-5px}.modal-dialog-sm .col-sm-1,.modal-dialog-sm .col-sm-2,.modal-dialog-sm .col-sm-3,.modal-dialog-sm .col-sm-4,.modal-dialog-sm .col-sm-5,.modal-dialog-sm .col-sm-6,.modal-dialog-sm .col-sm-7,.modal-dialog-sm .col-sm-8,.modal-dialog-sm .col-sm-9,.modal-dialog-sm .col-sm-10,.modal-dialog-sm .col-sm-11{padding-right:5px;padding-left:5px}.btn-single-line{white-space:nowrap}.btn-single-line .btn:first-child{border-top-left-radius:3px;border-bottom-left-radius:3px}.btn-single-line .btn:last-child{border-top-right-radius:3px;border-bottom-right-radius:3px}.remote-action-group{margin-bottom:3px;height:28px;min-width:390px}.remote-action-group ul{display:block;height:28px;margin:0;padding:0}.remote-action-group ul 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}.remote-action-group ul li.remote-action-btn{background:none;padding:0;border:none}.remote-action-group ul li.remote-action-input{background:none;padding:4px 0}.remote-action-group ul li.remote-action-input select{border:none}.remote-action-group ul li.remote-action-chk-protocol{width:86px}.remote-action-group ul li.remote-action-username,.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-protocol{width:96px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.remote-action-group ul li.remote-action-username{font-size:90%;color:#999}.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-protocol,.remote-action-group ul li.remote-action-chk-protocol{color:#000}.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-chk-protocol{font-weight:bold}.remote-action-group ul li.remote-action-password,.remote-action-group ul li.remote-action-sshkey,.remote-action-group ul li.remote-action-noauth{text-align:center;padding:4px 8px;width:45px}.remote-action-group ul li.remote-action-password{background-color:#e3ffe3;color:#999}.remote-action-group ul li.remote-action-sshkey{background-color:#fbe9c8;color:#666}.remote-action-group ul li.remote-action-noauth{background-color:#e0e0e0;color:#666}.remote-action-group ul li .btn{line-height:1.5;margin:0;padding:4px 8px;font-size:12px;border-radius:0}.remote-action-group ul li label{padding:0;display:block;float:left;margin-top:1px;cursor:pointer}.remote-action-group ul li input[type=checkbox]{display:block;float:left;margin:3px 5px 0 0}.remote-action-group ul li select{margin-top:-3px}.remote-action-group ul li:first-child{border-left:1px solid #ccc;border-top-left-radius:4px;border-bottom-left-radius:4px}.remote-action-group ul li:first-child .btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.remote-action-group ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.remote-action-group ul li:last-child .btn{border-top-right-radius:4px;border-bottom-right-radius:4px}hr.small{margin:5px 0}.dlg-protocol-group{margin-bottom:3px}.dlg-protocol-group ul{display:block;height:28px;margin:0;padding:0}.dlg-protocol-group ul 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}.dlg-protocol-group ul li.item-name{width:120px}.dlg-protocol-group ul li.item-btn{background:none;padding:0;border:none}.dlg-protocol-group ul li.item-input{background:none;border:none;padding:0}.dlg-protocol-group ul li .form-control{line-height:1.5;margin:0;padding:4px 5px;font-size:12px;height:28px;border-radius:0;border-left:none;width:100px}.dlg-protocol-group ul li label{padding:0;display:block;float:left;margin-top:1px;cursor:pointer}.dlg-protocol-group ul li input[type=checkbox]{display:block;float:left;margin:3px 5px 0 0}.dlg-protocol-group ul li:first-child{border-left:1px solid #ccc;border-top-left-radius:4px;border-bottom-left-radius:4px}.dlg-protocol-group ul li:first-child .btn,.dlg-protocol-group ul li:first-child .form-control{border-top-left-radius:4px;border-bottom-left-radius:4px}.dlg-protocol-group ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.dlg-protocol-group ul li:last-child .btn,.dlg-protocol-group ul li:last-child .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}.form-group-sm .form-control-static{padding:6px 0}.input-group :-moz-placeholder{color:#d2d2d2}.input-group ::-moz-placeholder{color:#d2d2d2}.input-group input:-ms-input-placeholder,.input-group textarea:-ms-input-placeholder{color:#d2d2d2}.input-group input::-webkit-input-placeholder,.input-group textarea::-webkit-input-placeholder{color:#d2d2d2}.table{margin-bottom:10px}.table>thead>tr>th{padding:5px 5px;outline:none;white-space:nowrap;font-weight:normal;text-align:center;background-color:#ededed}.table>tbody>tr>td{padding:5px;text-align:center;vertical-align:middle}.table>tbody>tr>td .nowrap{white-space:nowrap}.table.table-data thead .sorting,.table.table-data thead .sorting_asc,.table.table-data thead .sorting_desc{cursor:pointer;position:relative}.table.table-data thead .sorting>span:after,.table.table-data thead .sorting_asc>span:after,.table.table-data thead .sorting_desc>span:after{bottom:4px;padding-left:5px;display:inline-block;font-family:'FontAwesome';opacity:.8}.table.table-data thead .sorting>span:after{opacity:.2;content:"\f0dc"}.table.table-data thead .sorting_asc>span:after{content:"\f0de"}.table.table-data thead .sorting_desc>span:after{content:"\f0dd"}.host-id{display:block;font-size:16px;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace;color:#333}.host-id.not-active{font-size:14px;font-weight:400;color:#999}.host-desc{font-size:12px;color:#999;display:inline-block;white-space:nowrap;width:160px;overflow:hidden;text-overflow:ellipsis}a.host-desc:hover:before{display:inline-block;padding-right:3px;line-height:12px;content:"\f040";font-family:'FontAwesome'}.td-ip-list{padding-right:20px;padding-left:5px}.td-ip-show-more{font-size:14px;width:12px;float:right;display:block}.td-ip-item{min-width:12em;width:12em;height:18px;padding:2px 4px;margin:1px 0;color:#333;text-align:center;white-space:nowrap;border-radius:9px;line-height:11px;font-size:11px;background:#dfdfdf !important}.td-ip-item span{display:inline-block;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace;font-size:11px;font-weight:400}.td-ip-item a{display:inline-block;width:14px;float:right;font-size:14px}.admin{background-color:#930;color:#fff;padding:5px 15px;border-radius:5px}.page-header-fixed{padding-top:48px}.header{border:none;box-shadow:0 0 3px rgba(0,0,0,0.5)}.header .container-fluid{padding-left:0}.header .breadcrumb-container{display:inline-block;padding-top:6px}.header .breadcrumb{background-color:transparent;padding-left:20px;font-size:16px}.header.navbar{min-height:48px;height:48px;margin:0}.header.navbar .brand{display:inline-block;float:left;width:180px;height:48px;padding:12px 0 0;text-align:center;margin:0 auto;background-color:#3a3a3a}.header.navbar .brand .navbar-logo{display:inline-block;width:93px;height:30px;background:url(../img/site-logo-small.png) no-repeat}.header.navbar .breadcrumb>li+li:before{font-size:18px;padding:0 5px;color:#ccc;content:"\f105";font-family:'FontAwesome'}.page-sidebar-fixed .sidebar{position:fixed}.sidebar{top:0;bottom:0;left:0;width:180px;padding-top:48px;z-index:1010;background-color:#3a3a3a}.sidebar .nav-menu>li>a{padding:8px 0 8px 20px;line-height:24px;font-size:13px;color:#c2c2c2;border-left:5px solid #3a3a3a}.sidebar .nav-menu>li>a:focus{background-color:#3a3a3a;border-left:5px solid #3a3a3a}.sidebar .nav-menu>li>a:hover{background-color:#2d2d2d;border-left:5px solid #005c74}.sidebar .nav-menu>li>a.active{color:#fff;background-color:#0084a7;border-left:5px solid #0084a7}.sidebar .nav-menu>li>a.active:hover{border-left:5px solid #00acda}.sidebar .nav-menu>li>a.active:after{content:"\e251";font-family:'Glyphicons Halflings';position:relative;top:1px;display:inline-block;font-style:normal;font-weight:400;float:right;color:#e9e9e9;font-size:20px;line-height:24px;margin-right:-6px}.sidebar .nav-menu li .menu-caret:after{display:inline-block;width:12px;height:12px;margin-left:5px;top:1px;position:relative;border:none;font-family:'FontAwesome';font-style:normal}.sidebar .nav-menu li .menu-caret:after{content:'\f0da'}.sidebar .nav-menu li.expand .menu-caret:after{content:'\f0d7'}.sidebar .nav-menu>li.super-admin>a:hover{background-color:#620;border-left:5px solid #4d1a00}.sidebar .nav-menu>li.super-admin>a.active{background-color:#930;border-left:5px solid #930}.sidebar .nav-menu>li.super-admin>a.active:hover{border-left:5px solid #c40}.sidebar .nav-menu>li>a>i.icon{float:left;margin-top:1px;margin-right:15px;text-align:center;line-height:24px;font-size:14px}.sidebar .sub-menu{padding:0;margin:0;background-color:#292929;position:relative;list-style-type:none;border-top:1px solid #202020;border-bottom:1px solid #464646}.sidebar .sub-menu>li>a{padding:8px 0 8px 40px;line-height:20px;font-size:13px;display:block;position:relative;color:#889097;border-left:5px solid #292929}.sidebar .sub-menu>li>a:before{display:inline-block;padding-right:8px;line-height:20px;content:"\f105";font-family:'FontAwesome'}.sidebar .sub-menu>li>a:hover{color:#fff;border-left:5px solid #005c74}.sidebar .sub-menu>li>a.active{color:#fff;background-color:#0084a7;border-left:5px solid #0084a7}.sidebar .sub-menu>li>a.active:hover{border-left:5px solid #00acda}.sidebar .sub-menu>li>a.active:after{content:"\e251";font-family:'Glyphicons Halflings';position:relative;top:-2px;display:inline-block;font-style:normal;font-weight:400;float:right;color:#e9e9e9;font-size:20px;line-height:24px;margin-right:-6px}.sidebar .nav-profile{padding:15px 10px;color:#ccc;background-color:#333;border-bottom:1px solid #464646}.sidebar .nav-profile a.title{color:#ccc}.sidebar .nav-profile a.title:hover{color:#fff;background-color:transparent}.sidebar .nav-profile a.title:focus{background-color:transparent}.sidebar .nav-profile .image{float:left;margin-top:3px;font-size:24px;color:#69f;width:36px;height:36px;border-radius:5px;background-color:#eee;text-align:center;margin-right:10px;overflow:hidden}.sidebar .nav-profile .image img{margin-top:-3px}.sidebar .nav-profile .name{display:block;font-size:16px}.sidebar .nav-profile .role{display:block;font-size:12px;color:#999}.sidebar .nav-profile .dropdown-menu{font-size:13px}.sidebar .nav-profile .dropdown-menu>li>a{padding:5px 20px}.sidebar .nav-profile .dropdown-menu>li>a:hover{background-color:#ccc}.sidebar .nav-profile .dropdown-menu .divider{margin:5px 0}.sidebar .badge{margin-top:-10px;margin-left:5px}.content{margin-left:180px}.page-content{padding:15px}.page-content-dashboard{padding:20px 25px}.widget{overflow:hidden;border-radius:3px;padding:15px;margin-bottom:20px;color:#fff}.widget.widget-stats{position:relative}.widget .stats-icon{font-size:52px;top:12px;right:21px;width:56px;height:56px;text-align:center;line-height:56px;margin-left:15px;color:#fff;position:absolute;opacity:.2}.widget .stats-title{color:#fff;color:rgba(255,255,255,0.6)}.widget .stats-split{height:2px;margin:0 -15px 10px;background:rgba(0,0,0,0.2)}.widget .stats-content{font-size:24px;font-weight:300;margin-bottom:10px}.widget .stats-desc{display:inline-block;color:#fff;color:rgba(255,255,255,0.6)}.widget .stats-action{display:inline-block;float:right}.widget a{color:#eee;color:rgba(255,255,255,0.7)}.widget a:hover{color:#fff}.widget.widget-info{background-color:#33b7d0}.widget.widget-primary{background-color:#348fe2}.widget.widget-success{background-color:#368142}.widget.widget-warning{background-color:#f57523}.widget.widget-danger{background-color:#d34242}.panel{border:none;box-shadow:none;border-radius:3px}.panel .panel-heading{padding:6px 15px;color:#fff}.panel .panel-heading .panel-title{font-size:14px}.panel .panel-heading .panel-heading-btn{float:right}.panel .panel-heading .panel-heading-btn .btn{display:inline-block;padding:0;border:none;text-align:center}.panel .panel-heading .panel-heading-btn .btn.btn-xs{width:18px;height:18px;line-height:18px;font-size:12px}.panel .panel-heading .panel-heading-btn .btn.btn-circle{border-radius:50%}.place-holder-h200{width:100%;height:300px;background-color:#eee;border:1px solid #ccc;line-height:200px;text-align:center}.dashboard-panel2-holder{width:100%;height:1150px;background-color:#eee;border:1px solid #ccc;line-height:200px;text-align:center}.dashboard-panel-time{color:#cecece}.box{border:none;box-shadow:none;border-radius:3px;background-color:#fff;padding:15px;margin-bottom:15px}.box-fluid{border:none;box-shadow:none;border-radius:3px;margin-bottom:15px}.box.box-sm,.box-fluid.box-sm{padding:5px 15px}.box .box-title,.box-fluid .box-title{margin-bottom:10px}.box .box-title .title,.box-fluid .box-title .title{display:inline-block;font-size:18px;color:#333;height:30px;line-height:30px}.box .box-title .btn-sm,.box-fluid .box-title .btn-sm{padding:3px 8px;margin-top:-5px}.box .nav-tabs,.box-fluid .nav-tabs{font-size:14px;font-weight:bold}.box .nav-tabs>li:first-child,.box-fluid .nav-tabs>li:first-child{margin-left:50px}.box .tab-content>.tab-pane,.box-fluid .tab-content>.tab-pane{background-color:#fff;padding:20px;border:1px solid #ddd;border-top:none;border-bottom-left-radius:3px;border-bottom-right-radius:3px}.box-license{line-height:30px}.box-license .breadcrumb{padding:0;margin:0;border-radius:0;background-color:transparent}.box-btn-bar{line-height:30px}.box-btn-bar a.btn{margin-right:20px}.page-nav{height:30px;line-height:30px}.page-nav .breadcrumb{padding:0;margin:0;border-radius:0;background-color:transparent}.page-nav .pagination{margin:0 0}.page-nav .btn{margin-top:-3px}.page-filter{height:36px;line-height:36px;margin-bottom:10px}.page-filter .form-control{margin-top:5px;margin-right:4px}.btn.btn-sm .dropdown-menu li a{font-size:11px}.invite{text-align:center;padding-bottom:20px}.invite .code{color:#2f3991;font-size:36px;font-weight:700;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}.invite .link{padding:5px;color:#2f3991;font-size:13px;font-weight:700;background-color:#eee;border-radius:5px}.invite-send-box{width:300px;margin:0 auto}.form-group .input-group{margin-bottom:5px}.op_box{display:block;padding:5px;border-radius:3px;text-align:center;margin-top:5px}.op_error{background:#fbb}.op_wait{background:#ccc}.table-data td.loading{text-align:left;padding:20px}.table-data .btn-group.open .dropdown-toggle{-webkit-box-shadow:none;box-shadow:none}.more-action{position:absolute !important}.more-action .dropdown-menu{background-color:rgba(60,60,60,0.9);color:#fff;font-size:13px}.more-action .dropdown-menu.dropdown-menu-left{margin-left:-120px}.more-action .dropdown-menu>li>a{padding:5px 20px;color:#fff}.more-action .dropdown-menu>li>a:hover,.more-action .dropdown-menu>li>a:active,.more-action .dropdown-menu>li>a:visited{background-color:#0084a7}.more-action .dropdown-menu .divider{margin:5px 0;background-color:#666}.popover-inline-edit input,.popover-inline-edit .btn{height:30px}.popover-inline-edit .popover-title{background-color:#ddd}.popover-inline-edit .popover-content{padding:20px 10px}.popover-inline-edit .popover{padding:0;max-width:500px}.popover-inline-edit .popover .popover-content{padding:10px 10px 20px 10px}.popover-inline-edit .popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#ddd}.user-info-table{font-size:14px}.user-info-table tbody>tr>td{padding:8px}.user-info-table .user-field{min-width:100px;width:100px;color:#999;text-align:right}.user-info-table .user-value{color:#333;font-weight:bold}.user-info-table .user-value a{font-weight:normal}.breadcrumb.breadcrumb-trans{background-color:transparent}.biz-box{display:inline-block;width:20%;max-width:20%}.biz-box .bb-inner{background-color:#368142;margin:3px;border-radius:4px}.biz-box .bb-name{color:#fff;padding:9px;margin-bottom:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center;padding-top:1px;padding-bottom:1px}.biz-box .bb-ver{font-size:11px;height:16px;text-align:center;padding:0 5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-bottom-left-radius:4px;border-bottom-right-radius:4px;color:rgba(255,255,255,0.85);background-color:rgba(0,0,0,0.3)}.biz-box.mp-disabled .mp-inner{background-color:#e5e5e5}.biz-box.mp-disabled .mp-name{color:#999}.biz-box.mp-success .mp-inner{background-color:#368142}.biz-box.mp-success .mp-name{color:#fff}.biz-box.mp-danger .mp-inner{background-color:#d34242}.biz-box.mp-danger .mp-name{color:#fff}.biz-box.mp-warning .mp-inner{background-color:#f57523}.biz-box.mp-warning .mp-name{color:#fff}textarea.textarea-resize-y{resize:vertical}textarea.textarea-resize-none{resize:none}textarea.textarea-code{font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}textarea.cert_pub{width:100%;height:64px;border:1px solid #e2e2e2;background-color:#e4ffe5}.icon{display:inline-block}.icon16{width:16px;height:16px;line-height:16px;margin-right:3px;margin-top:0 !important;margin-bottom:-2px !important;vertical-align:top;background-image:url("img/desktop/mimetype-16.png") !important}.icon16.icon-disk{background-position:0 0 !important}.icon16.icon-folder{background-position:-16px 0 !important}.icon16.icon-file{background-position:0 -16px !important}.icon16.icon-txt{background-position:-16px -16px !important}.icon16.icon-help{background-position:-32px -16px !important}.icon16.icon-sys{background-position:-48px -16px !important}.icon16.icon-exe{background-position:-64px -16px !important}.icon16.icon-office{background-position:0 -32px !important}.icon16.icon-word{background-position:-16px -32px !important}.icon16.icon-excel{background-position:-32px -32px !important}.icon16.icon-ppt{background-position:-48px -32px !important}.icon16.icon-access{background-position:-64px -32px !important}.icon16.icon-visio{background-position:-80px -32px !important}.icon16.icon-audio{background-position:0 -48px !important}.icon16.icon-video{background-position:-16px -48px !important}.icon16.icon-pic{background-position:-32px -48px !important}.icon16.icon-pdf{background-position:-48px -48px !important}.icon16.icon-font{background-position:-64px -48px !important}.icon16.icon-script{background-position:0 -64px !important}.icon16.icon-html{background-position:-16px -64px !important}.icon16.icon-py{background-position:-32px -64px !important}.icon16.icon-h{background-position:-48px -64px !important}.icon16.icon-c{background-position:-64px -64px !important}.icon16.icon-cpp{background-position:-80px -64px !important}.icon16.icon-cs{background-position:-96px -64px !important}.icon16.icon-php{background-position:-112px -64px !important}.icon16.icon-ruby{background-position:-128px -64px !important}.icon16.icon-java{background-position:-144px -64px !important}.icon16.icon-vs{background-position:-160px -64px !important}.icon16.icon-js{background-position:-176px -64px !important}.icon16.icon-archive{background-position:0 -80px !important}.icon16.icon-rar{background-position:-16px -80px !important}.icon16.icon-zip{background-position:-32px -80px !important}.icon16.icon-7z{background-position:-48px -80px !important}.icon16.icon-tar{background-position:-64px -80px !important}.icon16.icon-gz{background-position:-80px -80px !important}.icon16.icon-jar{background-position:-96px -80px !important}.icon16.icon-bz2{background-position:-112px -80px !important}.icon24{width:24px;height:24px;line-height:24px;margin-right:3px;margin-top:0 !important;margin-bottom:-2px !important;vertical-align:top;background-image:url("img/desktop/icons-tree-24x24.png") !important}.icon24.icon-disk{background-position:0 0 !important}.icon24.icon-folder{background-position:-24px 0 !important}.icon24.icon-folder-open{background-position:-48px 0 !important}.os-icon-windows:after{color:#00bcf6;content:"\f17a";font-size:18px;width:24px;height:24px;line-height:24px;display:inline-block;font-family:'FontAwesome'}.os-icon-linux:after{color:#fff;content:"\f17c";font-size:18px;width:24px;height:24px;line-height:24px;background-color:#333;border-radius:50%;display:inline-block;font-family:'FontAwesome'}.os-icon-macos:after{color:#a7a7a7;content:"\f179";font-size:20px;width:24px;height:24px;line-height:24px;display:inline-block;font-family:'FontAwesome'}.os-icon-ubuntu:after,.os-icon-debian:after,.os-icon-centos:after,.os-icon-redhat:after{content:" ";width:24px;height:24px;line-height:24px;display:inline-block}.os-icon-ubuntu:after{background:url(../img/os-icon/ubuntu-24x24.png) no-repeat}.os-icon-debian:after{background:url(../img/os-icon/debian-24x24.png) no-repeat}.os-icon-centos:after{background:url(../img/os-icon/centos-24x24.png) no-repeat}.os-icon-redhat:after{background:url(../img/os-icon/redhat-24x24.png) no-repeat}#gritter-notice-wrapper{z-index:9999}.gritter-bottom,.gritter-item,.gritter-top{background:rgba(0,0,0,0.8) !important}.gritter-top{border-top-left-radius:3px;border-top-right-radius:3px}.gritter-bottom{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.gritter-close,.gritter-light .gritter-close{left:auto !important;right:5px !important;top:5px !important;width:16px !important;height:16px !important;line-height:16px !important;display:block !important;border-radius:50%}.gritter-close:before,.gritter-light .gritter-close:before{content:'\f00d' !important;font-family:FontAwesome !important;font-size:9px !important;width:16px !important;height:16px !important;line-height:16px !important;color:#fff !important;text-indent:0 !important;position:absolute !important;text-align:center !important;right:0 !important;top:0 !important}.gritter-title{font-size:13px !important;line-height:16px !important;padding-bottom:5px !important;font-weight:400 !important;color:#fff !important;text-shadow:none !important}.gritter-item{color:#aaa !important;font-size:13px !important;padding:2px 15px 5px !important}.gritter-error .gritter-bottom,.gritter-error .gritter-item,.gritter-error .gritter-top{background:rgba(123,32,32,0.9) !important}.gritter-error .gritter-title{color:#fff !important}.gritter-error .gritter-item{color:#ddd !important}.gritter-error .gritter-close{left:auto !important;right:5px !important;top:5px !important;width:16px !important;height:16px !important;line-height:16px !important;display:block !important;border-radius:50%;background:#e33b3b !important}.gritter-success .gritter-bottom,.gritter-success .gritter-item,.gritter-success .gritter-top{background:rgba(1,65,16,0.9) !important}.gritter-success .gritter-title{color:#ddd !important}.gritter-success .gritter-item{color:#ccc !important}.gritter-success .gritter-close{background:#0eb320 !important} \ No newline at end of file diff --git a/server/www/teleport/static/css/sub.css b/server/www/teleport/static/css/sub.css index 08a9bdf..73393db 100644 --- a/server/www/teleport/static/css/sub.css +++ b/server/www/teleport/static/css/sub.css @@ -1 +1 @@ -@charset "utf-8";body{font-family:"Open Sans","Helvetica Neue","Microsoft YaHei","寰蒋闆呴粦",Helvetica,Arial,sans-serif;font-size:13px;background-color:#e9e9e9;color:#333}html,body{height:100%}#page-container{min-width:1260px}a{text-decoration:none}a:link{text-decoration:none}a:hover{text-decoration:none}a:active{text-decoration:none}a:visited{text-decoration:none}select{outline:none}label{font-weight:normal}.clear-float{clear:both}.bigger{font-size:120%}.normal-text{font-size:13px;color:#333}.mono{font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}hr.hr-sm{margin-top:5px;margin-bottom:5px}.btn-group-sm>.btn,.btn-sm{padding:2px 5px}.btn.btn-sm{padding:3px 8px}.btn.btn-icon{padding:3px 6px}.btn.btn-icon.btn-sm{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}.pop-menu-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040}.form-group{margin-bottom:5px}.badge{display:inline-block;min-width:8px;padding:5px 10px;border-radius:10px;text-align:center;white-space:nowrap;vertical-align:middle;font-size:13px;font-weight:400;line-height:1em;background-color:#888;color:#fff;text-shadow:1px 1px 0 #525252}.badge.badge-plain{text-shadow:none}.badge.badge-sm{font-size:11px;padding:3px 6px;margin-top:0;border-radius:8px;text-shadow:none}.badge.badge-sup{margin-left:-6px;margin-top:-16px}.badge.badge-ignore{background-color:#e5e5e5;color:#999;text-shadow:none}.badge.badge-info{background-color:#33b7d0}.badge.badge-primary{background-color:#348fe2}.badge.badge-success{background-color:#368142}.badge.badge-warning{background-color:#f57523}.badge.badge-danger{background-color:#d34242}.label{display:inline-block;min-width:8px;padding:5px 10px;border-radius:5px;text-align:center;white-space:nowrap;vertical-align:middle;font-size:13px;font-weight:400;line-height:1em;background-color:#888;color:#fff;text-shadow:1px 1px 0 #525252}.label.label-plain{text-shadow:none}.label.label-sm{font-size:11px;padding:3px 8px;margin-top:0;border-radius:5px;text-shadow:none}.label.label-ignore{background-color:#e5e5e5;color:#999;text-shadow:none}.label.label-info{background-color:#33b7d0}.label.label-primary{background-color:#348fe2}.label.label-success{background-color:#368142}.label.label-warning{background-color:#f57523}.label.label-danger{background-color:#d34242}.progress.progress-sm{height:18px;margin-bottom:2px;background-color:#aaa}.progress.progress-sm.button{cursor:pointer}.progress.progress-sm .progress-bar{display:block;font-size:11px;float:none}.alert-sm{padding:5px;margin-bottom:10px}.modal-dialog-sm .modal-header{padding:10px}.modal-dialog-sm .modal-body{padding:10px}.modal-dialog-sm .modal-footer{padding:10px}.modal-dialog-sm .form-horizontal .form-group{margin-right:-5px;margin-left:-5px}.modal-dialog-sm .col-sm-1,.modal-dialog-sm .col-sm-2,.modal-dialog-sm .col-sm-3,.modal-dialog-sm .col-sm-4,.modal-dialog-sm .col-sm-5,.modal-dialog-sm .col-sm-6,.modal-dialog-sm .col-sm-7,.modal-dialog-sm .col-sm-8,.modal-dialog-sm .col-sm-9,.modal-dialog-sm .col-sm-10,.modal-dialog-sm .col-sm-11{padding-right:5px;padding-left:5px}.btn-single-line{white-space:nowrap}.btn-single-line .btn:first-child{border-top-left-radius:3px;border-bottom-left-radius:3px}.btn-single-line .btn:last-child{border-top-right-radius:3px;border-bottom-right-radius:3px}.remote-action-group{margin-bottom:3px;height:28px;min-width:390px}.remote-action-group ul{display:block;height:28px;margin:0;padding:0}.remote-action-group ul 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}.remote-action-group ul li.remote-action-btn{background:none;padding:0;border:none}.remote-action-group ul li.remote-action-input{background:none;padding:4px 0}.remote-action-group ul li.remote-action-input select{border:none}.remote-action-group ul li.remote-action-chk-protocol{width:86px}.remote-action-group ul li.remote-action-username,.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-protocol{width:96px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.remote-action-group ul li.remote-action-username{font-size:90%;color:#999}.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-protocol,.remote-action-group ul li.remote-action-chk-protocol{color:#000}.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-chk-protocol{font-weight:bold}.remote-action-group ul li.remote-action-password,.remote-action-group ul li.remote-action-sshkey,.remote-action-group ul li.remote-action-noauth{text-align:center;padding:4px 8px;width:45px}.remote-action-group ul li.remote-action-password{background-color:#e3ffe3;color:#999}.remote-action-group ul li.remote-action-sshkey{background-color:#fbe9c8;color:#666}.remote-action-group ul li.remote-action-noauth{background-color:#e0e0e0;color:#666}.remote-action-group ul li .btn{line-height:1.5;margin:0;padding:4px 8px;font-size:12px;border-radius:0}.remote-action-group ul li label{padding:0;display:block;float:left;margin-top:1px;cursor:pointer}.remote-action-group ul li input[type=checkbox]{display:block;float:left;margin:3px 5px 0 0}.remote-action-group ul li select{margin-top:-3px}.remote-action-group ul li:first-child{border-left:1px solid #ccc;border-top-left-radius:4px;border-bottom-left-radius:4px}.remote-action-group ul li:first-child .btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.remote-action-group ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.remote-action-group ul li:last-child .btn{border-top-right-radius:4px;border-bottom-right-radius:4px}hr.small{margin:5px 0}.dlg-protocol-group{margin-bottom:3px}.dlg-protocol-group ul{display:block;height:28px;margin:0;padding:0}.dlg-protocol-group ul 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}.dlg-protocol-group ul li.item-name{width:120px}.dlg-protocol-group ul li.item-btn{background:none;padding:0;border:none}.dlg-protocol-group ul li.item-input{background:none;border:none;padding:0}.dlg-protocol-group ul li .form-control{line-height:1.5;margin:0;padding:4px 5px;font-size:12px;height:28px;border-radius:0;border-left:none;width:100px}.dlg-protocol-group ul li label{padding:0;display:block;float:left;margin-top:1px;cursor:pointer}.dlg-protocol-group ul li input[type=checkbox]{display:block;float:left;margin:3px 5px 0 0}.dlg-protocol-group ul li:first-child{border-left:1px solid #ccc;border-top-left-radius:4px;border-bottom-left-radius:4px}.dlg-protocol-group ul li:first-child .btn,.dlg-protocol-group ul li:first-child .form-control{border-top-left-radius:4px;border-bottom-left-radius:4px}.dlg-protocol-group ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.dlg-protocol-group ul li:last-child .btn,.dlg-protocol-group ul li:last-child .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}.form-group-sm .form-control-static{padding:6px 0}.table{margin-bottom:10px}.table>thead>tr>th{padding:5px 5px;outline:none;white-space:nowrap;font-weight:normal;text-align:center;background-color:#ededed}.table>tbody>tr>td{padding:5px;text-align:center;vertical-align:middle}.table>tbody>tr>td .nowrap{white-space:nowrap}.table.table-data thead .sorting,.table.table-data thead .sorting_asc,.table.table-data thead .sorting_desc{cursor:pointer;position:relative}.table.table-data thead .sorting>span:after,.table.table-data thead .sorting_asc>span:after,.table.table-data thead .sorting_desc>span:after{bottom:4px;padding-left:5px;display:inline-block;font-family:'FontAwesome';opacity:.8}.table.table-data thead .sorting>span:after{opacity:.2;content:"\f0dc"}.table.table-data thead .sorting_asc>span:after{content:"\f0de"}.table.table-data thead .sorting_desc>span:after{content:"\f0dd"}.host-id{display:block;font-size:16px;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace;color:#333}.host-id.not-active{font-size:14px;font-weight:400;color:#999}.host-desc{font-size:12px;color:#999;display:inline-block;white-space:nowrap;width:160px;overflow:hidden;text-overflow:ellipsis}a.host-desc:hover:before{display:inline-block;padding-right:3px;line-height:12px;content:"\f040";font-family:'FontAwesome'}.td-ip-list{padding-right:20px;padding-left:5px}.td-ip-show-more{font-size:14px;width:12px;float:right;display:block}.td-ip-item{min-width:12em;width:12em;height:18px;padding:2px 4px;margin:1px 0;color:#333;text-align:center;white-space:nowrap;border-radius:9px;line-height:11px;font-size:11px;background:#dfdfdf !important}.td-ip-item span{display:inline-block;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace;font-size:11px;font-weight:400}.td-ip-item a{display:inline-block;width:14px;float:right;font-size:14px}.page-header-fixed{padding-top:48px}.header{border:none;box-shadow:0 0 3px rgba(0,0,0,0.5);min-height:48px;height:48px;top:0;width:100%;position:fixed;z-index:999}.header .top-navbar{min-height:48px;height:48px;line-height:48px;background-color:#3a3a3a;color:#ccc}.header .top-navbar a{color:#d5d5d5}.header .top-navbar a:hover{color:#5a8fee}.header .top-navbar .brand{float:left;display:inline-block;padding:12px 0;margin:0}.header .top-navbar .brand .site-logo{display:block;width:86px;height:24px;background:url(../img/site-logo-small.png) no-repeat}.header .top-navbar .title-container{float:left;display:inline-block;margin:0;padding:0;margin-left:20px}.header .top-navbar .title-container .title{font-size:16px}.header .top-navbar .breadcrumb-container{float:left;display:inline-block;margin:0;padding:0}.header .top-navbar .breadcrumb-container .breadcrumb{background-color:#3a3a3a;height:48px;margin:0;border-radius:0;border:none;padding:0 0 0 20px;font-size:16px;color:#ccc}.header .top-navbar .breadcrumb-container .breadcrumb>li+li:before{font-size:18px;padding:0 5px;color:#555;content:'|'}.header .top-navbar .breadcrumb-container .breadcrumb .title{font-size:18px}.header .top-navbar .breadcrumb-container .breadcrumb .sub-title{font-size:14px;color:#b3b3b3}.header .top-navbar .status-container{float:right}.page-content{margin-top:10px;margin-bottom:44px}.footer{width:100%;height:24px;line-height:24px;background-color:#d5d5d5;border-top:1px solid #a2a2a2;border-bottom:1px solid #efefef;z-index:998;text-align:center;font-size:12px}.footer.footer-fixed-bottom{bottom:0;position:fixed}.row-sm .col-sm-1,.row-sm .col-sm-2,.row-sm .col-sm-3,.row-sm .col-sm-4,.row-sm .col-sm-5,.row-sm .col-sm-6,.row-sm .col-sm-7,.row-sm .col-sm-8,.row-sm .col-sm-9,.row-sm .col-sm-10,.row-sm .col-sm-11{padding-right:5px;padding-left:5px}.content{margin-top:15px;margin-bottom:20px;background-color:#fff;border-radius:5px;padding:10px}.content:last-child{margin-bottom:54px}.table-host{width:100%;border-top:10px solid #b3cfe7;border-bottom:1px solid #b3cfe7}.table-host .cell-host-id{border-left:1px solid #e7e7e7;padding:5px;text-align:center;width:168px;vertical-align:middle}.table-host .cell-host-id .host-id{font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace;font-size:13px;color:#999;display:inline-block}.table-host .cell-host-id .host-name{display:block;width:168px;text-align:center;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:16px;margin:auto;margin-bottom:10px}.table-host .cell-host-id .td-ip-item{width:10em;height:18px;padding:2px 4px;margin:1px auto;color:#333;text-align:center;white-space:nowrap;border-radius:9px;line-height:11px;font-size:11px;background:#dfdfdf !important}.table-host .cell-host-id .td-ip-item span{display:inline-block;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace;font-size:11px;font-weight:400}.table-host .cell-host-id .actions{margin-top:20px}.table-host .cell-host-id .actions a{margin-left:5px;margin-right:5px}.table-host .cell-host-id .actions a:first-child{margin-left:0}.table-host .cell-host-id .actions a:last-child{margin-right:0}.table-host .cell-detail{border-left:1px solid #e7e7e7;border-right:1px solid #e7e7e7;vertical-align:top}.table-host .cell-detail tr{border-top:1px solid #e7e7e7}.table-host .cell-detail tr:last-child{border-bottom:1px solid #e7e7e7}.table-host .cell-detail .row-host-info{background-color:#ececed}.table-host .cell-detail.host-offline{background-color:#ffcecc;text-align:center;vertical-align:middle}.table-host .cell-detail.host-offline .host-offline-msg{color:#802506;font-size:24px}.table-host .cell-log td{border:1px solid #e7e7e7}.table-host .cell-log td .host-log{font-size:12px;outline:none;width:100%;height:120px;overflow-y:auto;resize:none;border:none;padding:5px}.table-host .cell-log td .host-log div{margin-bottom:3px}.table-host .cell-log td .host-log div .datetime{font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}.log-box{margin-top:15px}.log-box .log-list{margin-top:5px;border:1px solid #e7e7e7;font-size:12px;outline:none;width:100%;max-height:480px;overflow-y:auto;resize:none;padding:5px}.log-box .log-list div{margin-bottom:3px}.log-box .log-list div:hover{background-color:#f3f3f3}.log-box .log-list div .log-dt{padding:0 3px;padding-top:2px;padding-bottom:1px;margin-right:3px;background-color:#f57523;color:#fff;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}.log-box .log-list div .log-hid{padding:0 3px;margin-right:3px;background-color:#348fe2;color:#fff}.log-box .log-list div .log-hname{padding:0 3px;margin-right:3px;background-color:#348fe2;color:#fff}.page-nav{height:30px;line-height:30px}.page-nav .breadcrumb{padding:0;margin:0;border-radius:0;background-color:transparent}.page-nav .pagination{margin:0 0}.page-nav .btn{margin-top:-3px}.mp{display:inline-block;width:20%;max-width:20%}.mp .mp-inner{background-color:#e5e5e5;margin:3px;border-radius:4px}.mp .mp-name{color:#999;padding:9px;margin-bottom:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center}.mp .mp-name.with-target{padding-top:17px;padding-bottom:1px}.mp .mp-target{display:inline-block;float:left;position:absolute;font-size:11px;padding:0 5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-top-left-radius:4px;border-bottom-right-radius:4px;color:rgba(255,255,255,0.85);background-color:rgba(0,0,0,0.1)}.mp.mp-disabled .mp-inner{background-color:#e5e5e5}.mp.mp-disabled .mp-name{color:#999}.mp.mp-success .mp-inner{background-color:#368142}.mp.mp-success .mp-name{color:#fff}.mp.mp-danger .mp-inner{background-color:#d34242}.mp.mp-danger .mp-name{color:#fff}.mp.mp-warning .mp-inner{background-color:#f57523}.mp.mp-warning .mp-name{color:#fff}.host-offline{background-color:#ffcecc;height:36px;line-height:36px;padding:0 10px;color:#802506;font-size:20px;cursor:pointer}.host-offline .tips{display:none;font-size:12px}.host-offline:hover .tips{display:inline-block}.host-no-strategy{color:#999;font-size:16px}#gritter-notice-wrapper{z-index:9999}.gritter-bottom,.gritter-item,.gritter-top{background:rgba(0,0,0,0.8) !important}.gritter-top{border-top-left-radius:3px;border-top-right-radius:3px}.gritter-bottom{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.gritter-close,.gritter-light .gritter-close{left:auto !important;right:5px !important;top:5px !important;width:16px !important;height:16px !important;line-height:16px !important;display:block !important;border-radius:50%}.gritter-close:before,.gritter-light .gritter-close:before{content:'\f00d' !important;font-family:FontAwesome !important;font-size:9px !important;width:16px !important;height:16px !important;line-height:16px !important;color:#fff !important;text-indent:0 !important;position:absolute !important;text-align:center !important;right:0 !important;top:0 !important}.gritter-title{font-size:13px !important;line-height:16px !important;padding-bottom:5px !important;font-weight:400 !important;color:#fff !important;text-shadow:none !important}.gritter-item{color:#aaa !important;font-size:13px !important;padding:2px 15px 5px !important}.gritter-error .gritter-bottom,.gritter-error .gritter-item,.gritter-error .gritter-top{background:rgba(123,32,32,0.9) !important}.gritter-error .gritter-title{color:#fff !important}.gritter-error .gritter-item{color:#ddd !important}.gritter-error .gritter-close{left:auto !important;right:5px !important;top:5px !important;width:16px !important;height:16px !important;line-height:16px !important;display:block !important;border-radius:50%;background:#e33b3b !important}.gritter-success .gritter-bottom,.gritter-success .gritter-item,.gritter-success .gritter-top{background:rgba(1,65,16,0.9) !important}.gritter-success .gritter-title{color:#ddd !important}.gritter-success .gritter-item{color:#ccc !important}.gritter-success .gritter-close{background:#0eb320 !important}.icon{display:inline-block}.icon16{width:16px;height:16px;line-height:16px;margin-right:3px;margin-top:0 !important;margin-bottom:-2px !important;vertical-align:top;background-image:url("img/desktop/mimetype-16.png") !important}.icon16.icon-disk{background-position:0 0 !important}.icon16.icon-folder{background-position:-16px 0 !important}.icon16.icon-file{background-position:0 -16px !important}.icon16.icon-txt{background-position:-16px -16px !important}.icon16.icon-help{background-position:-32px -16px !important}.icon16.icon-sys{background-position:-48px -16px !important}.icon16.icon-exe{background-position:-64px -16px !important}.icon16.icon-office{background-position:0 -32px !important}.icon16.icon-word{background-position:-16px -32px !important}.icon16.icon-excel{background-position:-32px -32px !important}.icon16.icon-ppt{background-position:-48px -32px !important}.icon16.icon-access{background-position:-64px -32px !important}.icon16.icon-visio{background-position:-80px -32px !important}.icon16.icon-audio{background-position:0 -48px !important}.icon16.icon-video{background-position:-16px -48px !important}.icon16.icon-pic{background-position:-32px -48px !important}.icon16.icon-pdf{background-position:-48px -48px !important}.icon16.icon-font{background-position:-64px -48px !important}.icon16.icon-script{background-position:0 -64px !important}.icon16.icon-html{background-position:-16px -64px !important}.icon16.icon-py{background-position:-32px -64px !important}.icon16.icon-h{background-position:-48px -64px !important}.icon16.icon-c{background-position:-64px -64px !important}.icon16.icon-cpp{background-position:-80px -64px !important}.icon16.icon-cs{background-position:-96px -64px !important}.icon16.icon-php{background-position:-112px -64px !important}.icon16.icon-ruby{background-position:-128px -64px !important}.icon16.icon-java{background-position:-144px -64px !important}.icon16.icon-vs{background-position:-160px -64px !important}.icon16.icon-js{background-position:-176px -64px !important}.icon16.icon-archive{background-position:0 -80px !important}.icon16.icon-rar{background-position:-16px -80px !important}.icon16.icon-zip{background-position:-32px -80px !important}.icon16.icon-7z{background-position:-48px -80px !important}.icon16.icon-tar{background-position:-64px -80px !important}.icon16.icon-gz{background-position:-80px -80px !important}.icon16.icon-jar{background-position:-96px -80px !important}.icon16.icon-bz2{background-position:-112px -80px !important}.icon24{width:24px;height:24px;line-height:24px;margin-right:3px;margin-top:0 !important;margin-bottom:-2px !important;vertical-align:top;background-image:url("img/desktop/icons-tree-24x24.png") !important}.icon24.icon-disk{background-position:0 0 !important}.icon24.icon-folder{background-position:-24px 0 !important}.icon24.icon-folder-open{background-position:-48px 0 !important}.os-icon-windows:after{color:#00bcf6;content:"\f17a";font-size:18px;width:24px;height:24px;line-height:24px;display:inline-block;font-family:'FontAwesome'}.os-icon-linux:after{color:#fff;content:"\f17c";font-size:18px;width:24px;height:24px;line-height:24px;background-color:#333;border-radius:50%;display:inline-block;font-family:'FontAwesome'}.os-icon-macos:after{color:#a7a7a7;content:"\f179";font-size:20px;width:24px;height:24px;line-height:24px;display:inline-block;font-family:'FontAwesome'}.os-icon-ubuntu:after,.os-icon-debian:after,.os-icon-centos:after,.os-icon-redhat:after{content:" ";width:24px;height:24px;line-height:24px;display:inline-block}.os-icon-ubuntu:after{background:url(../img/os-icon/ubuntu-24x24.png) no-repeat}.os-icon-debian:after{background:url(../img/os-icon/debian-24x24.png) no-repeat}.os-icon-centos:after{background:url(../img/os-icon/centos-24x24.png) no-repeat}.os-icon-redhat:after{background:url(../img/os-icon/redhat-24x24.png) no-repeat} \ No newline at end of file +@charset "utf-8";body{font-family:"Open Sans","Helvetica Neue","Microsoft YaHei","寰蒋闆呴粦",Helvetica,Arial,sans-serif;font-size:13px;background-color:#e9e9e9;color:#333}html,body{height:100%}#page-container{min-width:1260px}a{text-decoration:none}a:link{text-decoration:none}a:hover{text-decoration:none}a:active{text-decoration:none}a:visited{text-decoration:none}select{outline:none}label{font-weight:normal}.clear-float{clear:both}.bigger{font-size:120%}.normal-text{font-size:13px;color:#333}.mono{font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}hr.hr-sm{margin-top:5px;margin-bottom:5px}.btn-group-sm>.btn,.btn-sm{padding:2px 5px}.btn.btn-sm{padding:3px 8px}.btn.btn-icon{padding:3px 6px}.btn.btn-icon.btn-sm{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}.pop-menu-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040}.form-group{margin-bottom:5px}.badge{display:inline-block;min-width:8px;padding:5px 10px;border-radius:10px;text-align:center;white-space:nowrap;vertical-align:middle;font-size:13px;font-weight:400;line-height:1em;background-color:#888;color:#fff;text-shadow:1px 1px 0 #525252}.badge.badge-plain{text-shadow:none}.badge.badge-sm{font-size:11px;padding:3px 6px;margin-top:0;border-radius:8px;text-shadow:none}.badge.badge-sup{margin-left:-6px;margin-top:-16px}.badge.badge-ignore{background-color:#e5e5e5;color:#999;text-shadow:none}.badge.badge-info{background-color:#33b7d0}.badge.badge-primary{background-color:#348fe2}.badge.badge-success{background-color:#368142}.badge.badge-warning{background-color:#f57523}.badge.badge-danger{background-color:#d34242}.label{display:inline-block;min-width:8px;padding:5px 10px;border-radius:5px;text-align:center;white-space:nowrap;vertical-align:middle;font-size:13px;font-weight:400;line-height:1em;background-color:#888;color:#fff;text-shadow:1px 1px 0 #525252}.label.label-plain{text-shadow:none}.label.label-sm{font-size:11px;padding:3px 8px;margin-top:0;border-radius:5px;text-shadow:none}.label.label-ignore{background-color:#e5e5e5;color:#999;text-shadow:none}.label.label-info{background-color:#33b7d0}.label.label-primary{background-color:#348fe2}.label.label-success{background-color:#368142}.label.label-warning{background-color:#f57523}.label.label-danger{background-color:#d34242}.progress.progress-sm{height:18px;margin-bottom:2px;background-color:#aaa}.progress.progress-sm.button{cursor:pointer}.progress.progress-sm .progress-bar{display:block;font-size:11px;float:none}.alert-sm{padding:5px;margin-bottom:10px}.modal-dialog-sm .modal-header{padding:10px}.modal-dialog-sm .modal-body{padding:10px}.modal-dialog-sm .modal-footer{padding:10px}.modal-dialog-sm .form-horizontal .form-group{margin-right:-5px;margin-left:-5px}.modal-dialog-sm .col-sm-1,.modal-dialog-sm .col-sm-2,.modal-dialog-sm .col-sm-3,.modal-dialog-sm .col-sm-4,.modal-dialog-sm .col-sm-5,.modal-dialog-sm .col-sm-6,.modal-dialog-sm .col-sm-7,.modal-dialog-sm .col-sm-8,.modal-dialog-sm .col-sm-9,.modal-dialog-sm .col-sm-10,.modal-dialog-sm .col-sm-11{padding-right:5px;padding-left:5px}.btn-single-line{white-space:nowrap}.btn-single-line .btn:first-child{border-top-left-radius:3px;border-bottom-left-radius:3px}.btn-single-line .btn:last-child{border-top-right-radius:3px;border-bottom-right-radius:3px}.remote-action-group{margin-bottom:3px;height:28px;min-width:390px}.remote-action-group ul{display:block;height:28px;margin:0;padding:0}.remote-action-group ul 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}.remote-action-group ul li.remote-action-btn{background:none;padding:0;border:none}.remote-action-group ul li.remote-action-input{background:none;padding:4px 0}.remote-action-group ul li.remote-action-input select{border:none}.remote-action-group ul li.remote-action-chk-protocol{width:86px}.remote-action-group ul li.remote-action-username,.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-protocol{width:96px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.remote-action-group ul li.remote-action-username{font-size:90%;color:#999}.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-protocol,.remote-action-group ul li.remote-action-chk-protocol{color:#000}.remote-action-group ul li.remote-action-name,.remote-action-group ul li.remote-action-chk-protocol{font-weight:bold}.remote-action-group ul li.remote-action-password,.remote-action-group ul li.remote-action-sshkey,.remote-action-group ul li.remote-action-noauth{text-align:center;padding:4px 8px;width:45px}.remote-action-group ul li.remote-action-password{background-color:#e3ffe3;color:#999}.remote-action-group ul li.remote-action-sshkey{background-color:#fbe9c8;color:#666}.remote-action-group ul li.remote-action-noauth{background-color:#e0e0e0;color:#666}.remote-action-group ul li .btn{line-height:1.5;margin:0;padding:4px 8px;font-size:12px;border-radius:0}.remote-action-group ul li label{padding:0;display:block;float:left;margin-top:1px;cursor:pointer}.remote-action-group ul li input[type=checkbox]{display:block;float:left;margin:3px 5px 0 0}.remote-action-group ul li select{margin-top:-3px}.remote-action-group ul li:first-child{border-left:1px solid #ccc;border-top-left-radius:4px;border-bottom-left-radius:4px}.remote-action-group ul li:first-child .btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.remote-action-group ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.remote-action-group ul li:last-child .btn{border-top-right-radius:4px;border-bottom-right-radius:4px}hr.small{margin:5px 0}.dlg-protocol-group{margin-bottom:3px}.dlg-protocol-group ul{display:block;height:28px;margin:0;padding:0}.dlg-protocol-group ul 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}.dlg-protocol-group ul li.item-name{width:120px}.dlg-protocol-group ul li.item-btn{background:none;padding:0;border:none}.dlg-protocol-group ul li.item-input{background:none;border:none;padding:0}.dlg-protocol-group ul li .form-control{line-height:1.5;margin:0;padding:4px 5px;font-size:12px;height:28px;border-radius:0;border-left:none;width:100px}.dlg-protocol-group ul li label{padding:0;display:block;float:left;margin-top:1px;cursor:pointer}.dlg-protocol-group ul li input[type=checkbox]{display:block;float:left;margin:3px 5px 0 0}.dlg-protocol-group ul li:first-child{border-left:1px solid #ccc;border-top-left-radius:4px;border-bottom-left-radius:4px}.dlg-protocol-group ul li:first-child .btn,.dlg-protocol-group ul li:first-child .form-control{border-top-left-radius:4px;border-bottom-left-radius:4px}.dlg-protocol-group ul li:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.dlg-protocol-group ul li:last-child .btn,.dlg-protocol-group ul li:last-child .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}.form-group-sm .form-control-static{padding:6px 0}.input-group :-moz-placeholder{color:#d2d2d2}.input-group ::-moz-placeholder{color:#d2d2d2}.input-group input:-ms-input-placeholder,.input-group textarea:-ms-input-placeholder{color:#d2d2d2}.input-group input::-webkit-input-placeholder,.input-group textarea::-webkit-input-placeholder{color:#d2d2d2}.table{margin-bottom:10px}.table>thead>tr>th{padding:5px 5px;outline:none;white-space:nowrap;font-weight:normal;text-align:center;background-color:#ededed}.table>tbody>tr>td{padding:5px;text-align:center;vertical-align:middle}.table>tbody>tr>td .nowrap{white-space:nowrap}.table.table-data thead .sorting,.table.table-data thead .sorting_asc,.table.table-data thead .sorting_desc{cursor:pointer;position:relative}.table.table-data thead .sorting>span:after,.table.table-data thead .sorting_asc>span:after,.table.table-data thead .sorting_desc>span:after{bottom:4px;padding-left:5px;display:inline-block;font-family:'FontAwesome';opacity:.8}.table.table-data thead .sorting>span:after{opacity:.2;content:"\f0dc"}.table.table-data thead .sorting_asc>span:after{content:"\f0de"}.table.table-data thead .sorting_desc>span:after{content:"\f0dd"}.host-id{display:block;font-size:16px;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace;color:#333}.host-id.not-active{font-size:14px;font-weight:400;color:#999}.host-desc{font-size:12px;color:#999;display:inline-block;white-space:nowrap;width:160px;overflow:hidden;text-overflow:ellipsis}a.host-desc:hover:before{display:inline-block;padding-right:3px;line-height:12px;content:"\f040";font-family:'FontAwesome'}.td-ip-list{padding-right:20px;padding-left:5px}.td-ip-show-more{font-size:14px;width:12px;float:right;display:block}.td-ip-item{min-width:12em;width:12em;height:18px;padding:2px 4px;margin:1px 0;color:#333;text-align:center;white-space:nowrap;border-radius:9px;line-height:11px;font-size:11px;background:#dfdfdf !important}.td-ip-item span{display:inline-block;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace;font-size:11px;font-weight:400}.td-ip-item a{display:inline-block;width:14px;float:right;font-size:14px}.page-header-fixed{padding-top:48px}.header{border:none;box-shadow:0 0 3px rgba(0,0,0,0.5);min-height:48px;height:48px;top:0;width:100%;position:fixed;z-index:999}.header .top-navbar{min-height:48px;height:48px;line-height:48px;background-color:#3a3a3a;color:#ccc}.header .top-navbar a{color:#d5d5d5}.header .top-navbar a:hover{color:#5a8fee}.header .top-navbar .brand{float:left;display:inline-block;padding:12px 0;margin:0}.header .top-navbar .brand .site-logo{display:block;width:86px;height:24px;background:url(../img/site-logo-small.png) no-repeat}.header .top-navbar .title-container{float:left;display:inline-block;margin:0;padding:0;margin-left:20px}.header .top-navbar .title-container .title{font-size:16px}.header .top-navbar .breadcrumb-container{float:left;display:inline-block;margin:0;padding:0}.header .top-navbar .breadcrumb-container .breadcrumb{background-color:#3a3a3a;height:48px;margin:0;border-radius:0;border:none;padding:0 0 0 20px;font-size:16px;color:#ccc}.header .top-navbar .breadcrumb-container .breadcrumb>li+li:before{font-size:18px;padding:0 5px;color:#555;content:'|'}.header .top-navbar .breadcrumb-container .breadcrumb .title{font-size:18px}.header .top-navbar .breadcrumb-container .breadcrumb .sub-title{font-size:14px;color:#b3b3b3}.header .top-navbar .status-container{float:right}.page-content{margin-top:10px;margin-bottom:44px}.footer{width:100%;height:24px;line-height:24px;background-color:#d5d5d5;border-top:1px solid #a2a2a2;border-bottom:1px solid #efefef;z-index:998;text-align:center;font-size:12px}.footer.footer-fixed-bottom{bottom:0;position:fixed}.row-sm .col-sm-1,.row-sm .col-sm-2,.row-sm .col-sm-3,.row-sm .col-sm-4,.row-sm .col-sm-5,.row-sm .col-sm-6,.row-sm .col-sm-7,.row-sm .col-sm-8,.row-sm .col-sm-9,.row-sm .col-sm-10,.row-sm .col-sm-11{padding-right:5px;padding-left:5px}.content{margin-top:15px;margin-bottom:20px;background-color:#fff;border-radius:5px;padding:10px}.content:last-child{margin-bottom:54px}.table-host{width:100%;border-top:10px solid #b3cfe7;border-bottom:1px solid #b3cfe7}.table-host .cell-host-id{border-left:1px solid #e7e7e7;padding:5px;text-align:center;width:168px;vertical-align:middle}.table-host .cell-host-id .host-id{font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace;font-size:13px;color:#999;display:inline-block}.table-host .cell-host-id .host-name{display:block;width:168px;text-align:center;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:16px;margin:auto;margin-bottom:10px}.table-host .cell-host-id .td-ip-item{width:10em;height:18px;padding:2px 4px;margin:1px auto;color:#333;text-align:center;white-space:nowrap;border-radius:9px;line-height:11px;font-size:11px;background:#dfdfdf !important}.table-host .cell-host-id .td-ip-item span{display:inline-block;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace;font-size:11px;font-weight:400}.table-host .cell-host-id .actions{margin-top:20px}.table-host .cell-host-id .actions a{margin-left:5px;margin-right:5px}.table-host .cell-host-id .actions a:first-child{margin-left:0}.table-host .cell-host-id .actions a:last-child{margin-right:0}.table-host .cell-detail{border-left:1px solid #e7e7e7;border-right:1px solid #e7e7e7;vertical-align:top}.table-host .cell-detail tr{border-top:1px solid #e7e7e7}.table-host .cell-detail tr:last-child{border-bottom:1px solid #e7e7e7}.table-host .cell-detail .row-host-info{background-color:#ececed}.table-host .cell-detail.host-offline{background-color:#ffcecc;text-align:center;vertical-align:middle}.table-host .cell-detail.host-offline .host-offline-msg{color:#802506;font-size:24px}.table-host .cell-log td{border:1px solid #e7e7e7}.table-host .cell-log td .host-log{font-size:12px;outline:none;width:100%;height:120px;overflow-y:auto;resize:none;border:none;padding:5px}.table-host .cell-log td .host-log div{margin-bottom:3px}.table-host .cell-log td .host-log div .datetime{font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}.log-box{margin-top:15px}.log-box .log-list{margin-top:5px;border:1px solid #e7e7e7;font-size:12px;outline:none;width:100%;max-height:480px;overflow-y:auto;resize:none;padding:5px}.log-box .log-list div{margin-bottom:3px}.log-box .log-list div:hover{background-color:#f3f3f3}.log-box .log-list div .log-dt{padding:0 3px;padding-top:2px;padding-bottom:1px;margin-right:3px;background-color:#f57523;color:#fff;font-family:Consolas,Lucida Console,Monaco,Courier,'Courier New',monospace}.log-box .log-list div .log-hid{padding:0 3px;margin-right:3px;background-color:#348fe2;color:#fff}.log-box .log-list div .log-hname{padding:0 3px;margin-right:3px;background-color:#348fe2;color:#fff}.page-nav{height:30px;line-height:30px}.page-nav .breadcrumb{padding:0;margin:0;border-radius:0;background-color:transparent}.page-nav .pagination{margin:0 0}.page-nav .btn{margin-top:-3px}.mp{display:inline-block;width:20%;max-width:20%}.mp .mp-inner{background-color:#e5e5e5;margin:3px;border-radius:4px}.mp .mp-name{color:#999;padding:9px;margin-bottom:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center}.mp .mp-name.with-target{padding-top:17px;padding-bottom:1px}.mp .mp-target{display:inline-block;float:left;position:absolute;font-size:11px;padding:0 5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-top-left-radius:4px;border-bottom-right-radius:4px;color:rgba(255,255,255,0.85);background-color:rgba(0,0,0,0.1)}.mp.mp-disabled .mp-inner{background-color:#e5e5e5}.mp.mp-disabled .mp-name{color:#999}.mp.mp-success .mp-inner{background-color:#368142}.mp.mp-success .mp-name{color:#fff}.mp.mp-danger .mp-inner{background-color:#d34242}.mp.mp-danger .mp-name{color:#fff}.mp.mp-warning .mp-inner{background-color:#f57523}.mp.mp-warning .mp-name{color:#fff}.host-offline{background-color:#ffcecc;height:36px;line-height:36px;padding:0 10px;color:#802506;font-size:20px;cursor:pointer}.host-offline .tips{display:none;font-size:12px}.host-offline:hover .tips{display:inline-block}.host-no-strategy{color:#999;font-size:16px}#gritter-notice-wrapper{z-index:9999}.gritter-bottom,.gritter-item,.gritter-top{background:rgba(0,0,0,0.8) !important}.gritter-top{border-top-left-radius:3px;border-top-right-radius:3px}.gritter-bottom{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.gritter-close,.gritter-light .gritter-close{left:auto !important;right:5px !important;top:5px !important;width:16px !important;height:16px !important;line-height:16px !important;display:block !important;border-radius:50%}.gritter-close:before,.gritter-light .gritter-close:before{content:'\f00d' !important;font-family:FontAwesome !important;font-size:9px !important;width:16px !important;height:16px !important;line-height:16px !important;color:#fff !important;text-indent:0 !important;position:absolute !important;text-align:center !important;right:0 !important;top:0 !important}.gritter-title{font-size:13px !important;line-height:16px !important;padding-bottom:5px !important;font-weight:400 !important;color:#fff !important;text-shadow:none !important}.gritter-item{color:#aaa !important;font-size:13px !important;padding:2px 15px 5px !important}.gritter-error .gritter-bottom,.gritter-error .gritter-item,.gritter-error .gritter-top{background:rgba(123,32,32,0.9) !important}.gritter-error .gritter-title{color:#fff !important}.gritter-error .gritter-item{color:#ddd !important}.gritter-error .gritter-close{left:auto !important;right:5px !important;top:5px !important;width:16px !important;height:16px !important;line-height:16px !important;display:block !important;border-radius:50%;background:#e33b3b !important}.gritter-success .gritter-bottom,.gritter-success .gritter-item,.gritter-success .gritter-top{background:rgba(1,65,16,0.9) !important}.gritter-success .gritter-title{color:#ddd !important}.gritter-success .gritter-item{color:#ccc !important}.gritter-success .gritter-close{background:#0eb320 !important}.icon{display:inline-block}.icon16{width:16px;height:16px;line-height:16px;margin-right:3px;margin-top:0 !important;margin-bottom:-2px !important;vertical-align:top;background-image:url("img/desktop/mimetype-16.png") !important}.icon16.icon-disk{background-position:0 0 !important}.icon16.icon-folder{background-position:-16px 0 !important}.icon16.icon-file{background-position:0 -16px !important}.icon16.icon-txt{background-position:-16px -16px !important}.icon16.icon-help{background-position:-32px -16px !important}.icon16.icon-sys{background-position:-48px -16px !important}.icon16.icon-exe{background-position:-64px -16px !important}.icon16.icon-office{background-position:0 -32px !important}.icon16.icon-word{background-position:-16px -32px !important}.icon16.icon-excel{background-position:-32px -32px !important}.icon16.icon-ppt{background-position:-48px -32px !important}.icon16.icon-access{background-position:-64px -32px !important}.icon16.icon-visio{background-position:-80px -32px !important}.icon16.icon-audio{background-position:0 -48px !important}.icon16.icon-video{background-position:-16px -48px !important}.icon16.icon-pic{background-position:-32px -48px !important}.icon16.icon-pdf{background-position:-48px -48px !important}.icon16.icon-font{background-position:-64px -48px !important}.icon16.icon-script{background-position:0 -64px !important}.icon16.icon-html{background-position:-16px -64px !important}.icon16.icon-py{background-position:-32px -64px !important}.icon16.icon-h{background-position:-48px -64px !important}.icon16.icon-c{background-position:-64px -64px !important}.icon16.icon-cpp{background-position:-80px -64px !important}.icon16.icon-cs{background-position:-96px -64px !important}.icon16.icon-php{background-position:-112px -64px !important}.icon16.icon-ruby{background-position:-128px -64px !important}.icon16.icon-java{background-position:-144px -64px !important}.icon16.icon-vs{background-position:-160px -64px !important}.icon16.icon-js{background-position:-176px -64px !important}.icon16.icon-archive{background-position:0 -80px !important}.icon16.icon-rar{background-position:-16px -80px !important}.icon16.icon-zip{background-position:-32px -80px !important}.icon16.icon-7z{background-position:-48px -80px !important}.icon16.icon-tar{background-position:-64px -80px !important}.icon16.icon-gz{background-position:-80px -80px !important}.icon16.icon-jar{background-position:-96px -80px !important}.icon16.icon-bz2{background-position:-112px -80px !important}.icon24{width:24px;height:24px;line-height:24px;margin-right:3px;margin-top:0 !important;margin-bottom:-2px !important;vertical-align:top;background-image:url("img/desktop/icons-tree-24x24.png") !important}.icon24.icon-disk{background-position:0 0 !important}.icon24.icon-folder{background-position:-24px 0 !important}.icon24.icon-folder-open{background-position:-48px 0 !important}.os-icon-windows:after{color:#00bcf6;content:"\f17a";font-size:18px;width:24px;height:24px;line-height:24px;display:inline-block;font-family:'FontAwesome'}.os-icon-linux:after{color:#fff;content:"\f17c";font-size:18px;width:24px;height:24px;line-height:24px;background-color:#333;border-radius:50%;display:inline-block;font-family:'FontAwesome'}.os-icon-macos:after{color:#a7a7a7;content:"\f179";font-size:20px;width:24px;height:24px;line-height:24px;display:inline-block;font-family:'FontAwesome'}.os-icon-ubuntu:after,.os-icon-debian:after,.os-icon-centos:after,.os-icon-redhat:after{content:" ";width:24px;height:24px;line-height:24px;display:inline-block}.os-icon-ubuntu:after{background:url(../img/os-icon/ubuntu-24x24.png) no-repeat}.os-icon-debian:after{background:url(../img/os-icon/debian-24x24.png) no-repeat}.os-icon-centos:after{background:url(../img/os-icon/centos-24x24.png) no-repeat}.os-icon-redhat:after{background:url(../img/os-icon/redhat-24x24.png) no-repeat} \ No newline at end of file diff --git a/server/www/teleport/static/img/qrcode/google-oath-appstore.png b/server/www/teleport/static/img/qrcode/google-oath-appstore.png new file mode 100644 index 0000000..66146e0 Binary files /dev/null and b/server/www/teleport/static/img/qrcode/google-oath-appstore.png differ diff --git a/server/www/teleport/static/img/qrcode/google-oath-baidu.png b/server/www/teleport/static/img/qrcode/google-oath-baidu.png new file mode 100644 index 0000000..b0421fd Binary files /dev/null and b/server/www/teleport/static/img/qrcode/google-oath-baidu.png differ diff --git a/server/www/teleport/static/img/qrcode/google-oath-googleplay.png b/server/www/teleport/static/img/qrcode/google-oath-googleplay.png new file mode 100644 index 0000000..2de59fe Binary files /dev/null and b/server/www/teleport/static/img/qrcode/google-oath-googleplay.png differ diff --git a/server/www/teleport/static/img/qrcode/xiaomi-oath-appstore.png b/server/www/teleport/static/img/qrcode/xiaomi-oath-appstore.png new file mode 100644 index 0000000..af86f8a Binary files /dev/null and b/server/www/teleport/static/img/qrcode/xiaomi-oath-appstore.png differ diff --git a/server/www/teleport/static/img/qrcode/xiaomi-oath-xiaomi.png b/server/www/teleport/static/img/qrcode/xiaomi-oath-xiaomi.png new file mode 100644 index 0000000..fddbe7b Binary files /dev/null and b/server/www/teleport/static/img/qrcode/xiaomi-oath-xiaomi.png differ diff --git a/server/www/teleport/static/js/ui/auth/login.js b/server/www/teleport/static/js/ui/auth/login.js index c423a7d..9065ba9 100644 --- a/server/www/teleport/static/js/ui/auth/login.js +++ b/server/www/teleport/static/js/ui/auth/login.js @@ -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', ' 姝e湪杩涜韬唤璁よ瘉锛岃绋嶅...'); - - // 鍏堝垽鏂竴涓媍aptcha鏄惁姝g‘锛屽鏋滀笉姝g‘锛屾嫆缁濈櫥褰 - ywl.ajax_post_json('/auth/verify-captcha', {captcha: str_captcha}, - function (ret) { - if (ret.code === TPE_OK) { - // 楠岃瘉鎴愬姛 - hide_op_box(); - show_op_box('wait', ' 姝e湪鐧诲綍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', ' 姝e湪杩涜韬唤璁よ瘉锛岃绋嶅...'); + + // 鍏堝垽鏂竴涓媍aptcha鏄惁姝g‘锛屽鏋滀笉姝g‘锛屾嫆缁濈櫥褰 + 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', ' 姝e湪鐧诲綍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; +}; + diff --git a/server/www/teleport/static/js/ui/common.js b/server/www/teleport/static/js/ui/common.js index 995c965..e61e86e 100644 --- a/server/www/teleport/static/js/ui/common.js +++ b/server/www/teleport/static/js/ui/common.js @@ -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 = [ '
', - ' <%block name="extend_content" /> - ## - @@ -102,7 +105,6 @@ <%block name="extend_js"/> - diff --git a/server/www/teleport/view/user/personal.mako b/server/www/teleport/view/user/personal.mako new file mode 100644 index 0000000..4b6c8cd --- /dev/null +++ b/server/www/teleport/view/user/personal.mako @@ -0,0 +1,222 @@ +<%! + page_title_ = '涓汉涓績' + page_menu_ = ['personal'] + page_id_ = 'personal' +%> +<%inherit file="../page_base.mako"/> + +<%block name="extend_js"> + + + + +<%block name="breadcrumb"> + + + +<%block name="extend_css"> + + + +## Begin Main Body. + +
+ +
+ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
鐧诲綍鍚嶏細${user['name']}
濮撳悕锛${user['nick_name']}
閭锛-
鎵嬫満锛-
娉ㄥ唽鏃堕棿锛-
涓婃鐧诲綍鏃堕棿锛-
+
+ +
+
+ 褰撳墠瀵嗙爜: + +
+
+ 鏂板瘑鐮: + +
+
+ 閲嶅鏂板瘑鐮: + +
+ +
+ +
+

璇峰湪浣犵殑鎵嬫満涓婂畨瑁呰韩浠介獙璇佸櫒锛岀劧鍚庡湪楠岃瘉鍣ㄤ腑娣诲姞浣犵殑鐧诲綍璐﹀彿銆

+ +

鏄剧ず涓嬭浇鍦板潃

+ +
+

瑕侀獙璇佸凡缁忕粦瀹氱殑韬唤楠岃瘉鍣紝鍙湪涓嬮潰鐨勮緭鍏ユ涓緭鍏ラ獙璇佸櫒鍣ㄤ笂鏄剧ず鐨勫姩鎬侀獙璇佺爜锛岀劧鍚庣偣鍑婚獙璇併

+

濡傛灉楠岃瘉澶辫触锛岃娉ㄦ剰妫鏌ユ偍鐨勮韩浠介獙璇佸櫒鐨勬椂闂翠笌鏈嶅姟鍣ㄦ椂闂存槸鍚︿竴鑷达紝濡傛灉涓よ呮椂闂村亸宸秴杩囦袱鍒嗛挓鍒欐棤娉曢獙璇侀氳繃锛

+
+
+ 鍔ㄦ侀獙璇佺爜锛6浣嶆暟瀛楋級锛 + + +
+ +
+
+
+ +
+

瑕佸皢浣犵殑鐧诲綍璐﹀彿娣诲姞鍒拌韩浠介獙璇佸櫒涓紝璇风偣鍑讳笅闈㈢殑鈥滅粦瀹氳韩浠介獙璇佸櫒鈥濇寜閽

+

+ +

+ +
+
+
+ +
+ + + + + + +<%block name="extend_content"> + + diff --git a/version.in b/version.in index acf43b0..7622b60 100644 --- a/version.in +++ b/version.in @@ -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