diff --git a/BTPanel/__init__.py b/BTPanel/__init__.py index e0029756..ba6686fc 100644 --- a/BTPanel/__init__.py +++ b/BTPanel/__init__.py @@ -18,7 +18,6 @@ sys.path.insert(0,'class/') import public from flask import Flask,current_app,session,render_template,send_file,request,redirect,g,url_for,make_response,render_template_string,abort from flask_session import Session - try: from werkzeug.contrib.cache import SimpleCache except: @@ -33,7 +32,6 @@ app = Flask(__name__,template_folder="templates/" + public.GetConfigValue('templ Compress(app) sockets = Sockets(app) - import common import db import jobs @@ -150,7 +148,7 @@ admin_path_checks = [ ] if admin_path in admin_path_checks: admin_path = '/bt' -@app.route('/service_status',methods = method_get) +@app.route('/service_status',methods = method_all) def service_status(): return 'True' @@ -176,7 +174,7 @@ def webssh(ws): session['ssh_obj'].run(ws,session['ssh_info']) -@app.route('/term_open',methods=method_all) +@app.route('/term_open',methods=method_get) def term_open(): comReturn = comm.local() if comReturn: return comReturn @@ -212,9 +210,18 @@ def reload_mod(): @app.before_request def request_check(): - #if not public.path_safe_check(request.path): return abort(404) + #路由和URI长度过滤 + if len(request.path) > 64: return abort(403) + if len(request.url) > 256: return abort(403) if request.path in ['/service_status']: return + #POST参数过滤 + if request.path in ['/login','/safe','/hook','/public','/down','/get_app_bind_status','/check_bind']: + pdata = request.form.to_dict() + for k in pdata.keys(): + if len(k) > 32: return abort(403) + if len(pdata[k]) > 128: return abort(403) + if not request.path in ['/safe','/hook','/public','/mail_sys','/down']: ip_check = public.check_ip_panel() if ip_check: return ip_check @@ -291,6 +298,29 @@ def login(): is_auth_path = False if admin_path != '/bt' and os.path.exists(admin_path_file) and not 'admin_auth' in session: is_auth_path = True + + #登录输入验证 + if request.method == method_post[0]: + v_list = ['username','password','code','vcode','cdn_url'] + for v in v_list: + pv = request.form.get(v,'').strip() + if v == 'cdn_url': + if len(pv) > 32: return public.returnMsg(False,'Wrong parameter length!') + continue + + if not pv: continue + p_len = 32 + if v == 'code': p_len = 4 + if v == 'vcode': p_len = 6 + if len(pv) != p_len: + return public.returnJson(False,'Wrong parameter length'),json_header + if not re.match(r"^\w+$",pv): + return public.returnJson(False,'Wrong parameter format'),json_header + + for n in request.form.keys(): + if not n in v_list: + return public.returnJson(False,'You cannot have extra parameters in the login parameters'),json_header + get = get_input() import userlogin if hasattr(get,'tmp_token'): @@ -840,6 +870,10 @@ def panel_public(): if panelWaf_data.is_xss(get.__dict__):return 'ERROR' except: pass + + if len("{}".format(get.__dict__)) > 1024 * 32: + return 'ERROR' + get.client_ip = public.GetClientIp() if not hasattr(get,'name'): get.name = '' if not hasattr(get,'fun'): return abort(404) @@ -1093,6 +1127,8 @@ def download(): comReturn = comm.local() if comReturn: return comReturn filename = request.args.get('filename') + if filename.find('|') != -1: + filename = filename.split('|')[1] if not filename: return public.ReturnJson(False,"INIT_ARGS_ERR"),json_header if filename in ['alioss','qiniu','upyun','txcos','ftp']: return panel_cloud() if filename in ['gdrive','gcloud_storage']: return "Google storage products do not currently support downloads" @@ -1210,17 +1246,24 @@ def panel_cloud(): comReturn = comm.local() if comReturn: return comReturn get = get_input() - if not os.path.exists('plugin/' + get.filename + '/' + get.filename+'_main.py'): - return public.returnJson(False,'INIT_PLUGIN_NOT_EXISTS'),json_header - sys.path.append('plugin/' + get.filename) - plugin_main = __import__(get.filename+'_main') - reload(plugin_main) - tmp = eval("plugin_main.%s_main()" % get.filename) - if not hasattr(tmp,'download_file'): return public.returnJson(False,'INIT_PLUGIN_NOT_DOWN_FUN'),json_header - if get.filename == 'ftp': - download_url = tmp.getFile(get.name) + _filename = get.filename + plugin_name = "" + if _filename.find('|') != -1: + plugin_name = get.filename.split('|')[1] + else: + plugin_name = get.filename + + if not os.path.exists('plugin/' + plugin_name + '/' + plugin_name+'_main.py'): + return public.returnJson(False,'INIT_PLUGIN_NOT_EXISTS'),json_header + sys.path.append('plugin/' + plugin_name) + plugin_main = __import__(plugin_name+'_main') + public.mod_reload(plugin_main) + tmp = eval("plugin_main.%s_main()" % plugin_name) + if not hasattr(tmp,'download_file'): return public.returnJson(False,'INIT_PLUGIN_NOT_DOWN_FUN'),json_header + download_url = tmp.download_file(get.name) + if plugin_name == 'ftp': + if download_url.find("ftp") != 0:download_url = "ftp://" + download_url else: - download_url = tmp.download_file(get.name) if download_url.find('http') != 0:download_url = 'http://' + download_url return redirect(download_url) diff --git a/BTPanel/static/css/site.css b/BTPanel/static/css/site.css index 42a099d6..6ae062e5 100644 --- a/BTPanel/static/css/site.css +++ b/BTPanel/static/css/site.css @@ -4925,11 +4925,11 @@ select[disabled]{ ::-webkit-scrollbar { /*滚动条整体样式*/ width : 10px; /*高宽分别对应横竖滚动条的尺寸*/ - height: 10px; + height: 5px; } ::-webkit-scrollbar-thumb { /*滚动条里面小方块*/ - border-radius: 0; + border-radius: 10px; box-shadow : inset 0 0 5px rgba(0, 0, 0, 0.2); background : #999; } @@ -5382,7 +5382,7 @@ select[disabled]{ .ace_catalogue_list::-webkit-scrollbar { /*滚动条整体样式*/ width: 9px; /*高宽分别对应横竖滚动条的尺寸*/ - height: 10px; + height: 1px; } .ace_catalogue_list::-webkit-scrollbar-thumb { /*滚动条里面小方块*/ @@ -5465,19 +5465,20 @@ select[disabled]{ } .ace_scrollbar::-webkit-scrollbar { /*滚动条整体样式*/ - width : 10px; /*高宽分别对应横竖滚动条的尺寸*/ + width : 15px; /*高宽分别对应横竖滚动条的尺寸*/ height: 10px; } .ace_scrollbar::-webkit-scrollbar-thumb { /*滚动条里面小方块*/ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); background: #777; + border-radius: 0; } .ace_scrollbar::-webkit-scrollbar-track{ /*滚动条里面轨道*/ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); background: #333; - border-radius: 10px; + border-radius: 0; } .ace_editors.active { display: block; @@ -6768,7 +6769,7 @@ select[disabled]{ .dropUpLoadFile::-webkit-scrollbar { /*滚动条整体样式*/ width : 15px; /*高宽分别对应横竖滚动条的尺寸*/ - height: 10px; + height: 1px; } .dropUpLoadFile::-webkit-scrollbar-thumb { /*滚动条里面小方块*/ diff --git a/BTPanel/templates/default/login.html b/BTPanel/templates/default/login.html index 6678acbc..fece54b9 100644 --- a/BTPanel/templates/default/login.html +++ b/BTPanel/templates/default/login.html @@ -9,6 +9,46 @@ {{g.title}} +
@@ -30,6 +70,14 @@
+ {% if data['hosts'] != '[]' %} + + Acceleration node + + + {% endif %} {{data['lan']['N13']}} @@ -59,7 +107,7 @@ - + + \ No newline at end of file diff --git a/class/acme_v2.py b/class/acme_v2.py index 7c3b5bdd..0b0f533a 100644 --- a/class/acme_v2.py +++ b/class/acme_v2.py @@ -403,6 +403,9 @@ class acme_v2: write_log("|-Verify the dir:{}".format(acme_path)) if not os.path.exists(acme_path): return True public.ExecShell("rm -f {}/*".format(acme_path)) + acme_path = '/www/server/stop/.well-known/acme-challenge' + if os.path.exists(acme_path): + public.ExecShell("rm -f {}/*".format(acme_path)) # 写验证文件 def write_auth_file(self, auth_to, token, acme_keyauthorization): @@ -414,9 +417,19 @@ class acme_v2: wellknown_path = '{}/{}'.format(acme_path, token) public.writeFile(wellknown_path, acme_keyauthorization) public.set_own(wellknown_path, 'www') + + acme_path = '/www/server/stop/.well-known/acme-challenge' + if not os.path.exists(acme_path): + os.makedirs(acme_path) + public.set_own(acme_path, 'www') + wellknown_path = '{}/{}'.format(acme_path,token) + public.writeFile(wellknown_path,acme_keyauthorization) + public.set_own(wellknown_path, 'www') return True except: - raise Exception("Writing verification file failed: {}".format(public.get_error_info())) + err = public.get_error_info() + print(err) + raise Exception("Writing verification file failed: {}".format(err)) # 解析域名 def create_dns_record(self, auth_to, domain, dns_value): @@ -574,6 +587,8 @@ class acme_v2: return "The verification timed out. Please check if the domain name is resolved correctly. If it is resolved correctly, the connection between the server and Let'sEncrypt may be abnormal. Please try again later!" elif error.find('Cannot issue for') != -1: return "Cannot issue a certificate for {}, cannot apply for a wildcard certificate with a domain name suffix directly!".format(re.findall(r'for\s+"(.+)"',error)) + elif error.find('too many failed authorizations recently'): + return 'The account has more than 5 failed orders within 1 hour, please wait 1 hour and try again!' elif error.find("Error creating new order") != -1: return "Order creation failed, please try again later!" elif error.find("Too Many Requests") != -1: @@ -1257,12 +1272,25 @@ fullchain.pem Paste into certificate input box import panelSite s = panelSite.panelSite() if args.auth_type in ['http','tls']: - if not 'siteName' in args: - args.siteName = public.M('sites').where('id=?',(args.id,)).getField('name') - args.sitename = args.siteName - if s.GetRedirectList(args): return public.returnMsg(False, 'SITE_SSL_ERR_301') - if s.GetProxyList(args): return public.returnMsg(False,'Sites with reverse proxy turned on cannot apply for SSL!') - + try: + if not 'siteName' in args: + args.siteName = public.M('sites').where('id=?',(args.id,)).getField('name') + args.sitename = args.siteName + data = s.GetRedirectList(args) + if type(data) == list: + for x in data: + if x['type']: return public.returnMsg(False, 'SITE_SSL_ERR_301') + data = s.GetProxyList(args) + if type(data) == list: + for x in data: + if s.GetProxyList(args): return public.returnMsg(False, + 'Sites with reverse proxy turned on cannot apply for SSL!') + + #判断是否强制HTTPS + if s.IsToHttps(args.siteName): + return public.returnMsg(False, 'After configuring Force HTTPS, you cannot use [File Verification] to apply for a certificate!') + except: + return False else: if args.auth_to.find('Dns_com') != -1: if not os.path.exists('plugin/dns/dns_main.py'): @@ -1317,10 +1345,9 @@ fullchain.pem Paste into certificate input box if 'cert' in self._config['orders'][i]: self._config['orders'][i]['cert_timeout'] = self._config['orders'][i]['cert']['cert_timeout'] if not 'cert_timeout' in self._config['orders'][i]: - continue + self._config['orders'][i]['cert_timeout'] = int(time.time()) if self._config['orders'][i]['cert_timeout'] > s_time or self._config['orders'][i]['auth_to'] == 'dns': continue - write_log(self._config['orders'][i]['cert_timeout']) order_index.append(i) if not order_index: @@ -1335,26 +1362,30 @@ fullchain.pem Paste into certificate input box write_log("|-Renewing certificate number of {},domain: {}..".format(n, self._config['orders'][index]['domains'])) write_log("|-Creating order...") - index = self.create_order( - self._config['orders'][index]['domains'], - self._config['orders'][index]['auth_type'], - self._config['orders'][index]['auth_to'], - index - ) - write_log("|-Getting verification information..") - self.get_auths(index) - write_log("|-Verifying domain name..") - self.auth_domain(index) - write_log("|-Sending CSR..") - self.remove_dns_record() - self.send_csr(index) - write_log("|-Downloading certificate..") - cert = self.download_cert(index) - self._config['orders'][index]['renew_time'] = int(time.time()) - self.save_config() - cert['status'] = True - cert['msg'] = 'Renewed successfully!' - write_log("|-Renewed successfully!") + try: + index = self.create_order( + self._config['orders'][index]['domains'], + self._config['orders'][index]['auth_type'], + self._config['orders'][index]['auth_to'], + index + ) + write_log("|-Getting verification information..") + self.get_auths(index) + write_log("|-Verifying domain name..") + self.auth_domain(index) + write_log("|-Sending CSR..") + self.remove_dns_record() + self.send_csr(index) + write_log("|-Downloading certificate..") + cert = self.download_cert(index) + self._config['orders'][index]['renew_time'] = int(time.time()) + self.save_config() + cert['status'] = True + cert['msg'] = 'Renewed successfully!' + write_log("|-Renewed successfully!") + except Exception as e: + write_log("|-" + str(e).split('>>>>')[0]) + write_log("-" * 70) return cert except Exception as ex: self.remove_dns_record() diff --git a/class/common.py b/class/common.py index c2269524..85927a28 100644 --- a/class/common.py +++ b/class/common.py @@ -34,7 +34,7 @@ class panelSetup: ua = ua.lower() if ua.find('spider') != -1 or ua.find('bot') != -1: return redirect('https://www.google.com') - g.version = '6.7.5' + g.version = '6.7.6' g.title = public.GetConfigValue('title') g.uri = request.path if not os.path.exists('data/debug.pl'): diff --git a/class/config.py b/class/config.py index 04f563e9..349d7305 100644 --- a/class/config.py +++ b/class/config.py @@ -59,7 +59,7 @@ class config: #添加接受邮件地址 def add_mail_address(self, get): if not hasattr(get, 'email'): return public.returnMsg(False, 'Please input your email') - emailformat = re.compile('[a-zA-Z0-9.-_+%]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+') + emailformat = re.compile(r'[a-zA-Z0-9.-_+%]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+') if not emailformat.search(get.email): return public.returnMsg(False, 'Please enter your vaild email') # 测试发送邮件 if get.email.strip() in self.__mail_list: return public.returnMsg(True, 'Email already exists') @@ -96,7 +96,7 @@ class config: # 用户自定义邮件发送 def user_stmp_mail_send(self, get): if not (hasattr(get, 'email')): return public.returnMsg(False, 'Please fill in the email address') - emailformat = re.compile('[a-zA-Z0-9.-_+%]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+') + emailformat = re.compile(r'[a-zA-Z0-9.-_+%]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+') if not emailformat.search(get.email): return public.returnMsg(False, 'Please enter your vaild email') # 测试发送邮件 if not get.email.strip() in self.__mail_list: return public.returnMsg(True, 'The mailbox does not exist, please add it to the mailbox list') @@ -173,7 +173,7 @@ class config: def setPassword(self,get): if get.password1 != get.password2: return public.returnMsg(False,'USER_PASSWORD_CHECK') if len(get.password1) < 5: return public.returnMsg(False,'USER_PASSWORD_LEN') - public.M('users').where("username=?",(session['username'],)).setField('password',public.md5(get.password1.strip())) + public.M('users').where("username=?",(session['username'],)).setField('password',public.password_salt(public.md5(get.password1.strip()),username=session['username'])) public.WriteLog('TYPE_PANEL','USER_PASSWORD_SUCCESS',(session['username'],)) self.reload_session() return public.returnMsg(True,'USER_PASSWORD_SUCCESS') @@ -189,7 +189,7 @@ class config: #取用户列表 def get_users(self,args): - data = public.M('users').field('username').select() + data = public.M('users').field('id,username').select() return data # 创建新用户 @@ -199,7 +199,7 @@ class config: if len(args.password) < 8: return public.returnMsg(False,'Password must be at least 8 characters') pdata = { "username": args.username.strip(), - "password": public.md5(args.password.strip()) + "password": public.password_salt(public.md5(args.password.strip()),username=args.username.strip()) } if(public.M('users').where('username=?',(pdata['username'],)).count()): @@ -233,7 +233,7 @@ class config: if 'password' in args: if args.password: if len(args.password) < 8: return public.returnMsg(False,'Password must be at least 8 characters') - pdata['password'] = public.md5(args.password.strip()) + pdata['password'] = public.password_salt(public.md5(args.password.strip()),username=username) if(public.M('users').where('id=?',(args.id,)).update(pdata)): public.WriteLog('User Management',"Edit user{}".format(username)) @@ -651,8 +651,8 @@ class config: #设置面板SSL def SetPanelSSL(self,get): if hasattr(get,"email"): - # rep_mail = "^[a-zA-Z0-9_-\.]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$" - rep_mail = "[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?" + #rep_mail = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$" + rep_mail = r"[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?" if not re.search(rep_mail,get.email): return public.returnMsg(False,'The E-Mail format is illegal') import setPanelLets diff --git a/class/database.py b/class/database.py index 81246b14..4be7bf51 100644 --- a/class/database.py +++ b/class/database.py @@ -827,13 +827,15 @@ SetLink try: if data[0] == 1045: return public.returnMsg(False,'MYSQL_PASS_ERR') - except:pass - for d in data: - for g in gets: - try: - if d[0] == g: result[g] = d[1] - except: - pass + for d in data: + for g in gets: + try: + if d[0] == g: result[g] = d[1] + except: + pass + except: + return public.returnMsg(False,str(data)) + if not 'Run' in result and result: result['Run'] = int(time.time()) - int(result['Uptime']) tmp = panelMysql.panelMysql().query('show master status') diff --git a/class/files.py b/class/files.py index 87550610..ebf0b6df 100644 --- a/class/files.py +++ b/class/files.py @@ -693,6 +693,10 @@ session.save_handler = files'''.format(path, sess_path, sess_path) try: tmp = {} fname = rPath + file + if sys.version_info[0] == 2: + fname = fname.encode('utf-8') + else: + fname.encode('utf-8') tmp1 = file.split('_bt_') tmp2 = tmp1[len(tmp1)-1].split('_t_') tmp['rname'] = file diff --git a/class/jobs.py b/class/jobs.py index bfb81f28..f2ed271f 100644 --- a/class/jobs.py +++ b/class/jobs.py @@ -74,9 +74,14 @@ def control_init(): public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sType' TEXT",()) public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'urladdress' TEXT",()) - public.M('users').where('email=? or email=?',('287962566@qq.com','amw_287962566@qq.com')).setField('email','test@message.com') + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'users','%salt%')).count(): + public.M('users').execute("ALTER TABLE 'users' ADD 'salt' TEXT",()) + + public.chdck_salt() + + filename = '/www/server/nginx/off' if os.path.exists(filename): os.remove(filename) diff --git a/class/panelBackup.py b/class/panelBackup.py index fe161269..d65c8b4f 100644 --- a/class/panelBackup.py +++ b/class/panelBackup.py @@ -19,7 +19,7 @@ import time os.chdir('/www/server/panel') sys.path.insert(0,'class/') import public -_VERSION = 1.4 +_VERSION = 1.5 class backup: _path = None @@ -323,7 +323,7 @@ class backup: #清理多余备份 if not self._cloud: - backups = public.M('backup').where("type=? and pid=? and filename LIKE '%/%'",('0',pid)).field('id,name,filename').select() + backups = public.M('backup').where("type=? and pid=? and filename NOT LIKE '%|%'",('0',pid)).field('id,name,filename').select() else: backups = public.M('backup').where('type=? and pid=? and filename LIKE "%{}%"'.format(self._cloud._name),('0',pid)).field('id,name,filename').select() @@ -449,7 +449,7 @@ class backup: #清理多余备份 if not self._cloud: - backups = public.M('backup').where("type=? and pid=? and filename LIKE '%/%'",('1',pid)).field('id,name,filename').select() + backups = public.M('backup').where("type=? and pid=? and filename NOT LIKE '%|%'",('1',pid)).field('id,name,filename').select() else: backups = public.M('backup').where('type=? and pid=? and filename LIKE "%{}%"'.format(self._cloud._name),('1',pid)).field('id,name,filename').select() self.delete_old(backups,save,'database') diff --git a/class/panelPlugin.py b/class/panelPlugin.py index 7aaadb65..fcefe855 100644 --- a/class/panelPlugin.py +++ b/class/panelPlugin.py @@ -128,8 +128,9 @@ class panelPlugin: if os.path.exists(p_node): if len(public.readFile(p_node)) < 100: os.remove(p_node) if not pluginInfo: return public.returnMsg(False, 'INIT_PLUGIN_NOT_EXISTS') + self.mutex_title = pluginInfo['mutex'] if not self.check_mutex(pluginInfo['mutex']): return public.returnMsg(False, 'UNINSTALL_FIRST', - (pluginInfo['mutex'],)) + (self.mutex_title,)) if not hasattr(get, 'id'): if not self.check_dependnet(pluginInfo['dependnet']): return public.returnMsg(False, 'DEP_PAGE', (pluginInfo['dependnet'],)) @@ -143,7 +144,15 @@ class panelPlugin: return public.returnMsg(False,'At least [{0}] CPU cores are required to install'.format(versionInfo['cpu_limit'])) if not self.check_mem_limit(versionInfo['mem_limit']): return public.returnMsg(False,'At least [{0} MB] memory is required to install'.format(versionInfo['mem_limit'])) - + if not self.check_os_limit(versionInfo['os_limit']): + m_ps = {0: "All", 1: "Centos", 2: "Ubuntu/Debian"} + return public.returnMsg(False, '仅支持[%s]系统' % m_ps[int(versionInfo['os_limit'])]) + if not hasattr(get, 'id'): + if not self.check_dependnet(versionInfo['dependnet']): return public.returnMsg(False, + 'Depend on the following software, please install first [%s]' % + versionInfo[ + 'dependnet']) + #安装插件 def install_plugin(self,get): if not self.check_sys_write(): return public.returnMsg(False,'CANT_WRITE_SYS_DIR') @@ -368,7 +377,7 @@ class panelPlugin: if expire_day > 15: return False if pm.is_level(level): #是否忽略 if level != name: #到期还是即将到期 - msg_last = '您的【{}】授权还有{}天到期'.format(title,int(expire_day)) + msg_last = '您的【{}】授权还有{}天到期'.format(title,int(expire_day) + 1) else: msg_last = '您的【{}】授权已到期'.format(title) pl_msg = 'true' @@ -1119,14 +1128,17 @@ class panelPlugin: #下载图标 def download_icon(self,name,iconFile,downFile): srcIcon = 'plugin/' + name + '/icon.png' + skey = name+'_icon' + if cache.get(skey): return None if os.path.exists(srcIcon): - public.ExecShell("\cp -a -r " + srcIcon + " " + iconFile) + public.ExecShell(r"\cp -a -r " + srcIcon + " " + iconFile) else: if downFile: public.ExecShell('wget -O ' + iconFile + ' ' + public.GetConfigValue('home') + downFile + '&') else: public.ExecShell('wget -O ' + iconFile + ' ' + public.get_url() + '/install/plugin/' + name + '/icon.png &') - + cache.set(skey,1,86400) + #取分页 def GetPage(self,data,get): diff --git a/class/panelSite.py b/class/panelSite.py index 32d59768..e5c34de9 100644 --- a/class/panelSite.py +++ b/class/panelSite.py @@ -409,7 +409,7 @@ include /www/server/panel/vhost/openlitespeed/proxy/BTSITENAME/*.conf #表单验证 if not files.files().CheckDir(self.sitePath) or not self.__check_site_path(self.sitePath): return public.returnMsg(False,'PATH_ERROR') if len(self.phpVersion) < 2: return public.returnMsg(False,'SITE_ADD_ERR_PHPEMPTY') - reg = "^([\w\-\*]{1,100}\.){1,4}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$" + reg = r"^([\w\-\*]{1,100}\.){1,4}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$" if not re.match(reg, self.siteName): return public.returnMsg(False,'SITE_ADD_ERR_DOMAIN') if self.siteName.find('*') != -1: return public.returnMsg(False,'SITE_ADD_ERR_DOMAIN_TOW') @@ -2501,7 +2501,7 @@ server #创建basedir userIni = Path + '/.user.ini' if os.path.exists(userIni): public.ExecShell("chattr -i "+userIni) - public.writeFile(userIni, 'open_basedir='+Path+'/:/tmp/:/proc/') + public.writeFile(userIni, 'open_basedir='+Path+'/:/tmp/') public.ExecShell('chmod 644 ' + userIni) public.ExecShell('chown root:root ' + userIni) public.ExecShell('chattr +i '+userIni) @@ -2677,6 +2677,7 @@ server # sitename = path.split('/')[-1] f = "/www/server/panel/vhost/openlitespeed/detail/{}.conf".format(sitename) c = public.readFile(f) + if not c: return False if f: rep = '\nphp_admin_value\s*open_basedir.*' result = re.search(rep, c) diff --git a/class/public.py b/class/public.py index 17fb4720..31c4f43a 100644 --- a/class/public.py +++ b/class/public.py @@ -21,16 +21,25 @@ if sys.version_info[0] == 2: sys.setdefaultencoding('utf8') def M(table): + """ + @name 访问面板数据库 + @author hwliang + @table 被访问的表名(必需) + @return db.Sql object + + ps: 默认访问data/default.db + """ import db sql = db.Sql() return sql.table(table) def HttpGet(url,timeout = 6,headers = {}): """ - 发送GET请求 - @url 被请求的URL地址(必需) - @timeout 超时时间默认60秒 - return string + @name 发送GET请求 + @author hwliang + @url 被请求的URL地址(必需) + @timeout 超时时间默认60秒 + @return string """ if is_local(): return False home = 'www.bt.cn' @@ -53,6 +62,16 @@ def HttpGet(url,timeout = 6,headers = {}): return s_body def http_get_home(url,timeout,ex): + """ + @name Get方式使用优选节点访问官网 + @author hwliang + @param url 当前官网URL地址 + @param timeout 用于测试超时时间 + @param ex 上一次错误的响应内容 + @return string 响应内容 + + 如果已经是优选节点,将直接返回ex + """ try: home = 'www.bt.cn' if url.find(home) == -1: return ex @@ -72,6 +91,12 @@ def http_get_home(url,timeout,ex): def set_home_host(host): + """ + @name 设置官网hosts + @author hwliang + @param host IP地址 + @return void + """ ExecShell('sed -i "/www.bt.cn/d" /etc/hosts') ExecShell("echo '' >> /etc/hosts") ExecShell("echo '%s www.bt.cn' >> /etc/hosts" % host) @@ -82,11 +107,11 @@ def httpGet(url,timeout=6): def HttpPost(url,data,timeout = 6,headers = {}): """ - 发送POST请求 - @url 被请求的URL地址(必需) - @data POST参数,可以是字符串或字典(必需) - @timeout 超时时间默认60秒 - return string + 发送POST请求 + @url 被请求的URL地址(必需) + @data POST参数,可以是字符串或字典(必需) + @timeout 超时时间默认60秒 + return string """ if is_local(): return False home = 'www.bt.cn' @@ -110,7 +135,18 @@ def HttpPost(url,data,timeout = 6,headers = {}): return s_body -def http_post_home(url, data, timeout, ex): +def http_post_home(url,data,timeout,ex): + """ + @name POST方式使用优选节点访问官网 + @author hwliang + @param url(string) 当前官网URL地址 + @param data(dict) POST数据 + @param timeout(int) 用于测试超时时间 + @param ex(string) 上一次错误的响应内容 + @return string 响应内容 + + 如果已经是优选节点,将直接返回ex + """ try: home = 'www.bt.cn' if url.find(home) == -1: return ex @@ -128,17 +164,26 @@ def http_post_home(url, data, timeout, ex): return ex except: return ex -def httpPost(url, data, timeout=6): - return HttpPost(url, data, timeout) +def httpPost(url,data,timeout=6): + """ + @name 发送POST请求 + @author hwliang + @param url 被请求的URL地址(必需) + @param data POST参数,可以是字符串或字典(必需) + @param timeout 超时时间默认60秒 + @return string + """ + return HttpPost(url,data,timeout) def check_home(): return True def Md5(strings): """ - 生成MD5 - @strings 要被处理的字符串 - return string(32) + @name 生成MD5 + @author hwliang + @param strings 要被处理的字符串 + @return string(32) """ if type(strings) == str: strings = strings.encode() @@ -152,9 +197,10 @@ def md5(strings): def FileMd5(filename): """ - 生成文件的MD5 - @filename 文件名 - return string(32) or False + @name 生成文件的MD5 + @author hwliang + @param filename 文件名 + @return string(32) or False """ if not os.path.isfile(filename): return False import hashlib @@ -171,9 +217,10 @@ def FileMd5(filename): def GetRandomString(length): """ - 取随机字符串 - @length 要获取的长度 - return string(length) + @name 取随机字符串 + @author hwliang + @param length 要获取的长度 + @return string(length) """ from random import Random strings = '' @@ -186,17 +233,32 @@ def GetRandomString(length): def ReturnJson(status,msg,args=()): """ - 取通用Json返回 - @status 返回状态 - @msg 返回消息 - return string(json) + @name 取通用Json返回 + @author hwliang + @param status 返回状态 + @param msg 返回消息 + @return string(json) """ return GetJson(ReturnMsg(status, msg, args)) def returnJson(status,msg,args=()): + """ + @name 取通用Json返回 + @author hwliang + @param status 返回状态 + @param msg 返回消息 + @return string(json) + """ return ReturnJson(status,msg,args) def ReturnMsg(status,msg,args = ()): + """ + @name 取通用dict返回 + @author hwliang + @param status 返回状态 + @param msg 返回消息 + @return dict {"status":bool,"msg":string} + """ log_message = json.loads(ReadFile('BTPanel/static/language/' + GetLanguage() + '/public.json')) keys = log_message.keys() if type(msg) == str: @@ -208,11 +270,23 @@ def ReturnMsg(status,msg,args = ()): return {'status':status,'msg':msg} def returnMsg(status,msg,args = ()): + """ + @name 取通用dict返回 + @author hwliang + @param status 返回状态 + @param msg 返回消息 + @return dict {"status":bool,"msg":string} + """ return ReturnMsg(status,msg,args) def GetFileMode(filename): - '''取文件权限''' + """ + @name 取文件权限字符串 + @author hwliang + @param filename 文件全路径 + @return string 如:644/777/755 + """ stat = os.stat(filename) accept = str(oct(stat.st_mode)[-3:]) return accept @@ -1332,7 +1406,8 @@ def set_own(filename, user, group=None): #校验路径安全 def path_safe_check(path,force=True): - checks = ['..','./','\\','%','$','^','&','*','~','#','"',"'",';','|','{','}','`'] + if len(path) > 256: return False + checks = ['..','./','\\','%','$','^','&','*','~','"',"'",';','|','{','}','`'] for c in checks: if path.find(c) != -1: return False if force: @@ -1573,22 +1648,22 @@ def en_hexb(data): if type(result) != str: result = result.decode('utf-8') return result -def upload_file_url(filename): - try: - if os.path.exists(filename): - data = ExecShell('/usr/bin/curl https://scanner.baidu.com/enqueue -F archive=@%s' % filename) - data = json.loads(data[0]) - time.sleep(1) - import requests - default_headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' - } - data_list = requests.get(url=data['url'], headers=default_headers, verify=False) - return (data_list.json()) - else: - return False - except: - return False +# def upload_file_url(filename): +# try: +# if os.path.exists(filename): +# data = ExecShell('/usr/bin/curl https://scanner.baidu.com/enqueue -F archive=@%s' % filename) +# data = json.loads(data[0]) +# time.sleep(1) +# import requests +# default_headers = { +# 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' +# } +# data_list = requests.get(url=data['url'], headers=default_headers, verify=False) +# return (data_list.json()) +# else: +# return False +# except: +# return False #直接请求到PHP-FPM #version php版本 @@ -1764,6 +1839,46 @@ def ip2long(ip): iplong = 2 ** 24 * int(ips[0]) + 2 ** 16 * int(ips[1]) + 2 ** 8 * int(ips[2]) + int(ips[3]) return iplong +#获取sessionid +def get_session_id(): + from BTPanel import request + return request.cookies.get('BT_PANEL_6') + +def chdck_salt(): + ''' + @name 检查所有用户密码是否加盐,若没有则自动加上 + @author hwliang<2020-07-08> + @return void + ''' + + if not M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'users','%salt%')).count(): + M('users').execute("ALTER TABLE 'users' ADD 'salt' TEXT",()) + u_list = M('users').where('salt is NULL',()).field('id,username,password,salt').select() + for u_info in u_list: + salt = GetRandomString(12) #12位随机 + pdata = {} + pdata['password'] = md5(md5(u_info['password']+'_bt.cn') + salt) + pdata['salt'] = salt + M('users').where('id=?',(u_info['id'],)).update(pdata) + + +def password_salt(password,username=None,uid=None): + ''' + @name 为指定密码加盐 + @author hwliang<2020-07-08> + @param password string(被md5加密一次的密码) + @param username string(用户名) 可选 + @param uid int(uid) 可选 + @return string + ''' + chdck_salt() + if not uid: + if not username: + raise Exception('username或uid必需传一项') + uid = M('users').where('username=?',(username,)).getField('id') + salt = M('users').where('id=?',(uid,)).getField('salt') + return md5(md5(password+'_bt.cn')+salt) + #取通用对象 class dict_obj: def __contains__(self, key): diff --git a/class/system.py b/class/system.py index 9106ad3f..a2a73bc0 100644 --- a/class/system.py +++ b/class/system.py @@ -50,54 +50,74 @@ class system: serviceName = 'nginx' tmp['setup'] = False phpversion = "54" - phpport = '888'; - pstatus = False; - pauth = False; + phpport = '888' + pstatus = False + pauth = False if os.path.exists(self.setupPath+'/nginx'): data['webserver'] = 'nginx' serviceName = 'nginx' - tmp['setup'] = os.path.exists(self.setupPath +'/nginx/sbin/nginx'); - configFile = self.setupPath + '/nginx/conf/nginx.conf'; + tmp['setup'] = os.path.exists(self.setupPath +'/nginx/sbin/nginx') + configFile = self.setupPath + '/nginx/conf/nginx.conf' try: if os.path.exists(configFile): - conf = public.readFile(configFile); - rep = "listen\s+([0-9]+)\s*;"; - rtmp = re.search(rep,conf); + conf = public.readFile(configFile) + rep = "listen\s+([0-9]+)\s*;" + rtmp = re.search(rep,conf) if rtmp: - phpport = rtmp.groups()[0]; + phpport = rtmp.groups()[0] if conf.find('AUTH_START') != -1: pauth = True; if conf.find(self.setupPath + '/stop') == -1: pstatus = True; - configFile = self.setupPath + '/nginx/conf/enable-php.conf'; - conf = public.readFile(configFile); - rep = "php-cgi-([0-9]+)\.sock"; - rtmp = re.search(rep,conf); + configFile = self.setupPath + '/nginx/conf/enable-php.conf' + conf = public.readFile(configFile) + rep = "php-cgi-([0-9]+)\.sock" + rtmp = re.search(rep,conf) if rtmp: - phpversion = rtmp.groups()[0]; + phpversion = rtmp.groups()[0] except: pass; elif os.path.exists(self.setupPath+'/apache'): data['webserver'] = 'apache' serviceName = 'httpd' - tmp['setup'] = os.path.exists(self.setupPath +'/apache/bin/httpd'); - configFile = self.setupPath + '/apache/conf/extra/httpd-vhosts.conf'; + tmp['setup'] = os.path.exists(self.setupPath +'/apache/bin/httpd') + configFile = self.setupPath + '/apache/conf/extra/httpd-vhosts.conf' try: if os.path.exists(configFile): - conf = public.readFile(configFile); - rep = "php-cgi-([0-9]+)\.sock"; - rtmp = re.search(rep,conf); + conf = public.readFile(configFile) + rep = "php-cgi-([0-9]+)\.sock" + rtmp = re.search(rep,conf) if rtmp: - phpversion = rtmp.groups()[0]; - rep = "Listen\s+([0-9]+)\s*\n"; - rtmp = re.search(rep,conf); + phpversion = rtmp.groups()[0] + rep = "Listen\s+([0-9]+)\s*\n" + rtmp = re.search(rep,conf) if rtmp: - phpport = rtmp.groups()[0]; + phpport = rtmp.groups()[0] if conf.find('AUTH_START') != -1: pauth = True; if conf.find(self.setupPath + '/stop') == -1: pstatus = True; except: pass - + elif os.path.exists('/usr/local/lsws/bin/lswsctrl'): + data['webserver'] = 'openlitespeed' + serviceName = 'openlitespeed' + tmp['setup'] = os.path.exists(self.setupPath +'/apache/bin/httpd') + configFile = '/usr/local/lsws/bin/lswsctrl' + try: + if os.path.exists(configFile): + conf = public.readFile('/www/server/panel/vhost/openlitespeed/detail/phpmyadmin.conf') + rep = "/usr/local/lsws/lsphp(\d+)/bin/lsphp" + rtmp = re.search(rep,conf) + if rtmp: + phpversion = rtmp.groups()[0] + conf = public.readFile('/www/server/panel/vhost/openlitespeed/listen/888.conf') + rep = "address\s+\*\:(\d+)" + rtmp = re.search(rep,conf) + if rtmp: + phpport = rtmp.groups()[0] + if conf.find('AUTH_START') != -1: pauth = True + if conf.find(self.setupPath + '/stop') == -1: pstatus = True + except: + pass tmp['type'] = data['webserver'] tmp['version'] = public.readFile(self.setupPath + '/'+data['webserver']+'/version.pl'); diff --git a/class/userlogin.py b/class/userlogin.py index 2ad2154e..2af3a70d 100644 --- a/class/userlogin.py +++ b/class/userlogin.py @@ -7,7 +7,7 @@ # | Author: hwliang # +------------------------------------------------------------------- -import public,os,sys,db,time,json +import public,os,sys,db,time,json,re from BTPanel import session,cache,json_header from flask import request,redirect,g @@ -19,14 +19,13 @@ class userlogin: self.error_num(False) if self.limit_address('?') < 1: return public.returnJson(False,'LOGIN_ERR_LIMIT'),json_header - post.username = post.username.strip() - password = public.md5(post.password.strip()) + sql = db.Sql() - user_list = sql.table('users').field('id,username,password').select() + user_list = sql.table('users').field('id,username,password,salt').select() userInfo = None for u_info in user_list: - if u_info['username'] == post.username: + if public.md5(u_info['username']) == post.username: userInfo = u_info if 'code' in session: if session['code'] and not 'is_verify_password' in session: @@ -35,14 +34,18 @@ class userlogin: public.WriteLog('TYPE_LOGIN','LOGIN_ERR_CODE',('****','****',public.GetClientIp())) return public.returnJson(False,'CODE_ERR'),json_header try: - s_pass = public.md5(public.md5(userInfo['password'] + '_bt.cn')) - if userInfo['username'] != post.username or s_pass != password: + if not userInfo['salt']: + public.chdck_salt() + userInfo = sql.table('users').where('id=?',(userInfo['id'],)).field('id,username,password,salt').find() + + password = public.md5(post.password.strip() + userInfo['salt']) + if public.md5(userInfo['username']) != post.username or userInfo['password'] != password: public.WriteLog('TYPE_LOGIN','LOGIN_ERR_PASS',('****','******',public.GetClientIp())) num = self.limit_address('+') return public.returnJson(False,'LOGIN_USER_ERR',(str(num),)),json_header _key_file = "/www/server/panel/data/two_step_auth.txt" if hasattr(post,'vcode'): - if self.limit_address('?',v="vcode") < 1: return public.returnJson(False,'您多次验证失败,禁止10分钟'),json_header + if self.limit_address('?',v="vcode") < 1: return public.returnJson(False,'You have failed verification many times, forbidden for 10 minutes'),json_header import pyotp secret_key = public.readFile(_key_file) if not secret_key: @@ -82,6 +85,8 @@ class userlogin: def request_tmp(self,get): try: if not hasattr(get,'tmp_token'): return public.returnJson(False,'INIT_ARGS_ERR'),json_header + if len(get.tmp_token) != 64: return public.returnJson(False,'INIT_ARGS_ERR'),json_header + if not re.match(r"^\w+$",get.tmp_token):return public.returnJson(False,'INIT_ARGS_ERR'),json_header save_path = '/www/server/panel/config/api.json' data = json.loads(public.ReadFile(save_path)) if not 'tmp_token' in data or not 'tmp_time' in data: return public.returnJson(False,'VERIFICATION_FAILED'),json_header @@ -91,7 +96,7 @@ class userlogin: session['login'] = True session['username'] = userInfo['username'] session['tmp_login'] = True - public.WriteLog('TYPE_LOGIN','LOGIN_SUCCESS',(userInfo['username'],public.GetClientIp())) + public.WriteLog('TYPE_LOGIN','LOGIN_SUCCESS',(userInfo['username'],public.GetClientIp()+ ":" + str(request.environ.get('REMOTE_PORT')))) self.limit_address('-') cache.delete('panelNum') cache.delete('dologin') diff --git a/task.py b/task.py index d61f2fbc..bd584552 100644 --- a/task.py +++ b/task.py @@ -45,7 +45,6 @@ def ExecShell(cmdstring, cwd=None, timeout=None, shell=True): import datetime import subprocess import time - subprocess.Popen('echo > ' + logPath, cwd=cwd, stdin=subprocess.PIPE, shell=shell, bufsize=4096) sub = subprocess.Popen(cmdstring+' &> '+logPath, cwd=cwd, stdin=subprocess.PIPE,shell=shell,bufsize=4096) while sub.poll() is None: @@ -434,7 +433,6 @@ def check502Task(): # 检查面板证书是否有更新 def check_panel_ssl(): - # info_file="" try: while True: lets_info = public.readFile("/www/server/panel/ssl/lets.info") diff --git a/tools.py b/tools.py index 2b41470b..b4f80977 100644 --- a/tools.py +++ b/tools.py @@ -67,7 +67,7 @@ echo "The root password set ${pwd} successuful"'''; def set_panel_pwd(password,ncli = False): import db sql = db.Sql() - result = sql.table('users').where('id=?',(1,)).setField('password',public.md5(password)) + result = sql.table('users').where('id=?',(1,)).setField('password',public.password_salt(public.md5(password),uid=1)) username = sql.table('users').where('id=?',(1,)).getField('username') if ncli: print("|-%s: " % public.GetMsg("USER_NAME") + username) @@ -159,12 +159,30 @@ history -c ''' os.system(command) print('\t\033[1;32m[done]\033[0m') - public.writeFile('/www/server/panel/install.pl',"True") + + + print("|-Please select user initialization method:") + print("="*50) + print(" (1) Display the initialization page when accessing the panel page") + print(" (2) A new account password is automatically generated randomly when first started") + print("="*50) + p_input = input("Please select the initialization method (default: 1):") + print(p_input) + if p_input in [2,'2']: + public.writeFile('/www/server/panel/aliyun.pl',"True") + s_file = '/www/server/panel/install.pl' + if os.path.exists(s_file): os.remove(s_file) + public.M('config').where("id=?",('1',)).setField('status',1) + else: + public.writeFile('/www/server/panel/install.pl',"True") + public.M('config').where("id=?",('1',)).setField('status',0) port = public.readFile('data/port.pl').strip() - public.M('config').where("id=?",('1',)).setField('status',0) print('========================================================') - print('\033[1;32m|-'+public.GetMsg("PANEL_TIPS")+'\033[0m') - print('\033[1;41m|-'+public.GetMsg("PANEL_INIT_ADD")+': http://{SERVERIP}:'+port+'/install\033[0m') + print('\033[1;32m|-The panel packaging is successful, please do not log in to the panel to do any other operations!\033[0m') + if not p_input in [2,'2']: + print('\033[1;41m|-Panel initialization address:http://{SERVERIP}:'+port+'/install\033[0m') + else: + print('\033[1;41m|-Get the initial account password command:bt default \033[0m') #清空正在执行的任务 def CloseTask():