@@ -324,6 +325,10 @@
+ // 编辑器终端 +
+
+
@@ -675,5 +680,7 @@ - + + + {% endblock %} \ No newline at end of file diff --git a/BTPanel/templates/default/layout.html b/BTPanel/templates/default/layout.html index 357c0c69..c6747bbd 100644 --- a/BTPanel/templates/default/layout.html +++ b/BTPanel/templates/default/layout.html @@ -43,7 +43,8 @@ } @@ -143,6 +144,7 @@

{{session['address']}}

{% endfor %} {% block scripts %} {% endblock %} {% endblock %} \ No newline at end of file diff --git a/BTPanel/templates/default/tips.html b/BTPanel/templates/default/tips.html index 63ea14de..59efc574 100644 --- a/BTPanel/templates/default/tips.html +++ b/BTPanel/templates/default/tips.html @@ -128,7 +128,7 @@ return -1 } } - if(IEVersion() == -1 || IEVersion() >=9){ + if(IEVersion() == -1 || IEVersion() >=10){ window.location.href = '/login'; } diff --git a/BTPanel/templates/default/xterm.html b/BTPanel/templates/default/xterm.html index c82d25f3..66b3557d 100644 --- a/BTPanel/templates/default/xterm.html +++ b/BTPanel/templates/default/xterm.html @@ -105,7 +105,7 @@ diff --git a/class/acme_v2.py b/class/acme_v2.py index 369ef86b..bb2de553 100644 --- a/class/acme_v2.py +++ b/class/acme_v2.py @@ -757,7 +757,8 @@ def save_cert(self, cert, index): # 替换服务器上的同域名同品牌证书 def sub_all_cert(self, key_file, pem_file): cert_init = self.get_cert_init(pem_file) # 获取新证书的基本信息 - paths = ['vhost/cert', 'vhost/ssl'] + paths = ['/www/server/panel/vhost/cert', '/www/server/panel/vhost/ssl','/www/server/panel'] + is_panel = False for path in paths: if not os.path.exists(path): continue @@ -768,7 +769,12 @@ def sub_all_cert(self, key_file, pem_file): to_info = to_path + '/info.json' # 判断目标证书是否存在 if not os.path.exists(to_pem_file): - continue + if not p_name in ['ssl']: continue + to_pem_file = to_path + '/certificate.pem' + to_key_file = to_path + '/privateKey.pem' + if not os.path.exists(to_pem_file): + continue + is_panel = True # 获取目标证书的基本信息 to_cert_init = self.get_cert_init(to_pem_file) # 判断证书品牌是否一致 @@ -797,6 +803,7 @@ def sub_all_cert(self, key_file, pem_file): "|-检测到{}下的证书与本次申请的证书重叠,且到期时间较早,已替换为新证书!".format(to_path)) # 重载web服务 public.serviceReload() + if is_panel: public.restart_panel() # 检查指定证书是否在订单列表 def check_order_exists(self, pem_file): @@ -1435,8 +1442,22 @@ def renew_cert(self, index): auth_to = self.get_ssl_used_site(self._config['orders'][i]['save_path']) if not auth_to: continue self._config['orders'][i]['auth_to'] = auth_to - order_index.append(i) + # 是否到了允许重试的时间 + if 'next_retry_time' in self._config['orders'][i]: + timeout = self._config['orders'][i]['next_retry_time'] - int(time.time()) + if timeout > 0: + write_log('|-本次跳过域名:{},因第上次续签失败,还需要等待{}小时后再重试'.format(self._config['orders'][index]['domains'],int(timeout / 60 / 60))) + continue + + # 是否到了最大重试次数 + if 'retry_count' in self._config['orders'][i]: + if self._config['orders'][i]['retry_count'] >= 3: + write_log('|-本次跳过域名:{},因连续3次续签失败,不再续签此证书'.format(self._config['orders'][index]['domains'])) + continue + + # 加入到续签订单 + order_index.append(i) if not order_index: write_log("|-没有找到30天内到期的SSL证书!") return @@ -1467,11 +1488,27 @@ def renew_cert(self, index): write_log("|-正在下载证书..") cert = self.download_cert(index) self._config['orders'][index]['renew_time'] = int(time.time()) + + # 清理失败重试记录 + self._config['orders'][index]['retry_count'] = 0 + self._config['orders'][index]['next_retry_time'] = 0 + + # 保存证书配置 self.save_config() cert['status'] = True cert['msg'] = '续签成功!' write_log("|-续签成功!") except Exception as e: + if str(e).find('请稍候重试') == -1: # 受其它证书影响和连接CA失败的的不记录重试次数 + # 设置下次重试时间 + self._config['orders'][index]['next_retry_time'] = int(time.time() + (86400 * 2)) + # 记录重试次数 + if not 'retry_count' in self._config['orders'][index].keys(): + self._config['orders'][index]['retry_count'] = 1 + self._config['orders'][index]['retry_count'] += 1 + # 保存证书配置 + self.save_config() + write_log("|-" + str(e).split('>>>>')[0]) write_log("-" * 70) return cert diff --git a/class/ajax.py b/class/ajax.py index d6a9ada7..1063186e 100644 --- a/class/ajax.py +++ b/class/ajax.py @@ -349,12 +349,14 @@ def ToAddtime(self,data,tomem = False): if length > 10000: he = 15 if he == 1: for i in range(length): - data[i]['addtime'] = time.strftime('%m/%d %H:%M',time.localtime(float(data[i]['addtime']))) - if tomem and data[i]['mem'] > 100: data[i]['mem'] = data[i]['mem'] / mPre - if tomem in [None]: - if type(data[i]['down_packets']) == str: - data[i]['down_packets'] = json.loads(data[i]['down_packets']) - data[i]['up_packets'] = json.loads(data[i]['up_packets']) + try: + data[i]['addtime'] = time.strftime('%m/%d %H:%M',time.localtime(float(data[i]['addtime']))) + if tomem and data[i]['mem'] > 100: data[i]['mem'] = data[i]['mem'] / mPre + if tomem in [None]: + if type(data[i]['down_packets']) == str: + data[i]['down_packets'] = json.loads(data[i]['down_packets']) + data[i]['up_packets'] = json.loads(data[i]['up_packets']) + except: continue return data else: count = 0 @@ -363,14 +365,16 @@ def ToAddtime(self,data,tomem = False): if count < he: count += 1 continue - value['addtime'] = time.strftime('%m/%d %H:%M',time.localtime(float(value['addtime']))) - if tomem and value['mem'] > 100: value['mem'] = value['mem'] / mPre - if tomem in [None]: - if type(value['down_packets']) == str: - value['down_packets'] = json.loads(value['down_packets']) - value['up_packets'] = json.loads(value['up_packets']) - tmp.append(value) - count = 0 + try: + value['addtime'] = time.strftime('%m/%d %H:%M',time.localtime(float(value['addtime']))) + if tomem and value['mem'] > 100: value['mem'] = value['mem'] / mPre + if tomem in [None]: + if type(value['down_packets']) == str: + value['down_packets'] = json.loads(value['down_packets']) + value['up_packets'] = json.loads(value['up_packets']) + tmp.append(value) + count = 0 + except: continue return tmp def GetInstalleds(self,softlist): diff --git a/class/common.py b/class/common.py index 325148dc..9cb01d2e 100644 --- a/class/common.py +++ b/class/common.py @@ -18,13 +18,16 @@ class panelSetup: def init(self): + panel_path = public.get_panel_path() + if os.getcwd() != panel_path: os.chdir(panel_path) + g.ua = request.headers.get('User-Agent','') if g.ua: ua = g.ua.lower() if ua.find('spider') != -1 or g.ua.find('bot') != -1: return redirect('https://www.baidu.com') - g.version = '7.8.11' + g.version = '7.8.17' g.title = public.GetConfigValue('title') g.uri = request.path g.debug = os.path.exists('data/debug.pl') @@ -42,6 +45,11 @@ def init(self): g.cdn_url = '/static' session['title'] = g.title + g.recycle_bin_open = 0 + if os.path.exists("data/recycle_bin.pl"): g.recycle_bin_open = 1 + + g.recycle_bin_db_open = 0 + if os.path.exists("data/recycle_bin_db.pl"): g.recycle_bin_db_open = 1 g.is_aes = False self.other_import() return None @@ -164,38 +172,26 @@ def check_login(self): return redirect('/login') if api_check: - try: - sess_out_path = 'data/session_timeout.pl' - sess_input_path = 'data/session_last.pl' - if not os.path.exists(sess_out_path): - public.writeFile(sess_out_path, '86400') - if not os.path.exists(sess_input_path): - public.writeFile( - sess_input_path, str(int(time.time()))) - session_timeout = int(public.readFile(sess_out_path)) - session_last = int(public.readFile(sess_input_path)) - if time.time() - session_last > session_timeout: - os.remove(sess_input_path) - session['login'] = False - cache.set('dologin', True) - session.clear() - return redirect('/login') - public.writeFile(sess_input_path, str(int(time.time()))) - except: - pass - - filename = '/www/server/panel/data/login_token.pl' - if os.path.exists(filename): - token = public.readFile(filename).strip() - if 'login_token' in session: - if session['login_token'] != token: - session.clear() - return redirect('/login?dologin=True&go=1') + now_time = time.time() + session_timeout = session.get('session_timeout',0) + if session_timeout < now_time and session_timeout != 0: + session.clear() + return redirect('/login?dologin=True&go=0') + + login_token = session.get('login_token','') + if login_token: + if login_token != public.get_login_token_auth(): + session.clear() + return redirect('/login?dologin=True&go=1') + if api_check: filename = 'data/sess_files/' + public.get_sess_key() if not os.path.exists(filename): session.clear() return redirect('/login?dologin=True&go=2') + + # 标记新的会话过期时间 + session['session_timeout'] = time.time() + public.get_session_timeout() except: session.clear() return redirect('/login') diff --git a/class/config.py b/class/config.py index 2c4b524b..d663689b 100644 --- a/class/config.py +++ b/class/config.py @@ -179,7 +179,8 @@ def reload_session(self): userInfo = public.M('users').where("id=?",(1,)).field('username,password').find() token = public.Md5(userInfo['username'] + '/' + userInfo['password']) public.writeFile('/www/server/panel/data/login_token.pl',token) - + skey = 'login_token' + cache.set(skey,token) sess_path = 'data/sess_files' if not os.path.exists(sess_path): os.makedirs(sess_path,384) diff --git a/class/crontab.py b/class/crontab.py index 63960974..7625226c 100644 --- a/class/crontab.py +++ b/class/crontab.py @@ -289,6 +289,7 @@ def GetDataList(self,get): data['orderOpt'] = [] import json tmp = public.readFile('data/libList.conf') + if not tmp: return data libs = json.loads(tmp) for lib in libs: if not 'opt' in lib: continue diff --git a/class/data.py b/class/data.py index 99eee185..e98b5ea3 100644 --- a/class/data.py +++ b/class/data.py @@ -294,6 +294,13 @@ def GetSql(self,get,result = '1,2,3,4,5,8'): where = type_where else: where += " and " + type_where + if get.table == 'sites': + if where: + where = "({}) AND project_type='PHP'".format(where) + else: + where = "project_type='PHP'" + + field = self.GetField(get.table) #实例化数据库对象 diff --git a/class/database.py b/class/database.py index 8e9d8d14..3f0158ff 100644 --- a/class/database.py +++ b/class/database.py @@ -10,7 +10,7 @@ #------------------------------ # 数据库管理类 #------------------------------ -import public,db,re,time,os,sys,panelMysql +import public,db,re,time,os,sys,panelMysql,json from BTPanel import session import datatool class database(datatool.datatools): @@ -887,4 +887,56 @@ def AlTable(self,get): if info: return public.returnMsg(True,"更改成功") else: - return public.returnMsg(False,"更改失败") \ No newline at end of file + return public.returnMsg(False,"更改失败") + + def get_average_num(self,slist): + """ + @获取平均值 + """ + count = len(slist) + limit_size = 1 * 1024 * 1024 + if count <= 0: return limit_size + + if len(slist) > 1: + slist = sorted(slist) + limit_size =int((slist[0] + slist[-1])/2 * 0.85) + return limit_size + + def get_database_size(self,get): + """ + 获取数据库大小 + """ + result = {} + tables = public.get_database_size() + data = public.M('databases').field('id,pid,name,ps,addtime').select() + public.print_log(data) + for x in data: + name = x['name'] + x['total'] = 0 + x['backup_count'] = public.M('backup').where("pid=? AND type=?",(x['id'],'1')).count() + if name in tables: x['total'] = tables[name] + + result[name] = x + return result + + def check_del_data(self,get): + """ + @删除数据库前置检测 + """ + ids = json.loads(get.ids) + slist = {};result = [];db_list_size = [] + db_data = self.get_database_size(None) + for key in db_data: + data = db_data[key] + if not data['id'] in ids: continue + + db_addtime = public.to_date(times = data['addtime']) + data['score'] = int(time.time() - db_addtime) + data['total'] + data['st_time'] = db_addtime + + if data['total'] > 0 : db_list_size.append(data['total']) + result.append(data) + + slist['data'] = sorted(result,key= lambda x:x['score'],reverse=True) + slist['db_size'] = self.get_average_num(db_list_size) + return slist \ No newline at end of file diff --git a/class/datatool.py b/class/datatool.py index 52846c46..ee0c58db 100644 --- a/class/datatool.py +++ b/class/datatool.py @@ -58,7 +58,7 @@ def GetdataInfo(self,get): ret2 = {} ret2['type']=table[0][1] data_size = table[0][6] - ret2['rows_count'] = table[0][4] + ret2['rows_count'] = self.DB_MySQL.query("select count(*) from `{}`.`{}`".format(db_name,i[0]))[0][0] #table[0][4] 实时获取行数 @authow hwliang<2021-08-05> 修改 ret2['collation'] = table[0][14] ret2['data_size'] = self.ToSize(int(data_size)) ret2['table_name'] = i[0] diff --git a/class/files.py b/class/files.py index aaadee3f..cdeade5f 100644 --- a/class/files.py +++ b/class/files.py @@ -328,6 +328,11 @@ def GetDir(self, get): get.path = get.path.encode('utf-8') if get.path == '': get.path = '/www' + + # 转换包含~的路径 + if get.path.find('~') != -1: + get.path = os.path.expanduser(get.path) + get.path = self.xssdecode(get.path) if not os.path.exists(get.path): get.path = '/www/wwwroot' @@ -336,9 +341,11 @@ def GetDir(self, get): return public.returnMsg(False, '此为回收站目录,请在右上角按【回收站】按钮打开') if not os.path.isdir(get.path): get.path = os.path.dirname(get.path) + if not os.path.isdir(get.path): return public.returnMsg(False, '这不是一个目录!') + import pwd dirnames = [] @@ -392,6 +399,7 @@ def GetDir(self, get): if search: if filename.lower().find(search) == -1: continue + i += 1 if n >= page.ROW: break @@ -556,6 +564,13 @@ def set_file_ps(self,args): + def check_file_sort(self,sort): + """ + @校验排序字段 + """ + slist = ['name','size','mtime','accept','user'] + if sort in slist: return sort + return 'name' def __list_dir(self, path, my_sort='name', reverse=False): ''' @@ -570,7 +585,8 @@ def __list_dir(self, path, my_sort='name', reverse=False): return [] py_v = sys.version_info[0] tmp_files = [] - + + for f_name in os.listdir(path): try: if py_v == 2: @@ -2412,9 +2428,21 @@ def get_file_attribute(self,args): attribute['history'] = self.get_history_info(filename) # 历史文件 return attribute + def files_search(self,args): + import panelSearch + adad=panelSearch.panelSearch() + return adad.get_search(args) + def files_replace(self,args): + import panelSearch + adad=panelSearch.panelSearch() + return adad.get_replace(args) + def get_replace_logs(self,args): + import panelSearch + adad=panelSearch.panelSearch() + return adad.get_replace_logs(args) diff --git a/class/http_requests.py b/class/http_requests.py index 03d1331c..357291bf 100644 --- a/class/http_requests.py +++ b/class/http_requests.py @@ -14,22 +14,31 @@ import ssl import public import json +import socket +import requests +import requests.packages.urllib3.util.connection as urllib3_conn +from requests.packages.urllib3.exceptions import InsecureRequestWarning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) class http: - def __init__(self): - pass - def get(self,url,timeout = 60,headers = {},verify = False,type = 'python'): url = self.quote(url) if type == 'python': + old_family = urllib3_conn.allowed_gai_family try: - import requests - from requests.packages.urllib3.exceptions import InsecureRequestWarning - requests.packages.urllib3.disable_warnings(InsecureRequestWarning) - from requests import get as req_get - return req_get(url,timeout=timeout,headers=get_headers(headers),verify=verify) + # 默认使用IPv4 + urllib3_conn.allowed_gai_family = lambda: socket.AF_INE + return requests.get(url,timeout=timeout,headers=get_headers(headers),verify=verify) except: - result = self._get_curl(url,timeout,headers,verify) + try: + # IPV6? + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET6 + return requests.get(url,timeout=timeout,headers=get_headers(headers),verify=verify) + except: + # 使用CURL + result = self._get_curl(url,timeout,headers,verify) + urllib3_conn.allowed_gai_family = old_family + elif type == 'curl': result = self._get_curl(url,timeout,headers,verify) elif type == 'php': @@ -44,14 +53,20 @@ def get(self,url,timeout = 60,headers = {},verify = False,type = 'python'): def post(self,url,data,timeout = 60,headers = {},verify = False,type = 'python'): url = self.quote(url) if type == 'python': + old_family = urllib3_conn.allowed_gai_family try: - import requests - from requests.packages.urllib3.exceptions import InsecureRequestWarning - requests.packages.urllib3.disable_warnings(InsecureRequestWarning) - from requests import post as req_post - return req_post(url,data,timeout=timeout,headers=headers,verify=verify) + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET + return requests.post(url,data,timeout=timeout,headers=headers,verify=verify) except: - result = self._post_curl(url,data,timeout,headers,verify) + try: + # IPV6? + urllib3_conn.allowed_gai_family = lambda: socket.AF_INET6 + return requests.post(url,data,timeout=timeout,headers=headers,verify=verify) + except: + # 使用CURL + result = self._post_curl(url,data,timeout,headers,verify) + urllib3_conn.allowed_gai_family = old_family + elif type == 'curl': result = self._post_curl(url,data,timeout,headers,verify) elif type == 'php': @@ -299,7 +314,7 @@ def _get_php_version(self): #取CURL路径 def _curl_bin(self): - c_bin = ['/usr/local/curl2/bin/curl','/usr/local/curl/bin/curl','/usr/bin/curl'] + c_bin = ['/usr/local/curl2/bin/curl','/usr/local/curl/bin/curl','/usr/local/bin/curl','/usr/bin/curl'] for cb in c_bin: if os.path.exists(cb): return cb return 'curl' @@ -420,7 +435,7 @@ def json(self): return self.text DEFAULT_HEADERS = {"Content-type":"application/x-www-form-urlencoded","User-Agent":"BT-Panel"} -s_types = ['python','php','curl'] +s_types = ['python','php','curl','src'] DEFAULT_TYPE = 'python' __version__ = 1.0 diff --git a/class/jobs.py b/class/jobs.py index 9d02004d..c47af27d 100644 --- a/class/jobs.py +++ b/class/jobs.py @@ -37,9 +37,17 @@ def control_init(): )''' sql.execute(csql,()) if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%type_id%')).count(): - public.M('sites').execute("alter TABLE sites add edate integer DEFAULT '0000-00-00'",()) public.M('sites').execute("alter TABLE sites add type_id integer DEFAULT 0",()) + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%edate%')).count(): + public.M('sites').execute("alter TABLE sites add edate integer DEFAULT '0000-00-00'",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%project_type%')).count(): + public.M('sites').execute("alter TABLE sites add project_type STRING DEFAULT 'PHP'",()) + + if not public.M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'sites','%project_config%')).count(): + public.M('sites').execute("alter TABLE sites add project_config STRING DEFAULT '{}'",()) + sql = db.Sql() if not sql.table('sqlite_master').where('type=? AND name=?', ('table', 'site_types')).count(): csql = '''CREATE TABLE IF NOT EXISTS `site_types` ( @@ -138,6 +146,8 @@ def control_init(): if os.path.exists("/www/server/mysql"): public.ExecShell("chown mysql:mysql /etc/my.cnf;chmod 600 /etc/my.cnf") public.ExecShell("rm -rf /www/server/panel/temp/*") + if not public.is_debug(): + public.ExecShell("rm -f /www/server/panel/class/pluginAuth.py") stop_path = '/www/server/stop' if not os.path.exists(stop_path): os.makedirs(stop_path) @@ -179,6 +189,7 @@ def write_run_script_log(_log,rn='\n'): def run_script(): + os.system("{} {}/script/run_script.py".format(public.get_python_bin(),public.get_panel_path())) run_tip = '/dev/shm/bt.pl' if os.path.exists(run_tip): return public.writeFile(run_tip,str(time.time())) diff --git a/class/libAuth.aarch64.so b/class/libAuth.aarch64.so index 0ad64ac8..e4f755f3 100644 Binary files a/class/libAuth.aarch64.so and b/class/libAuth.aarch64.so differ diff --git a/class/libAuth.glibc-2.14.x86_64.so b/class/libAuth.glibc-2.14.x86_64.so new file mode 100644 index 00000000..a5e0ca7d Binary files /dev/null and b/class/libAuth.glibc-2.14.x86_64.so differ diff --git a/class/libAuth.x86-64.so b/class/libAuth.x86-64.so index fa9ef3d7..007c2644 100644 Binary files a/class/libAuth.x86-64.so and b/class/libAuth.x86-64.so differ diff --git a/class/libAuth.x86.so b/class/libAuth.x86.so new file mode 100644 index 00000000..022afb96 Binary files /dev/null and b/class/libAuth.x86.so differ diff --git a/class/monitor.py b/class/monitor.py index f8cd5681..9f066156 100644 --- a/class/monitor.py +++ b/class/monitor.py @@ -5,6 +5,7 @@ # | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. # +------------------------------------------------------------------- # | Author: 王张杰 <750755014@qq.com> +# | Maintainer: linxiao # +------------------------------------------------------------------- import os @@ -12,6 +13,7 @@ import time import datetime import re +import sqlite3 import public @@ -38,8 +40,45 @@ def __get_file_nums(self, filepath): def _get_site_list(self): sites = public.M('sites').where('status=?', (1,)).field('name').get() return sites - + def _statuscode_distribute_site(self, site_name): + + try: + day_401 = 0 + day_500 = 0 + day_502 = 0 + day_503 = 0 + conn = None + ts = None + start_date, end_date = self.get_time_interval(time.localtime()) + select_sql = "select time/100 as time1, sum(status_401), sum(status_500), sum(status_502), sum(status_503) from request_stat where time between {} and {}"\ + .format(start_date, end_date) + + db_path = os.path.join("/www/server/total/", "logs/{}/logs.db".format(site_name)) + if os.path.isfile(db_path): + conn = sqlite3.connect(db_path) + ts = conn.cursor() + ts.execute(select_sql) + results = ts.fetchall() + + if type(results) == list: + for result in results: + time_key = str(result[0]) + day_401 = result[1] + day_500 = result[2] + day_502 = result[3] + day_503 = result[4] + except: + pass + finally: + if ts: + ts.close() + if conn: + conn.close() + + return day_401, day_500, day_502, day_503 + + def _statuscode_distribute_site_old(self, site_name): today = time.strftime('%Y-%m-%d', time.localtime()) path = '/www/server/total/total/' + site_name + '/request/' + today + '.json' @@ -52,10 +91,10 @@ def _statuscode_distribute_site(self, site_name): for c in spdata.values(): for d in c: - if '401' == d: day_401 += c['401'] - if '500' == d: day_500 += c['500'] - if '502' == d: day_502 += c['502'] - if '503' == d: day_503 += c['503'] + if '401' == d: day_401 += c['401'] or 0 + if '500' == d: day_500 += c['500'] or 0 + if '502' == d: day_502 += c['502'] or 0 + if '503' == d: day_503 += c['503'] or 0 return day_401, day_500, day_502, day_503 @@ -66,6 +105,10 @@ def _statuscode_distribute(self, args): for site in sites: site_name = site['name'] day_401, day_500, day_502, day_503 = self._statuscode_distribute_site(site_name) + day_401 = day_401 or 0 + day_500 = day_500 or 0 + day_502 = day_502 or 0 + day_503 = day_503 or 0 count_401 += day_401 count_500 += day_500 count_502 += day_502 @@ -176,9 +219,46 @@ def get_exception(self, args): statuscode_distribute = self._statuscode_distribute(args) data.update(statuscode_distribute) return data - - # 获取蜘蛛数量分布 + def get_spider(self, args): + request_data = {} + sites = public.M('sites').field('name').order("addtime").select(); + for site_info in sites: + ts = None + conn = None + try: + site_name = site_info["name"] + start_date, end_date = self.get_time_interval(time.localtime()) + select_sql = "select time, spider from request_stat where time between {} and {}"\ + .format(start_date, end_date) + + db_path = os.path.join("/www/server/total/", "logs/{}/logs.db".format(site_name)) + if not os.path.isfile(db_path): continue + conn = sqlite3.connect(db_path) + ts = conn.cursor() + ts.execute(select_sql) + results = ts.fetchall() + + if type(results) == list: + for result in results: + time_key = str(result[0]) + hour = time_key[len(time_key)-2:] + value = result[1] + if hour not in request_data: + request_data[hour] = value + else: + request_data[hour] += value + except: + pass + finally: + if ts: + ts.close() + if conn: + conn.close() + return request_data + + # 获取蜘蛛数量分布 + def get_spider_old(self, args): today = time.strftime('%Y-%m-%d', time.localtime()) sites = self._get_site_list() @@ -209,9 +289,54 @@ def load_and_up_flow(self, args): up_flow = round(sum([item['up'] for item in data]) / 5, 2) return {'load_five': load_five, 'cpu_count': cpu_count, 'up_flow': up_flow} - - # 取每小时的请求数 + + def get_time_interval(self, local_time): + start = None + end = None + time_key_format = "%Y%m%d00" + start = int(time.strftime(time_key_format, local_time)) + time_key_format = "%Y%m%d23" + end = int(time.strftime(time_key_format, local_time)) + return start, end + def get_request_count_by_hour(self, args): + # 获取站点每小时的请求数据 + request_data = {} + import sqlite3 + sites = public.M('sites').field('name').order("addtime").select(); + for site_info in sites: + ts = None + conn = None + try: + site_name = site_info["name"] + start_date, end_date = self.get_time_interval(time.localtime()) + select_sql = "select time, req from request_stat where time between {} and {}"\ + .format(start_date, end_date) + db_path = os.path.join("/www/server/total/", "logs/{}/logs.db".format(site_name)) + if not os.path.isfile(db_path): continue + conn = sqlite3.connect(db_path) + ts = conn.cursor() + ts.execute(select_sql) + results = ts.fetchall() + if type(results) == list: + for result in results: + time_key = str(result[0]) + hour = time_key[len(time_key)-2:] + value = result[1] + if hour not in request_data: + request_data[hour] = value + else: + request_data[hour] += value + except: pass + finally: + if ts: + ts.close() + if conn: + conn.close() + return request_data + + # 取每小时的请求数 + def get_request_count_by_hour_old(self, args): today = time.strftime('%Y-%m-%d', time.localtime()) request_data = {} diff --git a/class/panelAuth.py b/class/panelAuth.py index 8935fb72..fd633589 100644 --- a/class/panelAuth.py +++ b/class/panelAuth.py @@ -49,6 +49,28 @@ def get_serverid(self,force = False): public.writeFile(serverid_file,serverid) return serverid + def get_wx_order_status(self,get): + """ + 检查制服状态 + @get.wxoid 支付id + """ + params = {} + params['wxoid'] = get.wxoid + if 'kf' in get: params['kf'] = get.kf + + data = self.send_cloud('check_order_pay_status', params) + if not data: return public.returnMsg(False,'连接服务器失败!') + if data['status'] == True: + self.flush_pay_status(get) + if 'get_product_bay' in session: del(session['get_product_bay']) + + buy_oid = '_buy_code_id'.format(params['wxoid']) + buy_code_key = cache.get(buy_oid) + if buy_code_key: + cache.delete(buy_code_key) + cache.delete(buy_oid) + return data + def create_plugin_other_order(self,get): pdata = self.create_serverid(get) pdata['pid'] = get.pid @@ -312,7 +334,14 @@ def get_plugin_remarks(self,get): if not data: return public.returnMsg(False,'连接服务器失败!') session[ikey] = data return data - + + def set_user_adviser(self,get): + params = {} + params['status'] = get.status + data = self.send_cloud_wpanel('set_user_adviser',params) + if not data: return public.returnMsg(False,'连接服务器失败!'); + return data + def send_cloud_wpanel(self,module,params): try: cloudURL = public.GetConfigValue('home') + '/api/panel/' diff --git a/class/panelBackup.py b/class/panelBackup.py index 09303a54..0af5d784 100644 --- a/class/panelBackup.py +++ b/class/panelBackup.py @@ -218,6 +218,15 @@ def backup_path(self,spath,dfile = None,exclude=[],save=3): if self._is_save_local: _not_save_local = False + pdata = { + 'type': '2', + 'name': spath, + 'pid': 0, + 'filename': dfile, + 'addtime': public.format_date(), + 'size': os.path.getsize(dfile) + } + public.M('backup').insert(pdata) if _not_save_local: if os.path.exists(dfile): os.remove(dfile) @@ -382,6 +391,16 @@ def backup_site(self,siteName,save = 3 ,exclude = []): if self._is_save_local: _not_save_local = False + pdata = { + 'type': 0, + 'name': fname, + 'pid': pid, + 'filename': dfile, + 'addtime': public.format_date(), + 'size': os.path.getsize(dfile) + } + public.M('backup').insert(pdata) + if _not_save_local: if os.path.exists(dfile): os.remove(dfile) @@ -591,6 +610,16 @@ def backup_database(self,db_name,dfile = None,save=3): if self._is_save_local: _not_save_local = False + pdata = { + 'type': '1', + 'name': fname, + 'pid': pid, + 'filename': dfile, + 'addtime': public.format_date(), + 'size': os.path.getsize(dfile) + } + public.M('backup').insert(pdata) + if _not_save_local: if os.path.exists(dfile): os.remove(dfile) diff --git a/class/panelPlugin.py b/class/panelPlugin.py index 6ce593c2..68cb2b9b 100644 --- a/class/panelPlugin.py +++ b/class/panelPlugin.py @@ -373,6 +373,11 @@ def __input_plugin(self,filename,input_plugin_name,input_install_opt = 'i'): if os.path.exists(plugin_path_panel + '/icon.png'): shutil.copyfile(icon_sfile,icon_dfile) public.WriteLog('软件管理','{}插件[{}]'.format(opts[input_install_opt],p_info['title'])) + + # 标记一次重新加载插件 + reload_file = os.path.join(self.__panel_path,'data/{}.pl'.format(input_plugin_name)) + public.writeFile(reload_file,'') + return public.returnMsg(True,'{}成功!'.format(opts[input_install_opt])) # 安装失败清理安装文件? @@ -841,6 +846,8 @@ def install_async(self,pluginInfo,get): execstr = "cd /www/server/panel/install && /bin/bash install_soft.sh {} {} {} {} {}".format(get.type,mtype,get.sName,get.version,ols_execstr) if get.sName == "phpmyadmin": execstr += "&> /tmp/panelExec.log && sleep 1 && /usr/local/lsws/bin/lswsctrl restart" + + # execstr += " && echo '>>命令执行完成!'" public.M('tasks').add('id,name,type,status,addtime,execstr',(None, mmsg + '['+get.sName+'-'+get.version+']','execshell','0',time.strftime('%Y-%m-%d %H:%M:%S'),execstr)) cache.delete('install_task') public.writeFile('/tmp/panelTask.pl','True') @@ -1358,22 +1365,26 @@ def sort_link(self,get): def set_coexist(self,sList): softList = [] for sInfo in sList: - if sInfo['version_coexist'] == 1: - for versionA in sInfo['versions']: - sTmp = sInfo.copy() - v = versionA['m_version'].replace('.','') - sTmp['title'] = sTmp['title']+'-'+versionA['m_version'] - sTmp['name'] = sTmp['name']+'-'+versionA['m_version'] - sTmp['version'] = sTmp['version'].replace('{VERSION}',v) - sTmp['manager_version'] = sTmp['manager_version'].replace('{VERSION}',v) - sTmp['install_checks'] = sTmp['install_checks'].replace('{VERSION}',v) - sTmp['uninsatll_checks'] = sTmp['uninsatll_checks'].replace('{VERSION}',v) - sTmp['s_version'] = sTmp['s_version'].replace('{VERSION}',v) - sTmp['versions'] = [] - sTmp['versions'].append(versionA) - softList.append(sTmp) - else: - softList.append(sInfo) + try: + if sInfo['version_coexist'] == 1 and 'versions' in sInfo: + for versionA in sInfo['versions']: + try: + sTmp = sInfo.copy() + v = versionA['m_version'].replace('.','') + sTmp['title'] = sTmp['title']+'-'+versionA['m_version'] + sTmp['name'] = sTmp['name']+'-'+versionA['m_version'] + sTmp['version'] = sTmp['version'].replace('{VERSION}',v) + sTmp['manager_version'] = sTmp['manager_version'].replace('{VERSION}',v) + sTmp['install_checks'] = sTmp['install_checks'].replace('{VERSION}',v) + sTmp['uninsatll_checks'] = sTmp['uninsatll_checks'].replace('{VERSION}',v) + sTmp['s_version'] = sTmp['s_version'].replace('{VERSION}',v) + sTmp['versions'] = [] + sTmp['versions'].append(versionA) + softList.append(sTmp) + except: continue + else: + softList.append(sInfo) + except: continue return softList #检测是否安装 @@ -1420,8 +1431,8 @@ def check_status(self,softInfo): if softInfo['id'] != 10000: self.get_icon(softInfo['name'],softInfo['min_image']) else: - if softInfo['id'] != 10000: - self.get_icon(softInfo['name']) + # if softInfo['id'] != 10000: + self.get_icon(softInfo['name']) if softInfo['name'].find('php-') != -1: v2= softInfo['versions'][0]['m_version'].replace('.','') @@ -1782,7 +1793,6 @@ def get_icon(self,name,downFile = None): def download_icon(self,name,iconFile,downFile): srcIcon = 'plugin/' + name + '/icon.png' skey = name+'_icon' - #public.writeFile('/tmp/11.txt',name+'\n','a+') if cache.get(skey): return None if os.path.exists(srcIcon): public.ExecShell(r"\cp -a -r " + srcIcon + " " + iconFile) diff --git a/class/panelProjectController.py b/class/panelProjectController.py index 022eb9c5..818420ce 100644 --- a/class/panelProjectController.py +++ b/class/panelProjectController.py @@ -12,132 +12,54 @@ #------------------------------ import os,sys,public,json,re -from numpy import isin - class ProjectController: def __init__(self): pass - - def get_parser_list(self,args): - ''' - @name 获取支持的解释器列表 - @author hwliang<2021-07-13> - @param args - @return list - ''' - config_data = public.read_config('parser') - for i in range(len(config_data)): - versions = [] - if not config_data[i]['show']: continue - if not config_data[i]['versions']: continue - for version in config_data[i]['versions']: - if not isinstance(version['check'],list): - version['check'] = [version['check']] - - for check in version['check']: - if not check or os.path.exists(check): - versions.append(version['version']) - config_data[i]['versions'] = versions - return public.return_data(True,config_data) - - - def get_parser_versions(self,args): - ''' - @name 获取指定解释器可用版本列表 - @author hwliang<2021-07-13> - @param args{ - parser_name: string<解释器名称> - } - @return list - ''' - try: - public.exists_args('parser_name',args) - except Exception as ex: - return public.return_data(False,None,1001,ex) - parser_name = args.parser_name.strip() - config_data = public.read_config('parser') - versions = [] - result = public.return_data(False,versions) - for parser_data in config_data: - if parser_data['name'] != parser_name: continue - if not parser_data['show']: return result - if not parser_data['versions']: return result - for version in parser_data['versions']: - if not isinstance(version['check'],list): - version['check'] = [version['check']] - for check in version['check']: - if not check or os.path.exists(check): - - versions.append(version['version']) - return public.return_data(True,versions) - def model(self,args): ''' @name 调用指定项目模型 @author hwliang<2021-07-15> @param args { - data: { - project_name: string<项目名称>, - ps: string<项目备注>, - parser_type: string<解释器类型>, // 从 get_parser_list 接口中获取 - parser_version: string<解释器版本>, // 从 get_parser_versions 接口中获取 - type_id: int<分类标识>, - def_name: string<方法名称>, - mod_name: string<模型名称>, //如php、java、python、nodejs - - // 以下为不同模型下的附加参数示例 - php: { - domains: list<域名列表>, // 如:["www.bt.cn:80","bt.cn:80"] - path: string<网站根目录> - }, - stream: { // TCP、UDP时传入 - is_stream: bool<是否为stream>, - pool: string<协议类型TCP/UDP>, - dst_address: string<目标地址>, - dst_port: int<目标端口>, - local_port: int<本地映射端口> - }, - process: { //绑定进程时传入 - is_process: bool<是否为启动指定文件>, - cwd: string<运行目录>, - run_file: string<启动文件>, - run_args: string<启动参数>, - run_cmd: string<启动命令> //与 run_file/run_args 互斥 - env: list<环境变量> - } - } + mod_name: string<模型名称> + def_name: string<方法名称> + data: JSON } ''' try: # 表单验证 - public.exists_args('data',args) - try: # 解析为dict_obj - pdata = public.to_dict_obj(json.loads(args.data)) - except Exception as ex: - return public.return_status_code(1002,ex) - public.exists_args('project_name,def_name,mod_name',pdata) - if pdata['def_name'].find('__') != -1: return public.return_status_code(1000,'调用的方法名称中不能包含“__”字符') - if not re.match(r"^\w+$",pdata['mod_name']): return public.return_status_code(1000,'调用的模块名称中不能包含\w以外的字符') - if not re.match(r"^\w+$",pdata['def_name']): return public.return_status_code(1000,'调用的方法名称中不能包含\w以外的字符') - except Exception as ex: - return public.return_status_code(1000,ex) + if args['mod_name'] in ['base']: return public.return_status_code(1000,'错误的调用!') + public.exists_args('def_name,mod_name',args) + if args['def_name'].find('__') != -1: return public.return_status_code(1000,'调用的方法名称中不能包含“__”字符') + if not re.match(r"^\w+$",args['mod_name']): return public.return_status_code(1000,'调用的模块名称中不能包含\w以外的字符') + if not re.match(r"^\w+$",args['def_name']): return public.return_status_code(1000,'调用的方法名称中不能包含\w以外的字符') + except: + return public.get_error_object() # 参数处理 - mod_name = "{}Model".format(pdata['mod_name'].strip()) - def_name = pdata['def_name'].strip() + mod_name = "{}Model".format(args['mod_name'].strip()) + def_name = args['def_name'].strip() # 指定模型是否存在 mod_file = "{}/projectModel/{}.py".format(public.get_class_path(),mod_name) if not os.path.exists(mod_file): return public.return_status_code(1003,mod_name) # 实例化 - mod_object = __import__('projectModel.{}'.format(mod_name)) - def_object = getattr(mod_object,def_name,None) - - # 方法是否存在 - if not def_object: - return public.return_status_code(1004,def_name) + def_object = public.get_script_object(mod_file) + if not def_object: return public.return_status_code(1000,'没有找到{}模型'.format(mod_name)) + run_object = getattr(def_object.main(),def_name,None) + if not run_object: return public.return_status_code(1000,'没有在{}模型中找到{}方法'.format(mod_name,def_name)) + if not hasattr(args,'data'): args.data = {} + if args.data: + if isinstance(args.data,str): + try: # 解析为dict_obj + pdata = public.to_dict_obj(json.loads(args.data)) + except: + return public.get_error_object() + else: + pdata = args.data + else: + pdata = public.dict_obj() # 前置HOOK hook_index = '{}_{}_LAST'.format(mod_name.upper(),def_name.upper()) @@ -151,7 +73,7 @@ def model(self,args): return public.return_data(False,{},error_msg='前置HOOK中断操作') # 调用处理方法 - result = def_object(pdata) + result = run_object(pdata) # 后置HOOK hook_index = '{}_{}_END'.format(mod_name.upper(),def_name.upper()) @@ -159,7 +81,10 @@ def model(self,args): 'args': pdata, 'result': result }) - hook_result = hook_result = public.exec_hook(hook_index,hook_data) + hook_result = public.exec_hook(hook_index,hook_data) if isinstance(hook_result,dict): result = hook_result['result'] - return result \ No newline at end of file + return result + + + \ No newline at end of file diff --git a/class/panelRun.py b/class/panelRun.py new file mode 100644 index 00000000..77935968 --- /dev/null +++ b/class/panelRun.py @@ -0,0 +1,388 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# 宝塔Linux面板 +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http:#bt.cn) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +#------------------------------ +# 开机自启模块 +#------------------------------ + +import os,sys,time,json,psutil,re +import public +import signal + +class panelRun: + __panel_path = public.get_panel_path() + __run_config_path = '{}/config/run_config'.format(__panel_path) + __run_pids_path = '{}/logs/run_pids'.format(__panel_path) + __run_logs_path = '{}/logs/run_logs'.format(__panel_path) + __log_name = '开机启动项' + + + def __init__(self): + if not os.path.exists(self.__run_config_path): + os.makedirs(self.__run_config_path) + if not os.path.exists(self.__run_pids_path): + os.makedirs(self.__run_pids_path) + + def get_run_list(self,get): + ''' + @name 获取启动配置列表 + @author hwliang<2021-08-06> + @param get{ + run_type: string<启动类型> + } + @return list + ''' + run_type = None + if 'run_type' in get: + run_type = get['run_type'] + run_list = [] + for run_name in os.listdir(self.__run_config_path): + run_file = '{}/{}'.format(self.__run_config_path,run_name) + if not os.path.isfile(run_file): + continue + + run_info = json.loads(public.readFile(run_file)) + if run_type: + if run_info['run_type'] != run_type: continue + + + run_list.append(run_info) + + return run_list + + + def get_run_info(self,get = None,run_name = None): + ''' + @name 获取启动配置信息 + @author hwliang<2021-08-06> + @param get{ + run_name: string<启动项名称> + } + @return dict + ''' + if get: run_name = get['run_name'] + run_file = '{}/{}'.format(self.__run_config_path,run_name) + if not os.path.isfile(run_file): + return public.returnMsg(False,'启动配置不存在!') + + run_info = json.loads(public.readFile(run_file)) + return run_info + + + def create_run(self,get): + ''' + @name 创建启动配置 + @author hwliang<2021-08-06> + @param get{ + run_title: string<启动项显示标题> + run_name: string<启动项名称> 格式:\w + run_type: string<启动类型> python shell php node java等,也可以是一个可执行文件的路径 或直接为空 + run_path: string<运行目录> + run_script: string<启动脚本> + run_script_args: string<启动脚本参数> + run_env: list<启动环境变量> + } + @return dict + ''' + run_name = get['run_name'] + run_title = get['run_title'] + run_type = get['run_type'] + run_path = get['run_path'] + run_script = get['run_script'] + run_script_args = get['run_script_args'] + run_env = json.loads(get['run_env']) + if not os.path.exists(run_path): + return public.returnMsg(False,'指定运行目录{}不存在!'.format(run_path)) + + if not re.match(r'^\w+$',run_name): + return public.returnMsg(False, '启动项名称格式不正确,支持:[a-zA-Z0-9_]!') + + run_file = '{}/{}'.format(self.__run_config_path,run_name) + if os.path.exists(run_file): + return public.returnMsg(False,'启动配置已存在!') + + run_info = { + 'run_title': run_title, + 'run_name': run_name, + 'run_path': run_path, + 'run_script': run_script, + 'run_env':run_env, + 'run_status': 1 + } + run_info = json.dumps(run_info) + public.writeFile(run_file,run_info) + public.WriteLog(self.__log_name,'创建启动项[]成功!'.format(run_title)) + return public.returnMsg(True,'创建成功!') + + + def modify_run(self,get): + ''' + @name 修改启动配置 + @author hwliang<2021-08-06> + @param get{ + run_name: string<启动项名称> + run_title: string<启动项显示标题> + run_type: string<启动类型> + run_path: string<启动路径> + run_script: string<启动脚本> + run_script_args: string<启动脚本参数> + } + @return dict + ''' + + run_name = get['run_name'] + run_title = get['run_title'] + run_type = get['run_type'] + run_path = get['run_path'] + run_script = get['run_script'] + run_script_args = get['run_script_args'] + run_env = json.loads(get['run_env']) + + if not os.path.exists(run_path): + return public.returnMsg(False,'指定运行目录{}不存在!'.format(run_path)) + + if not re.match(r'^\w+$',run_name): + return public.returnMsg(False, '启动项名称格式不正确,支持:[a-zA-Z0-9_]!') + + + run_file = '{}/{}'.format(self.__run_config_path,run_name) + if not os.path.exists(run_file): + return public.returnMsg(False,'启动配置不存在!') + + run_info = json.loads(public.readFile(run_file)) + run_info['run_title'] = run_title + run_info['run_path'] = run_path + run_info['run_script'] = run_script + run_info['run_env'] = run_env + run_info = json.dumps(run_info) + public.writeFile(run_file,run_info) + public.WriteLog(self.__log_name,'修改启动项[]成功!'.format(run_title)) + return public.returnMsg(True,'修改成功!') + + + def remove_run(self,get): + ''' + @name 删除启动配置 + @author hwliang<2021-08-06> + @param get{ + run_name: string<启动项名称> + } + @return dict + ''' + run_name = get['run_name'] + run_file = '{}/{}'.format(self.__run_config_path,run_name) + if not os.path.isfile(run_file): + return public.returnMsg(False,'启动配置不存在!') + + os.remove(run_file) + public.WriteLog(self.__log_name,'删除启动项[]成功!'.format(run_name)) + return public.returnMsg(True,'删除成功!') + + def set_run_status(self,get): + ''' + @name 设置启动项状态 + @author hwliang<2021-08-06> + @param get{ + run_name: string<启动项名称> + run_status: int<启动项状态> + } + @return dict + ''' + run_name = get['run_name'] + run_status = get['run_status'] + + run_file = '{}/{}'.format(self.__run_config_path,run_name) + if not os.path.isfile(run_file): + return public.returnMsg(False,'启动配置不存在!') + + run_info = json.loads(public.readFile(run_file)) + run_info['run_status'] = run_status + run_info = json.dumps(run_info) + public.writeFile(run_file,run_info) + public.WriteLog(self.__log_name,'设置启动项[]状态成功!'.format(run_info['title'])) + return public.returnMsg(True,'设置成功!') + + + def stop_run(self,run_name = None): + ''' + @name 关闭启动进程 + @author hwliang<2021-08-06> + @param run_name: string<启动项名称> + @return dict + ''' + pid = self.get_run_pid(run_name) + if not pid: return True + os.kill(pid,signal.SIGKILL) + public.WriteLog(self.__log_name,'关闭启动项[]成功!'.format(run_name)) + return True + + + def pid_exists(self,pid): + ''' + @name 检测PID是否存在 + @author hwliang<2021-08-06> + @param pid int + @return bool + ''' + if not isinstance(pid,int): + pid = int(pid) + if pid == 0: + return True + if not os.path.exists('/proc/{}'.format(pid)): + return False + return True + + + def get_run_pid(self,run_name): + ''' + @name 获取启动项PID + @author hwliang<2021-08-06> + @param run_name string<启动项名称> + @return dict + ''' + pid_file = '{}/{}.pid'.format(self.__run_pids_path,run_name) + if not os.path.exists(pid_file): + return None + + run_pid = int(public.readFile(pid_file)) + if run_pid is 0: + return None + + if not self.pid_exists(run_pid): + return None + return run_pid + + + def get_run_status(self,run_name): + ''' + @name 获取启动项状态 + @author hwliang<2021-08-06> + @param run_name string<启动项名称> + @return dict + ''' + pid = self.get_run_pid(run_name) + if not pid: return public.returnMsg(False,'未启动') + process_info = self.get_process_info(pid) + if not process_info: return public.returnMsg(False,'无法获取进程信息') + return process_info + + def get_process_info(self,pid): + ''' + @name 获取进程信息 + @author hwliang<2021-08-06> + @param pid int + @return dict + ''' + process_info = {} + p = psutil.Process(pid) + status_ps = {'sleeping':'睡眠','running':'活动'} + with p.oneshot(): + p_mem = p.memory_full_info() + if p_mem.uss + p_mem.rss + p_mem.pss + p_mem.data == 0: return False + pio = p.io_counters() + p_cpus= p.cpu_times() + p_state = p.status() + if p_state in status_ps: p_state = status_ps[p_state] + process_info['exe'] = p.exe() + process_info['name'] = p.name() + process_info['pid'] = pid + process_info['ppid'] = p.ppid() + process_info['create_time'] = int(p.create_time()) + process_info['status'] = p_state + process_info['user'] = p.username() + process_info['memory_used'] = p_mem.uss + # process_info['cpu_percent'] = self.get_cpu_percent(str(pid),p_cpus,self.new_info['cpu_time']) + process_info['io_write_bytes'] = pio.write_bytes + process_info['io_read_bytes'] = pio.read_bytes + # process_info['io_write_speed'] = self.get_io_write(str(pid),pio.write_bytes) + # process_info['io_read_speed'] = self.get_io_read(str(pid),pio.read_bytes) + process_info['connects'] = self.get_connects(pid) + process_info['threads'] = p.num_threads() + return process_info + + + def get_connects(self,pid): + ''' + @name 获取进程连接数 + @author hwliang<2021-08-06> + @param pid int + @return dict + ''' + connects = 0 + if pid == 1: return connects + tp = '/proc/' + str(pid) + '/fd/' + if not os.path.exists(tp): return connects + for d in os.listdir(tp): + fname = tp + d + if os.path.islink(fname): + l = os.readlink(fname) + if l.find('socket:') != -1: connects += 1 + return connects + + def is_run(self,run_name): + ''' + @name 检测启动项是否在运行 + @author hwliang<2021-08-06> + @param run_name string<启动项名称> + @return bool + ''' + pid = self.get_run_pid(run_name) + if not pid: return False + return True + + + def get_script_pid(self,run_info): + ''' + @name 获取脚本进程PID + @author hwliang<2021-08-06> + @param run_info dict<脚本文件路径> + @return int + ''' + script_last = run_info['run_script'].split(' ')[0] + for pid in psutil.pids(): + p = psutil.Process(pid) + if p.exe() == script_last and p.cwd() == run_info['run_path']: + return pid + return None + + + def start_run(self,run_name): + ''' + @name 启动指定启动项 + @author hwliang<2021-08-06> + @param run_name string<启动项名称> + @return bool + ''' + run_info = self.get_run_info(run_name) + if not run_info: return False + + log_file = '{}/{}.log'.format(self.__run_logs_path,run_name) + pid_file = '{}/{}.pid'.format(self.__run_pids_path,run_name) + public.ExecShell("nohup {} 2>&1 >> {} & $! > {}".format(run_info['run_script'],log_file,pid_file),cwd=run_info['run_path'],env=run_info['run_env'])[0] + time.sleep(1) + pid = self.get_script_pid(run_info) + public.writeFile(pid_file,str(pid)) + public.WriteLog(self.__log_name, '开机启动{}成功, PID: {}'.format(run_name,pid)) + return True + + + def start(self): + ''' + @name 启动所有启动项 + @author hwliang<2021-08-06> + @param + @return bool + ''' + run_list = self.get_run_list(public.dict_obj()) + for run_name in run_list: + if not self.is_run(run_name): + self.start_run(run_name) + return True + + \ No newline at end of file diff --git a/class/panelSSL.py b/class/panelSSL.py index 8eecec8c..8085b03c 100644 --- a/class/panelSSL.py +++ b/class/panelSSL.py @@ -66,8 +66,7 @@ def GetToken(self,get): if result['data']: result['data']['serverid'] = data['serverid'] public.writeFile(self.__UPATH,json.dumps(result['data'])) - from pluginAuth import Plugin - Plugin(False).get_plugin_list(True) + public.flush_plugin_list() del(result['data']) session['focre_cloud'] = True return result @@ -741,7 +740,7 @@ def SetCertToSite(self,get): public.writeFile(keypath,result['privkey']) public.writeFile(csrpath,result['fullchain']) import panelSite - panelSite.panelSite().SetSSLConf(get) + return panelSite.panelSite().SetSSLConf(get) public.serviceReload() return public.returnMsg(True,'SET_SUCCESS') except Exception as ex: diff --git a/class/panelSearch.py b/class/panelSearch.py new file mode 100644 index 00000000..f0e7f6b1 --- /dev/null +++ b/class/panelSearch.py @@ -0,0 +1,260 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | version :1.0 +# +------------------------------------------------------------------- +# | Author: 梁凯强 <1249648969@qq.com> +# +------------------------------------------------------------------- +# | 快速检索 +# +-------------------------------------------------------------------- +import os,public,re +import zipfile,time,json +import db +class panelSearch: + __backup_path = '/www/server/panel/backup/panel_search/' + + def __init__(self): + if not os.path.exists(self.__backup_path): + os.makedirs(self.__backup_path) + if not public.M('sqlite_master').where('type=? AND name=?', ('table', 'panel_search_log')).count(): + csql = '''CREATE TABLE `panel_search_log` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `rtext` TEXT,`exts` TEXT,`path` TEXT,`mode` TEXT,`isword` TEXT,`iscase` TEXT,`noword` TEXT,`backup_path` TEXT,`time` TEXT)''' + public.M('sqlite_master').execute(csql,()) + + def dtchg(self, x): + try: + time_local = time.localtime(float(x)) + dt = time.strftime("%Y-%m-%d %H:%M:%S", time_local) + return dt + except: + return False + + def insert_settings(self, rtext, exts, path, mode, isword,iscase,noword,backup_path): + inser_time = self.dtchg(int(time.time())) + data = {"rtext": rtext, "exts": json.dumps(exts), "path": path, "mode": mode, + "isword": isword, "iscase": iscase,"noword":noword,"backup_path":backup_path,"time":inser_time} + return public.M('panel_search_log').insert(data) + + '''目录下所有的文件''' + def get_dir(self, path,exts,text,mode=0,isword=0,iscase=0,noword=0,is_backup=0,rtext=False): + if rtext or noword: + result=[] + else: + result={} + if is_backup: + t = time.strftime('%Y%m%d%H%M%S') + back_zip = os.path.join(self.__backup_path, "%s.zip" % t) + zfile = zipfile.ZipFile(back_zip, "w", compression=zipfile.ZIP_DEFLATED) + else: + zfile=False + back_zip=False + return_data = [] + [[return_data.append(os.path.join(root, file)) for file in files] for root, dirs, files in os.walk(path)] + for i in return_data: + for i2 in exts: + i3 = i.split('.') + if i3[-1] == i2: + + temp = self.get_files_lin(i, text, mode, isword, iscase, noword,is_backup,rtext,zfile) + if temp: + if rtext or noword: + if isinstance(result,list): + result.append(temp) + else: + result[i] = temp + if is_backup: + if zfile: + zfile.close() + self.insert_settings(rtext, exts, path, mode, isword, iscase, noword, back_zip) + return True + return result + + '''获取单目录''' + def get_dir_files(self,path,exts,text,mode=0,isword=0,iscase=0,noword=0,is_backup=0,rtext=False): + is_list = rtext or noword + if is_list: + result=[] + else: + result={} + if is_backup: + t = time.strftime('%Y%m%d%H%M%S') + back_zip = os.path.join(self.__backup_path, "%s.zip" % t) + zfile = zipfile.ZipFile(back_zip, "w", compression=zipfile.ZIP_DEFLATED) + else: + zfile=False + back_zip=False + list_data=[] + for root, dirs, files in os.walk(path): + list_data=files + break + for i in exts: + for i2 in list_data: + i3=i2.split('.') + if i3[-1]==i: + temp=self.get_files_lin(path+'/'+i2, text,mode,isword,iscase,noword,is_backup,rtext,zfile) + if temp: + if isinstance(result,list): + result.append(path+'/'+i2) + else: + result[path+'/'+i2]=temp + if is_backup: + if zfile: + zfile.close() + self.insert_settings(rtext, exts, path, mode, isword, iscase, noword, back_zip) + return result + ''' + 获取目录下所有的后缀文件 + ''' + def get_exts_files(self,path,exts,text,mode=0,isword=0,iscase=0,noword=0,is_subdir=0,is_backup=0,rtext=False): + if len(exts)==0:return [] + if is_subdir==0: + return self.get_dir_files(path,exts,text,mode,isword,iscase,noword,is_backup,rtext) + elif is_subdir==1: + return self.get_dir(path,exts,text,mode,isword,iscase,noword,is_backup,rtext) + + ''' + 获取文件内的关键词 + ''' + def get_files_lin(self, files, text,mode=0,isword=0,iscase=0,noword=0,is_backup=0,rtext=False,back_zip=False): + if not os.path.exists(files):return False + if os.path.getsize(files) > 1024 * 1024 * 20: return False + #文件替换部分 + if rtext: + resutl=[] + try: + fp = open(files, 'r', encoding='UTF-8') + except: + fp = open(files, 'r') + content = fp.read() + fp.close() + if mode==2: + if iscase: + if not re.search(text, content, flags=re.I): return False + content = re.sub(text, rtext, content, flags=re.I) + else: + if not re.search(text, content): return False + content = re.sub(text, rtext, content) + else: + if content.find(text) == -1: return False + content = content.replace(text, rtext) + if is_backup and back_zip: + bf = files.strip('/') + back_zip.write(files, bf) + with open(files, 'w') as f: + f.write(content) + f.close() + return files + else: + #查找部分 + if noword: + resutl = [] + else: + resutl={} + try: + fp = open(files, 'r', encoding='UTF-8') + except: + fp = open(files, 'r') + i = 0 + try: + for line in fp: + i += 1 + if mode==1: + if iscase and not re.search(text, line, flags=re.I): + continue + elif not iscase and not re.search(text, line): + continue + else: + if line.find(text) == -1: continue + if noword: + return files + resutl[i]=line + except: + pass + if resutl: + return resutl + return False + ''' + text 搜索内容 + exts 后缀名 参数例子 php,html + path 目录 + is_subdir 0 不包含子目录 1 包含子目录 + mode 0 为普通模式 1 为正则模式 + isword 1 全词匹配 0 默认 + iscase 1 不区分大小写 0 默认 + noword 1 不输出行信息 0 默认 + ''' + def get_search(self, args): + if 'text' not in args or not args.text: return {'error': '搜索信息不能为空'} + if 'exts' not in args or not args.exts: return {'error': '后缀不能为空;所有文件请输入*.*'} + if 'path' not in args or not args.path or args.path == '/': return {'error': '目录不能为空或者不能为/'} + if not os.path.isdir(args.path): return {'error': '目录不存在'} + text=args.text + exts=args.exts + path=args.path + mode = int(args.mode) if 'mode' in args else 0 + is_subdir = int(args.is_subdir) if 'is_subdir' in args else 0 + iscase = int(args.iscase) if 'iscase' in args else 0 + isword = int(args.isword) if 'isword' in args else 0 + noword = int(args.noword) if 'noword' in args else 0 + exts=exts.split(',') + is_tmpe_files=self.get_exts_files(path,exts,text,mode,isword,iscase,noword,is_subdir) + return is_tmpe_files + + ''' + text 搜索内容 + rtext 替换成的内容 + exts 后缀名 参数例子 php,html + path 目录 + is_subdir 0 不包含子目录 1 包含子目录 + mode 0 为普通模式 1 为正则模式 + isword 1 全词匹配 0 默认 + iscase 1 不区分大小写 0 默认 + noword 1 不输出行信息 0 默认 + ''' + def get_replace(self, args): + if 'text' not in args or not args.text: return {'error': '搜索信息不能为空'} + if 'rtext' not in args or not args.text: return {'error': '需要替换的内容不能为空'} + if 'exts' not in args or not args.exts: return {'error': '后缀不能为空;所有文件请输入*.*'} + if 'path' not in args or not args.path or args.path == '/': return {'error': '目录不能为空或者不能为/'} + if not os.path.isdir(args.path): return {'error': '目录不存在'} + is_backup = int(args.isbackup) if 'isbackup' in args else 0 + text = args.text + rtext = args.rtext + exts = args.exts + path = args.path + mode = int(args.mode) if 'mode' in args else 0 + is_subdir = int(args.is_subdir) if 'is_subdir' in args else 0 + iscase = int(args.iscase) if 'iscase' in args else 0 + isword = int(args.isword) if 'isword' in args else 0 + noword = int(args.noword) if 'noword' in args else 0 + exts = exts.split(',') + is_tmpe_files = self.get_exts_files(path, exts, text, mode, isword, iscase, noword, is_subdir,is_backup,rtext) + return is_tmpe_files + + #替換日志 + def get_replace_logs(self,get): + import page + page = page.Page() + count = public.M('panel_search_log').order('id desc').count() + limit = 12 + info = {} + info['count'] = count + info['row'] = limit + info['p'] = 1 + if hasattr(get, 'p'): + info['p'] = int(get['p']) + info['uri'] = get + info['return_js'] = '' + if hasattr(get, 'tojs'): + info['return_js'] = get.tojs + data = {} + data['page'] = page.GetPage(info, '1,2,3,4,5,8') + data['data'] = public.M('panel_search_log').field('id,rtext,exts,path,mode,isword,iscase,noword,backup_path,time').order('id desc').limit(str(page.SHIFT) + ',' + str(page.ROW)).select() + if isinstance(data['data'],str): return public.returnMsg(False,[]) + for i in data['data']: + if not isinstance(i,dict): continue + if 'backup_path' in i : + path=i['backup_path'] + if os.path.exists(path): + i['is_path_status']=True + else: + i['is_path_status'] = False + return public.returnMsg(True, data) \ No newline at end of file diff --git a/class/panelSite.py b/class/panelSite.py index 1a7df9c5..5108ff02 100644 --- a/class/panelSite.py +++ b/class/panelSite.py @@ -29,7 +29,7 @@ class panelSite(panelRedirect): is_ipv6 = False def __init__(self): - self.setupPath = '/www/server' + self.setupPath = public.get_setup_path() path = self.setupPath + '/panel/vhost/nginx' if not os.path.exists(path): public.ExecShell("mkdir -p " + path + " && chmod -R 644 " + path) path = self.setupPath + '/panel/vhost/apache' @@ -40,7 +40,7 @@ def __init__(self): if not os.path.exists(path + '/index.html'): public.ExecShell('mkdir -p ' + path) public.ExecShell('wget -O ' + path + '/index.html '+public.get_url()+'/stop.html &') - self.__proxyfile = '/www/server/panel/data/proxyfile.json' + self.__proxyfile = '{}/data/proxyfile.json'.format(public.get_panel_path()) self.OldConfigFile() if os.path.exists(self.nginx_conf_bak): os.remove(self.nginx_conf_bak) if os.path.exists(self.apache_conf_bak): os.remove(self.apache_conf_bak) @@ -79,6 +79,7 @@ def check_default(self): #添加apache端口 def apacheAddPort(self,port): + port = str(port) filename = self.setupPath+'/apache/conf/extra/httpd-ssl.conf' if os.path.exists(filename): ssl_conf = public.readFile(filename) @@ -1687,6 +1688,10 @@ def SetSSLConf(self, get): # Nginx配置 file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + + # Node项目 + if not os.path.exists(file): file = self.setupPath + '/panel/vhost/nginx/node_' + siteName + '.conf' + ng_file = file conf = public.readFile(file) @@ -1713,6 +1718,7 @@ def SetSSLConf(self, get): return public.returnMsg(True, 'SITE_SSL_OPEN_SUCCESS') conf = conf.replace('#error_page 404/404.html;', sslStr) + # 添加端口 rep = "listen.*[\s:]+(\d+).*;" tmp = re.findall(rep, conf) @@ -1729,10 +1735,12 @@ def SetSSLConf(self, get): if self.is_ipv6: listen_ipv6 = ";\n\tlisten [::]:443 ssl"+http2+default_site+";" conf = conf.replace(listen,listen + "\n\tlisten 443 ssl"+http2 + default_site + listen_ipv6) shutil.copyfile(file, self.nginx_conf_bak) + public.writeFile(file, conf) # Apache配置 file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if not os.path.exists(file): file = self.setupPath + '/panel/vhost/apache/node_' + siteName + '.conf' conf = public.readFile(file) ap_static_security = self._get_ap_static_security(conf) if conf: @@ -1847,6 +1855,8 @@ def HttpToHttps(self,get): siteName = get.siteName #Nginx配置 file = self.setupPath + '/panel/vhost/nginx/'+siteName+'.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/nginx/node_'+siteName+'.conf' conf = public.readFile(file) if conf: if conf.find('ssl_certificate') == -1: return public.returnMsg(False,'当前未开启SSL') @@ -1860,6 +1870,8 @@ def HttpToHttps(self,get): public.writeFile(file,conf) file = self.setupPath + '/panel/vhost/apache/'+siteName+'.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/apache/node_'+siteName+'.conf' conf = public.readFile(file) if conf: httpTohttos = '''combined @@ -1916,6 +1928,9 @@ def CloseToHttps(self,get): #是否跳转到https def IsToHttps(self,siteName): file = self.setupPath + '/panel/vhost/nginx/'+siteName+'.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/nginx/node_'+siteName+'.conf' + if not os.path.exists(file): return False conf = public.readFile(file) if conf: if conf.find('HTTP_TO_HTTPS_START') != -1: return True @@ -1927,6 +1942,8 @@ def CloseSSLConf(self, get): siteName = get.siteName file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/nginx/node_' + siteName + '.conf' conf = public.readFile(file) if conf: rep = "\n\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" @@ -1968,6 +1985,8 @@ def CloseSSLConf(self, get): public.writeFile(file, conf) file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' + if not os.path.exists(file): + file = self.setupPath + '/panel/vhost/apache/node_' + siteName + '.conf' conf = public.readFile(file) if conf: rep = "\n(.|\n)*<\/VirtualHost>" @@ -2025,7 +2044,11 @@ def GetSSL(self, get): keypath = path + "/privkey.pem" # 密钥文件路径 key = public.readFile(keypath) csr = public.readFile(csrpath) + file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/' + siteName + '.conf' + + # 是否为node项目 + if not os.path.exists(file): file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/node_' + siteName + '.conf' if public.get_webserver() == "openlitespeed": file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/detail/' + siteName + '.conf' conf = public.readFile(file) @@ -3369,8 +3392,8 @@ def __CheckStart(self,get,action=""): return public.returnMsg(False, "代理目录不能有以下特殊符号 ?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\,',\"]") # 检测发送域名格式 if get.todomain: - if not re.search(tod,get.todomain): - return public.returnMsg(False, '发送域名格式错误 ' + get.todomain) + if re.search("[\}\{\#\;\"\']+",get.todomain): + return public.returnMsg(False, '发送域名格式错误:'+get.todomain+'
不能存在以下特殊字符【 } { # ; \" \' 】 ') if public.get_webserver() != 'openlitespeed' and not get.todomain: get.todomain = "$host" @@ -3926,6 +3949,8 @@ def CheckProxy(self,get): #取伪静态规则应用列表 def GetRewriteList(self,get): + if get.siteName.find('node_') == 0: + get.siteName = get.siteName.replace('node_', '') rewriteList = {} ws = public.get_webserver() if ws == "openlitespeed": @@ -4166,15 +4191,16 @@ def logsOpen(self,get): rep = "\n#errorlog(.|\n)*compressArchive\s*1\s*\n#}" tmp = re.search(rep,conf) tmp = tmp.group() - result = '' - if s == 'on': - for l in tmp.strip().splitlines(): - result += "\n#"+l - else: - for l in tmp.splitlines(): - result += "\n"+l[1:] - conf = re.sub(rep,"\n"+result.strip(),conf) - public.writeFile(filename,conf) + if tmp: + result = '' + if s == 'on': + for l in tmp.strip().splitlines(): + result += "\n#"+l + else: + for l in tmp.splitlines(): + result += "\n"+l[1:] + conf = re.sub(rep,"\n"+result.strip(),conf) + public.writeFile(filename,conf) @@ -4405,6 +4431,7 @@ def CloseTomcat(self,get): def GetSiteRunPath(self,get): siteName = public.M('sites').where('id=?',(get.id,)).getField('name') sitePath = public.M('sites').where('id=?',(get.id,)).getField('path') + if not siteName: return {"runPath":"/",'dirs':[]} path = sitePath if public.get_webserver() == 'nginx': filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' @@ -4900,4 +4927,96 @@ def get_dir_auth(self,get): # 修改目录保护密码 def modify_dir_auth_pass(self,get): sd = site_dir_auth.SiteDirAuth() - return sd.modify_dir_auth_pass(get) \ No newline at end of file + return sd.modify_dir_auth_pass(get) + + def _check_path_total(self,path, limit): + """ + 根据路径获取文件/目录大小 + @path 文件或者目录路径 + return int + """ + + if not os.path.exists(path): return 0; + if not os.path.isdir(path): return os.path.getsize(path) + size_total = 0 + for nf in os.walk(path): + for f in nf[2]: + filename = nf[0] + '/' + f + if not os.path.exists(filename): continue; + if os.path.islink(filename): continue; + size_total += os.path.getsize(filename) + if size_total >= limit: return limit + return size_total + + def get_average_num(self,slist): + """ + @获取平均值 + """ + count = len(slist) + limit_size = 1 * 1024 * 1024 + if count <= 0: return limit_size + print(slist) + if len(slist) > 1: + slist = sorted(slist) + limit_size =int((slist[0] + slist[-1])/2 * 0.85) + return limit_size + + + def check_del_data(self,get): + """ + @删除前置检测 + @ids = [1,2,3] + """ + ids = json.loads(get['ids']) + slist = {} + result = [] + + import database + db_data = database.database().get_database_size(None) + + limit_size = 50 * 1024 * 1024 + f_list_size = [];db_list_size = [] + for id in ids: + data = public.M('sites').where("id=?",(id,)).field('id,name,path,addtime').find(); + if not data: continue + + addtime = public.to_date(times = data['addtime']) + + data['st_time'] = addtime + data['limit'] = False + data['backup_count'] = public.M('backup').where("pid=? AND type=?",(data['id'],'0')).count() + f_size = self._check_path_total(data['path'],limit_size) + data['total'] = f_size; + data['score'] = 0 + + #目录太小不计分 + if f_size > 0: + f_list_size.append(f_size) + + # 10k 目录不参与排序 + if f_size > 10 * 1024: data['score'] = int(time.time() - addtime) + f_size + + if data['total'] >= limit_size: data['limit'] = True + data['database'] = False + + find = public.M('databases').field('id,pid,name,ps,addtime').where('pid=?',(data['id'],)).find() + if find: + db_addtime = public.to_date(times = find['addtime']) + + data['database'] = db_data[find['name']] + data['database']['st_time'] = db_addtime + + db_score = 0; + db_size = data['database']['total'] + + if db_size > 0: + db_list_size.append(db_size) + if db_size > 50 * 1024: db_score += int(time.time() - db_addtime) + db_size + + data['score'] += db_score + result.append(data) + + slist['data'] = sorted(result,key= lambda x:x['score'],reverse=True) + slist['file_size'] = self.get_average_num(f_list_size) + slist['db_size'] = self.get_average_num(db_list_size) + return slist \ No newline at end of file diff --git a/class/panelSiteController.py b/class/panelSiteController.py new file mode 100644 index 00000000..ef725f00 --- /dev/null +++ b/class/panelSiteController.py @@ -0,0 +1,112 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# 宝塔Linux面板 +#------------------------------------------------------------------- +# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +#------------------------------ +# 网站管理控制器 +#------------------------------ +import os,sys,public + +class panelSiteController: + + + def __init__(self): + pass + + + + def get_parser_list(self,args): + ''' + @name 获取支持的解释器列表 + @author hwliang<2021-07-13> + @param args + @return list + ''' + return public.return_data(True,public.read_config('parser')) + + + def get_parser_versions(self,args): + ''' + @name 获取指定解释器可用版本列表 + @author hwliang<2021-07-13> + @param args{ + parser_name: string<解释器名称> + } + @return list + ''' + try: + public.exists_args('parser_name',args) + except Exception as ex: + return public.return_data(False,None,1001,ex) + parser_name = args.parser_name.strip() + config_data = public.read_config('parser') + versions = [] + result = public.return_data(False,versions) + for parser_data in config_data: + if parser_data['name'] != parser_name: continue + if not parser_data['show']: return result + if not parser_data['versions']: return result + for version in parser_data['versions']: + if isinstance(version['check'],str): + version['check'] = [version['check']] + for check in version['check']: + if os.path.exists(check): + versions.append(version) + return public.return_data(True,versions) + + + + + + + + def create_site(self,args): + ''' + @name 创建网站 + @author hwliang<2021-07-13> + @param args { + data: { + siteName: string<网站名称>, + domains: list<域名列表>, // 如:["www.bt.cn:80","bt.cn:80"] + parser_type: string<解释器类型>, // 从 get_parser_list 接口中获取 + parser_version: string<解释器版本>, // 从 get_parser_versions 接口中获取 + ps: string<网站备注>, + type_id: int<分类标识>, + path: string<网站根目录>, + stream_info: { // TCP、UDP时传入 + is_stream: bool<是否为stream>, + pool: string<协议类型TCP/UDP>, + dst_address: string<目标地址>, + dst_port: int<目标端口>, + local_port: int<本地映射端口> + }, + process_info: { //绑定进程时传入 + is_process: bool<是否为启动指定文件>, + cwd: string<运行目录>, + run_file: string<启动文件>, + run_args: string<启动参数>, + run_cmd: string<启动命令> //与 run_file/run_args 互斥 + env: list<环境变量> + }, + ftp_info: { //需要同时创建FTP时传入 + create: bool<是否创建>, + username: string<用户名>, + password: string<密码>, + path: string<根目录> + }, + database_info: { //需要同时创建数据库时传入 + create: bool<是否创建>, + username: string<用户名>, + password: string<密码>, + db_name: string<数据库名>, + codeing: string<字符集> + } + } + } + ''' + diff --git a/class/panelWarning.py b/class/panelWarning.py index 6dbe5bf3..d1165bea 100644 --- a/class/panelWarning.py +++ b/class/panelWarning.py @@ -53,7 +53,11 @@ def get_list(self,args): not_force = m_info['ignore'] if os.path.exists(result_file) and not_force: - m_info['status'],m_info['msg'],m_info['check_time'],m_info['taking'] = json.loads(public.readFile(result_file)) + try: + m_info['status'],m_info['msg'],m_info['check_time'],m_info['taking'] = json.loads(public.readFile(result_file)) + except: + if os.path.exists(result_file): os.remove(result_file) + continue else: try: s_time = time.time() diff --git a/class/panelWork.py b/class/panelWork.py new file mode 100644 index 00000000..e69de29b diff --git a/class/panelWorkorder.py b/class/panelWorkorder.py index 016b9692..e0b38b57 100644 --- a/class/panelWorkorder.py +++ b/class/panelWorkorder.py @@ -235,13 +235,13 @@ def create(self, get): try: from flask import jsonify data = get - debug_path = 'data/debug.pl' - if os.path.exists(debug_path): - return jsonify({ - "status": False, - "msg": "暂不支持在开发者模式提交工单!", - "error_code": 10004 - }) + # debug_path = 'data/debug.pl' + # if os.path.exists(debug_path): + # return jsonify({ + # "status": False, + # "msg": "暂不支持在开发者模式提交工单!", + # "error_code": 10004 + # }) contents = data.contents user_info = self.find_user_info() if not user_info: diff --git a/class/panel_search.py b/class/panel_search.py new file mode 100644 index 00000000..82f166e4 --- /dev/null +++ b/class/panel_search.py @@ -0,0 +1,260 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | version :1.0 +# +------------------------------------------------------------------- +# | Author: 梁凯强 <1249648969@qq.com> +# +------------------------------------------------------------------- +# | 快速检索 +# +-------------------------------------------------------------------- +import os,public,re +import zipfile,time,json +import db +class panel_search: + __backup_path = '/www/server/panel/backup/panel_search/' + + def __init__(self): + if not os.path.exists(self.__backup_path): + os.makedirs(self.__backup_path) + if not public.M('sqlite_master').where('type=? AND name=?', ('table', 'panel_search_log')).count(): + csql = '''CREATE TABLE `panel_search_log` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `rtext` TEXT,`exts` TEXT,`path` TEXT,`mode` TEXT,`isword` TEXT,`iscase` TEXT,`noword` TEXT,`backup_path` TEXT,`time` TEXT)''' + public.M('sqlite_master').execute(csql,()) + + def dtchg(self, x): + try: + time_local = time.localtime(float(x)) + dt = time.strftime("%Y-%m-%d %H:%M:%S", time_local) + return dt + except: + return False + + def insert_settings(self, rtext, exts, path, mode, isword,iscase,noword,backup_path): + inser_time = self.dtchg(int(time.time())) + data = {"rtext": rtext, "exts": json.dumps(exts), "path": path, "mode": mode, + "isword": isword, "iscase": iscase,"noword":noword,"backup_path":backup_path,"time":inser_time} + return public.M('panel_search_log').insert(data) + + '''目录下所有的文件''' + def get_dir(self, path,exts,text,mode=0,isword=0,iscase=0,noword=0,is_backup=0,rtext=False): + if rtext or noword: + result=[] + else: + result={} + if is_backup: + t = time.strftime('%Y%m%d%H%M%S') + back_zip = os.path.join(self.__backup_path, "%s.zip" % t) + zfile = zipfile.ZipFile(back_zip, "w", compression=zipfile.ZIP_DEFLATED) + else: + zfile=False + back_zip=False + return_data = [] + [[return_data.append(os.path.join(root, file)) for file in files] for root, dirs, files in os.walk(path)] + for i in return_data: + for i2 in exts: + i3 = i.split('.') + if i3[-1] == i2: + + temp = self.get_files_lin(i, text, mode, isword, iscase, noword,is_backup,rtext,zfile) + if temp: + if rtext or noword: + if isinstance(result,list): + result.append(temp) + else: + result[i] = temp + if is_backup: + if zfile: + zfile.close() + self.insert_settings(rtext, exts, path, mode, isword, iscase, noword, back_zip) + return True + return result + + '''获取单目录''' + def get_dir_files(self,path,exts,text,mode=0,isword=0,iscase=0,noword=0,is_backup=0,rtext=False): + is_list = rtext or noword + if is_list: + result=[] + else: + result={} + if is_backup: + t = time.strftime('%Y%m%d%H%M%S') + back_zip = os.path.join(self.__backup_path, "%s.zip" % t) + zfile = zipfile.ZipFile(back_zip, "w", compression=zipfile.ZIP_DEFLATED) + else: + zfile=False + back_zip=False + list_data=[] + for root, dirs, files in os.walk(path): + list_data=files + break + for i in exts: + for i2 in list_data: + i3=i2.split('.') + if i3[-1]==i: + temp=self.get_files_lin(path+'/'+i2, text,mode,isword,iscase,noword,is_backup,rtext,zfile) + if temp: + if isinstance(result,list): + result.append(path+'/'+i2) + else: + result[path+'/'+i2]=temp + if is_backup: + if zfile: + zfile.close() + self.insert_settings(rtext, exts, path, mode, isword, iscase, noword, back_zip) + return result + ''' + 获取目录下所有的后缀文件 + ''' + def get_exts_files(self,path,exts,text,mode=0,isword=0,iscase=0,noword=0,is_subdir=0,is_backup=0,rtext=False): + if len(exts)==0:return [] + if is_subdir==0: + return self.get_dir_files(path,exts,text,mode,isword,iscase,noword,is_backup,rtext) + elif is_subdir==1: + return self.get_dir(path,exts,text,mode,isword,iscase,noword,is_backup,rtext) + + ''' + 获取文件内的关键词 + ''' + def get_files_lin(self, files, text,mode=0,isword=0,iscase=0,noword=0,is_backup=0,rtext=False,back_zip=False): + if not os.path.exists(files):return False + if os.path.getsize(files) > 1024 * 1024 * 20: return False + #文件替换部分 + if rtext: + resutl=[] + try: + fp = open(files, 'r', encoding='UTF-8') + except: + fp = open(files, 'r') + content = fp.read() + fp.close() + if mode==2: + if iscase: + if not re.search(text, content, flags=re.I): return False + content = re.sub(text, rtext, content, flags=re.I) + else: + if not re.search(text, content): return False + content = re.sub(text, rtext, content) + else: + if content.find(text) == -1: return False + content = content.replace(text, rtext) + if is_backup and back_zip: + bf = files.strip('/') + back_zip.write(files, bf) + with open(files, 'w') as f: + f.write(content) + f.close() + return files + else: + #查找部分 + if noword: + resutl = [] + else: + resutl={} + try: + fp = open(files, 'r', encoding='UTF-8') + except: + fp = open(files, 'r') + i = 0 + try: + for line in fp: + i += 1 + if mode==1: + if iscase and not re.search(text, line, flags=re.I): + continue + elif not iscase and not re.search(text, line): + continue + else: + if line.find(text) == -1: continue + if noword: + return files + resutl[i]=line + except: + pass + if resutl: + return resutl + return False + ''' + text 搜索内容 + exts 后缀名 参数例子 php,html + path 目录 + is_subdir 0 不包含子目录 1 包含子目录 + mode 0 为普通模式 1 为正则模式 + isword 1 全词匹配 0 默认 + iscase 1 不区分大小写 0 默认 + noword 1 不输出行信息 0 默认 + ''' + def get_search(self, args): + if 'text' not in args or not args.text: return {'error': '搜索信息不能为空'} + if 'exts' not in args or not args.exts: return {'error': '后缀不能为空;所有文件请输入*.*'} + if 'path' not in args or not args.path or args.path == '/': return {'error': '目录不能为空或者不能为/'} + if not os.path.isdir(args.path): return {'error': '目录不存在'} + text=args.text + exts=args.exts + path=args.path + mode = int(args.mode) if 'mode' in args else 0 + is_subdir = int(args.is_subdir) if 'is_subdir' in args else 0 + iscase = int(args.iscase) if 'iscase' in args else 0 + isword = int(args.isword) if 'isword' in args else 0 + noword = int(args.noword) if 'noword' in args else 0 + exts=exts.split(',') + is_tmpe_files=self.get_exts_files(path,exts,text,mode,isword,iscase,noword,is_subdir) + return is_tmpe_files + + ''' + text 搜索内容 + rtext 替换成的内容 + exts 后缀名 参数例子 php,html + path 目录 + is_subdir 0 不包含子目录 1 包含子目录 + mode 0 为普通模式 1 为正则模式 + isword 1 全词匹配 0 默认 + iscase 1 不区分大小写 0 默认 + noword 1 不输出行信息 0 默认 + ''' + def get_replace(self, args): + if 'text' not in args or not args.text: return {'error': '搜索信息不能为空'} + if 'rtext' not in args or not args.text: return {'error': '需要替换的内容不能为空'} + if 'exts' not in args or not args.exts: return {'error': '后缀不能为空;所有文件请输入*.*'} + if 'path' not in args or not args.path or args.path == '/': return {'error': '目录不能为空或者不能为/'} + if not os.path.isdir(args.path): return {'error': '目录不存在'} + is_backup = int(args.isbackup) if 'isbackup' in args else 0 + text = args.text + rtext = args.rtext + exts = args.exts + path = args.path + mode = int(args.mode) if 'mode' in args else 0 + is_subdir = int(args.is_subdir) if 'is_subdir' in args else 0 + iscase = int(args.iscase) if 'iscase' in args else 0 + isword = int(args.isword) if 'isword' in args else 0 + noword = int(args.noword) if 'noword' in args else 0 + exts = exts.split(',') + is_tmpe_files = self.get_exts_files(path, exts, text, mode, isword, iscase, noword, is_subdir,is_backup,rtext) + return is_tmpe_files + + #替換日志 + def get_replace_logs(self,get): + import page + page = page.Page() + count = public.M('panel_search_log').order('id desc').count() + limit = 12 + info = {} + info['count'] = count + info['row'] = limit + info['p'] = 1 + if hasattr(get, 'p'): + info['p'] = int(get['p']) + info['uri'] = get + info['return_js'] = '' + if hasattr(get, 'tojs'): + info['return_js'] = get.tojs + data = {} + data['page'] = page.GetPage(info, '1,2,3,4,5,8') + data['data'] = public.M('panel_search_log').field('id,rtext,exts,path,mode,isword,iscase,noword,backup_path,time').order('id desc').limit(str(page.SHIFT) + ',' + str(page.ROW)).select() + if isinstance(data['data'],str): return public.returnMsg(False,[]) + for i in data['data']: + if not isinstance(i,dict): continue + if 'backup_path' in i : + path=i['backup_path'] + if os.path.exists(path): + i['is_path_status']=True + else: + i['is_path_status'] = False + return public.returnMsg(True, data) \ No newline at end of file diff --git a/class/pluginAuth.cpython-37m-aarch64-linux-gnu.so b/class/pluginAuth.cpython-37m-aarch64-linux-gnu.so index ddea7b11..f2c41088 100644 Binary files a/class/pluginAuth.cpython-37m-aarch64-linux-gnu.so and b/class/pluginAuth.cpython-37m-aarch64-linux-gnu.so differ diff --git a/class/pluginAuth.cpython-37m-i386-linux-gnu.so b/class/pluginAuth.cpython-37m-i386-linux-gnu.so new file mode 100644 index 00000000..f4a32a7f Binary files /dev/null and b/class/pluginAuth.cpython-37m-i386-linux-gnu.so differ diff --git a/class/pluginAuth.cpython-37m-x86_64-linux-gnu.so b/class/pluginAuth.cpython-37m-x86_64-linux-gnu.so index 08a499d2..9789ef36 100644 Binary files a/class/pluginAuth.cpython-37m-x86_64-linux-gnu.so and b/class/pluginAuth.cpython-37m-x86_64-linux-gnu.so differ diff --git a/class/plugin_deployment.py b/class/plugin_deployment.py index 5198a23f..475c8848 100644 --- a/class/plugin_deployment.py +++ b/class/plugin_deployment.py @@ -278,27 +278,29 @@ def SetupPackage(self,get): self.WriteLogs({'name':'设置权限','total':0,'used':0,'pre':0,'speed':0}) public.ExecShell('chmod -R 755 ' + path) public.ExecShell('chown -R www.www ' + path) - if pinfo['chmod']: - for chm in pinfo['chmod']: - public.ExecShell('chmod -R ' + str(chm['mode']) + ' ' + (path + '/' + chm['path']).replace('//','/')) - + if 'chmod' in pinfo: + if pinfo['chmod']: + for chm in pinfo['chmod']: + public.ExecShell('chmod -R ' + str(chm['mode']) + ' ' + (path + '/' + chm['path']).replace('//','/')) + #安装PHP扩展 self.WriteLogs({'name':'安装必要的PHP扩展','total':0,'used':0,'pre':0,'speed':0}) - import files - mfile = files.files() - if type(pinfo['php_ext']) != list : pinfo['php_ext'] = pinfo['php_ext'].strip().split(',') - for ext in pinfo['php_ext']: - if ext == 'pathinfo': - import config - con = config.config() - get.version = php_version - get.type = 'on' - con.setPathInfo(get) - else: - get.name = ext - get.version = php_version - get.type = '1' - mfile.InstallSoft(get) + if 'php_ext' in pinfo: + import files + mfile = files.files() + if type(pinfo['php_ext']) != list : pinfo['php_ext'] = pinfo['php_ext'].strip().split(',') + for ext in pinfo['php_ext']: + if ext == 'pathinfo': + import config + con = config.config() + get.version = php_version + get.type = 'on' + con.setPathInfo(get) + else: + get.name = ext + get.version = php_version + get.type = '1' + mfile.InstallSoft(get) #解禁PHP函数 if 'enable_functions' in pinfo: @@ -366,13 +368,14 @@ def SetupPackage(self,get): #设置运行目录 self.WriteLogs({'name':'设置运行目录','total':0,'used':0,'pre':0,'speed':0}) - if pinfo['run_path'] != '/': - import panelSite; - siteObj = panelSite.panelSite() - mobj = obj() - mobj.id = find['id'] - mobj.runPath = pinfo['run_path'] - siteObj.SetSiteRunPath(mobj) + if 'run_path' in pinfo: + if pinfo['run_path'] != '/': + import panelSite; + siteObj = panelSite.panelSite() + mobj = obj() + mobj.id = find['id'] + mobj.runPath = pinfo['run_path'] + siteObj.SetSiteRunPath(mobj) #导入数据 self.WriteLogs({'name':'导入数据库','total':0,'used':0,'pre':0,'speed':0}) @@ -391,18 +394,18 @@ def SetupPackage(self,get): #清理文件和目录 self.WriteLogs({'name':'清理多余的文件','total':0,'used':0,'pre':0,'speed':0}) - if type(pinfo['remove_file']) == str : pinfo['remove_file'] = pinfo['remove_file'].strip().split(',') - print(pinfo['remove_file']) - for f_path in pinfo['remove_file']: - if not f_path: continue - filename = (path + '/' + f_path).replace('//','/') - if os.path.exists(filename): - if not os.path.isdir(filename): - if f_path.find('.user.ini') != -1: - public.ExecShell("chattr -i " + filename) - os.remove(filename) - else: - public.ExecShell("rm -rf " + filename) + if 'remove_file' in pinfo: + if type(pinfo['remove_file']) == str : pinfo['remove_file'] = pinfo['remove_file'].strip().split(',') + for f_path in pinfo['remove_file']: + if not f_path: continue + filename = (path + '/' + f_path).replace('//','/') + if os.path.exists(filename): + if not os.path.isdir(filename): + if f_path.find('.user.ini') != -1: + public.ExecShell("chattr -i " + filename) + os.remove(filename) + else: + public.ExecShell("rm -rf " + filename) public.serviceReload() if id: self.depTotal(id) diff --git a/class/projectModel/__init__.py b/class/projectModel/__init__.py index e69de29b..8b137891 100644 --- a/class/projectModel/__init__.py +++ b/class/projectModel/__init__.py @@ -0,0 +1 @@ + diff --git a/class/projectModel/base.py b/class/projectModel/base.py new file mode 100644 index 00000000..37f47432 --- /dev/null +++ b/class/projectModel/base.py @@ -0,0 +1,62 @@ +import public,re + +class projectBase: + + def check_port(self, port): + ''' + @name 检查端口是否被占用 + @args port:端口号 + @return: 被占用返回True,否则返回False + @author: lkq 2021-08-28 + ''' + a = public.ExecShell("netstat -nltp|awk '{print $4}'") + if a[0]: + if re.search(':' + port + '\n', a[0]): + return True + else: + return False + else: + return False + + def is_domain(self, domain): + ''' + @name 验证域名合法性 + @args domain:域名 + @return: 合法返回True,否则返回False + @author: lkq 2021-08-28 + ''' + import re + domain_regex = re.compile(r'(?:[A-Z0-9_](?:[A-Z0-9-_]{0,247}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,}(? +#------------------------------------------------------------------- + +#------------------------------ +# node.js模型 +#------------------------------ +import os,sys,re,json,shutil,psutil,time +from projectModel.base import projectBase +import public +try: + from BTPanel import cache +except: + pass + +class main(projectBase): + _panel_path = public.get_panel_path() + _nodejs_plugin_path = public.get_plugin_path('nodejs') + _nodejs_path = '{}/nodejs'.format(public.get_setup_path()) + _log_name = '项目管理' + _npm_exec_log = '{}/logs/npm-exec.log'.format(_panel_path) + _node_pid_path = '{}/vhost/pids'.format(_nodejs_path) + _node_logs_path = '{}/vhost/logs'.format(_nodejs_path) + _node_run_scripts = '{}/vhost/scripts'.format(_nodejs_path) + _pids = None + _vhost_path = '{}/vhost'.format(_panel_path) + _www_home = '/home/www' + + + + def __init__(self): + if not os.path.exists(self._node_run_scripts): + os.makedirs(self._node_run_scripts,493) + + if not os.path.exists(self._node_pid_path): + os.makedirs(self._node_pid_path,493) + + if not os.path.exists(self._node_logs_path): + os.makedirs(self._node_logs_path,493) + + if not os.path.exists(self._www_home): + os.makedirs(self._www_home,493) + public.set_own(self._www_home,'www') + + + def get_exec_logs(self,get): + ''' + @name 获取执行日志 + @author hwliang<2021-08-09> + @param get + @return string + ''' + if not os.path.exists(self._npm_exec_log): return public.returnMsg(False,'NODE_NOT_EXISTS') + return public.GetNumLines(self._npm_exec_log,20) + + + def get_project_list(self,get): + ''' + @name 获取项目列表 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + + if not 'p' in get: get.p = 1 + if not 'limit' in get: get.limit = 20 + if not 'callback' in get: get.callback = '' + if not 'order' in get: get.order = 'id desc' + + if 'search' in get: + get.project_name = get.search.strip() + search = "%{}%".format(get.project_name) + count = public.M('sites').where('project_type=? AND (name LIKE ? OR ps LIKE ?)',('Node',search,search)).count() + data = public.get_page(count,int(get.p),int(get.limit),get.callback) + data['data'] = public.M('sites').where('project_type=? AND (name LIKE ? OR ps LIKE ?)',('Node',search,search)).limit(data['shift'] + ',' + data['row']).order(get.order).select() + else: + count = public.M('sites').where('project_type=?','Node').count() + data = public.get_page(count,int(get.p),int(get.limit),get.callback) + data['data'] = public.M('sites').where('project_type=?','Node').limit(data['shift'] + ',' + data['row']).order(get.order).select() + + for i in range(len(data['data'])): + data['data'][i] = self.get_project_stat(data['data'][i]) + return data + + + def get_ssl_end_date(self,project_name): + ''' + @name 获取SSL信息 + @author hwliang<2021-08-09> + @param project_name 项目名称 + @return dict + ''' + import data + return data.data().get_site_ssl_info('node_{}'.format(project_name)) + + + + def is_install_nodejs(self,get): + ''' + @name 是否安装nodejs版本管理器 + @author hwliang<2021-08-09> + @param get 请求数据 + @return bool + ''' + return os.path.exists(self._nodejs_plugin_path) + + + def get_nodejs_version(self,get): + ''' + @name 获取已安装的nodejs版本 + @author hwliang<2021-08-09> + @param get 请求数据 + @return list + ''' + nodejs_list = [] + if not os.path.exists(self._nodejs_path): return nodejs_list + for v in os.listdir(self._nodejs_path): + if v[0] != 'v' or v.find('.') == -1: continue + nodejs_list.append(v) + return nodejs_list + + + + def get_run_list(self,get): + ''' + @name 获取node项目启动列表 + @author hwliang<2021-08-10> + @param get{ + project_cwd: string<项目目录> + } + ''' + project_cwd = get.project_cwd.strip() + if not os.path.exists(project_cwd): return public.return_error('项目目录不存在!') + package_file = '{}/package.json'.format(project_cwd) + if not os.path.exists(package_file): return {} #public.return_error('没有在项目目录中找到package.json配置文件!') + package_info = json.loads(public.readFile(package_file)) + if not 'scripts' in package_info: return {}# public.return_error('没有在项目配置文件package.json中找到scripts配置项!') + if not package_info['scripts']: return {}# public.return_error('没有找到可用的启动选项!') + return package_info['scripts'] + + + def get_npm_bin(self,nodejs_version): + ''' + @name 获取指定node版本的npm路径 + @author hwliang<2021-08-10> + @param nodejs_version nodejs版本 + @return string + ''' + npm_path = '{}/{}/bin/npm'.format(self._nodejs_path,nodejs_version) + if not os.path.exists(npm_path): return False + return npm_path + + def get_yarn_bin(self,nodejs_version): + ''' + @name 获取指定node版本的yarn路径 + @author hwliang<2021-08-28> + @param nodejs_version nodejs版本 + @return string + ''' + yarn_path = '{}/{}/bin/yarn'.format(self._nodejs_path,nodejs_version) + if not os.path.exists(yarn_path): return False + return yarn_path + + + def get_node_bin(self,nodejs_version): + ''' + @name 获取指定node版本的node路径 + @author hwliang<2021-08-10> + @param nodejs_version nodejs版本 + @return string + ''' + node_path = '{}/{}/bin/node'.format(self._nodejs_path,nodejs_version) + if not os.path.exists(node_path): return False + return node_path + + + def get_last_env(self,nodejs_version,project_cwd = None): + ''' + @name 获取前置环境变量 + @author hwliang<2021-08-25> + @param nodejs_version Node版本 + @return string + ''' + nodejs_bin_path = '{}/{}/bin'.format(self._nodejs_path,nodejs_version) + if project_cwd: + _bin = '{}/node_modules/.bin'.format(project_cwd) + if os.path.exists(_bin): + nodejs_bin_path = _bin + ':' + nodejs_bin_path + + last_env = '''PATH={nodejs_bin_path}:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH +'''.format(nodejs_bin_path = nodejs_bin_path) + return last_env + + + def install_packages(self,get): + ''' + @name 安装指定项目的依赖包 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + } + return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('指定项目不存在!') + if not os.path.exists(project_find['path']): return public.return_error('项目目录不存在!') + package_file = '{}/package.json'.format(project_find['path']) + if not os.path.exists(package_file): return public.return_error('没有在项目目录中找到package.json配置文件!') + nodejs_version = project_find['project_config']['nodejs_version'] + + package_lock_file = '{}/package-lock.json'.format(project_find['path']) + node_modules_path = '{}/node_modules'.format(project_find['path']) + + # 已经安装过的依赖包的情况下,可能存在不同node版本导致的问题,可能需要重新构建依赖包 + rebuild = False + if os.path.exists(package_lock_file) and os.path.exists(node_modules_path): + rebuild = True + + npm_bin = self.get_npm_bin(nodejs_version) + yarn_bin = self.get_yarn_bin(nodejs_version) + if not npm_bin and not yarn_bin: + return public.return_error('指定nodejs版本不存在!') + public.writeFile(self._npm_exec_log,"正在安装依赖包...\n") + public.writeFile(self._npm_exec_log,"正在下载依赖包,请稍后...\n") + if yarn_bin: + if os.path.exists(package_lock_file): + os.remove(package_lock_file) + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} install 2>&1 >> {}".format(project_find['path'],yarn_bin,self._npm_exec_log)) + else: + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} install 2>&1 >> {}".format(project_find['path'],npm_bin,self._npm_exec_log)) + public.writeFile(self._npm_exec_log,"|-Successify --- 命令已执行! ---",'a+') + public.WriteLog(self._log_name, 'Node项目:{}, 安装依赖包完成!'.format(project_find['name'])) + if rebuild: # 重新构建已安装模块? + self.rebuild_project(get.project_name) + return public.return_data(True,'安装依赖包成功!') + + + + def update_packages(self,get): + ''' + @name 更新指定项目的依赖包 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + } + return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('指定项目不存在!') + if not os.path.exists(project_find['path']): return public.return_error('项目目录不存在!') + package_file = '{}/package.json'.format(project_find['path']) + if not os.path.exists(package_file): return public.return_error('没有在项目目录中找到package.json配置文件!') + package_lock_file = '{}/package-lock.json'.format(project_find['path']) + if not os.path.exists(package_lock_file): return public.return_error('请先安装依赖包!') + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + if not npm_bin: + return public.return_error('指定nodejs版本不存在!') + + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} update &> {}".format(project_find['path'],npm_bin,self._npm_exec_log)) + public.WriteLog(self._log_name, '项目[{}]更新所有依赖包'.format(get.project_name)) + return public.return_data(True,'依赖包更新成功!') + + + def reinstall_packages(self,get): + ''' + @name 重新安装指定项目的依赖包 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + } + return dict + ''' + + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('指定项目不存在!') + if not os.path.exists(project_find['path']): return public.return_error('项目目录不存在!') + package_file = '{}/package.json'.format(project_find['path']) + if not os.path.exists(package_file): return public.return_error('没有在项目目录中找到package.json配置文件!') + + package_lock_file = '{}/package-lock.json'.format(project_find['path']) + if os.path.exists(package_lock_file): os.remove(package_lock_file) + package_path = '{}/node_modules' + if os.path.exists(package_path): shutil.rmtree(package_path) + + + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + if not npm_bin: + return public.return_error('指定nodejs版本不存在!') + public.WriteLog(self._log_name,'Node项目:{},已重装所有依赖包') + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} install &> {}".format(project_find['path'],npm_bin,self._npm_exec_log)) + return public.return_data(True,'依赖包重装成功!') + + def get_project_modules(self,get): + ''' + @name 获取指定项目的依赖包列表 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + project_cwd: string<项目目录> 可选 + } + return list + ''' + if not 'project_cwd' in get: + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('指定项目不存在!') + project_cwd = project_find['path'] + else: + project_cwd = get.project_cwd + mod_path = os.path.join(project_cwd,'node_modules') + modules = [] + if not os.path.exists(mod_path): return modules + for mod_name in os.listdir(mod_path): + try: + mod_pack_file = os.path.join(mod_path,mod_name,'package.json') + if not os.path.exists(mod_pack_file): continue + mod_pack_info = json.loads(public.readFile(mod_pack_file)) + pack_info = { + "name": mod_name, + "version": mod_pack_info['version'], + "description":mod_pack_info['description'], + "license": mod_pack_info['license'] if 'license' in mod_pack_info else 'NULL', + "homepage": mod_pack_info['homepage'] + } + modules.append(pack_info) + except: + continue + return modules + + def install_module(self,get): + ''' + @name 安装指定模块 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + mod_name: string<模块名称> + } + @return dict + ''' + + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('指定项目不存在!') + project_cwd = project_find['path'] + + + mod_name = get.mod_name + filename = '{}/node_modules/{}/package.json'.format(project_cwd,mod_name) + if os.path.exists(filename): return public.return_error('指定模块已经安装过了!') + + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + yarn_bin = self.get_yarn_bin(nodejs_version) + + if not npm_bin and not yarn_bin: + return public.return_error('指定nodejs版本不存在!') + if yarn_bin: + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} add {} &> {}".format(project_find['path'],yarn_bin,mod_name,self._npm_exec_log)) + else: + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} install {} &> {}".format(project_find['path'],npm_bin,mod_name,self._npm_exec_log)) + if not os.path.exists(filename): return public.return_error('指定模块安装失败!') + public.WriteLog(self._log_name,'Node项目{} , {}模块安装完成!'.format(get.project_name,mod_name)) + return public.return_data(True,'安装成功!') + + def uninstall_module(self,get): + ''' + @name 卸载指定模块 + @author hwliang<2021-04-08> + @param get{ + project_name: string<项目名称> + mod_name: string<模块名称> + } + @return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('指定项目不存在!') + project_cwd = project_find['path'] + + mod_name = get.mod_name + filename = '{}/node_modules/{}/package.json'.format(project_cwd,mod_name) + if not os.path.exists(filename): return public.return_error('指定模块未安装!') + + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + yarn_bin = self.get_yarn_bin(nodejs_version) + if not npm_bin and not yarn_bin: + return public.return_error('指定nodejs版本不存在!') + if yarn_bin: + result = public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} remove {}".format(project_find['path'],yarn_bin,mod_name)) + else: + result = public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} uninstall {}".format(project_find['path'],npm_bin,mod_name)) + if os.path.exists(filename): + result = "\n".join(result) + if result.find('looking for funding') != -1: + return public.return_error("此模块被其它已安装模块依赖,无法卸载!") + return public.return_error("无法卸载此模块!") + + public.WriteLog(self._log_name,'Node项目{} , {}模块卸载完成!'.format(get.project_name,mod_name)) + return public.return_data(True,'模块卸载成功!') + + + def upgrade_module(self,get): + ''' + @name 更新指定模块 + @author hwliang<2021-08-10> + @param get{ + project_name: string<项目名称> + mod_name: string<模块名称> + } + @return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('指定项目不存在!') + project_cwd = project_find['path'] + + mod_name = get.mod_name + filename = '{}/node_modules/{}/package.json'.format(project_cwd,mod_name) + if not os.path.exists(filename): return public.return_error('指定模块未安装!') + + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + + if not npm_bin: + return public.return_error('指定nodejs版本不存在!') + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} update {} &> {}".format(project_find['path'],npm_bin,mod_name,self._npm_exec_log)) + public.WriteLog(self._log_name,'Node项目{} , {}模块更新完成!'.format(get.project_name,mod_name)) + return public.return_data(True,'模块更新成功!') + + + def create_project(self,get): + ''' + @name 创建新的项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + project_cwd: string<项目目录> + project_script: string<项目脚本> + project_ps: string<项目备注信息> + bind_extranet: int<是否绑定外网> 1:是 0:否 + domains: list<域名列表> ["domain1:80","domain2:80"] // 在bind_extranet=1时,需要填写 + is_power_on: int<是否开机启动> 1:是 0:否 + run_user: string<运行用户> + max_memory_limit: int<最大内存限制> // 超出此值项目将被强制重启 + nodejs_version: string + } + @return dict + ''' + if not isinstance(get,public.dict_obj): return public.return_error('参数类型错误,需要dict_obj对像') + if not self.is_install_nodejs(get): + return public.return_error('请先安装nodejs版本管理器,并安装至少1个node.js版本') + + project_name = get.project_name.strip() + if not re.match("^\w+$",project_name): + return public.return_error('项目名称格式不正确,支持字母、数字、下划线,表达式: ^[0-9A-Za-z_]$') + + if public.M('sites').where('name=?',(get.project_name,)).count(): + return public.return_error('指定项目名称已存在: {}'.format(get.project_name)) + get.project_cwd = get.project_cwd.strip() + if not os.path.exists(get.project_cwd): + return public.return_error('项目目录不存在: {}'.format(get.project_cwd)) + + # 端口占用检测 + if self.check_port_is_used(get.get('port/port')): + return public.return_error('指定端口已被其它应用占用,请修改您的项目配置使用其它端口, 端口: {}'.format(get.port)) + + domains = [] + if get.bind_extranet == 1: + domains = get.domains + if not public.is_apache_nginx(): return public.return_error('需要安装Nginx或Apache才能使用外网映射功能') + for domain in domains: + domain_arr = domain.split(':') + if public.M('domain').where('name=?',domain_arr[0]).count(): + return public.return_error('指定域名已存在: {}'.format(domain)) + pdata = { + 'name': get.project_name, + 'path': get.project_cwd, + 'ps': get.project_ps, + 'status':1, + 'type_id':0, + 'project_type': 'Node', + 'project_config': json.dumps( + { + 'project_name': get.project_name, + 'project_cwd': get.project_cwd, + 'project_script': get.project_script, + 'bind_extranet': get.bind_extranet, + 'domains': [], + 'is_power_on': get.is_power_on, + 'run_user': get.run_user, + 'max_memory_limit': get.max_memory_limit, + 'nodejs_version': get.nodejs_version, + 'port': int(get.port) + } + ), + 'addtime': public.getDate() + } + + project_id = public.M('sites').insert(pdata) + if get.bind_extranet == 1: + format_domains = [] + for domain in domains: + if domain.find(':') == -1: domain += ':80' + format_domains.append(domain) + get.domains = format_domains + self.project_add_domain(get) + self.set_config(get.project_name) + public.WriteLog(self._log_name,'添加Node.js项目{}'.format(get.project_name)) + self.install_packages(get) + self.start_project(get) + return public.return_data(True,'添加项目成功',project_id) + + def modify_project(self,get): + ''' + @name 修改指定项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + project_cwd: string<项目目录> + project_script: string<项目脚本> + project_ps: string<项目备注信息> + is_power_on: int<是否开机启动> 1:是 0:否 + run_user: string<运行用户> + max_memory_limit: int<最大内存限制> // 超出此值项目将被强制重启 + nodejs_version: string + } + @return dict + ''' + if not isinstance(get,public.dict_obj): return public.return_error('参数类型错误,需要dict_obj对像') + if not self.is_install_nodejs(get): + return public.return_error('请先安装nodejs版本管理器,并安装至少1个node.js版本') + project_find = self.get_project_find(get.project_name) + if not project_find: + return public.return_error('指定项目不存在: {}'.format(get.project_name)) + + if not os.path.exists(get.project_cwd): + return public.return_error('项目目录不存在: {}'.format(get.project_cwd)) + rebuild = False + if hasattr(get,'port'): + if int(project_find['project_config']['port']) != int(get.port): + if self.check_port_is_used(get.get('port/port'),True): + return public.return_error('指定端口已被其它应用占用,请修改您的项目配置使用其它端口, 端口: {}'.format(get.port)) + project_find['project_config']['port'] = int(get.port) + if hasattr(get,'project_cwd'): project_find['project_config']['project_cwd'] = get.project_cwd + if hasattr(get,'project_script'): + if not get.project_script.strip(): + return public.return_error('启动命令不能为空') + project_find['project_config']['project_script'] = get.project_script.strip() + if hasattr(get,'is_power_on'): project_find['project_config']['is_power_on'] = get.is_power_on + if hasattr(get,'run_user'): project_find['project_config']['run_user'] = get.run_user + if hasattr(get,'max_memory_limit'): project_find['project_config']['max_memory_limit'] = get.max_memory_limit + if hasattr(get,'nodejs_version'): + if project_find['project_config']['nodejs_version'] != get.nodejs_version: + rebuild = True + project_find['project_config']['nodejs_version'] = get.nodejs_version + pdata = { + 'path': get.project_cwd, + 'ps': get.project_ps, + 'project_config': json.dumps(project_find['project_config']) + } + + public.M('sites').where('name=?',(get.project_name,)).update(pdata) + self.set_config(get.project_name) + public.WriteLog(self._log_name,'修改Node.js项目{}'.format(get.project_name)) + if rebuild: + self.rebuild_project(get.project_name) + return public.return_data(True,'修改项目成功') + + def rebuild_project(self,project_name): + ''' + @name 重新构建指定项目 + @author hwliang<2021-08-26> + @param project_name: string<项目名称> + @return bool + ''' + project_find = self.get_project_find(project_name) + if not project_find: return False + nodejs_version = project_find['project_config']['nodejs_version'] + npm_bin = self.get_npm_bin(nodejs_version) + + public.ExecShell(self.get_last_env(nodejs_version) + "cd {} && {} rebuild 2>&1 >> {}".format(project_find['path'],npm_bin,self._npm_exec_log)) + return True + + + def remove_project(self,get): + ''' + @name 删除指定项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: + return public.return_error('指定项目不存在: {}'.format(get.project_name)) + + self.stop_project(get) + self.clear_config(get.project_name) + public.M('domain').where('pid=?',(project_find['id'],)).delete() + public.M('sites').where('name=?',(get.project_name,)).delete() + + pid_file = "{}/{}.pid".format(self._node_pid_path,get.project_name) + if os.path.exists(pid_file): os.remove(pid_file) + script_file = '{}/{}.sh'.format(self._node_run_scripts,get.project_name) + if os.path.exists(script_file): os.remove(script_file) + log_file = '{}/{}.log'.format(self._node_logs_path,get.project_name) + if os.path.exists(log_file): os.remove(log_file) + public.WriteLog(self._log_name,'删除Node.js项目{}'.format(get.project_name)) + return public.return_data(True,'删除项目成功') + + + def project_get_domain(self,get): + ''' + @name 获取指定项目的域名列表 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + project_id = public.M('sites').where('name=?',(get.project_name,)).getField('id') + domains = public.M('domain').where('pid=?',(project_id,)).order('id desc').select() + project_find = self.get_project_find(get.project_name) + if len(domains) != len(project_find['project_config']['domains']): + public.M('domain').where('pid=?',(project_id,)).delete() + if not project_find: return [] + for d in project_find['project_config']['domains']: + domain = {} + arr = d.split(':') + if len(arr) < 2: arr.append(80) + domain['name'] = arr[0] + domain['port'] = int(arr[1]) + domain['pid'] = project_id + domain['addtime'] = public.getDate() + public.M('domain').insert(domain) + if project_find['project_config']['domains']: + domains = public.M('domain').where('pid=?',(project_id,)).select() + return domains + + + def project_add_domain(self,get): + ''' + @name 为指定项目添加域名 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + domains: list<域名列表> + } + @return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: + return public.return_error('指定项目不存在') + project_id = project_find['id'] + + domains = get.domains + success_list = [] + error_list = [] + for domain in domains: + domain = domain.strip() + domain_arr = domain.split(':') + if len(domain_arr) == 1: + domain_arr.append(80) + domain += ':80' + if not public.M('domain').where('name=?',(domain_arr[0],)).count(): + public.M('domain').add('name,pid,port,addtime',(domain_arr[0],project_id,domain_arr[1],public.getDate())) + if not domain in project_find['project_config']['domains']: + project_find['project_config']['domains'].append(domain) + public.WriteLog(self._log_name,'成功添加域名{}到项目{}'.format(domain,get.project_name)) + success_list.append(domain) + else: + public.WriteLog(self._log_name,'添加域名错误,域名{}已存在'.format(domain)) + error_list.append(domain) + + if success_list: + public.M('sites').where('id=?',(project_id,)).save('project_config',json.dumps(project_find['project_config'])) + self.set_config(get.project_name) + + return public.return_data(True,"成功添加{}个域名,失败{}个!".format(len(success_list),len(error_list)),error_msg=error_list) + + + def project_remove_domain(self,get): + ''' + @name 为指定项目删除域名 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + domain: string<域名> + } + @return dict + ''' + project_find = self.get_project_find(get.project_name) + if not project_find: + return public.return_error('指定项目不存在') + last_domain = get.domain + domain_arr = get.domain.split(':') + if len(domain_arr) == 1: + domain_arr.append(80) + + project_id = public.M('sites').where('name=?',(get.project_name,)).getField('id') + if project_find['project_config']['bind_extranet']: + if len(project_find['project_config']['domains']) == 1: return public.return_error('已映射外网的项目至少需要一个域名') + domain_id = public.M('domain').where('name=? AND pid=?',(domain_arr[0],project_id)).getField('id') + if not domain_id: + return public.return_error('指定域名不存在') + public.M('domain').where('id=?',(domain_id,)).delete() + + if get.domain in project_find['project_config']['domains']: + project_find['project_config']['domains'].remove(get.domain) + if get.domain+":80" in project_find['project_config']['domains']: + project_find['project_config']['domains'].remove(get.domain + ":80") + + public.M('sites').where('id=?',(project_id,)).save('project_config',json.dumps(project_find['project_config'])) + public.WriteLog(self._log_name,'从项目:{},删除域名{}'.format(get.project_name,get.domain)) + self.set_config(get.project_name) + return public.return_data(True,'删除域名成功') + + + def bind_extranet(self,get): + ''' + @name 绑定外网 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + if not public.is_apache_nginx(): return public.return_error('需要安装Nginx或Apache才能使用外网映射功能') + project_name = get.project_name.strip() + project_find = self.get_project_find(project_name) + if not project_find: return public.return_error('项目不存在') + if not project_find['project_config']['domains']: return public.return_error('请先到【域名管理】选项中至少添加一个域名') + project_find['project_config']['bind_extranet'] = 1 + public.M('sites').where("id=?",(project_find['id'],)).setField('project_config',json.dumps(project_find['project_config'])) + self.set_config(project_name) + public.WriteLog(self._log_name,'Node项目{}, 开启外网映射'.format(project_name)) + return public.return_data(True,'开启外网映射成功') + + + def set_config(self,project_name): + ''' + @name 设置项目配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return bool + ''' + project_find = self.get_project_find(project_name) + if not project_find: return False + if not project_find['project_config']: return False + if not project_find['project_config']['bind_extranet']: return False + if not project_find['project_config']['domains']: return False + self.set_nginx_config(project_find) + self.set_apache_config(project_find) + public.serviceReload() + return True + + def clear_config(self,project_name): + ''' + @name 清除项目配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return bool + ''' + project_find = self.get_project_find(project_name) + if not project_find: return False + self.clear_nginx_config(project_find) + self.clear_apache_config(project_find) + public.serviceReload() + return True + + def clear_apache_config(self,project_find): + ''' + @name 清除apache配置 + @author hwliang<2021-08-09> + @param project_find: dict<项目信息> + @return bool + ''' + project_name = project_find['name'] + config_file = "{}/apache/node_{}.conf".format(self._vhost_path,project_name) + if os.path.exists(config_file): + os.remove(config_file) + return True + + + def clear_nginx_config(self,project_find): + ''' + @name 清除nginx配置 + @author hwliang<2021-08-09> + @param project_find: dict<项目信息> + @return bool + ''' + project_name = project_find['name'] + config_file = "{}/nginx/node_{}.conf".format(self._vhost_path,project_name) + if os.path.exists(config_file): + os.remove(config_file) + rewrite_file = "{panel_path}/vhost/rewrite/node_{project_name}.conf".format(panel_path = self._panel_path,project_name = project_name) + if os.path.exists(rewrite_file): + os.remove(rewrite_file) + return True + + + def set_nginx_config(self,project_find): + ''' + @name 设置Nginx配置 + @author hwliang<2021-08-09> + @param project_find: dict<项目信息> + @return bool + ''' + project_name = project_find['name'] + ports = [] + domains = [] + + for d in project_find['project_config']['domains']: + domain_tmp = d.split(':') + if len(domain_tmp) == 1: domain_tmp.append(80) + if not int(domain_tmp[1]) in ports: + ports.append(int(domain_tmp[1])) + if not domain_tmp[0] in domains: + domains.append(domain_tmp[0]) + listen_ipv6 = public.listen_ipv6() + listen_ports = '' + for p in ports: + listen_ports += " listen {};\n".format(p) + if listen_ipv6: + listen_ports += " listen [::]:{};\n".format(p) + listen_ports = listen_ports.strip() + + is_ssl,is_force_ssl = self.exists_nginx_ssl(project_name) + ssl_config = '' + if is_ssl: + listen_ports += "\n listen 443 ssl http2;" + if listen_ipv6: listen_ports += "\n listen [::]:443 ssl http2;" + + ssl_config = '''ssl_certificate {vhost_path}/cert/{priject_name}/fullchain.pem; + ssl_certificate_key {vhost_path}/cert/{priject_name}/privkey.pem; + ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + add_header Strict-Transport-Security "max-age=31536000"; + error_page 497 https://$host$request_uri;'''.format(vhost_path = self._vhost_path,priject_name = project_name) + + if is_force_ssl: + ssl_config += ''' + #HTTP_TO_HTTPS_START + if ($server_port !~ 443){ + rewrite ^(/.*)$ https://$host$1 permanent; + } + #HTTP_TO_HTTPS_END''' + + config_file = "{}/nginx/node_{}.conf".format(self._vhost_path,project_name) + template_file = "{}/template/nginx/node_http.conf".format(self._vhost_path) + + config_body = public.readFile(template_file) + config_body = config_body.format( + site_path = project_find['path'], + domains = ' '.join(domains), + project_name = project_name, + panel_path = self._panel_path, + log_path = public.get_logs_path(), + url = 'http://127.0.0.1:{}'.format(project_find['project_config']['port']), + host = '127.0.0.1', + listen_ports = listen_ports, + ssl_config = ssl_config + ) + + # # 恢复旧的SSL配置 + # ssl_config = self.get_nginx_ssl_config(project_name) + # if ssl_config: + # config_body.replace('#error_page 404/404.html;',ssl_config) + + + rewrite_file = "{panel_path}/vhost/rewrite/node_{project_name}.conf".format(panel_path = self._panel_path,project_name = project_name) + if not os.path.exists(rewrite_file): public.writeFile(rewrite_file,'# 请将伪静态规则或自定义NGINX配置填写到此处\n') + public.writeFile(config_file,config_body) + return True + + def get_nginx_ssl_config(self,project_name): + ''' + @name 获取项目Nginx SSL配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return string + ''' + result = '' + config_file = "{}/nginx/node_{}".format(self._vhost_path,project_name) + if not os.path.exists(config_file): + return result + + config_body = public.readFile(config_file) + if not config_body: + return result + if config_body.find('ssl_certificate') == -1: + return result + + ssl_body = re.search("#SSL-START(.|\n)+#SSL-END",config_body) + if not ssl_body: return result + result = ssl_body.group() + return result + + def exists_nginx_ssl(self,project_name): + ''' + @name 判断项目是否配置Nginx SSL配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return tuple + ''' + config_file = "{}/nginx/node_{}.conf".format(public.get_vhost_path(),project_name) + if not os.path.exists(config_file): + return False,False + + config_body = public.readFile(config_file) + if not config_body: + return False,False + + is_ssl,is_force_ssl = False,False + if config_body.find('ssl_certificate') != -1: + is_ssl = True + if config_body.find('HTTP_TO_HTTPS_START') != -1: + is_force_ssl = True + return is_ssl,is_force_ssl + + def exists_apache_ssl(self,project_name): + ''' + @name 判断项目是否配置Apache SSL配置 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return bool + ''' + config_file = "{}/apache/node_{}.conf".format(public.get_vhost_path(),project_name) + if not os.path.exists(config_file): + return False,False + + config_body = public.readFile(config_file) + if not config_body: + return False,False + + is_ssl,is_force_ssl = False,False + if config_body.find('SSLCertificateFile') != -1: + is_ssl = True + if config_body.find('HTTP_TO_HTTPS_START') != -1: + is_force_ssl = True + return is_ssl,is_force_ssl + + def set_apache_config(self,project_find): + ''' + @name 设置Apache配置 + @author hwliang<2021-08-09> + @param project_find: dict<项目信息> + @return bool + ''' + project_name = project_find['name'] + + # 处理域名和端口 + ports = [] + domains = [] + for d in project_find['project_config']['domains']: + domain_tmp = d.split(':') + if len(domain_tmp) == 1: domain_tmp.append(80) + if not int(domain_tmp[1]) in ports: + ports.append(int(domain_tmp[1])) + if not domain_tmp[0] in domains: + domains.append(domain_tmp[0]) + + + config_file = "{}/apache/node_{}.conf".format(self._vhost_path,project_name) + template_file = "{}/template/apache/node_http.conf".format(self._vhost_path) + config_body = public.readFile(template_file) + apache_config_body = '' + + # 旧的配置文件是否配置SSL + is_ssl,is_force_ssl = self.exists_apache_ssl(project_name) + if is_ssl: + if not 443 in ports: ports.append(443) + + from panelSite import panelSite + s = panelSite() + + # 根据端口列表生成配置 + for p in ports: + # 生成SSL配置 + ssl_config = '' + if p == 443 and is_ssl: + ssl_key_file = "{vhost_path}/cert/{project_name}/privkey.pem".format(project_name = project_name,vhost_path = public.get_vhost_path()) + if not os.path.exists(ssl_key_file): continue # 不存在证书文件则跳过 + ssl_config = '''#SSL + SSLEngine On + SSLCertificateFile {vhost_path}/cert/{project_name}/fullchain.pem + SSLCertificateKeyFile {vhost_path}/cert/{project_name}/privkey.pem + SSLCipherSuite EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5 + SSLProtocol All -SSLv2 -SSLv3 -TLSv1 + SSLHonorCipherOrder On'''.format(project_name = project_name,vhost_path = public.get_vhost_path()) + else: + if is_force_ssl: + ssl_config = '''#HTTP_TO_HTTPS_START + + RewriteEngine on + RewriteCond %{SERVER_PORT} !^443$ + RewriteRule (.*) https://%{SERVER_NAME}$1 [L,R=301] + + #HTTP_TO_HTTPS_END''' + + # 生成vhost主体配置 + apache_config_body += config_body.format( + site_path = project_find['path'], + server_name = '{}.{}'.format(p,project_name), + domains = ' '.join(domains), + log_path = public.get_logs_path(), + server_admin = 'admin@{}'.format(project_name), + url = 'http://127.0.0.1:{}'.format(project_find['project_config']['port']), + port = p, + ssl_config = ssl_config, + project_name = project_name + ) + apache_config_body += "\n" + + # 添加端口到主配置文件 + if not p in [80]: + s.apacheAddPort(p) + + # 写.htaccess + rewrite_file = "{}/.htaccess".format(project_find['path']) + if not os.path.exists(rewrite_file): public.writeFile(rewrite_file,'# 请将伪静态规则或自定义Apache配置填写到此处\n') + + # 写配置文件 + public.writeFile(config_file,apache_config_body) + return True + + + def unbind_extranet(self,get): + ''' + @name 解绑外网 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + project_name = get.project_name.strip() + self.clear_config(project_name) + public.serviceReload() + project_find = self.get_project_find(project_name) + project_find['project_config']['bind_extranet'] = 0 + public.M('sites').where("id=?",(project_find['id'],)).setField('project_config',json.dumps(project_find['project_config'])) + public.WriteLog(self._log_name,'Node项目{}, 关闭外网映射'.format(project_name)) + return public.return_data(True,'关闭成功') + + + def get_project_pids(self,get = None,pid = None): + ''' + @name 获取项目进程pid列表 + @author hwliang<2021-08-10> + @param pid: string<项目pid> + @return list + ''' + if get: pid = int(get.pid) + if not self._pids: self._pids = psutil.pids() + project_pids = [] + + for i in self._pids: + try: + p = psutil.Process(i) + except: continue + if p.ppid() == pid: + if i in project_pids: continue + if p.name() in ['bash']: continue + project_pids.append(i) + + other_pids = [] + for i in project_pids: + other_pids += self.get_project_pids(pid=i) + if os.path.exists('/proc/{}'.format(pid)): + project_pids.append(pid) + + all_pids = list(set(project_pids + other_pids)) + if not all_pids: + all_pids = self.get_other_pids(pid) + return sorted(all_pids) + + def get_other_pids(self,pid): + ''' + @name 获取其他进程pid列表 + @author hwliang<2021-08-10> + @param pid: string<项目pid> + @return list + ''' + plugin_name = None + for pid_name in os.listdir(self._node_pid_path): + pid_file = '{}/{}'.format(self._node_pid_path,pid_name) + s_pid = int(public.readFile(pid_file)) + if pid == s_pid: + plugin_name = pid_name[:-4] + break + project_find = self.get_project_find(plugin_name) + if not project_find: return [] + if not self._pids: self._pids = psutil.pids() + all_pids = [] + for i in self._pids: + try: + p = psutil.Process(i) + if p.cwd() == project_find['path'] and p.username() == project_find['project_config']['run_user']: + if p.name() in ['node','npm','pm2']: + all_pids.append(i) + except: continue + return all_pids + + def kill_pids(self,get=None,pids = None): + ''' + @name 结束进程列表 + @author hwliang<2021-08-10> + @param pids: string<进程pid列表> + @return dict + ''' + if get: pids = get.pids + if not pids: return public.return_data(True, '没有进程') + pids = sorted(pids,reverse=True) + for i in pids: + try: + p = psutil.Process(i) + p.kill() + except: + pass + return public.return_data(True, '进程已全部结束') + + + + + def start_project(self,get): + ''' + @name 启动项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + pid_file = "{}/{}.pid".format(self._node_pid_path,get.project_name) + if os.path.exists(pid_file): + self.stop_project(get) + + project_find = self.get_project_find(get.project_name) + if not project_find: return public.return_error('项目不存在') + + if not os.path.exists(project_find['path']): + error_msg = '启动失败,Nodejs项目{},运行目录{}不存在!'.format(get.project_name,project_find['path']) + public.WriteLog(self._log_name,error_msg) + return public.return_error(error_msg) + + # 是否安装依赖模块? + package_file = "{}/package.json".format(project_find['path']) + package_info = {} + if os.path.exists(package_file): + node_modules_path = "{}/node_modules".format(project_find['path']) + if not os.path.exists(node_modules_path): + return public.return_error('请先到模块管理中点击【一键安装项目模块】来安装模块依赖!') + package_info = json.loads(public.readFile(package_file)) + if not package_info: package_info['scripts'] = {} + if 'scripts' not in package_info: package_info['scripts'] = {} + try: + scripts_keys = package_info['scripts'].keys() + except: + scripts_keys = [] + + + # 前置准备 + nodejs_version = project_find['project_config']['nodejs_version'] + node_bin = self.get_node_bin(nodejs_version) + npm_bin = self.get_npm_bin(nodejs_version) + project_script = project_find['project_config']['project_script'].strip() + log_file = "{}/{}.log".format(self._node_logs_path,get.project_name) + if not project_script: return public.return_error('未配置启动脚本') + + last_env = self.get_last_env(nodejs_version,project_find['path']) + + # 生成启动脚本 + if os.path.exists(project_script): + start_cmd = '''{last_env} +cd {project_cwd} +nohup {node_bin} {project_script} 2>&1 >> {log_file} & +echo $! > {pid_file} +'''.format( + project_cwd = project_find['path'], + node_bin = node_bin, + project_script = project_script, + log_file = log_file, + pid_file = pid_file, + last_env = last_env +) + elif project_script in scripts_keys: + start_cmd = '''{last_env} +cd {project_cwd} +nohup {npm_bin} run {project_script} 2>&1 >> {log_file} & +echo $! > {pid_file} +'''.format( + project_cwd = project_find['path'], + npm_bin = npm_bin, + project_script = project_script, + pid_file = pid_file, + log_file = log_file, + last_env = last_env +) + else: + start_cmd = '''{last_env} +cd {project_cwd} +nohup {project_script} 2>&1 >> {log_file} & +echo $! > {pid_file} +'''.format( + project_cwd = project_find['path'], + project_script = project_script, + pid_file = pid_file, + log_file = log_file, + last_env = last_env +) + script_file = "{}/{}.sh".format(self._node_run_scripts,get.project_name) + + # 写入启动脚本 + public.writeFile(script_file,start_cmd) + if os.path.exists(pid_file): os.remove(pid_file) + + # 处理前置权限 + public.ExecShell("chown -R {user}:{user} {project_cwd}".format(user=project_find['project_config']['run_user'],project_cwd=project_find['path'])) + public.ExecShell("chown -R www:www {}/vhost".format(self._nodejs_path)) + public.ExecShell("chmod 755 {} {} {}".format(self._nodejs_path,public.get_setup_path(),'/www')) + public.set_own(script_file,project_find['project_config']['run_user'],project_find['project_config']['run_user']) + public.set_mode(script_file,755) + + # 执行脚本文件 + p = public.ExecShell("bash {}".format(script_file),user=project_find['project_config']['run_user']) + time.sleep(1) + if not os.path.exists(pid_file): + p = '\n'.join(p) + if p.find('[Errno 0]') != -1: + if os.path.exists('{}/bt_security'.format(public.get_plugin_path())): + return public.return_error('启动命令被【堡塔防提权】拦截,请关闭{}用户的防护'.format(project_find['project_config']['run_user'])) + return public.return_error('启动命令被未知安全软件拦截,请检查安装软件日志') + return public.return_error('启动失败
{}
'.format(p)) + + # 获取PID + pid = int(public.readFile(pid_file)) + pids = self.get_project_pids(pid=pid) + if not pids: + if os.path.exists(pid_file): os.remove(pid_file) + return public.return_error('启动失败
{}'.format(public.GetNumLines(log_file,20))) + + return public.return_data(True, '启动成功', pids) + + + def stop_project(self,get): + ''' + @name 停止项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + pid_file = "{}/{}.pid".format(self._node_pid_path,get.project_name) + if not os.path.exists(pid_file): return public.return_error('项目未启动') + pid = int(public.readFile(pid_file)) + pids = self.get_project_pids(pid=pid) + if not pids: return public.return_error('项目未启动') + self.kill_pids(pids=pids) + if os.path.exists(pid_file): os.remove(pid_file) + return public.return_data(True, '停止成功') + + def restart_project(self,get): + ''' + @name 重启项目 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + res = self.stop_project(get) + if not res['status']: return res + res = self.start_project(get) + if not res['status']: return res + return public.return_data(True, '重启成功') + + def get_project_log(self,get): + ''' + @name 获取项目日志 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + log_file = "{}/{}.log".format(self._node_logs_path,get.project_name) + if not os.path.exists(log_file): return public.return_error('日志文件不存在') + return public.GetNumLines(log_file,200) + + + def get_project_load_info(self,get = None,project_name = None): + ''' + @name 获取项目负载信息 + @author hwliang<2021-08-12> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + if get: project_name = get.project_name.strip() + load_info = {} + pid_file = "{}/{}.pid".format(self._node_pid_path,project_name) + if not os.path.exists(pid_file): return load_info + pid = int(public.readFile(pid_file)) + pids = self.get_project_pids(pid=pid) + if not pids: return load_info + for i in pids: + process_info = self.get_process_info_by_pid(i) + if process_info: load_info[i] = process_info + return load_info + + + def object_to_dict(self,obj): + ''' + @name 将对象转换为字典 + @author hwliang<2021-08-09> + @param obj + @return dict + ''' + result = {} + for name in dir(obj): + value = getattr(obj, name) + if not name.startswith('__') and not callable(value) and not name.startswith('_'): result[name] = value + return result + + + def list_to_dict(self,data): + ''' + @name 将列表转换为字典 + @author hwliang<2021-08-09> + @param data + @return dict + ''' + result = [] + for s in data: + result.append(self.object_to_dict(s)) + return result + + + def get_connects(self,pid): + ''' + @name 获取进程连接信息 + @author hwliang<2021-08-09> + @param pid + @return dict + ''' + connects = 0 + try: + if pid == 1: return connects + tp = '/proc/' + str(pid) + '/fd/' + if not os.path.exists(tp): return connects + for d in os.listdir(tp): + fname = tp + d + if os.path.islink(fname): + l = os.readlink(fname) + if l.find('socket:') != -1: connects += 1 + except:pass + return connects + + + def format_connections(self,connects): + ''' + @name 获取进程网络连接信息 + @author hwliang<2021-08-09> + @param connects + @return list + ''' + result = [] + for i in connects: + raddr = i.raddr + if not i.raddr: + raddr = ('',0) + laddr = i.laddr + if not i.laddr: + laddr = ('',0) + result.append({ + "fd": i.fd, + "family": i.family, + "local_addr": laddr[0], + "local_port": laddr[1], + "client_addr": raddr[0], + "client_rport": raddr[1], + "status": i.status + }) + return result + + + def get_process_info_by_pid(self,pid): + ''' + @name 获取进程信息 + @author hwliang<2021-08-12> + @param pid: int<进程id> + @return dict + ''' + process_info = {} + try: + if not os.path.exists('/proc/{}'.format(pid)): return process_info + p = psutil.Process(pid) + status_ps = {'sleeping':'睡眠','running':'活动'} + with p.oneshot(): + p_mem = p.memory_full_info() + if p_mem.uss + p_mem.rss + p_mem.pss + p_mem.data == 0: return process_info + p_state = p.status() + if p_state in status_ps: p_state = status_ps[p_state] + # process_info['exe'] = p.exe() + process_info['name'] = p.name() + process_info['pid'] = pid + process_info['ppid'] = p.ppid() + process_info['create_time'] = int(p.create_time()) + process_info['status'] = p_state + process_info['user'] = p.username() + process_info['memory_used'] = p_mem.uss + process_info['cpu_percent'] = self.get_cpu_precent(p) + process_info['io_write_bytes'],process_info['io_read_bytes'] = self.get_io_speed(p) + process_info['connections'] = self.format_connections(p.connections()) + process_info['connects'] = self.get_connects(pid) + process_info['open_files'] = self.list_to_dict(p.open_files()) + process_info['threads'] = p.num_threads() + process_info['exe'] = ' '.join(p.cmdline()) + return process_info + except: + return process_info + + + def get_io_speed(self,p): + ''' + @name 获取磁盘IO速度 + @author hwliang<2021-08-12> + @param p: Process<进程对像> + @return list + ''' + skey = "io_speed_{}".format(p.pid) + old_pio = cache.get(skey) + pio = p.io_counters() + if not old_pio: + cache.set(skey,[pio,time.time()],3600) + # time.sleep(0.1) + old_pio = cache.get(skey) + pio = p.io_counters() + + old_write_bytes = old_pio[0].write_bytes + old_read_bytes = old_pio[0].read_bytes + old_time = old_pio[1] + + new_time = time.time() + write_bytes = pio.write_bytes + read_bytes = pio.read_bytes + + cache.set(skey,[pio,new_time],3600) + + write_speed = int((write_bytes - old_write_bytes) / (new_time - old_time)) + read_speed = int((read_bytes - old_read_bytes) / (new_time - old_time)) + + return write_speed,read_speed + + + + + + def get_cpu_precent(self,p): + ''' + @name 获取进程cpu使用率 + @author hwliang<2021-08-09> + @param p: Process<进程对像> + @return dict + ''' + skey = "cpu_pre_{}".format(p.pid) + old_cpu_times = cache.get(skey) + + process_cpu_time = self.get_process_cpu_time(p.cpu_times()) + if not old_cpu_times: + cache.set(skey,[process_cpu_time,time.time()],3600) + # time.sleep(0.1) + old_cpu_times = cache.get(skey) + process_cpu_time = self.get_process_cpu_time(p.cpu_times()) + + old_process_cpu_time = old_cpu_times[0] + old_time = old_cpu_times[1] + new_time = time.time() + cache.set(skey,[process_cpu_time,new_time],3600) + percent = round(100.00 * (process_cpu_time - old_process_cpu_time) / (new_time - old_time) / psutil.cpu_count(),2) + return percent + + + def get_process_cpu_time(self,cpu_times): + cpu_time = 0.00 + for s in cpu_times: cpu_time += s + return cpu_time + + + def get_project_run_state(self,get = None,project_name = None): + ''' + @name 获取项目运行状态 + @author hwliang<2021-08-12> + @param get{ + project_name: string<项目名称> + } + @param project_name 项目名称 + @return bool + ''' + if get: project_name = get.project_name.strip() + pid_file = "{}/{}.pid".format(self._node_pid_path,project_name) + if not os.path.exists(pid_file): return False + pid = int(public.readFile(pid_file)) + pids = self.get_project_pids(pid=pid) + if not pids: return False + return True + + def get_project_find(self,project_name): + ''' + @name 获取指定项目配置 + @author hwliang<2021-08-09> + @param project_name 项目名称 + @return dict + ''' + project_info = public.M('sites').where('project_type=? AND name=?',('Node',project_name)).find() + if not project_info: return False + project_info['project_config'] = json.loads(project_info['project_config']) + return project_info + + + def get_project_info(self,get): + ''' + @name 获取指定项目信息 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + } + @return dict + ''' + project_info = public.M('sites').where('project_type=? AND name=?',('Node',get.project_name)).find() + if not project_info: return public.return_error('指定项目不存在!') + project_info = self.get_project_stat(project_info) + return project_info + + + def get_project_stat(self,project_info): + ''' + @name 获取项目状态信息 + @author hwliang<2021-08-09> + @param project_info 项目信息 + @return list + ''' + project_info['project_config'] = json.loads(project_info['project_config']) + project_info['run'] = self.get_project_run_state(project_name = project_info['name']) + project_info['load_info'] = self.get_project_load_info(project_name = project_info['name']) + project_info['ssl'] = self.get_ssl_end_date(project_name = project_info['name']) + project_info['listen'] = [] + project_info['listen_ok'] = True + if project_info['load_info']: + for pid in project_info['load_info'].keys(): + for conn in project_info['load_info'][pid]['connections']: + if not conn['status'] == 'LISTEN': continue + if not conn['local_port'] in project_info['listen']: + project_info['listen'].append(conn['local_port']) + if project_info['listen']: + project_info['listen_ok'] = project_info['project_config']['port'] in project_info['listen'] + return project_info + + + + def get_project_state(self,project_name): + ''' + @name 获取项目状态 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return dict + ''' + project_info = public.M('sites').where('project_type=? AND name=?',('Node',project_name)).find() + if not project_info: return False + return project_info['status'] + + def get_project_listen(self,project_name): + ''' + @name 获取项目监听端口 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return dict + ''' + project_config = json.loads(public.M('sites').where('name=?',project_name).getField('project_config')) + if 'listen_port' in project_config: return project_config['listen_port'] + return False + + + def set_project_listen(self,get): + ''' + @name 设置项目监听端口(请设置与实际端口相符的,仅在自动获取不正确时使用) + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + port: int<端口> + } + @return dict + ''' + project_config = json.loads(public.M('sites').where('name=?',get.project_name).getField('project_config')) + project_config['listen_port'] = get.port + public.M('sites').where('name=?',get.project_name).save('project_config',json.dumps(project_config)) + public.WriteLog(self._log_name, '修改项目['+get.project_name+']的端口为为['+get.port+']') + return public.return_data(True,'设置成功') + + + def set_project_nodejs_version(self,get): + ''' + @name 设置nodejs版本 + @author hwliang<2021-08-09> + @param get{ + project_name: string<项目名称> + nodejs_version: string + } + @return dict + ''' + + project_config = json.loads(public.M('sites').where('name=?',get.project_name).getField('project_config')) + project_config['nodejs_version'] = get.nodejs_version + public.M('sites').where('name=?',get.project_name).save('project_config',json.dumps(project_config)) + public.WriteLog(self._log_name, '修改项目['+get.project_name+']的nodejs版本为['+get.nodejs_version+']') + return public.return_data(True,'设置成功') + + def get_project_nodejs_version(self,project_name): + ''' + @name 获取nodejs版本 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return string + ''' + + project_config = json.loads(public.M('sites').where('name=?',project_name).getField('project_config')) + if 'nodejs_version' in project_config: return project_config['nodejs_version'] + return False + + + def check_port_is_used(self,port,sock=False): + ''' + @name 检查端口是否被占用 + @author hwliang<2021-08-09> + @param port: int<端口> + @return bool + ''' + if not isinstance(port,int): port = int(port) + if port == 0: return False + project_list = public.M('sites').where('status=? AND project_type=?',(1,'Node')).field('name,path,project_config').select() + for project_find in project_list: + project_config = json.loads(project_find['project_config']) + if not 'port' in project_config: continue + if int(project_config['port']) == port: return True + if sock: return False + return public.check_tcp('127.0.0.1',port) + + def get_project_run_state_byaotu(self,project_name): + ''' + @name 获取项目运行状态 + @author hwliang<2021-08-09> + @param project_name: string<项目名称> + @return dict + ''' + pid_file = "{}/{}.pid".format(self._node_pid_path,project_name) + if not os.path.exists(pid_file): return False + pid = public.readFile(pid_file) + pids = self.get_project_pids(pid=pid) + if not pids: return False + return True + + def auto_run(self): + ''' + @name 自动启动所有项目 + @author hwliang<2021-08-09> + @return bool + ''' + project_list = public.M('sites').where('project_type=?',('Node',)).field('name,path,project_config').select() + get= public.dict_obj() + success_count = 0 + error_count = 0 + for project_find in project_list: + try: + project_config = json.loads(project_find['project_config']) + if project_config['is_power_on'] in [0,False,'0',None]: continue + project_name = project_find['name'] + project_state = self.get_project_run_state(project_name=project_name) + if not project_state: + get.project_name = project_name + result = self.start_project(get) + if not result['status']: + error_count += 1 + error_msg = '自动启动Nodej项目['+project_name+']失败!' + public.WriteLog(self._log_name, error_msg) + public.print_log(error_msg + ", " + result['error_msg'],'ERROR') + else: + success_count += 1 + success_msg = '自动启动Nodej项目['+project_name+']成功!' + public.WriteLog(self._log_name, success_msg) + public.print_log(success_msg,'INFO') + except: + error_count += 1 + public.print_log(public.get_error_info(),'ERROR') + if (success_count + error_count) < 1: return False + dene_msg = '共需要启动{}个Nodejs项目,成功{}个,失败{}个'.format(success_count + error_count,success_count,error_count) + public.WriteLog(self._log_name, dene_msg) + public.print_log(dene_msg,'INFO') + return True diff --git a/class/projectModel/phpModel.py b/class/projectModel/phpModel.py deleted file mode 100644 index 1a7df9c5..00000000 --- a/class/projectModel/phpModel.py +++ /dev/null @@ -1,4903 +0,0 @@ -#coding: utf-8 -#------------------------------------------------------------------- -# 宝塔Linux面板 -#------------------------------------------------------------------- -# Copyright (c) 2015-2017 宝塔软件(http:#bt.cn) All rights reserved. -#------------------------------------------------------------------- -# Author: hwliang -#------------------------------------------------------------------- - -#------------------------------ -# 网站管理类 -#------------------------------ -import io,re,public,os,sys,shutil,json,hashlib,socket,time -try: - from BTPanel import session -except: - pass -from panelRedirect import panelRedirect -import site_dir_auth -class panelSite(panelRedirect): - siteName = None #网站名称 - sitePath = None #根目录 - sitePort = None #端口 - phpVersion = None #PHP版本 - setupPath = None #安装路径 - isWriteLogs = None #是否写日志 - nginx_conf_bak = '/tmp/backup_nginx.conf' - apache_conf_bak = '/tmp/backup_apache.conf' - is_ipv6 = False - - def __init__(self): - self.setupPath = '/www/server' - path = self.setupPath + '/panel/vhost/nginx' - if not os.path.exists(path): public.ExecShell("mkdir -p " + path + " && chmod -R 644 " + path) - path = self.setupPath + '/panel/vhost/apache' - if not os.path.exists(path): public.ExecShell("mkdir -p " + path + " && chmod -R 644 " + path) - path = self.setupPath + '/panel/vhost/rewrite' - if not os.path.exists(path): public.ExecShell("mkdir -p " + path + " && chmod -R 644 " + path) - path = self.setupPath + '/stop' - if not os.path.exists(path + '/index.html'): - public.ExecShell('mkdir -p ' + path) - public.ExecShell('wget -O ' + path + '/index.html '+public.get_url()+'/stop.html &') - self.__proxyfile = '/www/server/panel/data/proxyfile.json' - self.OldConfigFile() - if os.path.exists(self.nginx_conf_bak): os.remove(self.nginx_conf_bak) - if os.path.exists(self.apache_conf_bak): os.remove(self.apache_conf_bak) - self.is_ipv6 = os.path.exists(self.setupPath + '/panel/data/ipv6.pl') - sys.setrecursionlimit(1000000) - - #默认配置文件 - def check_default(self): - nginx = self.setupPath + '/panel/vhost/nginx' - httpd = self.setupPath + '/panel/vhost/apache' - httpd_default = ''' - ServerAdmin webmaster@example.com - DocumentRoot "/www/server/apache/htdocs" - ServerName bt.default.com - - SetOutputFilter DEFLATE - Options FollowSymLinks - AllowOverride All - Order allow,deny - Allow from all - DirectoryIndex index.html - -''' - - listen_ipv6 = '' - if self.is_ipv6: listen_ipv6 = "\n listen [::]:80;" - nginx_default = '''server -{ - listen 80;%s - server_name _; - index index.html; - root /www/server/nginx/html; -}''' % listen_ipv6 - if not os.path.exists(httpd + '/0.default.conf') and not os.path.exists(httpd + '/default.conf'): public.writeFile(httpd + '/0.default.conf',httpd_default) - if not os.path.exists(nginx + '/0.default.conf') and not os.path.exists(nginx + '/default.conf'): public.writeFile(nginx + '/0.default.conf',nginx_default) - - #添加apache端口 - def apacheAddPort(self,port): - filename = self.setupPath+'/apache/conf/extra/httpd-ssl.conf' - if os.path.exists(filename): - ssl_conf = public.readFile(filename) - if ssl_conf: - if ssl_conf.find('Listen 443') != -1: - ssl_conf = ssl_conf.replace('Listen 443','') - public.writeFile(filename,ssl_conf) - - filename = self.setupPath+'/apache/conf/httpd.conf' - if not os.path.exists(filename): return - allConf = public.readFile(filename) - rep = r"Listen\s+([0-9]+)\n" - tmp = re.findall(rep,allConf) - if not tmp: return False - for key in tmp: - if key == port: return False - - listen = "\nListen "+ tmp[0] + "\n" - listen_ipv6 = '' - #if self.is_ipv6: listen_ipv6 = "\nListen [::]:" + port - allConf = allConf.replace(listen,listen + "Listen " + port + listen_ipv6 + "\n") - public.writeFile(filename, allConf) - return True - - #添加到apache - def apacheAdd(self): - import time - listen = '' - if self.sitePort != '80': self.apacheAddPort(self.sitePort) - acc = public.md5(str(time.time()))[0:8] - try: - httpdVersion = public.readFile(self.setupPath+'/apache/version.pl').strip() - except: - httpdVersion = "" - if httpdVersion == '2.2': - vName = '' - if self.sitePort != '80' and self.sitePort != '443': - vName = "NameVirtualHost *:"+self.sitePort+"\n" - phpConfig = "" - apaOpt = "Order allow,deny\n\t\tAllow from all" - else: - vName = "" - phpConfig =''' - #PHP - - SetHandler "proxy:%s" - - ''' % (public.get_php_proxy(self.phpVersion,'apache'),) - apaOpt = 'Require all granted' - - conf='''%s - ServerAdmin webmaster@example.com - DocumentRoot "%s" - ServerName %s.%s - ServerAlias %s - #errorDocument 404 /404.html - ErrorLog "%s-error_log" - CustomLog "%s-access_log" combined - - #DENY FILES - - Order allow,deny - Deny from all - - %s - #PATH - - SetOutputFilter DEFLATE - Options FollowSymLinks - AllowOverride All - %s - DirectoryIndex index.php index.html index.htm default.php default.html default.htm - -''' % (vName,self.sitePort,self.sitePath,acc,self.siteName,self.siteName,public.GetConfigValue('logs_path')+'/'+self.siteName,public.GetConfigValue('logs_path')+'/'+self.siteName,phpConfig,self.sitePath,apaOpt) - - htaccess = self.sitePath+'/.htaccess' - if not os.path.exists(htaccess): public.writeFile(htaccess, ' ') - public.ExecShell('chmod -R 755 ' + htaccess) - public.ExecShell('chown -R www:www ' + htaccess) - - filename = self.setupPath+'/panel/vhost/apache/'+self.siteName+'.conf' - public.writeFile(filename,conf) - return True - - #添加到nginx - def nginxAdd(self): - listen_ipv6 = '' - if self.is_ipv6: listen_ipv6 = "\n listen [::]:%s;" % self.sitePort - - conf='''server -{{ - listen {listen_port};{listen_ipv6} - server_name {site_name}; - index index.php index.html index.htm default.php default.htm default.html; - root {site_path}; - - #SSL-START {ssl_start_msg} - #error_page 404/404.html; - #SSL-END - - #ERROR-PAGE-START {err_page_msg} - #error_page 404 /404.html; - #error_page 502 /502.html; - #ERROR-PAGE-END - - #PHP-INFO-START {php_info_start} - include enable-php-{php_version}.conf; - #PHP-INFO-END - - #REWRITE-START {rewrite_start_msg} - include {setup_path}/panel/vhost/rewrite/{site_name}.conf; - #REWRITE-END - - #禁止访问的文件或目录 - location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md) - {{ - return 404; - }} - - #一键申请SSL证书验证目录相关设置 - location ~ \.well-known{{ - allow all; - }} - - location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$ - {{ - expires 30d; - error_log /dev/null; - access_log /dev/null; - }} - - location ~ .*\\.(js|css)?$ - {{ - expires 12h; - error_log /dev/null; - access_log /dev/null; - }} - access_log {log_path}/{site_name}.log; - error_log {log_path}/{site_name}.error.log; -}}'''.format( - listen_port=self.sitePort, - listen_ipv6=listen_ipv6, - site_path=self.sitePath, - ssl_start_msg=public.getMsg('NGINX_CONF_MSG1'), - err_page_msg=public.getMsg('NGINX_CONF_MSG2'), - php_info_start=public.getMsg('NGINX_CONF_MSG3'), - php_version=self.phpVersion, - setup_path=self.setupPath, - rewrite_start_msg = public.getMsg('NGINX_CONF_MSG4'), - log_path = public.GetConfigValue('logs_path'), - site_name = self.siteName - ) - - #写配置文件 - filename = self.setupPath+'/panel/vhost/nginx/'+self.siteName+'.conf' - public.writeFile(filename,conf) - - - - #生成伪静态文件 - urlrewritePath = self.setupPath+'/panel/vhost/rewrite' - urlrewriteFile = urlrewritePath+'/'+self.siteName+'.conf' - if not os.path.exists(urlrewritePath): os.makedirs(urlrewritePath) - open(urlrewriteFile,'w+').close() - if not os.path.exists(urlrewritePath): - public.writeFile(urlrewritePath,'') - - return True - - #重新生成nginx配置文件 - def rep_site_config(self,get): - self.siteName = get.siteName - siteInfo = public.M('sites').where('name=?',(self.siteName,)).field('id,path,port').find() - siteInfo['domains'] = public.M('domains').where('pid=?',(siteInfo['id'],)).field('name,port').select() - siteInfo['binding'] = public.M('binding').where('pid=?',(siteInfo['id'],)).field('domain,path').select() - - # openlitespeed - def openlitespeed_add_site(self,get,init_args=None): - # 写主配置httpd_config.conf - # 操作默认监听配置 - if not self.sitePath: - return public.returnMsg(False,"Not specify parameter [sitePath]") - if init_args: - self.siteName = init_args['sitename'] - self.phpVersion = init_args['phpv'] - self.sitePath = init_args['rundir'] - conf_dir = self.setupPath+'/panel/vhost/openlitespeed/' - if not os.path.exists(conf_dir): - os.makedirs(conf_dir) - file = conf_dir+self.siteName+'.conf' - - v_h = """ -#VHOST_TYPE BT_SITENAME START -virtualhost BT_SITENAME { -vhRoot BT_RUN_PATH -configFile /www/server/panel/vhost/openlitespeed/detail/BT_SITENAME.conf -allowSymbolLink 1 -enableScript 1 -restrained 1 -setUIDMode 0 -} -#VHOST_TYPE BT_SITENAME END -""" - self.old_name = self.siteName - if hasattr(get,"dirName"): - self.siteName = self.siteName + "_" + get.dirName - # sub_dir = self.sitePath + "/" + get.dirName - v_h = v_h.replace("VHOST_TYPE","SUBDIR") - v_h = v_h.replace("BT_SITENAME", self.siteName) - v_h = v_h.replace("BT_RUN_PATH", self.sitePath) - # extp_name = self.siteName + "_" + get.dirName - else: - self.openlitespeed_domain(get) - v_h = v_h.replace("VHOST_TYPE", "VHOST") - v_h = v_h.replace("BT_SITENAME", self.siteName) - v_h = v_h.replace("BT_RUN_PATH", self.sitePath) - # extp_name = self.siteName - public.writeFile(file,v_h,"a+") - # 写vhost - conf = '''docRoot $VH_ROOT -vhDomain $VH_NAME -adminEmails example@example.com -enableGzip 1 -enableIpGeo 1 - -index { - useServer 0 - indexFiles index.php,index.html -} - -errorlog /www/wwwlogs/$VH_NAME_ols.error_log { - useServer 0 - logLevel ERROR - rollingSize 10M -} - -accesslog /www/wwwlogs/$VH_NAME_ols.access_log { - useServer 0 - logFormat '%{X-Forwarded-For}i %h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"' - logHeaders 5 - rollingSize 10M - keepDays 10 compressArchive 1 -} - -scripthandler { - add lsapi:BT_EXTP_NAME php -} - -extprocessor BTSITENAME { - type lsapi - address UDS://tmp/lshttpd/BT_EXTP_NAME.sock - maxConns 20 - env LSAPI_CHILDREN=20 - initTimeout 600 - retryTimeout 0 - persistConn 1 - pcKeepAliveTimeout 1 - respBuffer 0 - autoStart 1 - path /usr/local/lsws/lsphpBTPHPV/bin/lsphp - extUser www - extGroup www - memSoftLimit 2047M - memHardLimit 2047M - procSoftLimit 400 - procHardLimit 500 -} - -phpIniOverride { -php_admin_value open_basedir "/tmp/:BT_RUN_PATH" -} - -expires { - enableExpires 1 - expiresByType image/*=A43200,text/css=A43200,application/x-javascript=A43200,application/javascript=A43200,font/*=A43200,application/x-font-ttf=A43200 -} - -rewrite { - enable 1 - autoLoadHtaccess 1 - include /www/server/panel/vhost/openlitespeed/proxy/BTSITENAME/urlrewrite/*.conf - include /www/server/panel/vhost/apache/redirect/BTSITENAME/*.conf - include /www/server/panel/vhost/openlitespeed/redirect/BTSITENAME/*.conf -} -include /www/server/panel/vhost/openlitespeed/proxy/BTSITENAME/*.conf -''' - open_base_path = self.sitePath - if self.sitePath[-1] != '/': - open_base_path = self.sitePath + '/' - conf = conf.replace('BT_RUN_PATH',open_base_path) - conf = conf.replace('BT_EXTP_NAME',self.siteName) - conf = conf.replace('BTPHPV',self.phpVersion) - conf = conf.replace('BTSITENAME',self.siteName) - - # 写配置文件 - conf_dir = self.setupPath + '/panel/vhost/openlitespeed/detail/' - if not os.path.exists(conf_dir): - os.makedirs(conf_dir) - file = conf_dir + self.siteName + '.conf' - # if hasattr(get,"dirName"): - # file = conf_dir + self.siteName +'_'+get.dirName+ '.conf' - public.writeFile(file, conf) - - # 生成伪静态文件 - # urlrewritePath = self.setupPath + '/panel/vhost/rewrite' - # urlrewriteFile = urlrewritePath + '/' + self.old_name + '.conf' - # if not os.path.exists(urlrewritePath): os.makedirs(urlrewritePath) - # open(urlrewriteFile, 'w+').close() - return True - - # 上传CSV文件 - # def upload_csv(self, get): - # import files - # f = files.files() - # get.f_path = '/tmp/multiple_website.csv' - # result = f.upload(get) - # return result - - # 处理CSV内容 - def __process_cvs(self, key): - import csv - with open('/tmp/multiple_website.csv')as f: - f_csv = csv.reader(f) - # result = [i for i in f_csv] - return [dict(zip(key, i)) for i in [i for i in f_csv if "FTP" not in i]] - - # 批量创建网站 - def __create_website_mulitiple(self, websites_info, site_path, get): - create_successfully = {} - create_failed = {} - for data in websites_info: - if not data: - continue - try: - domains = data['website'].split(',') - website_name = domains[0].split(':')[0] - data['port'] = '80' if len(domains[0].split(':')) < 2 else domains[0].split(':')[1] - get.webname = json.dumps({"domain": website_name, "domainlist": domains[1:], "count": 0}) - get.path = data['path'] if 'path' in data and data['path'] != '0' and data['path'] != '1' else site_path + '/' + website_name - get.version = data['version'] if 'version' in data and data['version'] !='0' else '00' - get.ftp = 'true' if 'ftp' in data and data['ftp'] == '1' else False - get.sql = 'true' if 'sql' in data and data['sql'] == '1' else False - get.port = data['port'] if 'port' in data else '80' - get.codeing = 'utf8' - get.type = 'PHP' - get.type_id = '0' - get.ps = '' - create_other = {} - create_other['db_status'] = False - create_other['ftp_status'] = False - if get.sql == 'true': - create_other['db_pass'] = get.datapassword = public.gen_password(16) - create_other['db_user'] = get.datauser = website_name.replace('.', '_') - create_other['db_status'] = True - if get.ftp == 'true': - create_other['ftp_pass'] = get.ftp_password = public.gen_password(16) - create_other['ftp_user'] = get.ftp_username = website_name.replace('.', '_') - create_other['ftp_status'] = True - result = self.AddSite(get,multiple=1) - if 'status' in result: - create_failed[domains[0]] = result['msg'] - continue - create_successfully[domains[0]] = create_other - except: - create_failed[domains[0]] = '创建出错了,请再试一次' - return {'status': True, 'msg': '创建网站 [ {} ] 成功'.format(','.join(create_successfully)), 'error': create_failed, - 'success': create_successfully} - - # 批量创建网站 - def create_website_multiple(self, get): - ''' - @name 批量创建网站 - @author zhwen<2020-11-26> - @param create_type txt/csv txt格式为 “网站名|网站路径|是否创建FTP|是否创建数据库|PHP版本” 每个网站一行 - "aaa.com:88,bbb.com|/www/wwwserver/aaa.com/或1|1/0|1/0|0/73" - csv格式为 “网站名|网站端口|网站路径|PHP版本|是否创建数据库|是否创建FTP” - @param websites_content "[[aaa.com|80|/www/wwwserver/aaa.com/|1|1|73]...." - ''' - key = ['website', 'path', 'ftp', 'sql', 'version'] - site_path = public.M('config').getField('sites_path') - if get.create_type == 'txt': - websites_info = [dict(zip(key, i)) for i in [i.strip().split('|') for i in json.loads(get.websites_content)]] - else: - websites_info = self.__process_cvs(key) - res = self.__create_website_mulitiple(websites_info, site_path, get) - public.serviceReload() - return res - - #添加站点 - def AddSite(self,get,multiple=None): - self.check_default() - isError = public.checkWebConfig() - if isError != True: - return public.returnMsg(False,'ERROR: 检测到配置文件有错误,请先排除后再操作

'+isError.replace("\n",'
')+'
') - - import json,files - - get.path = self.__get_site_format_path(get.path) - - if not public.check_site_path(get.path): - a,c = public.get_sys_path() - return public.returnMsg(False,'请不要将网站根目录设置到以下关键目录中:
{}'.format("
".join(a+c))) - try: - siteMenu = json.loads(get.webname) - except: - return public.returnMsg(False,'webname参数格式不正确,应该是可被解析的JSON字符串') - self.siteName = self.ToPunycode(siteMenu['domain'].strip().split(':')[0]).strip().lower() - self.sitePath = self.ToPunycodePath(self.GetPath(get.path.replace(' ',''))).strip() - self.sitePort = get.port.strip().replace(' ','') - - if self.sitePort == "": get.port = "80" - if not public.checkPort(self.sitePort): return public.returnMsg(False,'SITE_ADD_ERR_PORT') - for domain in siteMenu['domainlist']: - if not len(domain.split(':')) == 2: - continue - if not public.checkPort(domain.split(':')[1]): return public.returnMsg(False, 'SITE_ADD_ERR_PORT') - - - if hasattr(get,'version'): - self.phpVersion = get.version.replace(' ','') - else: - self.phpVersion = '00' - - if not self.phpVersion: self.phpVersion = '00' - - php_version = self.GetPHPVersion(get) - is_phpv = False - for php_v in php_version: - if self.phpVersion == php_v['version']: - is_phpv = True - break - if not is_phpv: return public.returnMsg(False,'指定PHP版本不存在!') - - domain = None - #if siteMenu['count']: - # domain = get.domain.replace(' ','') - #表单验证 - if 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 = 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') - if self.sitePath[-1] == '.':return public.returnMsg(False, '网站目录结尾不可以是 "."') - - - if not domain: domain = self.siteName - - - #是否重复 - sql = public.M('sites') - if sql.where("name=?",(self.siteName,)).count(): return public.returnMsg(False,'SITE_ADD_ERR_EXISTS') - opid = public.M('domain').where("name=?",(self.siteName,)).getField('pid') - - if opid: - if public.M('sites').where('id=?',(opid,)).count(): - return public.returnMsg(False,'SITE_ADD_ERR_DOMAIN_EXISTS') - public.M('domain').where('pid=?',(opid,)).delete() - - if public.M('binding').where('domain=?',(self.siteName,)).count(): - return public.returnMsg(False,'SITE_ADD_ERR_DOMAIN_EXISTS') - - #创建根目录 - if not os.path.exists(self.sitePath): - try: - os.makedirs(self.sitePath) - except Exception as ex: - return public.returnMsg(False,'创建根目录失败, %s' % ex) - public.ExecShell('chmod -R 755 ' + self.sitePath) - public.ExecShell('chown -R www:www ' + self.sitePath) - - #创建basedir - self.DelUserInI(self.sitePath) - userIni = self.sitePath+'/.user.ini' - if not os.path.exists(userIni): - public.writeFile(userIni, 'open_basedir='+self.sitePath+'/:/tmp/') - public.ExecShell('chmod 644 ' + userIni) - public.ExecShell('chown root:root ' + userIni) - public.ExecShell('chattr +i '+userIni) - - ngx_open_basedir_path = self.setupPath + '/panel/vhost/open_basedir/nginx' - if not os.path.exists(ngx_open_basedir_path): - os.makedirs(ngx_open_basedir_path,384) - ngx_open_basedir_file = ngx_open_basedir_path + '/{}.conf'.format(self.siteName) - ngx_open_basedir_body = '''set $bt_safe_dir "open_basedir"; -set $bt_safe_open "{}/:/tmp/";'''.format(self.sitePath) - public.writeFile(ngx_open_basedir_file,ngx_open_basedir_body) - - #创建默认文档 - index = self.sitePath+'/index.html' - if not os.path.exists(index): - public.writeFile(index, public.readFile('data/defaultDoc.html')) - public.ExecShell('chmod -R 755 ' + index) - public.ExecShell('chown -R www:www ' + index) - - #创建自定义404页 - doc404 = self.sitePath+'/404.html' - if not os.path.exists(doc404): - public.writeFile(doc404, public.readFile('data/404.html')) - public.ExecShell('chmod -R 755 ' + doc404) - public.ExecShell('chown -R www:www ' + doc404) - - #写入配置 - result = self.nginxAdd() - result = self.apacheAdd() - result = self.openlitespeed_add_site(get) - - #检查处理结果 - if not result: return public.returnMsg(False,'SITE_ADD_ERR_WRITE') - - - ps = get.ps - #添加放行端口 - if self.sitePort != '80': - import firewalls - get.port = self.sitePort - get.ps = self.siteName - firewalls.firewalls().AddAcceptPort(get) - - if not hasattr(get,'type_id'): get.type_id = 0 - public.check_domain_cloud(self.siteName) - #写入数据库 - get.pid = sql.table('sites').add('name,path,status,ps,type_id,addtime',(self.siteName,self.sitePath,'1',ps,get.type_id,public.getDate())) - - #添加更多域名 - for domain in siteMenu['domainlist']: - get.domain = domain - get.webname = self.siteName - get.id = str(get.pid) - self.AddDomain(get,multiple) - - sql.table('domain').add('pid,name,port,addtime',(get.pid,self.siteName,self.sitePort,public.getDate())) - - data = {} - data['siteStatus'] = True - data['siteId'] = get.pid - - #添加FTP - data['ftpStatus'] = False - if get.ftp == 'true': - import ftp - get.ps = self.siteName - result = ftp.ftp().AddUser(get) - if result['status']: - data['ftpStatus'] = True - data['ftpUser'] = get.ftp_username - data['ftpPass'] = get.ftp_password - - #添加数据库 - data['databaseStatus'] = False - if get.sql == 'true' or get.sql == 'MySQL': - import database - if len(get.datauser) > 16: get.datauser = get.datauser[:16] - get.name = get.datauser - get.db_user = get.datauser - get.password = get.datapassword - get.address = '127.0.0.1' - get.ps = self.siteName - result = database.database().AddDatabase(get) - if result['status']: - data['databaseStatus'] = True - data['databaseUser'] = get.datauser - data['databasePass'] = get.datapassword - if not multiple: - public.serviceReload() - public.WriteLog('TYPE_SITE','SITE_ADD_SUCCESS',(self.siteName,)) - return data - - def __get_site_format_path(self,path): - path = path.replace('//','/') - if path[-1:] == '/': - path = path[:-1] - return path - - def __check_site_path(self,path): - path = self.__get_site_format_path(path) - other_path = public.M('config').where("id=?",('1',)).field('sites_path,backup_path').find() - if path == other_path['sites_path'] or path == other_path['backup_path']: return False - return True - - def delete_website_multiple(self,get): - ''' - @name 批量删除网站 - @author zhwen<2020-11-17> - @param sites_id "1,2" - @param ftp 0/1 - @param database 0/1 - @param path 0/1 - ''' - sites_id = get.sites_id.split(',') - del_successfully = [] - del_failed = {} - for site_id in sites_id: - get.id = site_id - get.webname = public.M('sites').where("id=?", (site_id,)).getField('name') - if not get.webname: - continue - try: - self.DeleteSite(get,multiple=1) - del_successfully.append(get.webname) - except: - del_failed[get.webname]='删除时出错了,请再试一次' - pass - public.serviceReload() - return {'status': True, 'msg': '删除网站 [ {} ] 成功'.format(','.join(del_successfully)), 'error': del_failed, - 'success': del_successfully} - - #删除站点 - def DeleteSite(self,get,multiple=None): - proxyconf = self.__read_config(self.__proxyfile) - id = get.id - if public.M('sites').where('id=?',(id,)).count() < 1: return public.returnMsg(False,'指定站点不存在!') - siteName = get.webname - get.siteName = siteName - self.CloseTomcat(get) - # 删除反向代理 - for i in range(len(proxyconf)-1,-1,-1): - if proxyconf[i]["sitename"] == siteName: - del proxyconf[i] - self.__write_config(self.__proxyfile,proxyconf) - - m_path = self.setupPath+'/panel/vhost/nginx/proxy/'+siteName - if os.path.exists(m_path): public.ExecShell("rm -rf %s" % m_path) - - m_path = self.setupPath+'/panel/vhost/apache/proxy/'+siteName - if os.path.exists(m_path): public.ExecShell("rm -rf %s" % m_path) - - # 删除目录保护 - _dir_aith_file = "%s/panel/data/site_dir_auth.json" % self.setupPath - _dir_aith_conf = public.readFile(_dir_aith_file) - if _dir_aith_conf: - try: - _dir_aith_conf = json.loads(_dir_aith_conf) - if siteName in _dir_aith_conf: - del(_dir_aith_conf[siteName]) - except: - pass - self.__write_config(_dir_aith_file,_dir_aith_conf) - - dir_aith_path = self.setupPath+'/panel/vhost/nginx/dir_auth/'+siteName - if os.path.exists(dir_aith_path): public.ExecShell("rm -rf %s" % dir_aith_path) - - dir_aith_path = self.setupPath+'/panel/vhost/apache/dir_auth/'+siteName - if os.path.exists(dir_aith_path): public.ExecShell("rm -rf %s" % dir_aith_path) - - #删除重定向 - __redirectfile = "%s/panel/data/redirect.conf" % self.setupPath - redirectconf = self.__read_config(__redirectfile) - for i in range(len(redirectconf)-1,-1,-1): - if redirectconf[i]["sitename"] == siteName: - del redirectconf[i] - self.__write_config(__redirectfile,redirectconf) - m_path = self.setupPath+'/panel/vhost/nginx/redirect/'+siteName - if os.path.exists(m_path): public.ExecShell("rm -rf %s" % m_path) - m_path = self.setupPath+'/panel/vhost/apache/redirect/'+siteName - if os.path.exists(m_path): public.ExecShell("rm -rf %s" % m_path) - - #删除配置文件 - confPath = self.setupPath+'/panel/vhost/nginx/'+siteName+'.conf' - if os.path.exists(confPath): os.remove(confPath) - - confPath = self.setupPath+'/panel/vhost/apache/' + siteName + '.conf' - if os.path.exists(confPath): os.remove(confPath) - open_basedir_file = self.setupPath+'/panel/vhost/open_basedir/nginx/'+siteName+'.conf' - if os.path.exists(open_basedir_file): os.remove(open_basedir_file) - - # 删除openlitespeed配置 - vhost_file = "/www/server/panel/vhost/openlitespeed/{}.conf".format(siteName) - if os.path.exists(vhost_file): - public.ExecShell('rm -f {}*'.format(vhost_file)) - vhost_detail_file = "/www/server/panel/vhost/openlitespeed/detail/{}.conf".format(siteName) - if os.path.exists(vhost_detail_file): - public.ExecShell('rm -f {}*'.format(vhost_detail_file)) - vhost_ssl_file = "/www/server/panel/vhost/openlitespeed/detail/ssl/{}.conf".format(siteName) - if os.path.exists(vhost_ssl_file): - public.ExecShell('rm -f {}*'.format(vhost_ssl_file)) - vhost_sub_file = "/www/server/panel/vhost/openlitespeed/detail/{}_sub.conf".format(siteName) - if os.path.exists(vhost_sub_file): - public.ExecShell('rm -f {}*'.format(vhost_sub_file)) - - vhost_redirect_file = "/www/server/panel/vhost/openlitespeed/redirect/{}".format(siteName) - if os.path.exists(vhost_redirect_file): - public.ExecShell('rm -rf {}*'.format(vhost_redirect_file)) - vhost_proxy_file = "/www/server/panel/vhost/openlitespeed/proxy/{}".format(siteName) - if os.path.exists(vhost_proxy_file): - public.ExecShell('rm -rf {}*'.format(vhost_proxy_file)) - - - # 删除openlitespeed监听配置 - self._del_ols_listen_conf(siteName) - - - - - #删除伪静态文件 - # filename = confPath+'/rewrite/'+siteName+'.conf' - filename = '/www/server/panel/vhost/rewrite/'+siteName+'.conf' - if os.path.exists(filename): - os.remove(filename) - public.ExecShell("rm -f " + confPath + '/rewrite/' + siteName + "_*") - - #删除日志文件 - filename = public.GetConfigValue('logs_path')+'/'+siteName+'*' - public.ExecShell("rm -f " + filename) - - - #删除证书 - #crtPath = '/etc/letsencrypt/live/'+siteName - #if os.path.exists(crtPath): - # import shutil - # shutil.rmtree(crtPath) - - #删除日志 - public.ExecShell("rm -f " + public.GetConfigValue('logs_path') + '/' + siteName + "-*") - - #删除备份 - #public.ExecShell("rm -f "+session['config']['backup_path']+'/site/'+siteName+'_*') - - #删除根目录 - if 'path' in get: - if get.path == '1': - import files - get.path = self.__get_site_format_path(public.M('sites').where("id=?",(id,)).getField('path')) - if self.__check_site_path(get.path): files.files().DeleteDir(get) - get.path = '1' - - #重载配置 - if not multiple: - public.serviceReload() - - #从数据库删除 - public.M('sites').where("id=?",(id,)).delete() - public.M('binding').where("pid=?",(id,)).delete() - public.M('domain').where("pid=?",(id,)).delete() - public.WriteLog('TYPE_SITE', "SITE_DEL_SUCCESS",(siteName,)) - - #是否删除关联数据库 - if hasattr(get,'database'): - if get.database == '1': - find = public.M('databases').where("pid=?",(id,)).field('id,name').find() - if find: - import database - get.name = find['name'] - get.id = find['id'] - database.database().DeleteDatabase(get) - - #是否删除关联FTP - if hasattr(get,'ftp'): - if get.ftp == '1': - find = public.M('ftps').where("pid=?",(id,)).field('id,name').find() - if find: - import ftp - get.username = find['name'] - get.id = find['id'] - ftp.ftp().DeleteUser(get) - - return public.returnMsg(True,'SITE_DEL_SUCCESS') - - def _del_ols_listen_conf(self,sitename): - conf_dir = '/www/server/panel/vhost/openlitespeed/listen/' - if not os.path.exists(conf_dir): - return False - for i in os.listdir(conf_dir): - file_name = conf_dir + i - if os.path.isdir(file_name): - continue - conf = public.readFile(file_name) - if not conf: - continue - map_rep = 'map\s+{}.*'.format(sitename) - conf = re.sub(map_rep,'',conf) - if "map" not in conf: - public.ExecShell('rm -f {}*'.format(file_name)) - continue - public.writeFile(file_name,conf) - - - #域名编码转换 - def ToPunycode(self,domain): - import re - if sys.version_info[0] == 2: domain = domain.encode('utf8') - tmp = domain.split('.') - newdomain = '' - for dkey in tmp: - if dkey == '*': continue - #匹配非ascii字符 - match = re.search(u"[\x80-\xff]+",dkey) - if not match: match = re.search(u"[\u4e00-\u9fa5]+",dkey) - if not match: - newdomain += dkey + '.' - else: - if sys.version_info[0] == 2: - newdomain += 'xn--' + dkey.decode('utf-8').encode('punycode') + '.' - else: - newdomain += 'xn--' + dkey.encode('punycode').decode('utf-8') + '.' - if tmp[0] == '*': newdomain = "*." + newdomain - return newdomain[0:-1] - - #中文路径处理 - def ToPunycodePath(self,path): - if sys.version_info[0] == 2: path = path.encode('utf-8') - if os.path.exists(path): return path - import re - match = re.search(u"[\x80-\xff]+",path) - if not match: match = re.search(u"[\u4e00-\u9fa5]+",path) - if not match: return path - npath = '' - for ph in path.split('/'): - npath += '/' + self.ToPunycode(ph) - return npath.replace('//','/') - - - def export_domains(self,args): - ''' - @name 导出域名列表 - @author hwliang<2020-10-27> - @param args{ - siteName: string<网站名称> - } - @return string - ''' - - pid = public.M('sites').where('name=?',args.siteName).getField('id') - domains = public.M('domain').where('pid=?',pid).field('name,port').select() - text_data = [] - for domain in domains: - text_data.append("{}:{}".format(domain['name'], domain['port'])) - data = "\n".join(text_data) - return public.send_file(data,'{}_domains'.format(args.siteName)) - - - def import_domains(self,args): - ''' - @name 导入域名 - @author hwliang<2020-10-27> - @param args{ - siteName: string<网站名称> - domains: string<域名列表> 每行一个 格式: 域名:端口 - } - @return string - ''' - - domains_tmp = args.domains.split("\n") - get = public.dict_obj() - get.webname = args.siteName - get.id = public.M('sites').where('name=?',args.siteName).getField('id') - domains = [] - for domain in domains_tmp: - if public.M('domain').where('name=?',domain.split(':')[0]).count(): - continue - domains.append(domain) - - get.domain = ','.join(domains) - return self.AddDomain(get) - - - - #添加域名 - def AddDomain(self,get,multiple = None): - #检查配置文件 - isError = public.checkWebConfig() - if isError != True: - return public.returnMsg(False,'ERROR: 检测到配置文件有错误,请先排除后再操作

'+isError.replace("\n",'
')+'
') - - if not 'domain' in get: return public.returnMsg(False,'请填写域名!') - if len(get.domain) < 3: return public.returnMsg(False,'SITE_ADD_DOMAIN_ERR_EMPTY') - domains = get.domain.replace(' ','').split(',') - - for domain in domains: - if domain == "": continue - domain = domain.strip().split(':') - get.domain = self.ToPunycode(domain[0]).lower() - get.port = '80' - - reg = "^([\w\-\*]{1,100}\.){1,4}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$" - if not re.match(reg, get.domain): return public.returnMsg(False,'SITE_ADD_DOMAIN_ERR_FORMAT') - - if len(domain) == 2: - get.port = domain[1] - if get.port == "": get.port = "80" - - if not public.checkPort(get.port): return public.returnMsg(False,'SITE_ADD_DOMAIN_ERR_POER') - #检查域名是否存在 - sql = public.M('domain') - opid = sql.where("name=? AND (port=? OR pid=?)",(get.domain,get.port,get.id)).getField('pid') - if opid: - if public.M('sites').where('id=?',(opid,)).count(): - return public.returnMsg(False,'SITE_ADD_DOMAIN_ERR_EXISTS') - sql.where('pid=?',(opid,)).delete() - - if public.M('binding').where('domain=?',(get.domain,)).count(): - return public.returnMsg(False,'SITE_ADD_ERR_DOMAIN_EXISTS') - - #写配置文件 - self.NginxDomain(get) - try: - self.ApacheDomain(get) - self.openlitespeed_domain(get) - if self._check_ols_ssl(get.webname): - get.port='443' - self.openlitespeed_domain(get) - get.port = '80' - except: - pass - - #检查实际端口 - if len(domain) == 2: get.port = domain[1] - - #添加放行端口 - if get.port != '80': - import firewalls - get.ps = get.domain - firewalls.firewalls().AddAcceptPort(get) - if not multiple: - public.serviceReload() - public.check_domain_cloud(get.domain) - public.WriteLog('TYPE_SITE', 'DOMAIN_ADD_SUCCESS',(get.webname,get.domain)) - sql.table('domain').add('pid,name,port,addtime',(get.id,get.domain,get.port,public.getDate())) - - - return public.returnMsg(True,'SITE_ADD_DOMAIN') - - # 判断ols_ssl是否已经设置 - def _check_ols_ssl(self,webname): - conf = public.readFile('/www/server/panel/vhost/openlitespeed/listen/443.conf') - if conf and webname in conf: - return True - return False - - # 添加openlitespeed 80端口监听 - def openlitespeed_set_80_domain(self,get,conf): - rep = 'map\s+{}.*'.format(get.webname) - domains = get.webname.strip().split(',') - if conf: - map_tmp = re.search(rep, conf) - if map_tmp: - map_tmp = map_tmp.group() - domains = map_tmp.strip().split(',') - if not public.inArray(domains, get.domain): - new_map = '{},{}'.format(conf, get.domain) - conf = re.sub(rep, new_map, conf) - else: - map_tmp = '\tmap\t{d} {d}\n'.format(d=domains[0]) - listen_rep = "secure\s*0" - conf = re.sub(listen_rep,"secure 0\n"+map_tmp,conf) - return conf - - else: - rep_default = 'listener\s+Default\{(\n|[\s\w\*\:\#\.\,])*' - tmp = re.search(rep_default, conf) - # domains = get.webname.strip().split(',') - if tmp: - tmp = tmp.group() - new_map = '\tmap\t{d} {d}\n'.format(d=domains[0]) - tmp += new_map - conf = re.sub(rep_default, tmp, conf) - return conf - - # openlitespeed写域名配置 - def openlitespeed_domain(self, get): - listen_dir = '/www/server/panel/vhost/openlitespeed/listen/' - if not os.path.exists(listen_dir): - os.makedirs(listen_dir) - listen_file = listen_dir + get.port + ".conf" - listen_conf = public.readFile(listen_file) - try: - get.webname = json.loads(get.webname) - get.domain = get.webname['domain'].replace('\r', '') - get.webname = get.domain + "," + ",".join(get.webname["domainlist"]) - if get.webname[-1] == ',': - get.webname = get.webname[:-1] - except: - pass - - if listen_conf: - # 添加域名 - rep = 'map\s+{}.*'.format(get.webname) - map_tmp = re.search(rep, listen_conf) - if map_tmp: - map_tmp = map_tmp.group() - domains = map_tmp.strip().split(',') - if not public.inArray(domains, get.domain): - new_map = '{},{}'.format(map_tmp, get.domain) - listen_conf = re.sub(rep, new_map, listen_conf) - else: - domains = get.webname.strip().split(',') - map_tmp = '\tmap\t{d} {d}'.format(d=domains[0]) - listen_rep = "secure\s*0" - listen_conf = re.sub(listen_rep,"secure 0\n"+map_tmp,listen_conf) - else: - listen_conf = """ -listener Default%s{ - address *:%s - secure 0 - map %s %s -} -""" % (get.port, get.port, get.webname, get.domain) - # 保存配置文件 - public.writeFile(listen_file, listen_conf) - return True - - #Nginx写域名配置 - def NginxDomain(self,get): - file = self.setupPath + '/panel/vhost/nginx/'+get.webname+'.conf' - conf = public.readFile(file) - if not conf: return - - #添加域名 - rep = r"server_name\s*(.*);" - tmp = re.search(rep,conf).group() - domains = tmp.replace(';','').strip().split(' ') - if not public.inArray(domains,get.domain): - newServerName = tmp.replace(';',' ' + get.domain + ';') - conf = conf.replace(tmp,newServerName) - - #添加端口 - rep = r"listen\s+[\[\]\:]*([0-9]+).*;" - tmp = re.findall(rep,conf) - if not public.inArray(tmp,get.port): - listen = re.search(rep,conf).group() - listen_ipv6 = '' - if self.is_ipv6: listen_ipv6 = "\n\t\tlisten [::]:"+get.port+';' - conf = conf.replace(listen,listen + "\n\t\tlisten "+get.port+';' + listen_ipv6) - #保存配置文件 - public.writeFile(file,conf) - return True - - #Apache写域名配置 - def ApacheDomain(self,get): - file = self.setupPath + '/panel/vhost/apache/'+get.webname+'.conf' - conf = public.readFile(file) - if not conf: return - - port = get.port - siteName = get.webname - newDomain = get.domain - find = public.M('sites').where("id=?",(get.id,)).field('id,name,path').find() - sitePath = find['path'] - siteIndex = 'index.php index.html index.htm default.php default.html default.htm' - - #添加域名 - if conf.find('') != -1: - repV = r"(.|\n)*" - domainV = re.search(repV,conf).group() - rep = r"ServerAlias\s*(.*)\n" - tmp = re.search(rep,domainV).group(0) - domains = tmp.strip().split(' ') - if not public.inArray(domains,newDomain): - rs = tmp.replace("\n","") - newServerName = rs+' '+newDomain+"\n" - myconf = domainV.replace(tmp,newServerName) - conf = re.sub(repV, myconf, conf) - if conf.find('') != -1: - repV = r"(.|\n)*" - domainV = re.search(repV,conf).group() - rep = r"ServerAlias\s*(.*)\n" - tmp = re.search(rep,domainV).group(0) - domains = tmp.strip().split(' ') - if not public.inArray(domains,newDomain): - rs = tmp.replace("\n","") - newServerName = rs+' '+newDomain+"\n" - myconf = domainV.replace(tmp,newServerName) - conf = re.sub(repV, myconf, conf) - else: - try: - httpdVersion = public.readFile(self.setupPath+'/apache/version.pl').strip() - except: - httpdVersion = "" - if httpdVersion == '2.2': - vName = '' - if self.sitePort != '80' and self.sitePort != '443': - vName = "NameVirtualHost *:"+port+"\n" - phpConfig = "" - apaOpt = "Order allow,deny\n\t\tAllow from all" - else: - vName = "" - # rep = "php-cgi-([0-9]{2,3})\.sock" - # version = re.search(rep,conf).groups()[0] - version = public.get_php_version_conf(conf) - if len(version) < 2: return public.returnMsg(False,'PHP_GET_ERR') - phpConfig =''' - #PHP - - SetHandler "proxy:%s" - - ''' % (public.get_php_proxy(version,'apache'),) - apaOpt = 'Require all granted' - - newconf=''' - ServerAdmin webmaster@example.com - DocumentRoot "%s" - ServerName %s.%s - ServerAlias %s - #errorDocument 404 /404.html - ErrorLog "%s-error_log" - CustomLog "%s-access_log" combined - %s - - #DENY FILES - - Order allow,deny - Deny from all - - - #PATH - - SetOutputFilter DEFLATE - Options FollowSymLinks - AllowOverride All - %s - DirectoryIndex %s - -''' % (port,sitePath,siteName,port,newDomain,public.GetConfigValue('logs_path')+'/'+siteName,public.GetConfigValue('logs_path')+'/'+siteName,phpConfig,sitePath,apaOpt,siteIndex) - conf += "\n\n"+newconf - - #添加端口 - if port != '80' and port != '888': self.apacheAddPort(port) - - #保存配置文件 - public.writeFile(file,conf) - return True - - def delete_domain_multiple(self,get): - ''' - @name 批量删除网站 - @author zhwen<2020-11-17> - @param id "1" - @param domains_id 1,2,3 - ''' - domains_id = get.domains_id.split(',') - get.webname = public.M('sites').where("id=?", (get.id,)).getField('name') - del_successfully = [] - del_failed = {} - for domain_id in domains_id: - get.domain = public.M('domain').where("id=? and pid=?", (domain_id,get.id)).getField('name') - get.port = str(public.M('domain').where("id=? and pid=?", (domain_id, get.id)).getField('port')) - if not get.webname: - continue - try: - result = self.DelDomain(get,multiple=1) - tmp = get.domain + ':' + get.port - if not result['status']: - del_failed[tmp] = result['msg'] - continue - del_successfully.append(tmp) - except: - tmp = get.domain + ':' + get.port - del_failed[tmp]='删除时错误了,请再试一次' - pass - public.serviceReload() - return {'status': True, 'msg': '删除域名 [ {} ] 成功'.format(','.join(del_successfully)), 'error': del_failed, - 'success': del_successfully} - - #删除域名 - def DelDomain(self,get,multiple=None): - if not 'id' in get:return public.returnMsg(False,'请选择域名') - if not 'port' in get: return public.returnMsg(False, '请选择端口') - sql = public.M('domain') - id=get['id'] - port = get.port - find = sql.where("pid=? AND name=?",(get.id,get.domain)).field('id,name').find() - domain_count = sql.table('domain').where("pid=?",(id,)).count() - if domain_count == 1: return public.returnMsg(False,'SITE_DEL_DOMAIN_ERR_ONLY') - - #nginx - file = self.setupPath+'/panel/vhost/nginx/'+get['webname']+'.conf' - conf = public.readFile(file) - if conf: - #删除域名 - rep = r"server_name\s+(.+);" - tmp = re.search(rep,conf).group() - newServerName = tmp.replace(' '+get['domain']+';',';') - newServerName = newServerName.replace(' '+get['domain']+' ',' ') - conf = conf.replace(tmp,newServerName) - - #删除端口 - rep = r"listen.*[\s:]+(\d+).*;" - tmp = re.findall(rep,conf) - port_count = sql.table('domain').where('pid=? AND port=?',(get.id,get.port)).count() - if public.inArray(tmp,port) == True and port_count < 2: - rep = r"\n*\s+listen.*[\s:]+"+port+r"\s*;" - conf = re.sub(rep,'',conf) - #保存配置 - public.writeFile(file,conf) - - #apache - file = self.setupPath+'/panel/vhost/apache/'+get['webname']+'.conf' - conf = public.readFile(file) - if conf: - #删除域名 - try: - rep = r"\n*(.|\n)*" - tmp = re.search(rep, conf).group() - - rep1 = "ServerAlias\s+(.+)\n" - tmp1 = re.findall(rep1,tmp) - tmp2 = tmp1[0].split(' ') - if len(tmp2) < 2: - conf = re.sub(rep,'',conf) - rep = "NameVirtualHost.+\:" + port + "\n" - conf = re.sub(rep,'',conf) - else: - newServerName = tmp.replace(' '+get['domain']+"\n","\n") - newServerName = newServerName.replace(' '+get['domain']+' ',' ') - conf = conf.replace(tmp,newServerName) - - #保存配置 - public.writeFile(file,conf) - except: - pass - - # openlitespeed - self._del_ols_domain(get) - - sql.table('domain').where("id=?",(find['id'],)).delete() - public.WriteLog('TYPE_SITE', 'DOMAIN_DEL_SUCCESS',(get.webname,get.domain)) - if not multiple: - public.serviceReload() - return public.returnMsg(True,'DEL_SUCCESS') - - #openlitespeed删除域名 - def _del_ols_domain(self,get): - conf_dir = '/www/server/panel/vhost/openlitespeed/listen/' - if not os.path.exists(conf_dir): - return False - for i in os.listdir(conf_dir): - file_name = conf_dir + i - if os.path.isdir(file_name): - continue - conf = public.readFile(file_name) - map_rep = 'map\s+{}\s+(.*)'.format(get.webname) - domains = re.search(map_rep,conf) - if domains: - domains = domains.group(1).split(',') - if get.domain in domains: - domains.remove(get.domain) - if len(domains) == 0: - os.remove(file_name) - continue - else: - domains = ",".join(domains) - map_c = "map\t{} ".format(get.webname) + domains - conf = re.sub(map_rep,map_c,conf) - public.writeFile(file_name,conf) - - #检查域名是否解析 - def CheckDomainPing(self,get): - try: - epass = public.GetRandomString(32) - spath = get.path + '/.well-known/pki-validation' - if not os.path.exists(spath): public.ExecShell("mkdir -p '" + spath + "'") - public.writeFile(spath + '/fileauth.txt',epass) - result = public.httpGet('http://' + get.domain.replace('*.','') + '/.well-known/pki-validation/fileauth.txt') - if result == epass: return True - return False - except: - return False - - # 保存第三方证书 - def SetSSL(self, get): - siteName = get.siteName - path = '/www/server/panel/vhost/cert/' + siteName - csrpath = path + "/fullchain.pem" - keypath = path + "/privkey.pem" - - if (get.key.find('KEY') == -1): return public.returnMsg(False, 'SITE_SSL_ERR_PRIVATE') - if (get.csr.find('CERTIFICATE') == -1): return public.returnMsg(False, 'SITE_SSL_ERR_CERT') - public.writeFile('/tmp/cert.pl', get.csr) - if not public.CheckCert('/tmp/cert.pl'): return public.returnMsg(False, '证书错误,请粘贴正确的PEM格式证书!') - backup_cert = '/tmp/backup_cert_' + siteName - - import shutil - if os.path.exists(backup_cert): shutil.rmtree(backup_cert) - if os.path.exists(path): shutil.move(path,backup_cert) - if os.path.exists(path): shutil.rmtree(path) - - public.ExecShell('mkdir -p ' + path) - public.writeFile(keypath, get.key) - public.writeFile(csrpath, get.csr) - - # 写入配置文件 - result = self.SetSSLConf(get) - if not result['status']: return result - isError = public.checkWebConfig() - - if (type(isError) == str): - if os.path.exists(path): shutil.rmtree(backup_cert) - shutil.move(backup_cert,path) - return public.returnMsg(False, 'ERROR:
' + isError.replace("\n", '
') + '
') - public.serviceReload() - - if os.path.exists(path + '/partnerOrderId'): os.remove(path + '/partnerOrderId') - if os.path.exists(path + '/certOrderId'): os.remove(path + '/certOrderId') - p_file = '/etc/letsencrypt/live/' + get.siteName - if os.path.exists(p_file): shutil.rmtree(p_file) - public.WriteLog('TYPE_SITE', 'SITE_SSL_SAVE_SUCCESS') - - #清理备份证书 - if os.path.exists(backup_cert): shutil.rmtree(backup_cert) - return public.returnMsg(True, 'SITE_SSL_SUCCESS') - - #获取运行目录 - def GetRunPath(self,get): - if not hasattr(get,'id'): - if hasattr(get,'siteName'): - get.id = public.M('sites').where('name=?',(get.siteName,)).getField('id') - else: - get.id = public.M('sites').where('path=?',(get.path,)).getField('id') - if not get.id: return False - if type(get.id) == list: get.id = get.id[0]['id'] - result = self.GetSiteRunPath(get) - if 'runPath' in result: - return result['runPath'] - return False - - - # 创建Let's Encrypt免费证书 - def CreateLet(self,get): - - domains = json.loads(get.domains) - if not len(domains): - return public.returnMsg(False, '请选择域名') - - file_auth = True - if hasattr(get, 'dnsapi'): - file_auth = False - - if not hasattr(get, 'dnssleep'): - get.dnssleep = 10 - - email = public.M('users').getField('email') - if hasattr(get, 'email'): - if get.email.find('@') == -1: - get.email = email - else: - get.email = get.email.strip() - public.M('users').where('id=?',(1,)).setField('email',get.email) - else: - get.email = email - - for domain in domains: - if public.checkIp(domain): continue - if domain.find('*.') >=0 and file_auth: - return public.returnMsg(False, '泛域名不能使用【文件验证】的方式申请证书!') - - if file_auth: - get.sitename = get.siteName - if self.GetRedirectList(get): return public.returnMsg(False, 'SITE_SSL_ERR_301') - if self.GetProxyList(get): return public.returnMsg(False,'已开启反向代理的站点无法申请SSL!') - data = self.get_site_info(get.siteName) - get.id = data['id'] - runPath = self.GetRunPath(get) - if runPath != '/': - if runPath[:1] != '/': runPath = '/' + runPath - else: - runPath = '' - get.site_dir = data['path'] + runPath - - else: - dns_api_list = self.GetDnsApi(get) - get.dns_param = None - for dns in dns_api_list: - if dns['name'] == get.dnsapi: - param = [] - if not dns['data']: continue - for val in dns['data']: - param.append(val['value']) - get.dns_param = '|'.join(param) - n_list = ['dns' , 'dns_bt'] - if not get.dnsapi in n_list: - if len(get.dns_param) < 16: return public.returnMsg(False, '请先设置【%s】的API接口参数.' % get.dnsapi) - if get.dnsapi == 'dns_bt': - if not os.path.exists('plugin/dns/dns_main.py'): - return public.returnMsg(False, '请先到软件商店安装【云解析】,并完成域名NS绑定.') - - self.check_ssl_pack() - - try: - import panelLets - public.mod_reload(panelLets) - except Exception as ex: - if str(ex).find('No module named requests') != -1: - public.ExecShell("pip install requests &") - return public.returnMsg(False,'缺少requests组件,请尝试修复面板!') - return public.returnMsg(False,str(ex)) - - lets = panelLets.panelLets() - result = lets.apple_lest_cert(get) - if result['status'] and not 'code' in result: - get.onkey = 1 - path = '/www/server/panel/cert/' + get.siteName - if os.path.exists(path + '/certOrderId'): os.remove(path + '/certOrderId') - result = self.SetSSLConf(get) - return result - - def get_site_info(self,siteName): - data = public.M("sites").where('name=?',siteName).field('id,path,name').find() - return data - - - #检测依赖库 - def check_ssl_pack(self): - try: - import requests - except: - public.ExecShell('pip install requests') - try: - import OpenSSL - except: - public.ExecShell('pip install pyopenssl') - - - #判断DNS-API是否设置 - def Check_DnsApi(self,dnsapi): - dnsapis = self.GetDnsApi(None) - for dapi in dnsapis: - if dapi['name'] == dnsapi: - if not dapi['data']: return True - for d in dapi['data']: - if d['key'] == '': return False - return True - - #获取DNS-API列表 - def GetDnsApi(self,get): - api_path = './config/dns_api.json' - api_init = './config/dns_api_init.json' - if not os.path.exists(api_path): - if os.path.exists(api_init): - import shutil - shutil.copyfile(api_init,api_path) - apis = json.loads(public.ReadFile(api_path)) - - path = '/root/.acme.sh' - if not os.path.exists(path + '/account.conf'): path = "/.acme.sh" - account = public.readFile(path + '/account.conf') - if not account: account = '' - is_write = False - for i in range(len(apis)): - if not apis[i]['data']: continue - for j in range(len(apis[i]['data'])): - if apis[i]['data'][j]['value']: continue - match = re.search(apis[i]['data'][j]['key'] + "\s*=\s*'(.+)'",account) - if match: apis[i]['data'][j]['value'] = match.groups()[0] - if apis[i]['data'][j]['value']: is_write = True - if is_write: public.writeFile('./config/dns_api.json',json.dumps(apis)) - result = [] - for i in apis: - if i['name'] == 'Dns_com': continue - result.insert(0,i) - return result - - #设置DNS-API - def SetDnsApi(self,get): - pdata = json.loads(get.pdata) - apis = json.loads(public.ReadFile('./config/dns_api.json')) - is_write = False - for key in pdata.keys(): - for i in range(len(apis)): - if not apis[i]['data']: continue - for j in range(len(apis[i]['data'])): - if apis[i]['data'][j]['key'] != key: continue - apis[i]['data'][j]['value'] = pdata[key] - is_write = True - - if is_write: public.writeFile('./config/dns_api.json',json.dumps(apis)) - return public.returnMsg(True,"设置成功!") - - - #获取站点所有域名 - def GetSiteDomains(self,get): - data = {} - domains = public.M('domain').where('pid=?',(get.id,)).field('name,id').select() - binding = public.M('binding').where('pid=?',(get.id,)).field('domain,id').select() - if type(binding) == str: return binding - for b in binding: - tmp = {} - tmp['name'] = b['domain'] - tmp['id'] = b['id'] - tmp['binding'] = True - domains.append(tmp) - data['domains'] = domains - data['email'] = public.M('users').where('id=?',(1,)).getField('email') - if data['email'] == '287962566@qq.com': data['email'] = '' - return data - - def GetFormatSSLResult(self,result): - try: - import re - rep = "\s*Domain:.+\n\s+Type:.+\n\s+Detail:.+" - tmps = re.findall(rep,result) - - statusList = [] - for tmp in tmps: - arr = tmp.strip().split('\n') - status={} - for ar in arr: - tmp1 = ar.strip().split(':') - status[tmp1[0].strip()] = tmp1[1].strip() - if len(tmp1) > 2: - status[tmp1[0].strip()] = tmp1[1].strip() + ':' + tmp1[2] - statusList.append(status) - return statusList - except: - return None - - #获取TLS1.3标记 - def get_tls13(self): - nginx_bin = '/www/server/nginx/sbin/nginx' - nginx_v = public.ExecShell(nginx_bin + ' -V 2>&1')[0] - nginx_v_re = re.findall("nginx/(\d\.\d+).+OpenSSL\s+(\d\.\d+)",nginx_v,re.DOTALL) - if nginx_v_re: - if nginx_v_re[0][0] in ['1.8','1.9','1.7','1.6','1.5','1.4']: - return '' - if float(nginx_v_re[0][0]) >= 1.15 and float(nginx_v_re[0][-1]) >= 1.1: - return ' TLSv1.3' - else: - _v = re.search('nginx/1\.1(5|6|7|8|9).\d',nginx_v) - if not _v: - _v = re.search('nginx/1\.2\d\.\d',nginx_v) - openssl_v = public.ExecShell(nginx_bin + ' -V 2>&1|grep OpenSSL')[0].find('OpenSSL 1.1.') != -1 - if _v and openssl_v: - return ' TLSv1.3' - return '' - - # 获取apache反向代理 - def get_apache_proxy(self,conf): - rep = "\n*#引用反向代理规则,注释后配置的反向代理将无效\n+\s+IncludeOptiona.*" - proxy = re.search(rep,conf) - if proxy: - return proxy.group() - return "" - - def _get_site_domains(self,sitename): - site_id = public.M('sites').where('name=?', (sitename,)).field('id').find() - domains = public.M('domain').where('pid=?',(site_id['id'],)).field('name').select() - domains = [d['name'] for d in domains] - return domains - - # 设置OLS ssl - def set_ols_ssl(self,get,siteName): - listen_conf = self.setupPath + '/panel/vhost/openlitespeed/listen/443.conf' - conf = public.readFile(listen_conf) - ssl_conf = """ - vhssl { - keyFile /www/server/panel/vhost/cert/BTDOMAIN/privkey.pem - certFile /www/server/panel/vhost/cert/BTDOMAIN/fullchain.pem - certChain 1 - sslProtocol 24 - ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:ECDHE-RSA-AES128-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA128:DHE-RSA-AES128-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA128:ECDHE-RSA-AES128-SHA384:ECDHE-RSA-AES128-SHA128:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA384:AES128-GCM-SHA128:AES128-SHA128:AES128-SHA128:AES128-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4 - enableECDHE 1 - renegProtection 1 - sslSessionCache 1 - enableSpdy 15 - enableStapling 1 - ocspRespMaxAge 86400 - } - """ - ssl_dir = self.setupPath + '/panel/vhost/openlitespeed/detail/ssl/' - if not os.path.exists(ssl_dir): - os.makedirs(ssl_dir) - ssl_file = ssl_dir + '{}.conf'.format(siteName) - if not os.path.exists(ssl_file): - ssl_conf = ssl_conf.replace('BTDOMAIN', siteName) - public.writeFile(ssl_file, ssl_conf, "a+") - include_ssl = '\ninclude {}'.format(ssl_file) - detail_file = self.setupPath + '/panel/vhost/openlitespeed/detail/{}.conf'.format(siteName) - public.writeFile(detail_file, include_ssl, 'a+') - if not conf: - conf = """ -listener SSL443 { - map BTSITENAME BTDOMAIN - address *:443 - secure 1 - keyFile /www/server/panel/vhost/cert/BTSITENAME/privkey.pem - certFile /www/server/panel/vhost/cert/BTSITENAME/fullchain.pem - certChain 1 - sslProtocol 24 - ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:ECDHE-RSA-AES128-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA128:DHE-RSA-AES128-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA128:ECDHE-RSA-AES128-SHA384:ECDHE-RSA-AES128-SHA128:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA384:AES128-GCM-SHA128:AES128-SHA128:AES128-SHA128:AES128-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4 - enableECDHE 1 - renegProtection 1 - sslSessionCache 1 - enableSpdy 15 - enableStapling 1 - ocspRespMaxAge 86400 -} -""" - - else: - rep = 'listener\s*SSL443\s*{' - map = '\n map {s} {s}'.format(s=siteName) - conf = re.sub(rep, 'listener SSL443 {' + map, conf) - domain = ",".join(self._get_site_domains(siteName)) - conf = conf.replace('BTSITENAME', siteName).replace('BTDOMAIN', domain) - public.writeFile(listen_conf, conf) - - def _get_ap_static_security(self,ap_conf): - if not ap_conf: return '' - ap_static_security = re.search('#SECURITY-START(.|\n)*#SECURITY-END',ap_conf) - if ap_static_security: - return ap_static_security.group() - return '' - - # 添加SSL配置 - def SetSSLConf(self, get): - siteName = get.siteName - if not 'first_domain' in get: get.first_domain = siteName - - # Nginx配置 - file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' - ng_file = file - conf = public.readFile(file) - - # 是否为子目录设置SSL - # if hasattr(get,'binding'): - # allconf = conf; - # conf = re.search("#BINDING-"+get.binding+"-START(.|\n)*#BINDING-"+get.binding+"-END",conf).group() - - if conf: - if conf.find('ssl_certificate') == -1: - sslStr = """#error_page 404/404.html; - ssl_certificate /www/server/panel/vhost/cert/%s/fullchain.pem; - ssl_certificate_key /www/server/panel/vhost/cert/%s/privkey.pem; - ssl_protocols TLSv1.1 TLSv1.2%s; - ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - add_header Strict-Transport-Security "max-age=31536000"; - error_page 497 https://$host$request_uri; -""" % (get.first_domain, get.first_domain,self.get_tls13()) - if (conf.find('ssl_certificate') != -1): - public.serviceReload() - return public.returnMsg(True, 'SITE_SSL_OPEN_SUCCESS') - - conf = conf.replace('#error_page 404/404.html;', sslStr) - # 添加端口 - rep = "listen.*[\s:]+(\d+).*;" - tmp = re.findall(rep, conf) - if not public.inArray(tmp, '443'): - listen = re.search(rep,conf).group() - versionStr = public.readFile('/www/server/nginx/version.pl') - http2 = '' - if versionStr: - if versionStr.find('1.8.1') == -1: http2 = ' http2' - default_site = '' - if conf.find('default_server') != -1: default_site = ' default_server' - - listen_ipv6 = ';' - if self.is_ipv6: listen_ipv6 = ";\n\tlisten [::]:443 ssl"+http2+default_site+";" - conf = conf.replace(listen,listen + "\n\tlisten 443 ssl"+http2 + default_site + listen_ipv6) - shutil.copyfile(file, self.nginx_conf_bak) - public.writeFile(file, conf) - - # Apache配置 - file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' - conf = public.readFile(file) - ap_static_security = self._get_ap_static_security(conf) - if conf: - ap_proxy = self.get_apache_proxy(conf) - if conf.find('SSLCertificateFile') == -1 and conf.find('VirtualHost') != -1: - find = public.M('sites').where("name=?", (siteName,)).field('id,path').find() - tmp = public.M('domain').where('pid=?', (find['id'],)).field('name').select() - domains = '' - for key in tmp: - domains += key['name'] + ' ' - path = (find['path'] + '/' + self.GetRunPath(get)).replace('//', '/') - index = 'index.php index.html index.htm default.php default.html default.htm' - - try: - httpdVersion = public.readFile(self.setupPath + '/apache/version.pl').strip() - except: - httpdVersion = "" - if httpdVersion == '2.2': - vName = "" - phpConfig = "" - apaOpt = "Order allow,deny\n\t\tAllow from all" - else: - vName = "" - # rep = r"php-cgi-([0-9]{2,3})\.sock" - # version = re.search(rep, conf).groups()[0] - version = public.get_php_version_conf(conf) - if len(version) < 2: return public.returnMsg(False, 'PHP_GET_ERR') - phpConfig = ''' - #PHP - - SetHandler "proxy:%s" - - ''' % (public.get_php_proxy(version,'apache'),) - apaOpt = 'Require all granted' - - sslStr = '''%s - ServerAdmin webmaster@example.com - DocumentRoot "%s" - ServerName SSL.%s - ServerAlias %s - #errorDocument 404 /404.html - ErrorLog "%s-error_log" - CustomLog "%s-access_log" combined - %s - #SSL - SSLEngine On - SSLCertificateFile /www/server/panel/vhost/cert/%s/fullchain.pem - SSLCertificateKeyFile /www/server/panel/vhost/cert/%s/privkey.pem - SSLCipherSuite EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5 - SSLProtocol All -SSLv2 -SSLv3 -TLSv1 - SSLHonorCipherOrder On - %s - %s - - #DENY FILES - - Order allow,deny - Deny from all - - - #PATH - - SetOutputFilter DEFLATE - Options FollowSymLinks - AllowOverride All - %s - DirectoryIndex %s - -''' % (vName, path, siteName, domains, public.GetConfigValue('logs_path') + '/' + siteName, - public.GetConfigValue('logs_path') + '/' + siteName ,ap_proxy ,get.first_domain, get.first_domain, - ap_static_security,phpConfig, path, apaOpt, index) - conf = conf + "\n" + sslStr - self.apacheAddPort('443') - shutil.copyfile(file, self.apache_conf_bak) - public.writeFile(file, conf) - - # OLS - self.set_ols_ssl(get,siteName) - isError = public.checkWebConfig() - if (isError != True): - if os.path.exists(self.nginx_conf_bak): shutil.copyfile(self.nginx_conf_bak, ng_file) - if os.path.exists(self.apache_conf_bak): shutil.copyfile(self.apache_conf_bak, file) - public.ExecShell("rm -f /tmp/backup_*.conf") - return public.returnMsg(False, '证书错误:
' + isError.replace("\n", '
') + '
') - - sql = public.M('firewall') - import firewalls - get.port = '443' - get.ps = 'HTTPS' - firewalls.firewalls().AddAcceptPort(get) - public.serviceReload() - self.save_cert(get) - public.WriteLog('TYPE_SITE', 'SITE_SSL_OPEN_SUCCESS', (siteName,)) - result = public.returnMsg(True, 'SITE_SSL_OPEN_SUCCESS') - result['csr'] = public.readFile('/www/server/panel/vhost/cert/' + get.siteName + '/fullchain.pem') - result['key'] = public.readFile( '/www/server/panel/vhost/cert/' + get.siteName + '/privkey.pem') - return result - - def save_cert(self, get): - # try: - import panelSSL - ss = panelSSL.panelSSL() - get.keyPath = '/www/server/panel/vhost/cert/' + get.siteName + '/privkey.pem' - get.certPath = '/www/server/panel/vhost/cert/' + get.siteName + '/fullchain.pem' - return ss.SaveCert(get) - return True - # except: - # return False; - - #HttpToHttps - def HttpToHttps(self,get): - siteName = get.siteName - #Nginx配置 - file = self.setupPath + '/panel/vhost/nginx/'+siteName+'.conf' - conf = public.readFile(file) - if conf: - if conf.find('ssl_certificate') == -1: return public.returnMsg(False,'当前未开启SSL') - to = """#error_page 404/404.html; - #HTTP_TO_HTTPS_START - if ($server_port !~ 443){ - rewrite ^(/.*)$ https://$host$1 permanent; - } - #HTTP_TO_HTTPS_END""" - conf = conf.replace('#error_page 404/404.html;',to) - public.writeFile(file,conf) - - file = self.setupPath + '/panel/vhost/apache/'+siteName+'.conf' - conf = public.readFile(file) - if conf: - httpTohttos = '''combined - #HTTP_TO_HTTPS_START - - RewriteEngine on - RewriteCond %{SERVER_PORT} !^443$ - RewriteRule (.*) https://%{SERVER_NAME}$1 [L,R=301] - - #HTTP_TO_HTTPS_END''' - conf = re.sub('combined',httpTohttos,conf,1) - public.writeFile(file,conf) - # OLS - conf_dir = '{}/panel/vhost/openlitespeed/redirect/{}/'.format(self.setupPath,siteName) - if not os.path.exists(conf_dir): - os.makedirs(conf_dir) - file = conf_dir+'force_https.conf' - ols_force_https = ''' -#HTTP_TO_HTTPS_START - - RewriteEngine on - RewriteCond %{SERVER_PORT} !^443$ - RewriteRule (.*) https://%{SERVER_NAME}$1 [L,R=301] - -#HTTP_TO_HTTPS_END''' - public.writeFile(file,ols_force_https) - public.serviceReload() - return public.returnMsg(True,'SET_SUCCESS') - - #CloseToHttps - def CloseToHttps(self,get): - siteName = get.siteName - file = self.setupPath + '/panel/vhost/nginx/'+siteName+'.conf' - conf = public.readFile(file) - if conf: - rep = "\n\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" - conf = re.sub(rep,'',conf) - rep = "\s+if.+server_port.+\n.+\n\s+\s*}" - conf = re.sub(rep,'',conf) - public.writeFile(file,conf) - - file = self.setupPath + '/panel/vhost/apache/'+siteName+'.conf' - conf = public.readFile(file) - if conf: - rep = "\n\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" - conf = re.sub(rep,'',conf) - public.writeFile(file,conf) - # OLS - file = '{}/panel/vhost/openlitespeed/redirect/{}/force_https.conf'.format(self.setupPath,siteName) - public.ExecShell('rm -f {}*'.format(file)) - public.serviceReload() - return public.returnMsg(True,'SET_SUCCESS') - - #是否跳转到https - def IsToHttps(self,siteName): - file = self.setupPath + '/panel/vhost/nginx/'+siteName+'.conf' - conf = public.readFile(file) - if conf: - if conf.find('HTTP_TO_HTTPS_START') != -1: return True - if conf.find('$server_port !~ 443') != -1: return True - return False - - # 清理SSL配置 - def CloseSSLConf(self, get): - siteName = get.siteName - - file = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' - conf = public.readFile(file) - if conf: - rep = "\n\s*#HTTP_TO_HTTPS_START(.|\n){1,300}#HTTP_TO_HTTPS_END" - conf = re.sub(rep, '', conf) - rep = "\s+ssl_certificate\s+.+;\s+ssl_certificate_key\s+.+;" - conf = re.sub(rep, '', conf) - rep = "\s+ssl_protocols\s+.+;\n" - conf = re.sub(rep, '', conf) - rep = "\s+ssl_ciphers\s+.+;\n" - conf = re.sub(rep, '', conf) - rep = "\s+ssl_prefer_server_ciphers\s+.+;\n" - conf = re.sub(rep, '', conf) - rep = "\s+ssl_session_cache\s+.+;\n" - conf = re.sub(rep, '', conf) - rep = "\s+ssl_session_timeout\s+.+;\n" - conf = re.sub(rep, '', conf) - rep = "\s+ssl_ecdh_curve\s+.+;\n" - conf = re.sub(rep, '', conf) - rep = "\s+ssl_session_tickets\s+.+;\n" - conf = re.sub(rep, '', conf) - rep = "\s+ssl_stapling\s+.+;\n" - conf = re.sub(rep, '', conf) - rep = "\s+ssl_stapling_verify\s+.+;\n" - conf = re.sub(rep, '', conf) - rep = "\s+add_header\s+.+;\n" - conf = re.sub(rep, '', conf) - rep = "\s+add_header\s+.+;\n" - conf = re.sub(rep, '', conf) - rep = "\s+ssl\s+on;" - conf = re.sub(rep, '', conf) - rep = "\s+error_page\s497.+;" - conf = re.sub(rep, '', conf) - rep = "\s+if.+server_port.+\n.+\n\s+\s*}" - conf = re.sub(rep, '', conf) - rep = "\s+listen\s+443.*;" - conf = re.sub(rep, '', conf) - rep = "\s+listen\s+\[::\]:443.*;" - conf = re.sub(rep, '', conf) - public.writeFile(file, conf) - - file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' - conf = public.readFile(file) - if conf: - rep = "\n(.|\n)*<\/VirtualHost>" - conf = re.sub(rep, '', conf) - rep = "\n\s*#HTTP_TO_HTTPS_START(.|\n){1,250}#HTTP_TO_HTTPS_END" - conf = re.sub(rep, '', conf) - rep = "NameVirtualHost *:443\n" - conf = conf.replace(rep, '') - public.writeFile(file, conf) - - # OLS - ssl_file = self.setupPath + '/panel/vhost/openlitespeed/detail/ssl/{}.conf'.format(siteName) - detail_file = self.setupPath + '/panel/vhost/openlitespeed/detail/' + siteName + '.conf' - force_https = self.setupPath + '/panel/vhost/openlitespeed/redirect/' + siteName - string = 'rm -f {}/force_https.conf*'.format(force_https) - public.ExecShell(string) - detail_conf = public.readFile(detail_file) - if detail_conf: - detail_conf = detail_conf.replace('\ninclude '+ssl_file,'') - public.writeFile(detail_file,detail_conf) - public.ExecShell('rm -f {}*'.format(ssl_file)) - - self._del_ols_443_domain(siteName) - partnerOrderId = '/www/server/panel/vhost/cert/' + siteName + '/partnerOrderId' - if os.path.exists(partnerOrderId): public.ExecShell('rm -f ' + partnerOrderId) - p_file = '/etc/letsencrypt/live/' + siteName + '/partnerOrderId' - if os.path.exists(p_file): public.ExecShell('rm -f ' + p_file) - - public.WriteLog('TYPE_SITE', 'SITE_SSL_CLOSE_SUCCESS', (siteName,)) - public.serviceReload() - return public.returnMsg(True, 'SITE_SSL_CLOSE_SUCCESS') - - def _del_ols_443_domain(self,sitename): - file = "/www/server/panel/vhost/openlitespeed/listen/443.conf" - conf = public.readFile(file) - if conf: - rep = '\n\s*map\s*{}'.format(sitename) - conf = re.sub(rep,'',conf) - if not "map " in conf: - public.ExecShell('rm -f {}*'.format(file)) - return - public.writeFile(file,conf) - - # 取SSL状态 - def GetSSL(self, get): - siteName = get.siteName - path = os.path.join('/www/server/panel/vhost/cert/', siteName) - if not os.path.isfile(os.path.join(path, "fullchain.pem")) and not os.path.isfile(os.path.join(path, "privkey.pem")): - path = os.path.join('/etc/letsencrypt/live/', siteName) - type = 0 - if os.path.exists(path + '/README'): type = 1 - if os.path.exists(path + '/partnerOrderId'): type = 2 - if os.path.exists(path + '/certOrderId'): type = 3 - csrpath = path + "/fullchain.pem" # 生成证书路径 - keypath = path + "/privkey.pem" # 密钥文件路径 - key = public.readFile(keypath) - csr = public.readFile(csrpath) - file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/' + siteName + '.conf' - if public.get_webserver() == "openlitespeed": - file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/detail/' + siteName + '.conf' - conf = public.readFile(file) - if not conf: return public.returnMsg(False,'指定网站配置文件不存在!') - - if public.get_webserver() == 'nginx': - keyText = 'ssl_certificate' - elif public.get_webserver() == 'apache': - keyText = 'SSLCertificateFile' - else: - keyText = 'openlitespeed/detail/ssl' - - status = True - if (conf.find(keyText) == -1): - status = False - type = -1 - - toHttps = self.IsToHttps(siteName) - id = public.M('sites').where("name=?", (siteName,)).getField('id') - domains = public.M('domain').where("pid=?", (id,)).field('name').select() - cert_data= {} - if csr: - get.certPath = csrpath - import panelSSL - cert_data = panelSSL.panelSSL().GetCertName(get) - - email = public.M('users').where('id=?',(1,)).getField('email') - if email == '287962566@qq.com': email = '' - index = '' - auth_type = 'http' - if status == True: - if type != 1: - import acme_v2 - acme = acme_v2.acme_v2() - index = acme.check_order_exists(csrpath) - if index: - if index.find('/') == -1: - auth_type = acme._config['orders'][index]['auth_type'] - type = 1 - else: - crontab_file = 'vhost/cert/crontab.json' - tmp = public.readFile(crontab_file) - if tmp: - crontab_config = json.loads(tmp) - if siteName in crontab_config: - if 'dnsapi' in crontab_config[siteName]: - auth_type = 'dns' - - if os.path.exists(path + '/certOrderId'): type = 3 - oid = -1 - if type == 3: - oid = int(public.readFile(path + '/certOrderId')) - return {'status': status,'oid':oid, 'domain': domains, 'key': key, 'csr': csr, 'type': type, 'httpTohttps': toHttps,'cert_data':cert_data,'email':email,"index":index,'auth_type':auth_type} - - def set_site_status_multiple(self,get): - ''' - @name 批量设置网站状态 - @author zhwen<2020-11-17> - @param sites_id "1,2" - @param status 0/1 - ''' - sites_id = get.sites_id.split(',') - sites_name = [] - for site_id in sites_id: - get.id = site_id - get.name = public.M('sites').where("id=?", (site_id,)).getField('name') - sites_name.append(get.name) - if get.status == '1': - self.SiteStart(get,multiple=1) - else: - self.SiteStop(get,multiple=1) - public.serviceReload() - if get.status == '1': - return {'status': True, 'msg': '开启网站 [ {} ] 成功'.format(','.join(sites_name)), 'error': {}, 'success': sites_name} - else: - return {'status': True, 'msg': '停止网站 [ {} ] 成功'.format(','.join(sites_name)), 'error': {}, 'success':sites_name} - - - #启动站点 - def SiteStart(self,get,multiple=None): - id = get.id - Path = self.setupPath + '/stop' - sitePath = public.M('sites').where("id=?",(id,)).getField('path') - - #nginx - file = self.setupPath + '/panel/vhost/nginx/'+get.name+'.conf' - conf = public.readFile(file) - if conf: - conf = conf.replace(Path, sitePath) - conf = conf.replace("#include","include") - public.writeFile(file,conf) - #apache - file = self.setupPath + '/panel/vhost/apache/'+get.name+'.conf' - conf = public.readFile(file) - if conf: - conf = conf.replace(Path, sitePath) - conf = conf.replace("#IncludeOptional", "IncludeOptional") - public.writeFile(file,conf) - - # OLS - file = self.setupPath + '/panel/vhost/openlitespeed/' + get.name + '.conf' - conf = public.readFile(file) - if conf: - rep = 'vhRoot\s*{}'.format(Path) - new_content = 'vhRoot {}'.format(sitePath) - conf = re.sub(rep, new_content,conf) - public.writeFile(file, conf) - - public.M('sites').where("id=?",(id,)).setField('status','1') - if not multiple: - public.serviceReload() - public.WriteLog('TYPE_SITE','SITE_START_SUCCESS',(get.name,)) - return public.returnMsg(True,'SITE_START_SUCCESS') - - def _process_has_run_dir(self, website_name, website_path, stop_path): - ''' - @name 当网站存在允许目录时停止网站需要做处理 - @author zhwen<2020-11-17> - @param site_id 1 - @param names test,baohu - ''' - conf = public.readFile(self.setupPath + '/panel/vhost/nginx/' + website_name + '.conf') - if not conf: - return False - try: - really_path = re.search('root\s+(.*);', conf).group(1) - tmp = stop_path + '/' + really_path.replace(website_path + '/', '') - public.ExecShell('mkdir {t} && ln -s {s}/index.html {t}/index.html'.format(t=tmp, s=stop_path)) - except: - pass - - # 停止站点 - def SiteStop(self, get, multiple=None): - path = self.setupPath + '/stop' - id = get.id - if not os.path.exists(path): - os.makedirs(path) - public.downloadFile('http://{}/stop.html'.format(public.get_url()), path + '/index.html') - - binding = public.M('binding').where('pid=?', (id,)).field('id,pid,domain,path,port,addtime').select() - for b in binding: - bpath = path + '/' + b['path'] - if not os.path.exists(bpath): - public.ExecShell('mkdir -p ' + bpath) - public.ExecShell('ln -sf ' + path + '/index.html ' + bpath + '/index.html') - - sitePath = public.M('sites').where("id=?", (id,)).getField('path') - self._process_has_run_dir(get.name, sitePath, path) - #nginx - file = self.setupPath + '/panel/vhost/nginx/'+get.name+'.conf' - conf = public.readFile(file) - if conf: - src_path = 'root ' + sitePath - dst_path = 'root ' + path - if conf.find(src_path) != -1: - conf = conf.replace(src_path,dst_path) - else: - conf = conf.replace(sitePath,path) - conf = conf.replace("include","#include") - public.writeFile(file,conf) - - #apache - file = self.setupPath + '/panel/vhost/apache/'+get.name+'.conf' - conf = public.readFile(file) - if conf: - conf = conf.replace(sitePath,path) - conf = conf.replace("IncludeOptional", "#IncludeOptional") - public.writeFile(file,conf) - # OLS - file = self.setupPath + '/panel/vhost/openlitespeed/' + get.name + '.conf' - conf = public.readFile(file) - if conf: - rep = 'vhRoot\s*{}'.format(sitePath) - new_content = 'vhRoot {}'.format(path) - conf = re.sub(rep, new_content,conf) - public.writeFile(file, conf) - - public.M('sites').where("id=?",(id,)).setField('status','0') - if not multiple: - public.serviceReload() - public.WriteLog('TYPE_SITE','SITE_STOP_SUCCESS',(get.name,)) - return public.returnMsg(True,'SITE_STOP_SUCCESS') - - - #取流量限制值 - def GetLimitNet(self,get): - id = get.id - - #取回配置文件 - siteName = public.M('sites').where("id=?",(id,)).getField('name') - filename = self.setupPath + '/panel/vhost/nginx/'+siteName+'.conf' - - #站点总并发 - data = {} - conf = public.readFile(filename) - try: - rep = "\s+limit_conn\s+perserver\s+([0-9]+);" - tmp = re.search(rep, conf).groups() - data['perserver'] = int(tmp[0]) - - #IP并发限制 - rep = "\s+limit_conn\s+perip\s+([0-9]+);" - tmp = re.search(rep, conf).groups() - data['perip'] = int(tmp[0]) - - #请求并发限制 - rep = "\s+limit_rate\s+([0-9]+)\w+;" - tmp = re.search(rep, conf).groups() - data['limit_rate'] = int(tmp[0]) - except: - data['perserver'] = 0 - data['perip'] = 0 - data['limit_rate'] = 0 - - return data - - - #设置流量限制 - def SetLimitNet(self,get): - if(public.get_webserver() != 'nginx'): return public.returnMsg(False, 'SITE_NETLIMIT_ERR') - - id = get.id - if int(get.perserver) < 1 or int(get.perip) < 1 or int(get.perip) < 1: - return public.returnMsg(False,'并发限制,IP限制,流量限制必需大于0') - perserver = 'limit_conn perserver ' + get.perserver + ';' - perip = 'limit_conn perip ' + get.perip + ';' - limit_rate = 'limit_rate ' + get.limit_rate + 'k;' - - #取回配置文件 - siteName = public.M('sites').where("id=?",(id,)).getField('name') - filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' - conf = public.readFile(filename) - - #设置共享内存 - oldLimit = self.setupPath + '/panel/vhost/nginx/limit.conf' - if(os.path.exists(oldLimit)): os.remove(oldLimit) - limit = self.setupPath + '/nginx/conf/nginx.conf' - nginxConf = public.readFile(limit) - limitConf = "limit_conn_zone $binary_remote_addr zone=perip:10m;\n\t\tlimit_conn_zone $server_name zone=perserver:10m;" - nginxConf = nginxConf.replace("#limit_conn_zone $binary_remote_addr zone=perip:10m;",limitConf) - public.writeFile(limit,nginxConf) - - if(conf.find('limit_conn perserver') != -1): - #替换总并发 - rep = "limit_conn\s+perserver\s+([0-9]+);" - conf = re.sub(rep,perserver,conf) - - #替换IP并发限制 - rep = "limit_conn\s+perip\s+([0-9]+);" - conf = re.sub(rep,perip,conf) - - #替换请求流量限制 - rep = "limit_rate\s+([0-9]+)\w+;" - conf = re.sub(rep,limit_rate,conf) - else: - conf = conf.replace('#error_page 404/404.html;',"#error_page 404/404.html;\n " + perserver + "\n " + perip + "\n " + limit_rate) - - - import shutil - shutil.copyfile(filename, self.nginx_conf_bak) - public.writeFile(filename,conf) - isError = public.checkWebConfig() - if(isError != True): - if os.path.exists(self.nginx_conf_bak): shutil.copyfile(self.nginx_conf_bak,filename) - return public.returnMsg(False,'ERROR:
'+isError.replace("\n",'
')+'
') - - public.serviceReload() - public.WriteLog('TYPE_SITE','SITE_NETLIMIT_OPEN_SUCCESS',(siteName,)) - return public.returnMsg(True, 'SET_SUCCESS') - - - #关闭流量限制 - def CloseLimitNet(self,get): - id = get.id - #取回配置文件 - siteName = public.M('sites').where("id=?",(id,)).getField('name') - filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' - conf = public.readFile(filename) - #清理总并发 - rep = "\s+limit_conn\s+perserver\s+([0-9]+);" - conf = re.sub(rep,'',conf) - - #清理IP并发限制 - rep = "\s+limit_conn\s+perip\s+([0-9]+);" - conf = re.sub(rep,'',conf) - - #清理请求流量限制 - rep = "\s+limit_rate\s+([0-9]+)\w+;" - conf = re.sub(rep,'',conf) - public.writeFile(filename,conf) - public.serviceReload() - public.WriteLog('TYPE_SITE','SITE_NETLIMIT_CLOSE_SUCCESS',(siteName,)) - return public.returnMsg(True, 'SITE_NETLIMIT_CLOSE_SUCCESS') - - #取301配置状态 - def Get301Status(self,get): - siteName = get.siteName - result = {} - domains = '' - id = public.M('sites').where("name=?",(siteName,)).getField('id') - tmp = public.M('domain').where("pid=?",(id,)).field('name').select() - for key in tmp: - domains += key['name'] + ',' - try: - if(public.get_webserver() == 'nginx'): - conf = public.readFile(self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf') - if conf.find('301-START') == -1: - result['domain'] = domains[:-1] - result['src'] = "" - result['status'] = False - result['url'] = "http://" - return result - rep = "return\s+301\s+((http|https)\://.+);" - arr = re.search(rep, conf).groups()[0] - rep = "'\^(([\w-]+\.)+[\w-]+)'" - tmp = re.search(rep, conf) - src = '' - if tmp : src = tmp.groups()[0] - elif public.get_webserver() == 'apache': - conf = public.readFile(self.setupPath + '/panel/vhost/apache/' + siteName + '.conf') - if conf.find('301-START') == -1: - result['domain'] = domains[:-1] - result['src'] = "" - result['status'] = False - result['url'] = "http://" - return result - rep = "RewriteRule\s+.+\s+((http|https)\://.+)\s+\[" - arr = re.search(rep, conf).groups()[0] - rep = "\^((\w+\.)+\w+)\s+\[NC" - tmp = re.search(rep, conf) - src = '' - if tmp : src = tmp.groups()[0] - else: - conf = public.readFile(self.setupPath + '/panel/vhost/openlitespeed/redirect/{s}/{s}.conf'.format(s=siteName)) - if not conf: - result['domain'] = domains[:-1] - result['src'] = "" - result['status'] = False - result['url'] = "http://" - return result - rep = "RewriteRule\s+.+\s+((http|https)\://.+)\s+\[" - arr = re.search(rep, conf).groups()[0] - rep = "\^((\w+\.)+\w+)\s+\[NC" - tmp = re.search(rep, conf) - src = '' - if tmp : src = tmp.groups()[0] - except: - src = '' - arr = 'http://' - - result['domain'] = domains[:-1] - result['src'] = src.replace("'", '') - result['status'] = True - if(len(arr) < 3): result['status'] = False - result['url'] = arr - - return result - - - #设置301配置 - def Set301Status(self,get): - siteName = get.siteName - srcDomain = get.srcDomain - toDomain = get.toDomain - type = get.type - rep = "(http|https)\://.+" - if not re.match(rep, toDomain): return public.returnMsg(False,'Url地址不正确!') - - - #nginx - filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' - mconf = public.readFile(filename) - if mconf == False: return public.returnMsg(False,'指定配置文件不存在!') - if mconf: - if(srcDomain == 'all'): - conf301 = "\t#301-START\n\t\treturn 301 "+toDomain+"$request_uri;\n\t#301-END" - else: - conf301 = "\t#301-START\n\t\tif ($host ~ '^"+srcDomain+"'){\n\t\t\treturn 301 "+toDomain+"$request_uri;\n\t\t}\n\t#301-END" - if type == '1': - mconf = mconf.replace("#error_page 404/404.html;","#error_page 404/404.html;\n"+conf301) - else: - rep = "\s+#301-START(.|\n){1,300}#301-END" - mconf = re.sub(rep, '', mconf) - - public.writeFile(filename,mconf) - - - #apache - filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' - mconf = public.readFile(filename) - if mconf: - if type == '1': - if(srcDomain == 'all'): - conf301 = "\n\t#301-START\n\t\n\t\tRewriteEngine on\n\t\tRewriteRule ^(.*)$ "+toDomain+"$1 [L,R=301]\n\t\n\t#301-END\n" - else: - conf301 = "\n\t#301-START\n\t\n\t\tRewriteEngine on\n\t\tRewriteCond %{HTTP_HOST} ^"+srcDomain+" [NC]\n\t\tRewriteRule ^(.*) "+toDomain+"$1 [L,R=301]\n\t\n\t#301-END\n" - rep = "combined" - mconf = mconf.replace(rep,rep + "\n\t" + conf301) - else: - rep = "\n\s+#301-START(.|\n){1,300}#301-END\n*" - mconf = re.sub(rep, '\n\n', mconf,1) - mconf = re.sub(rep, '\n\n', mconf,1) - - public.writeFile(filename,mconf) - - # OLS - conf_dir = self.setupPath + '/panel/vhost/openlitespeed/redirect/{}/'.format(siteName) - if not os.path.exists(conf_dir): - os.makedirs(conf_dir) - file = conf_dir+ siteName + '.conf' - if type == '1': - if (srcDomain == 'all'): - conf301 = "#301-START\nRewriteEngine on\nRewriteRule ^(.*)$ " + toDomain + "$1 [L,R=301]#301-END\n" - else: - conf301 = "#301-START\nRewriteEngine on\nRewriteCond %{HTTP_HOST} ^" + srcDomain + " [NC]\nRewriteRule ^(.*) " + toDomain + "$1 [L,R=301]\n#301-END\n" - public.writeFile(file,conf301) - else: - public.ExecShell('rm -f {}*'.format(file)) - - isError = public.checkWebConfig() - if(isError != True): - return public.returnMsg(False,'ERROR:
'+isError.replace("\n",'
')+'
') - - public.serviceReload() - return public.returnMsg(True,'SUCCESS') - - #取子目录绑定 - def GetDirBinding(self,get): - path = public.M('sites').where('id=?',(get.id,)).getField('path') - if not os.path.exists(path): - checks = ['/','/usr','/etc'] - if path in checks: - data = {} - data['dirs'] = [] - data['binding'] = [] - return data - public.ExecShell('mkdir -p ' + path) - public.ExecShell('chmod 755 ' + path) - public.ExecShell('chown www:www ' + path) - get.path = path - self.SetDirUserINI(get) - siteName = public.M('sites').where('id=?',(get.id,)).getField('name') - public.WriteLog('网站管理','站点['+siteName+'],根目录['+path+']不存在,已重新创建!') - dirnames = [] - for filename in os.listdir(path): - try: - json.dumps(filename) - if sys.version_info[0] == 2: - filename = filename.encode('utf-8') - else: - filename.encode('utf-8') - filePath = path + '/' + filename - if os.path.islink(filePath): continue - if os.path.isdir(filePath): - dirnames.append(filename) - except: - pass - - data = {} - data['dirs'] = dirnames - data['binding'] = public.M('binding').where('pid=?',(get.id,)).field('id,pid,domain,path,port,addtime').select() - return data - - #添加子目录绑定 - def AddDirBinding(self,get): - import shutil - id = get.id - tmp = get.domain.split(':') - domain = tmp[0].lower() - port = '80' - version = '' - if len(tmp) > 1: port = tmp[1] - if not hasattr(get,'dirName'): public.returnMsg(False, 'DIR_EMPTY') - dirName = get.dirName - - reg = "^([\w\-\*]{1,100}\.){1,4}(\w{1,10}|\w{1,10}\.\w{1,10})$" - if not re.match(reg, domain): return public.returnMsg(False,'SITE_ADD_ERR_DOMAIN') - - siteInfo = public.M('sites').where("id=?",(id,)).field('id,path,name').find() - webdir = siteInfo['path'] + '/' + dirName - sql = public.M('binding') - if sql.where("domain=?",(domain,)).count() > 0: return public.returnMsg(False, 'SITE_ADD_ERR_DOMAIN_EXISTS') - if public.M('domain').where("name=?",(domain,)).count() > 0: return public.returnMsg(False, 'SITE_ADD_ERR_DOMAIN_EXISTS') - - filename = self.setupPath + '/panel/vhost/nginx/' + siteInfo['name'] + '.conf' - nginx_conf_file = filename - conf = public.readFile(filename) - if conf: - listen_ipv6 = '' - if self.is_ipv6: listen_ipv6 = "\n listen [::]:%s;" % port - rep = "enable-php-(\w{2,5})\.conf" - tmp = re.search(rep,conf).groups() - version = tmp[0] - bindingConf =''' -#BINDING-%s-START -server -{ - listen %s;%s - server_name %s; - index index.php index.html index.htm default.php default.htm default.html; - root %s; - - include enable-php-%s.conf; - include %s/panel/vhost/rewrite/%s.conf; - #禁止访问的文件或目录 - location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md) - { - return 404; - } - - #一键申请SSL证书验证目录相关设置 - location ~ \.well-known{ - allow all; - } - - location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$ - { - expires 30d; - error_log /dev/null; - access_log /dev/null; - } - location ~ .*\\.(js|css)?$ - { - expires 12h; - error_log /dev/null; - access_log /dev/null; - } - access_log %s.log; - error_log %s.error.log; -} -#BINDING-%s-END''' % (domain,port,listen_ipv6,domain,webdir,version,self.setupPath,siteInfo['name'],public.GetConfigValue('logs_path')+'/'+siteInfo['name'],public.GetConfigValue('logs_path')+'/'+siteInfo['name'],domain) - - conf += bindingConf - shutil.copyfile(filename, self.nginx_conf_bak) - public.writeFile(filename,conf) - - - - filename = self.setupPath + '/panel/vhost/apache/' + siteInfo['name'] + '.conf' - conf = public.readFile(filename) - if conf: - try: - try: - httpdVersion = public.readFile(self.setupPath+'/apache/version.pl').strip() - except: - httpdVersion = "" - if httpdVersion == '2.2': - phpConfig = "" - apaOpt = "Order allow,deny\n\t\tAllow from all" - else: - # rep = "php-cgi-([0-9]{2,3})\.sock" - # tmp = re.search(rep,conf).groups() - # version = tmp[0] - version = public.get_php_version_conf(conf) - phpConfig =''' - #PHP - - SetHandler "proxy:%s" - - ''' % (public.get_php_proxy(version,'apache'),) - apaOpt = 'Require all granted' - - bindingConf =''' -\n#BINDING-%s-START - - ServerAdmin webmaster@example.com - DocumentRoot "%s" - ServerAlias %s - #errorDocument 404 /404.html - ErrorLog "%s-error_log" - CustomLog "%s-access_log" combined - %s - - #DENY FILES - - Order allow,deny - Deny from all - - - #PATH - - SetOutputFilter DEFLATE - Options FollowSymLinks - AllowOverride All - %s - DirectoryIndex index.php index.html index.htm default.php default.html default.htm - - -#BINDING-%s-END''' % (domain,port,webdir,domain,public.GetConfigValue('logs_path')+'/'+siteInfo['name'],public.GetConfigValue('logs_path')+'/'+siteInfo['name'],phpConfig,webdir,apaOpt,domain) - - conf += bindingConf - shutil.copyfile(filename, self.apache_conf_bak) - public.writeFile(filename,conf) - except: - pass - get.webname = siteInfo['name'] - get.port = port - self.phpVersion = version - self.siteName = siteInfo['name'] - self.sitePath = webdir - listen_file = self.setupPath+"/panel/vhost/openlitespeed/listen/80.conf" - listen_conf = public.readFile(listen_file) - if listen_conf: - rep = 'secure\s*0' - map = '\tmap {}_{} {}'.format(siteInfo['name'],dirName,domain) - listen_conf = re.sub(rep,'secure 0\n'+map,listen_conf) - public.writeFile(listen_file,listen_conf) - self.openlitespeed_add_site(get) - - #检查配置是否有误 - isError = public.checkWebConfig() - if isError != True: - if os.path.exists(self.nginx_conf_bak): shutil.copyfile(self.nginx_conf_bak,nginx_conf_file) - if os.path.exists(self.apache_conf_bak): shutil.copyfile(self.apache_conf_bak,filename) - return public.returnMsg(False,'ERROR:
'+isError.replace("\n",'
')+'
') - - public.M('binding').add('pid,domain,port,path,addtime',(id,domain,port,dirName,public.getDate())) - public.serviceReload() - public.WriteLog('TYPE_SITE', 'SITE_BINDING_ADD_SUCCESS',(siteInfo['name'],dirName,domain)) - return public.returnMsg(True, 'ADD_SUCCESS') - - def delete_dir_bind_multiple(self,get): - ''' - @name 批量删除网站 - @author zhwen<2020-11-17> - @param bind_ids 1,2,3 - ''' - bind_ids = get.bind_ids.split(',') - del_successfully = [] - del_failed = {} - for bind_id in bind_ids: - get.id = bind_id - domain = public.M('binding').where("id=?", (get.id,)).getField('domain') - if not domain: - continue - try: - self.DelDirBinding(get, multiple=1) - del_successfully.append(domain) - except: - del_failed[domain] = '删除时错误了,请再试一次' - pass - public.serviceReload() - return {'status': True, 'msg': '删除 [ {} ] 子目录绑定成功'.format(','.join(del_successfully)), 'error': del_failed, - 'success': del_successfully} - - #删除子目录绑定 - def DelDirBinding(self,get,multiple=None): - id = get.id - binding = public.M('binding').where("id=?",(id,)).field('id,pid,domain,path').find() - siteName = public.M('sites').where("id=?",(binding['pid'],)).getField('name') - - #nginx - filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' - conf = public.readFile(filename) - if conf: - rep = "\s*.+BINDING-" + binding['domain'] + "-START(.|\n)+BINDING-" + binding['domain'] + "-END" - conf = re.sub(rep, '', conf) - public.writeFile(filename,conf) - - #apache - filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' - conf = public.readFile(filename) - if conf: - rep = "\s*.+BINDING-" + binding['domain'] + "-START(.|\n)+BINDING-" + binding['domain'] + "-END" - conf = re.sub(rep, '', conf) - public.writeFile(filename,conf) - - # openlitespeed - filename = self.setupPath + '/panel/vhost/openlitespeed/' + siteName + '.conf' - conf = public.readFile(filename) - rep = "#SUBDIR\s*{s}_{d}\s*START(\n|.)+#SUBDIR\s*{s}_{d}\s*END".format(s=siteName,d=binding['path']) - if conf: - conf = re.sub(rep, '', conf) - public.writeFile(filename, conf) - # 删除域名,前端需要传域名 - get.webname = siteName - get.domain = binding['domain'] - self._del_ols_domain(get) - - # 清理子域名监听文件 - listen_file = self.setupPath+"/panel/vhost/openlitespeed/listen/80.conf" - listen_conf = public.readFile(listen_file) - if listen_conf: - map_reg = '\s*map\s*{}_{}.*'.format(siteName,binding['path']) - listen_conf = re.sub(map_reg,'',listen_conf) - public.writeFile(listen_file,listen_conf) - # 清理detail文件 - detail_file = "{}/panel/vhost/openlitespeed/detail/{}_{}.conf".format(self.setupPath,siteName,binding['path']) - public.ExecShell("rm -f {}*".format(detail_file)) - - public.M('binding').where("id=?",(id,)).delete() - filename = self.setupPath + '/panel/vhost/rewrite/' + siteName + '_' + binding['path'] + '.conf' - if os.path.exists(filename): public.ExecShell('rm -rf %s'%filename) - if not multiple: - public.serviceReload() - public.WriteLog('TYPE_SITE', 'SITE_BINDING_DEL_SUCCESS',(siteName,binding['path'])) - return public.returnMsg(True,'DEL_SUCCESS') - - #取子目录Rewrite - def GetDirRewrite(self,get): - id = get.id - find = public.M('binding').where("id=?",(id,)).field('id,pid,domain,path').find() - site = public.M('sites').where("id=?",(find['pid'],)).field('id,name,path').find() - - if(public.get_webserver() != 'nginx'): - filename = site['path']+'/'+find['path']+'/.htaccess' - else: - filename = self.setupPath + '/panel/vhost/rewrite/'+site['name']+'_'+find['path']+'.conf' - - if hasattr(get,'add'): - public.writeFile(filename,'') - if public.get_webserver() == 'nginx': - file = self.setupPath + '/panel/vhost/nginx/'+site['name']+'.conf' - conf = public.readFile(file) - domain = find['domain'] - rep = "\n#BINDING-"+domain+"-START(.|\n)+BINDING-"+domain+"-END" - tmp = re.search(rep, conf).group() - dirConf = tmp.replace('rewrite/'+site['name']+'.conf;', 'rewrite/'+site['name']+'_'+find['path']+'.conf;') - conf = conf.replace(tmp, dirConf) - public.writeFile(file,conf) - data = {} - data['status'] = False - if os.path.exists(filename): - data['status'] = True - data['data'] = public.readFile(filename) - data['rlist'] = ['0.当前'] - webserver = public.get_webserver() - if webserver == "openlitespeed": - webserver = "apache" - for ds in os.listdir('rewrite/' + webserver): - if ds == 'list.txt': continue - data['rlist'].append(ds[0:len(ds)-5]) - data['filename'] = filename - return data - - #取默认文档 - def GetIndex(self,get): - id = get.id - Name = public.M('sites').where("id=?",(id,)).getField('name') - file = self.setupPath + '/panel/vhost/'+public.get_webserver()+'/' + Name + '.conf' - if public.get_webserver() == 'openlitespeed': - file = self.setupPath + '/panel/vhost/' + public.get_webserver() + '/detail/' + Name + '.conf' - conf = public.readFile(file) - if conf == False: return public.returnMsg(False,'指定网站配置文件不存在!') - if public.get_webserver() == 'nginx': - rep = "\s+index\s+(.+);" - elif public.get_webserver() == 'apache': - rep = "DirectoryIndex\s+(.+)\n" - else: - rep = "indexFiles\s+(.+)\n" - if re.search(rep,conf): - tmp = re.search(rep,conf).groups() - if public.get_webserver() == 'openlitespeed': - return tmp[0] - return tmp[0].replace(' ',',') - return public.returnMsg(False,'获取失败,配置文件中不存在默认文档') - - #设置默认文档 - def SetIndex(self,get): - id = get.id - if get.Index.find('.') == -1: return public.returnMsg(False, 'SITE_INDEX_ERR_FORMAT') - - Index = get.Index.replace(' ', '') - Index = get.Index.replace(',,', ',') - - if len(Index) < 3: return public.returnMsg(False, 'SITE_INDEX_ERR_EMPTY') - - - Name = public.M('sites').where("id=?",(id,)).getField('name') - #准备指令 - Index_L = Index.replace(",", " ") - - #nginx - file = self.setupPath + '/panel/vhost/nginx/' + Name + '.conf' - conf = public.readFile(file) - if conf: - rep = "\s+index\s+.+;" - conf = re.sub(rep,"\n\tindex " + Index_L + ";",conf) - public.writeFile(file,conf) - - #apache - file = self.setupPath + '/panel/vhost/apache/' + Name + '.conf' - conf = public.readFile(file) - if conf: - rep = "DirectoryIndex\s+.+\n" - conf = re.sub(rep,'DirectoryIndex ' + Index_L + "\n",conf) - public.writeFile(file,conf) - - #openlitespeed - file = self.setupPath + '/panel/vhost/openlitespeed/detail/' + Name + '.conf' - conf = public.readFile(file) - if conf: - rep = "indexFiles\s+.+\n" - Index = Index.split(',') - Index = [i for i in Index if i] - Index = ",".join(Index) - conf = re.sub(rep,'indexFiles ' + Index + "\n",conf) - public.writeFile(file,conf) - - public.serviceReload() - public.WriteLog('TYPE_SITE', 'SITE_INDEX_SUCCESS',(Name,Index_L)) - return public.returnMsg(True, 'SET_SUCCESS') - - #修改物理路径 - def SetPath(self,get): - id = get.id - Path = self.GetPath(get.path) - if Path == "" or id == '0': return public.returnMsg(False, "DIR_EMPTY") - - if not self.__check_site_path(Path): return public.returnMsg(False, "PATH_ERROR") - if not public.check_site_path(Path): - a,c = public.get_sys_path() - return public.returnMsg(False,'请不要将网站根目录设置到以下关键目录中:
{}'.format("
".join(a+c))) - - SiteFind = public.M("sites").where("id=?",(id,)).field('path,name').find() - if SiteFind["path"] == Path: return public.returnMsg(False, "SITE_PATH_ERR_RE") - Name = SiteFind['name'] - file = self.setupPath + '/panel/vhost/nginx/' + Name + '.conf' - conf = public.readFile(file) - if conf: - conf = conf.replace(SiteFind['path'],Path ) - public.writeFile(file,conf) - - file = self.setupPath + '/panel/vhost/apache/' + Name + '.conf' - conf = public.readFile(file) - if conf: - rep = "DocumentRoot\s+.+\n" - conf = re.sub(rep,'DocumentRoot "' + Path + '"\n',conf) - rep = "\n",conf) - public.writeFile(file,conf) - - # OLS - file = self.setupPath + '/panel/vhost/openlitespeed/' + Name + '.conf' - conf = public.readFile(file) - if conf: - reg = 'vhRoot.*' - conf = re.sub(reg,'vhRoot '+Path,conf) - public.writeFile(file,conf) - - #创建basedir - userIni = Path + '/.user.ini' - if os.path.exists(userIni): public.ExecShell("chattr -i "+userIni) - public.writeFile(userIni, 'open_basedir='+Path+'/:/tmp/') - public.ExecShell('chmod 644 ' + userIni) - public.ExecShell('chown root:root ' + userIni) - public.ExecShell('chattr +i '+userIni) - public.set_site_open_basedir_nginx(Name) - - public.serviceReload() - public.M("sites").where("id=?",(id,)).setField('path',Path) - public.WriteLog('TYPE_SITE', 'SITE_PATH_SUCCESS',(Name,)) - return public.returnMsg(True, "SET_SUCCESS") - - #取当前可用PHP版本 - def GetPHPVersion(self,get): - phpVersions = ('00','other','52','53','54','55','56','70','71','72','73','74','80') - httpdVersion = "" - filename = self.setupPath+'/apache/version.pl' - if os.path.exists(filename): httpdVersion = public.readFile(filename).strip() - - if httpdVersion == '2.2': phpVersions = ('00','52','53','54') - if httpdVersion == '2.4': phpVersions = ('00','other','53','54','55','56','70','71','72','73','74','80') - if os.path.exists('/www/server/nginx/sbin/nginx'): - cfile = '/www/server/nginx/conf/enable-php-00.conf' - if not os.path.exists(cfile): public.writeFile(cfile,'') - - s_type = getattr(get,'s_type',0) - data = [] - for val in phpVersions: - tmp = {} - checkPath = self.setupPath+'/php/'+val+'/bin/php' - if val in ['00','other']: checkPath = '/etc/init.d/bt' - if httpdVersion == '2.2': checkPath = self.setupPath+'/php/'+val+'/libphp5.so' - if os.path.exists(checkPath): - tmp['version'] = val - tmp['name'] = 'PHP-'+val - if val == '00': - tmp['name'] = '纯静态' - - if val == 'other': - if s_type: - tmp['name'] = '自定义' - else: - continue - data.append(tmp) - return data - - - #取指定站点的PHP版本 - def GetSitePHPVersion(self,get): - try: - siteName = get.siteName - data = {} - data['phpversion'] = public.get_site_php_version(siteName) - conf = public.readFile(self.setupPath + '/panel/vhost/'+public.get_webserver()+'/'+siteName+'.conf') - data['tomcat'] = conf.find('#TOMCAT-START') - data['tomcatversion'] = public.readFile(self.setupPath + '/tomcat/version.pl') - data['nodejsversion'] = public.readFile(self.setupPath + '/node.js/version.pl') - data['php_other'] = '' - if data['phpversion'] == 'other': - other_file = '/www/server/panel/vhost/other_php/{}/enable-php-other.conf'.format(siteName) - if os.path.exists(other_file): - conf = public.readFile(other_file) - data['php_other'] = re.findall(r"fastcgi_pass\s+(.+);",conf)[0] - return data - except: - return public.returnMsg(False,'SITE_PHPVERSION_ERR_A22,{}'.format(public.get_error_info())) - - def set_site_php_version_multiple(self,get): - ''' - @name 批量设置PHP版本 - @author zhwen<2020-11-17> - @param sites_id "1,2" - @param version 52...74 - ''' - sites_id = get.sites_id.split(',') - set_phpv_successfully = [] - set_phpv_failed = {} - for site_id in sites_id: - get.id = site_id - get.siteName = public.M('sites').where("id=?", (site_id,)).getField('name') - if not get.siteName: - continue - try: - result = self.SetPHPVersion(get, multiple=1) - if not result['status']: - set_phpv_failed[get.siteName] = result['msg'] - continue - set_phpv_successfully.append(get.siteName) - except: - set_phpv_failed[get.siteName] = '设置时错误了,请再试一次' - pass - public.serviceReload() - return {'status': True, 'msg': '设置网站 [ {} ] PHP版本成功'.format(','.join(set_phpv_successfully)), 'error': set_phpv_failed, - 'success': set_phpv_successfully} - - - #设置指定站点的PHP版本 - def SetPHPVersion(self,get,multiple=None): - siteName = get.siteName - version = get.version - if version == 'other' and not public.get_webserver() in ['nginx','tengine']: - return public.returnMsg(False,'自定义PHP配置只支持Nginx') - try: - #nginx - file = self.setupPath + '/panel/vhost/nginx/'+siteName+'.conf' - conf = public.readFile(file) - if conf: - other_path = '/www/server/panel/vhost/other_php/{}'.format(siteName) - if not os.path.exists(other_path): os.makedirs(other_path) - other_rep = "{}/enable-php-other.conf".format(other_path) - - - if version == 'other': - dst = other_rep - get.other = get.other.strip() - - if not get.other: - return public.returnMsg(False,'自定义版本时PHP连接配置不能为空!') - - if not re.match(r"^(\d+\.\d+\.\d+\.\d+:\d+|unix:[\w/\.-]+)$",get.other): - return public.returnMsg(False,'PHP连接配置格式不正确,请参考示例!') - - other_tmp = get.other.split(':') - if other_tmp[0] == 'unix': - if not os.path.exists(other_tmp[1]): - return public.returnMsg(False,'指定unix套接字[{}]不存在,请核实!'.format(other_tmp[1])) - else: - if not public.check_tcp(other_tmp[0],int(other_tmp[1])): - return public.returnMsg(False,'无法连接[{}],请排查本机是否可连接目标服务器'.format(get.other)) - - other_conf = '''location ~ [^/]\.php(/|$) -{{ - try_files $uri =404; - fastcgi_pass {}; - fastcgi_index index.php; - include fastcgi.conf; - include pathinfo.conf; -}}'''.format(get.other) - public.writeFile(other_rep,other_conf) - conf = conf.replace(other_rep,dst) - rep = "include\s+enable-php-(\w{2,5})\.conf" - tmp = re.search(rep,conf) - if tmp: conf = conf.replace(tmp.group(),'include ' + dst) - else: - dst = 'enable-php-'+version+'.conf' - conf = conf.replace(other_rep,dst) - rep = "enable-php-(\w{2,5})\.conf" - tmp = re.search(rep,conf) - if tmp: conf = conf.replace(tmp.group(),dst) - - public.writeFile(file,conf) - try: - import site_dir_auth - site_dir_auth_module = site_dir_auth.SiteDirAuth() - auth_list = site_dir_auth_module.get_dir_auth(get) - if auth_list: - for i in auth_list[siteName]: - auth_name = i['name'] - auth_file = "{setup_path}/panel/vhost/nginx/dir_auth/{site_name}/{auth_name}.conf".format( - setup_path=self.setupPath,site_name=siteName,auth_name = auth_name) - if os.path.exists(auth_file): - site_dir_auth_module.change_dir_auth_file_nginx_phpver(siteName,version,auth_name) - except: - pass - - #apache - file = self.setupPath + '/panel/vhost/apache/'+siteName+'.conf' - conf = public.readFile(file) - if conf and version != 'other': - rep = "(unix:/tmp/php-cgi-(\w{2,5})\.sock\|fcgi://localhost|fcgi://127.0.0.1:\d+)" - tmp = re.search(rep,conf).group() - conf = conf.replace(tmp,public.get_php_proxy(version,'apache')) - public.writeFile(file,conf) - #OLS - if version != 'other': - file = self.setupPath + '/panel/vhost/openlitespeed/detail/'+siteName+'.conf' - conf = public.readFile(file) - if conf: - rep = 'lsphp\d+' - tmp = re.search(rep, conf) - if tmp: - conf = conf.replace(tmp.group(), 'lsphp' + version) - public.writeFile(file, conf) - if not multiple: - public.serviceReload() - public.WriteLog("TYPE_SITE", "SITE_PHPVERSION_SUCCESS",(siteName,version)) - return public.returnMsg(True,'SITE_PHPVERSION_SUCCESS',(siteName,version)) - except: - return public.get_error_info() - return public.returnMsg(False,'设置失败,没有在网站配置文件中找到enable-php-xx相关配置项!') - - - #是否开启目录防御 - def GetDirUserINI(self,get): - path = get.path + self.GetRunPath(get) - if not path:return public.returnMsg(False,'获取目录失败') - id = get.id - get.name = public.M('sites').where("id=?",(id,)).getField('name') - data = {} - data['logs'] = self.GetLogsStatus(get) - data['userini'] = False - user_ini_file = path+'/.user.ini' - user_ini_conf = public.readFile(user_ini_file) - if user_ini_conf and "open_basedir" in user_ini_conf: - data['userini'] = True - data['runPath'] = self.GetSiteRunPath(get) - data['pass'] = self.GetHasPwd(get) - return data - - #清除多余user.ini - def DelUserInI(self,path,up = 0): - useriniPath = path + '/.user.ini' - if os.path.exists(useriniPath): - public.ExecShell('chattr -i ' + useriniPath) - try: - os.remove(useriniPath) - except:pass - - for p1 in os.listdir(path): - try: - npath = path + '/' + p1 - if not os.path.isdir(npath): continue - useriniPath = npath + '/.user.ini' - if os.path.exists(useriniPath): - public.ExecShell('chattr -i ' + useriniPath) - os.remove(useriniPath) - if up < 3: self.DelUserInI(npath, up + 1) - except: continue - return True - - - #设置目录防御 - def SetDirUserINI(self,get): - path = get.path - runPath = self.GetRunPath(get) - filename = path+runPath+'/.user.ini' - siteName = public.M('sites').where('path=?',(get.path,)).getField('name') - conf = public.readFile(filename) - try: - self._set_ols_open_basedir(get) - public.ExecShell("chattr -i " + filename) - if conf and "open_basedir" in conf: - rep = "\n*open_basedir.*" - conf = re.sub(rep,"",conf) - if not conf: - os.remove(filename) - else: - public.writeFile(filename,conf) - public.ExecShell("chattr +i " + filename) - public.set_site_open_basedir_nginx(siteName) - return public.returnMsg(True, 'SITE_BASEDIR_CLOSE_SUCCESS') - - if conf and "session.save_path" in conf: - rep = "session.save_path\s*=\s*(.*)" - s_path = re.search(rep,conf).groups(1)[0] - public.writeFile(filename, conf + '\nopen_basedir={}/:/tmp/:{}'.format(path,s_path)) - else: - public.writeFile(filename,'open_basedir={}/:/tmp/'.format(path)) - public.ExecShell("chattr +i " + filename) - public.set_site_open_basedir_nginx(siteName) - public.serviceReload() - return public.returnMsg(True,'SITE_BASEDIR_OPEN_SUCCESS') - except Exception as e: - public.ExecShell("chattr +i " + filename) - return str(e) - - def _set_ols_open_basedir(self,get): - # 设置ols - try: - sitename = public.M('sites').where("id=?", (get.id,)).getField('name') - # 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) - s = 'on' - if not result: - s = 'off' - rep = '\n#php_admin_value\s*open_basedir.*' - result = re.search(rep, c) - result = result.group() - if s == 'on': - c = re.sub(rep, '\n#' + result[1:], c) - else: - result = result.replace('#', '') - c = re.sub(rep, result, c) - public.writeFile(f, c) - except: - pass - - # 读配置 - def __read_config(self, path): - if not os.path.exists(path): - public.writeFile(path, '[]') - upBody = public.readFile(path) - if not upBody: upBody = '[]' - return json.loads(upBody) - - # 写配置 - def __write_config(self, path, data): - return public.writeFile(path, json.dumps(data)) - - # 取某个站点某条反向代理详情 - def GetProxyDetals(self, get): - proxyUrl = self.__read_config(self.__proxyfile) - sitename = get.sitename - proxyname = get.proxyname - for i in proxyUrl: - if i["proxyname"] == proxyname and i["sitename"] == sitename: - return i - - # 取某个站点反向代理列表 - def GetProxyList(self, get): - n = 0 - for w in ["nginx", "apache"]: - conf_path = "%s/panel/vhost/%s/%s.conf" % (self.setupPath, w, get.sitename) - old_conf = "" - if os.path.exists(conf_path): - old_conf = public.readFile(conf_path) - rep = "(#PROXY-START(\n|.)+#PROXY-END)" - url_rep = "proxy_pass (.*);|ProxyPass\s/\s(.*)|Host\s(.*);" - host_rep = "Host\s(.*);" - if re.search(rep, old_conf): - # 构造代理配置 - if w == "nginx": - get.todomain = str(re.search(host_rep, old_conf).group(1)) - get.proxysite = str(re.search(url_rep, old_conf).group(1)) - else: - get.todomain = "" - get.proxysite = str(re.search(url_rep, old_conf).group(2)) - get.proxyname = "旧代理" - get.type = 1 - get.proxydir = "/" - get.advanced = 0 - get.cachetime = 1 - get.cache = 0 - get.subfilter = "[{\"sub1\":\"\",\"sub2\":\"\"},{\"sub1\":\"\",\"sub2\":\"\"},{\"sub1\":\"\",\"sub2\":\"\"}]" - - #proxyname_md5 = self.__calc_md5(get.proxyname) - # 备份并替换老虚拟主机配置文件 - public.ExecShell("cp %s %s_bak" % (conf_path, conf_path)) - conf = re.sub(rep, "", old_conf) - public.writeFile(conf_path, conf) - if n == 0: - self.CreateProxy(get) - n += 1 - # 写入代理配置 - #proxypath = "%s/panel/vhost/%s/proxy/%s/%s_%s.conf" % ( - #self.setupPath, w, get.sitename, proxyname_md5, get.sitename) - # proxycontent = str(re.search(rep, old_conf).group(1)) - # public.writeFile(proxypath, proxycontent) - if n == "1": - public.serviceReload() - proxyUrl = self.__read_config(self.__proxyfile) - sitename = get.sitename - proxylist = [] - for i in proxyUrl: - if i["sitename"] == sitename: - proxylist.append(i) - return proxylist - - def del_proxy_multiple(self,get): - ''' - @name 批量网站到期时间 - @author zhwen<2020-11-20> - @param site_id 1 - @param proxynames ces,aaa - ''' - proxynames = get.proxynames.split(',') - del_successfully = [] - del_failed = {} - get.sitename = public.M('sites').where("id=?", (get.site_id,)).getField('name') - for proxyname in proxynames: - if not proxyname: - continue - get.proxyname = proxyname - try: - resule = self.RemoveProxy(get,multiple=1) - if not resule['status']: - del_failed[proxyname] = resule['msg'] - del_successfully.append(proxyname) - except: - del_failed[proxyname] = '删除时错误,请再试一次' - pass - return {'status': True, 'msg': '删除反向代理 [ {} ] 成功'.format(','.join(del_failed)), 'error': del_failed, - 'success': del_successfully} - - # 删除反向代理 - def RemoveProxy(self, get, multiple=None): - conf = self.__read_config(self.__proxyfile) - sitename = get.sitename - proxyname = get.proxyname - for i in range(len(conf)): - c_sitename = conf[i]["sitename"] - c_proxyname = conf[i]["proxyname"] - if c_sitename == sitename and c_proxyname == proxyname: - proxyname_md5 = self.__calc_md5(c_proxyname) - for w in ["apache","nginx","openlitespeed"]: - p = "{sp}/panel/vhost/{w}/proxy/{s}/{m}_{s}.conf*".format(sp=self.setupPath,w=w,s=c_sitename,m=proxyname_md5) - - public.ExecShell('rm -f {}'.format(p)) - p = "{sp}/panel/vhost/openlitespeed/proxy/{s}/urlrewrite/{m}_{s}.conf*".format(sp=self.setupPath,m=proxyname_md5,s=get.sitename) - public.ExecShell('rm -f {}'.format(p)) - del conf[i] - self.__write_config(self.__proxyfile,conf) - self.SetNginx(get) - self.SetApache(get.sitename) - if not multiple: - public.serviceReload() - return public.returnMsg(True, '删除成功') - - - # 检查代理是否存在 - def __check_even(self,get,action=""): - conf_data = self.__read_config(self.__proxyfile) - for i in conf_data: - if i["sitename"] == get.sitename: - if action == "create": - if i["proxydir"] == get.proxydir or i["proxyname"] == get.proxyname: - return i - else: - if i["proxyname"] != get.proxyname and i["proxydir"] == get.proxydir: - return i - - # 检测全局代理和目录代理是否同时存在 - def __check_proxy_even(self,get,action=""): - conf_data = self.__read_config(self.__proxyfile) - n = 0 - if action == "": - for i in conf_data: - if i["sitename"] == get.sitename: - n += 1 - if n == 1: - return - for i in conf_data: - if i["sitename"] == get.sitename: - if i["advanced"] != int(get.advanced): - return i - # 计算proxyname md5 - def __calc_md5(self,proxyname): - md5 = hashlib.md5() - md5.update(proxyname.encode('utf-8')) - return md5.hexdigest() - - # 检测URL是否可以访问 - def __CheckUrl(self, get): - sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sk.settimeout(5) - rep = "(https?)://([\w\.\-]+):?([\d]+)?" - h = re.search(rep, get.proxysite).group(1) - d = re.search(rep, get.proxysite).group(2) - try: - p = re.search(rep, get.proxysite).group(3) - except: - p = "" - try: - if p: - sk.connect((d, int(p))) - else: - if h == "http": - sk.connect((d, 80)) - else: - sk.connect((d, 443)) - except: - return public.returnMsg(False, "目标URL无法访问") - - # 基本设置检查 - def __CheckStart(self,get,action=""): - isError = public.checkWebConfig() - if (isError != True): - return public.returnMsg(False, '配置文件出错请先排查配置') - if action == "create": - if sys.version_info.major < 3: - if len(get.proxyname) < 3 or len(get.proxyname) > 15: - return public.returnMsg(False, '名称必须大于3小于15个字符串') - else: - if len(get.proxyname.encode("utf-8")) < 3 or len(get.proxyname.encode("utf-8")) > 15: - return public.returnMsg(False, '名称必须大于3小于15个字符串') - if self.__check_even(get,action): - return public.returnMsg(False, '指定反向代理名称或代理文件夹已存在') - # 判断代理,只能有全局代理或目录代理 - if self.__check_proxy_even(get,action): - return public.returnMsg(False, '不能同时设置目录代理和全局代理') - #判断cachetime类型 - if get.cachetime: - try: - int(get.cachetime) - except: - return public.returnMsg(False, "请输入数字") - - rep = "http(s)?\:\/\/" - #repd = "http(s)?\:\/\/([a-zA-Z0-9][-a-zA-Z0-9]{0,62}\.)+([a-zA-Z0-9][a-zA-Z0-9]{0,62})+.?" - tod = "[a-zA-Z]+$" - repte = "[\?\=\[\]\)\(\*\&\^\%\$\#\@\!\~\`{\}\>\<\,\',\"]+" - # 检测代理目录格式 - if re.search(repte,get.proxydir): - return public.returnMsg(False, "代理目录不能有以下特殊符号 ?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\,',\"]") - # 检测发送域名格式 - if get.todomain: - if not re.search(tod,get.todomain): - return public.returnMsg(False, '发送域名格式错误 ' + get.todomain) - if public.get_webserver() != 'openlitespeed' and not get.todomain: - get.todomain = "$host" - - # 检测目标URL格式 - if not re.match(rep, get.proxysite): - return public.returnMsg(False, '域名格式错误 ' + get.proxysite) - if re.search(repte,get.proxysite): - return public.returnMsg(False, "目标URL不能有以下特殊符号 ?,=,[,],),(,*,&,^,%,$,#,@,!,~,`,{,},>,<,\,',\"]" ) - # 检测目标url是否可用 - # if re.match(repd, get.proxysite): - # if self.__CheckUrl(get): - # return public.returnMsg(False, "目标URL无法访问") - subfilter = json.loads(get.subfilter) - # 检测替换内容 - if subfilter: - for s in subfilter: - if not s["sub1"]: - if s["sub2"]: - return public.returnMsg(False, '请输入被替换的内容') - elif s["sub1"] == s["sub2"]: - return public.returnMsg(False, '替换内容与被替换内容不能一致') - # 设置Nginx配置 - def SetNginx(self,get): - ng_proxyfile = "%s/panel/vhost/nginx/proxy/%s/*.conf" % (self.setupPath,get.sitename) - ng_file = self.setupPath + "/panel/vhost/nginx/" + get.sitename + ".conf" - p_conf = self.__read_config(self.__proxyfile) - cureCache = '' - - if public.get_webserver() == 'nginx': - shutil.copyfile(ng_file, '/tmp/ng_file_bk.conf') - - #if os.path.exists('/www/server/nginx/src/ngx_cache_purge'): - cureCache += ''' - location ~ /purge(/.*) { - proxy_cache_purge cache_one $host$1$is_args$args; - #access_log /www/wwwlogs/%s_purge_cache.log; - }''' % (get.sitename) - if os.path.exists(ng_file): - self.CheckProxy(get) - ng_conf = public.readFile(ng_file) - if not p_conf: - rep = "#清理缓存规则[\w\s\~\/\(\)\.\*\{\}\;\$\n\#]+.{1,66}[\s\w\/\*\.\;]+include enable-php-" - ng_conf = re.sub(rep, 'include enable-php-', ng_conf) - oldconf = '''location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$ - { - expires 30d; - error_log /dev/null; - access_log /dev/null; - } - location ~ .*\\.(js|css)?$ - { - expires 12h; - error_log /dev/null; - access_log /dev/null; - }''' - if "(gif|jpg|jpeg|png|bmp|swf)$" not in ng_conf: - ng_conf = ng_conf.replace('access_log', oldconf + "\n\taccess_log") - public.writeFile(ng_file, ng_conf) - return - sitenamelist = [] - for i in p_conf: - sitenamelist.append(i["sitename"]) - - if get.sitename in sitenamelist: - rep = "include.*\/proxy\/.*\*.conf;" - if not re.search(rep,ng_conf): - rep = "location.+\(gif[\w\|\$\(\)\n\{\}\s\;\/\~\.\*\\\\\?]+access_log\s+/" - ng_conf = re.sub(rep, 'access_log /', ng_conf) - ng_conf = ng_conf.replace("include enable-php-","#清理缓存规则\n" +cureCache +"\n\t#引用反向代理规则,注释后配置的反向代理将无效\n\t" + "include " + ng_proxyfile + ";\n\n\tinclude enable-php-") - public.writeFile(ng_file,ng_conf) - - else: - rep = "#清理缓存规则[\w\s\~\/\(\)\.\*\{\}\;\$\n\#]+.{1,66}[\s\w\/\*\.\;]+include enable-php-" - ng_conf = re.sub(rep,'include enable-php-',ng_conf) - oldconf = '''location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$ - { - expires 30d; - error_log /dev/null; - access_log /dev/null; - } - location ~ .*\\.(js|css)?$ - { - expires 12h; - error_log /dev/null; - access_log /dev/null; - }''' - if "(gif|jpg|jpeg|png|bmp|swf)$" not in ng_conf: - ng_conf = ng_conf.replace('access_log', oldconf + "\n\taccess_log") - public.writeFile(ng_file, ng_conf) - - # 设置apache配置 - def SetApache(self,sitename): - ap_proxyfile = "%s/panel/vhost/apache/proxy/%s/*.conf" % (self.setupPath,sitename) - ap_file = self.setupPath + "/panel/vhost/apache/" + sitename + ".conf" - p_conf = public.readFile(self.__proxyfile) - - if public.get_webserver() == 'apache': - shutil.copyfile(ap_file, '/tmp/ap_file_bk.conf') - - if os.path.exists(ap_file): - ap_conf = public.readFile(ap_file) - if p_conf == "[]": - rep = "\n*#引用反向代理规则,注释后配置的反向代理将无效\n+\s+IncludeOptiona[\s\w\/\.\*]+" - ap_conf = re.sub(rep, '', ap_conf) - public.writeFile(ap_file, ap_conf) - return - if sitename in p_conf: - rep = "combined(\n|.)+IncludeOptional.*\/proxy\/.*conf" - rep1 = "combined" - if not re.search(rep,ap_conf): - ap_conf = ap_conf.replace(rep1, rep1 + "\n\t#引用反向代理规则,注释后配置的反向代理将无效\n\t" + "\n\tIncludeOptional " + ap_proxyfile) - public.writeFile(ap_file,ap_conf) - else: - # rep = "\n*#引用反向代理(\n|.)+IncludeOptional.*\/proxy\/.*conf" - rep = "\n*#引用反向代理规则,注释后配置的反向代理将无效\n+\s+IncludeOptiona[\s\w\/\.\*]+" - ap_conf = re.sub(rep,'', ap_conf) - public.writeFile(ap_file, ap_conf) - - # 设置OLS - def _set_ols_proxy(self,get): - # 添加反代配置 - proxyname_md5 = self.__calc_md5(get.proxyname) - dir_path = "%s/panel/vhost/openlitespeed/proxy/%s/" % (self.setupPath,get.sitename) - if not os.path.exists(dir_path): - os.makedirs(dir_path) - file_path = "{}{}_{}.conf".format(dir_path,proxyname_md5,get.sitename) - reverse_proxy_conf = """ -extprocessor %s { - type proxy - address %s - maxConns 1000 - pcKeepAliveTimeout 600 - initTimeout 600 - retryTimeout 0 - respBuffer 0 -} -""" % (get.proxyname,get.proxysite) - public.writeFile(file_path,reverse_proxy_conf) - # 添加urlrewrite - dir_path = "%s/panel/vhost/openlitespeed/proxy/%s/urlrewrite/" % (self.setupPath, get.sitename) - if not os.path.exists(dir_path): - os.makedirs(dir_path) - file_path = "{}{}_{}.conf".format(dir_path,proxyname_md5,get.sitename) - reverse_urlrewrite_conf = """ -RewriteRule ^%s(.*)$ http://%s/$1 [P,E=Proxy-Host:%s] -""" % (get.proxydir,get.proxyname,get.todomain) - public.writeFile(file_path,reverse_urlrewrite_conf) - - # 检查伪静态、主配置文件是否有location冲突 - def CheckLocation(self,get): - #伪静态文件路径 - rewriteconfpath = "%s/panel/vhost/rewrite/%s.conf" % (self.setupPath,get.sitename) - # 主配置文件路径 - nginxconfpath = "%s/nginx/conf/nginx.conf" % (self.setupPath) - # vhost文件 - vhostpath = "%s/panel/vhost/nginx/%s.conf" % (self.setupPath,get.sitename) - - rep = "location\s+/[\n\s]+{" - - for i in [rewriteconfpath,nginxconfpath,vhostpath]: - conf = public.readFile(i) - if re.findall(rep,conf): - return public.returnMsg(False, '伪静态/nginx主配置/vhost/文件已经存在全局反向代理') - - # 创建反向代理 - def CreateProxy(self, get): - try: - nocheck = get.nocheck - except: - nocheck = "" - if not nocheck: - if self.__CheckStart(get,"create"): - return self.__CheckStart(get,"create") - if public.get_webserver() == 'nginx': - if self.CheckLocation(get): - return self.CheckLocation(get) - - proxyUrl = self.__read_config(self.__proxyfile) - proxyUrl.append({ - "proxyname": get.proxyname, - "sitename": get.sitename, - "proxydir": get.proxydir, - "proxysite": get.proxysite, - "todomain": get.todomain, - "type": int(get.type), - "cache": int(get.cache), - "subfilter": json.loads(get.subfilter), - "advanced": int(get.advanced), - "cachetime": int(get.cachetime) - }) - self.__write_config(self.__proxyfile, proxyUrl) - self.SetNginx(get) - self.SetApache(get.sitename) - self._set_ols_proxy(get) - status = self.SetProxy(get) - if not status["status"]: - return status - if get.proxydir == '/': - get.version = '00' - get.siteName = get.sitename - self.SetPHPVersion(get) - public.serviceReload() - return public.returnMsg(True, '添加成功') - - # 取代理配置文件 - def GetProxyFile(self,get): - import files - conf = self.__read_config(self.__proxyfile) - sitename = get.sitename - proxyname = get.proxyname - proxyname_md5 = self.__calc_md5(proxyname) - get.path = "%s/panel/vhost/%s/proxy/%s/%s_%s.conf" % (self.setupPath, get.webserver, sitename,proxyname_md5,sitename) - for i in conf: - if proxyname == i["proxyname"] and sitename == i["sitename"] and i["type"] != 1: - return public.returnMsg(False, '代理已暂停') - f = files.files() - return f.GetFileBody(get),get.path - - # 保存代理配置文件 - def SaveProxyFile(self,get): - import files - f = files.files() - return f.SaveFileBody(get) - # return public.returnMsg(True, '保存成功') - - # 检查是否存在#Set Nginx Cache - def check_annotate(self,data): - rep = "\n\s*#Set\s*Nginx\s*Cache" - if re.search(rep,data): - return True - - # 修改反向代理 - def ModifyProxy(self, get): - proxyname_md5 = self.__calc_md5(get.proxyname) - ap_conf_file = "{p}/panel/vhost/apache/proxy/{s}/{n}_{s}.conf".format( - p=self.setupPath, s=get.sitename, n=proxyname_md5) - ng_conf_file = "{p}/panel/vhost/nginx/proxy/{s}/{n}_{s}.conf".format( - p=self.setupPath, s=get.sitename, n=proxyname_md5) - ols_conf_file = "{p}/panel/vhost/openlitespeed/proxy/{s}/urlrewrite/{n}_{s}.conf".format( - p=self.setupPath, s=get.sitename, n=proxyname_md5) - if self.__CheckStart(get): - return self.__CheckStart(get) - conf = self.__read_config(self.__proxyfile) - for i in range(len(conf)): - if conf[i]["proxyname"] == get.proxyname and conf[i]["sitename"] == get.sitename: - if int(get.type) != 1: - public.ExecShell("mv {f} {f}_bak".format(f=ap_conf_file)) - public.ExecShell("mv {f} {f}_bak".format(f=ng_conf_file)) - public.ExecShell("mv {f} {f}_bak".format(f=ols_conf_file)) - conf[i]["type"] = int(get.type) - self.__write_config(self.__proxyfile, conf) - public.serviceReload() - return public.returnMsg(True, '修改成功') - else: - if os.path.exists(ap_conf_file+"_bak"): - public.ExecShell("mv {f}_bak {f}".format(f=ap_conf_file)) - public.ExecShell("mv {f}_bak {f}".format(f=ng_conf_file)) - public.ExecShell("mv {f}_bak {f}".format(f=ols_conf_file)) - ng_conf = public.readFile(ng_conf_file) - # 修改nginx配置 - # 如果代理URL后缀带有URI则删除URI,正则匹配不支持proxypass处带有uri - php_pass_proxy = get.proxysite - if get.proxysite[-1] == '/' or get.proxysite.count('/') > 2 or '?' in get.proxysite: - php_pass_proxy = re.search('(https?\:\/\/[\w\.]+)', get.proxysite).group(0) - ng_conf = re.sub("location\s+%s" % conf[i]["proxydir"],"location "+get.proxydir,ng_conf) - ng_conf = re.sub("proxy_pass\s+%s" % conf[i]["proxysite"],"proxy_pass "+get.proxysite,ng_conf) - ng_conf = re.sub("location\s+\~\*\s+\\\.\(php.*\n\{\s*proxy_pass\s+%s.*" % (php_pass_proxy), - "location ~* \.(php|jsp|cgi|asp|aspx)$\n{\n\tproxy_pass %s;" % php_pass_proxy,ng_conf) - ng_conf = re.sub("location\s+\~\*\s+\\\.\(gif.*\n\{\s*proxy_pass\s+%s.*" % (php_pass_proxy), - "location ~* \.(gif|png|jpg|css|js|woff|woff2)$\n{\n\tproxy_pass %s;" % php_pass_proxy,ng_conf) - - backslash = "" - if "Host $host" in ng_conf: - backslash = "\\" - - ng_conf = re.sub("\sHost\s+%s" % backslash + conf[i]["todomain"], " Host " + get.todomain, ng_conf) - cache_rep = r"proxy_cache_valid\s+200\s+304\s+301\s+302\s+\d+m;((\n|.)+expires\s+\d+m;)*" - if int(get.cache) == 1: - if re.search(cache_rep,ng_conf): - expires_rep = "\{\n\s+expires\s+12h;" - ng_conf = re.sub(expires_rep, "{",ng_conf) - ng_conf = re.sub(cache_rep, "proxy_cache_valid 200 304 301 302 {0}m;".format(get.cachetime), ng_conf) - else: - ng_cache = """ - proxy_ignore_headers Set-Cookie Cache-Control expires; - proxy_cache cache_one; - proxy_cache_key $host$uri$is_args$args; - proxy_cache_valid 200 304 301 302 %sm;""" % (get.cachetime) - if self.check_annotate(ng_conf): - cache_rep = '\n\s*#Set\s*Nginx\s*Cache(.|\n)*no-cache;' - ng_conf = re.sub(cache_rep,'\n\t#Set Nginx Cache\n'+ng_cache,ng_conf) - else: - # cache_rep = '#proxy_set_header\s+Connection\s+"upgrade";' - cache_rep = r"proxy_set_header\s+REMOTE-HOST\s+\$remote_addr;" - ng_conf = re.sub(cache_rep, r"\n\tproxy_set_header\s+REMOTE-HOST\s+\$remote_addr;\n\t#Set Nginx Cache" + ng_cache, - ng_conf) - else: - if self.check_annotate(ng_conf): - rep = r'\n\s*#Set\s*Nginx\s*Cache(.|\n)*\d+m;' - ng_conf = re.sub(rep, "\n\t#Set Nginx Cache\n\tproxy_ignore_headers Set-Cookie Cache-Control expires;\n\tadd_header Cache-Control no-cache;", ng_conf) - else: - rep = r"\s+proxy_cache\s+cache_one.*[\n\s\w\_\";\$]+m;" - ng_conf = re.sub(rep, r"\n\t#Set Nginx Cache\n\tproxy_ignore_headers Set-Cookie Cache-Control expires;\n\tadd_header Cache-Control no-cache;", ng_conf) - - sub_rep = "sub_filter" - subfilter = json.loads(get.subfilter) - if str(conf[i]["subfilter"]) != str(subfilter): - if re.search(sub_rep, ng_conf): - sub_rep = "\s+proxy_set_header\s+Accept-Encoding(.|\n)+off;" - ng_conf = re.sub(sub_rep,"",ng_conf) - - # 构造替换字符串 - ng_subdata = '' - ng_sub_filter = ''' - proxy_set_header Accept-Encoding "";%s - sub_filter_once off;''' - if subfilter: - for s in subfilter: - if not s["sub1"]: - continue - if '"' in s["sub1"]: - s["sub1"] = s["sub1"].replace('"', '\\"') - if '"' in s["sub2"]: - s["sub2"] = s["sub2"].replace('"', '\\"') - ng_subdata += '\n\tsub_filter "%s" "%s";' % (s["sub1"], s["sub2"]) - if ng_subdata: - ng_sub_filter = ng_sub_filter % (ng_subdata) - else: - ng_sub_filter = '' - sub_rep = '#Set\s+Nginx\s+Cache' - ng_conf = re.sub(sub_rep,'#Set Nginx Cache\n'+ng_sub_filter,ng_conf) - - # 修改apache配置 - ap_conf = public.readFile(ap_conf_file) - ap_conf = re.sub("ProxyPass\s+%s\s+%s" % (conf[i]["proxydir"], conf[i]["proxysite"]),"ProxyPass %s %s" % (get.proxydir,get.proxysite), ap_conf) - ap_conf = re.sub("ProxyPassReverse\s+%s\s+%s" % (conf[i]["proxydir"], conf[i]["proxysite"]), - "ProxyPassReverse %s %s" % (get.proxydir, get.proxysite), ap_conf) - # 修改OLS配置 - p = "{p}/panel/vhost/openlitespeed/proxy/{s}/{n}_{s}.conf".format(p=self.setupPath,n=proxyname_md5,s=get.sitename) - c = public.readFile(p) - if c: - rep = 'address\s+(.*)' - new_proxysite = 'address\t{}'.format(get.proxysite) - c = re.sub(rep,new_proxysite,c) - public.writeFile(p,c) - - # p = "{p}/panel/vhost/openlitespeed/proxy/{s}/urlrewrite/{n}_{s}.conf".format(p=self.setupPath,n=proxyname_md5,s=get.sitename) - c = public.readFile(ols_conf_file) - if c: - rep = 'RewriteRule\s*\^{}\(\.\*\)\$\s+http://{}/\$1\s*\[P,E=Proxy-Host:{}\]'.format(conf[i]["proxydir"],get.proxyname,conf[i]["todomain"]) - new_content = 'RewriteRule ^{}(.*)$ http://{}/$1 [P,E=Proxy-Host:{}]'.format(get.proxydir,get.proxyname,get.todomain) - c = re.sub(rep,new_content,c) - public.writeFile(ols_conf_file,c) - - conf[i]["proxydir"] = get.proxydir - conf[i]["proxysite"] = get.proxysite - conf[i]["todomain"] = get.todomain - conf[i]["type"] = int(get.type) - conf[i]["cache"] = int(get.cache) - conf[i]["subfilter"] = json.loads(get.subfilter) - conf[i]["advanced"] = int(get.advanced) - conf[i]["cachetime"] = int(get.cachetime) - - public.writeFile(ng_conf_file,ng_conf) - public.writeFile(ap_conf_file,ap_conf) - self.__write_config(self.__proxyfile, conf) - self.SetNginx(get) - self.SetApache(get.sitename) - # self.SetProxy(get) - - - # if int(get.type) != 1: - # public.ExecShell("mv %s %s_bak" % (ap_conf_file, ap_conf_file)) - # public.ExecShell("mv %s %s_bak" % (ng_conf_file, ng_conf_file)) - public.serviceReload() - return public.returnMsg(True, '修改成功') - - # 设置反向代理 - def SetProxy(self,get): - sitename = get.sitename # 站点名称 - advanced = int(get.advanced) - type = int(get.type) - cache = int(get.cache) - cachetime = int(get.cachetime) - ng_file = self.setupPath + "/panel/vhost/nginx/" + sitename + ".conf" - ap_file = self.setupPath + "/panel/vhost/apache/" + sitename + ".conf" - p_conf = self.__read_config(self.__proxyfile) - # 配置Nginx - # 构造清理缓存连接 - - - # 构造缓存配置 - ng_cache = """ - proxy_ignore_headers Set-Cookie Cache-Control expires; - proxy_cache cache_one; - proxy_cache_key $host$uri$is_args$args; - proxy_cache_valid 200 304 301 302 %sm;""" % (cachetime) - # rep = "(https?://[\w\.]+)" - # proxysite1 = re.search(rep,get.proxysite).group(1) - ng_proxy = ''' -#PROXY-START%s -location ~* \.(gif|png|jpg|css|js|woff|woff2)$ -{ - proxy_pass %s; - proxy_set_header Host %s; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header REMOTE-HOST $remote_addr; - expires 12h; -} -location %s -{ - proxy_pass %s; - proxy_set_header Host %s; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header REMOTE-HOST $remote_addr; - - add_header X-Cache $upstream_cache_status; - - #Set Nginx Cache - %s - %s -} - -#PROXY-END%s''' - ng_proxy_cache = '' - proxyname_md5 = self.__calc_md5(get.proxyname) - ng_proxyfile = "%s/panel/vhost/nginx/proxy/%s/%s_%s.conf" % (self.setupPath,sitename,proxyname_md5, sitename) - ng_proxydir = "%s/panel/vhost/nginx/proxy/%s" % (self.setupPath, sitename) - if not os.path.exists(ng_proxydir): - public.ExecShell("mkdir -p %s" % ng_proxydir) - - - # 构造替换字符串 - ng_subdata = '' - ng_sub_filter = ''' - proxy_set_header Accept-Encoding "";%s - sub_filter_once off;''' - if get.subfilter: - for s in json.loads(get.subfilter): - if not s["sub1"]: - continue - if '"' in s["sub1"]: - s["sub1"] = s["sub1"].replace('"','\\"') - if '"' in s["sub2"]: - s["sub2"] = s["sub2"].replace('"', '\\"') - ng_subdata += '\n\tsub_filter "%s" "%s";' % (s["sub1"], s["sub2"]) - if ng_subdata: - ng_sub_filter = ng_sub_filter % (ng_subdata) - else: - ng_sub_filter = '' - # 构造反向代理 - # 如果代理URL后缀带有URI则删除URI,正则匹配不支持proxypass处带有uri - php_pass_proxy = get.proxysite - if get.proxysite[-1] == '/' or get.proxysite.count('/') > 2 or '?' in get.proxysite: - php_pass_proxy = re.search('(https?\:\/\/[\w\.]+)',get.proxysite).group(0) - if advanced == 1: - if type == 1 and cache == 1: - ng_proxy_cache += ng_proxy % ( - get.proxydir, php_pass_proxy ,get.todomain,get.proxydir,get.proxysite,get.todomain ,ng_sub_filter, ng_cache ,get.proxydir) - if type == 1 and cache == 0: - ng_proxy_cache += ng_proxy % ( - get.proxydir,php_pass_proxy ,get.todomain, get.proxydir,get.proxysite,get.todomain ,ng_sub_filter,'\tadd_header Cache-Control no-cache;' ,get.proxydir) - else: - if type == 1 and cache == 1: - ng_proxy_cache += ng_proxy % ( - get.proxydir, php_pass_proxy ,get.todomain, get.proxydir,get.proxysite,get.todomain ,ng_sub_filter, ng_cache, get.proxydir) - if type == 1 and cache == 0: - ng_proxy_cache += ng_proxy % ( - get.proxydir, php_pass_proxy ,get.todomain,get.proxydir,get.proxysite,get.todomain ,ng_sub_filter, '\tadd_header Cache-Control no-cache;', get.proxydir) - public.writeFile(ng_proxyfile, ng_proxy_cache) - - - # APACHE - # 反向代理文件 - ap_proxyfile = "%s/panel/vhost/apache/proxy/%s/%s_%s.conf" % (self.setupPath,get.sitename,proxyname_md5,get.sitename) - ap_proxydir = "%s/panel/vhost/apache/proxy/%s" % (self.setupPath,get.sitename) - if not os.path.exists(ap_proxydir): - public.ExecShell("mkdir -p %s" % ap_proxydir) - ap_proxy = '' - if type == 1: - ap_proxy += '''#PROXY-START%s - - ProxyRequests Off - SSLProxyEngine on - ProxyPass %s %s/ - ProxyPassReverse %s %s/ - -#PROXY-END%s''' % (get.proxydir, get.proxydir, get.proxysite, get.proxydir, - get.proxysite, get.proxydir) - public.writeFile(ap_proxyfile,ap_proxy) - isError = public.checkWebConfig() - if (isError != True): - if public.get_webserver() == "nginx": - shutil.copyfile('/tmp/ng_file_bk.conf', ng_file) - else: - shutil.copyfile('/tmp/ap_file_bk.conf', ap_file) - for i in range(len(p_conf)-1,-1,-1): - if get.sitename == p_conf[i]["sitename"] and p_conf[i]["proxyname"]: - del p_conf[i] - self.RemoveProxy(get) - return public.returnMsg(False, 'ERROR: %s
' % public.GetMsg("CONFIG_ERROR") + isError.replace("\n", - '
') + '
') - return public.returnMsg(True, 'SUCCESS') - - - #开启缓存 - def ProxyCache(self,get): - if public.get_webserver() != 'nginx': return public.returnMsg(False,'WAF_NOT_NGINX') - file = self.setupPath + "/panel/vhost/nginx/"+get.siteName+".conf" - conf = public.readFile(file) - if conf.find('proxy_pass') == -1: return public.returnMsg(False,'SET_ERROR') - if conf.find('#proxy_cache') != -1: - conf = conf.replace('#proxy_cache','proxy_cache') - conf = conf.replace('#expires 12h','expires 12h') - else: - conf = conf.replace('proxy_cache','#proxy_cache') - conf = conf.replace('expires 12h','#expires 12h') - - public.writeFile(file,conf) - public.serviceReload() - return public.returnMsg(True,'SET_SUCCESS') - - - #检查反向代理配置 - def CheckProxy(self,get): - if public.get_webserver() != 'nginx': return True - file = self.setupPath + "/nginx/conf/proxy.conf" - if not os.path.exists(file): - conf='''proxy_temp_path %s/nginx/proxy_temp_dir; - proxy_cache_path %s/nginx/proxy_cache_dir levels=1:2 keys_zone=cache_one:10m inactive=1d max_size=5g; - client_body_buffer_size 512k; - proxy_connect_timeout 60; - proxy_read_timeout 60; - proxy_send_timeout 60; - proxy_buffer_size 32k; - proxy_buffers 4 64k; - proxy_busy_buffers_size 128k; - proxy_temp_file_write_size 128k; - proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; - proxy_cache cache_one;''' % (self.setupPath,self.setupPath) - public.writeFile(file,conf) - - - file = self.setupPath + "/nginx/conf/nginx.conf" - conf = public.readFile(file) - if(conf.find('include proxy.conf;') == -1): - rep = "include\s+mime.types;" - conf = re.sub(rep, "include mime.types;\n\tinclude proxy.conf;", conf) - public.writeFile(file,conf) - - - #取伪静态规则应用列表 - def GetRewriteList(self,get): - rewriteList = {} - ws = public.get_webserver() - if ws == "openlitespeed": - ws = "apache" - if ws == 'apache': - get.id = public.M('sites').where("name=?",(get.siteName,)).getField('id') - runPath = self.GetSiteRunPath(get) - if runPath['runPath'].find('/www/server/stop') != -1: - runPath['runPath'] = runPath['runPath'].replace('/www/server/stop','') - rewriteList['sitePath'] = public.M('sites').where("name=?",(get.siteName,)).getField('path') + runPath['runPath'] - - rewriteList['rewrite'] = [] - rewriteList['rewrite'].append('0.'+public.getMsg('SITE_REWRITE_NOW')) - for ds in os.listdir('rewrite/' + ws): - if ds == 'list.txt': continue - rewriteList['rewrite'].append(ds[0:len(ds)-5]) - rewriteList['rewrite'] = sorted(rewriteList['rewrite']) - return rewriteList - - #保存伪静态模板 - def SetRewriteTel(self,get): - ws = public.get_webserver() - if ws == "openlitespeed": - ws = "apache" - if sys.version_info[0] == 2: get.name = get.name.encode('utf-8') - filename = 'rewrite/' + ws + '/' + get.name + '.conf' - public.writeFile(filename,get.data) - return public.returnMsg(True, 'SITE_REWRITE_SAVE') - - #打包 - def ToBackup(self,get): - id = get.id - find = public.M('sites').where("id=?",(id,)).field('name,path,id').find() - import time - fileName = find['name']+'_'+time.strftime('%Y%m%d_%H%M%S',time.localtime())+'.zip' - backupPath = session['config']['backup_path'] + '/site' - zipName = backupPath + '/'+fileName - if not (os.path.exists(backupPath)): os.makedirs(backupPath) - tmps = '/tmp/panelExec.log' - execStr = "cd '" + find['path'] + "' && zip '" + zipName + "' -x .user.ini -r ./ > " + tmps + " 2>&1" - public.ExecShell(execStr) - sql = public.M('backup').add('type,name,pid,filename,size,addtime',(0,fileName,find['id'],zipName,0,public.getDate())) - public.WriteLog('TYPE_SITE', 'SITE_BACKUP_SUCCESS',(find['name'],)) - return public.returnMsg(True, 'BACKUP_SUCCESS') - - - #删除备份文件 - def DelBackup(self,get): - id = get.id - where = "id=?" - filename = public.M('backup').where(where,(id,)).getField('filename') - if os.path.exists(filename): os.remove(filename) - name = '' - if filename == 'qiniu': - name = public.M('backup').where(where,(id,)).getField('name') - public.ExecShell(public.get_python_bin() + " "+self.setupPath + '/panel/script/backup_qiniu.py delete_file ' + name) - - public.WriteLog('TYPE_SITE', 'SITE_BACKUP_DEL_SUCCESS',(name,filename)) - public.M('backup').where(where,(id,)).delete() - return public.returnMsg(True, 'DEL_SUCCESS') - - #旧版本配置文件处理 - def OldConfigFile(self): - #检查是否需要处理 - moveTo = 'data/moveTo.pl' - if os.path.exists(moveTo): return - - #处理Nginx配置文件 - filename = self.setupPath + "/nginx/conf/nginx.conf" - if os.path.exists(filename): - conf = public.readFile(filename) - if conf.find('include vhost/*.conf;') != -1: - conf = conf.replace('include vhost/*.conf;','include ' + self.setupPath + '/panel/vhost/nginx/*.conf;') - public.writeFile(filename,conf) - - self.moveConf(self.setupPath + "/nginx/conf/vhost", self.setupPath + '/panel/vhost/nginx','rewrite',self.setupPath+'/panel/vhost/rewrite') - self.moveConf(self.setupPath + "/nginx/conf/rewrite", self.setupPath + '/panel/vhost/rewrite') - - - - #处理Apache配置文件 - filename = self.setupPath + "/apache/conf/httpd.conf" - if os.path.exists(filename): - conf = public.readFile(filename) - if conf.find('IncludeOptional conf/vhost/*.conf') != -1: - conf = conf.replace('IncludeOptional conf/vhost/*.conf','IncludeOptional ' + self.setupPath + '/panel/vhost/apache/*.conf') - public.writeFile(filename,conf) - - self.moveConf(self.setupPath + "/apache/conf/vhost", self.setupPath + '/panel/vhost/apache') - - #标记处理记录 - public.writeFile(moveTo,'True') - public.serviceReload() - - #移动旧版本配置文件 - def moveConf(self,Path,toPath,Replace=None,ReplaceTo=None): - if not os.path.exists(Path): return - import shutil - - letPath = '/etc/letsencrypt/live' - nginxPath = self.setupPath + '/nginx/conf/key' - apachePath = self.setupPath + '/apache/conf/key' - for filename in os.listdir(Path): - #准备配置文件 - name = filename[0:len(filename) - 5] - filename = Path + '/' + filename - conf = public.readFile(filename) - - #替换关键词 - if Replace: conf = conf.replace(Replace,ReplaceTo) - ReplaceTo = letPath + name - Replace = 'conf/key/' + name - if conf.find(Replace) != -1: conf = conf.replace(Replace,ReplaceTo) - Replace = 'key/' + name - if conf.find(Replace) != -1: conf = conf.replace(Replace,ReplaceTo) - public.writeFile(filename,conf) - - #提取配置信息 - if conf.find('server_name') != -1: - self.formatNginxConf(filename) - elif conf.find(' 0: return - public.M('sites').add('name,path,status,ps,addtime',(name,path,'1','请输入备注',public.getDate())) - pid = public.M('sites').where("name=?",(name,)).getField('id') - for domain in domains: - public.M('domain').add('pid,name,port,addtime',(pid,domain,'80',public.getDate())) - - #移动旧版本证书 - def moveKey(self,srcPath,dstPath): - if not os.path.exists(srcPath): return - import shutil - os.makedirs(dstPath) - srcKey = srcPath + '/key.key' - srcCsr = srcPath + '/csr.key' - if os.path.exists(srcKey): shutil.move(srcKey,dstPath + '/privkey.pem') - if os.path.exists(srcCsr): shutil.move(srcCsr,dstPath + '/fullchain.pem') - - #路径处理 - def GetPath(self,path): - if path[-1] == '/': - return path[0:-1] - return path - - #日志开关 - def logsOpen(self,get): - get.name = public.M('sites').where("id=?",(get.id,)).getField('name') - # APACHE - filename = public.GetConfigValue('setup_path') + '/panel/vhost/apache/' + get.name + '.conf' - if os.path.exists(filename): - conf = public.readFile(filename) - if conf.find('#ErrorLog') != -1: - conf = conf.replace("#ErrorLog","ErrorLog").replace('#CustomLog','CustomLog') - else: - conf = conf.replace("ErrorLog","#ErrorLog").replace('CustomLog','#CustomLog') - public.writeFile(filename,conf) - - #NGINX - filename = public.GetConfigValue('setup_path') + '/panel/vhost/nginx/' + get.name + '.conf' - if os.path.exists(filename): - conf = public.readFile(filename) - rep = public.GetConfigValue('logs_path') + "/"+get.name+".log" - if conf.find(rep) != -1: - conf = conf.replace(rep,"/dev/null") - else: - conf = conf.replace('access_log /dev/null','access_log ' + rep) - public.writeFile(filename,conf) - - # OLS - filename = public.GetConfigValue('setup_path') + '/panel/vhost/openlitespeed/detail/' + get.name + '.conf' - conf = public.readFile(filename) - if conf: - rep = "\nerrorlog(.|\n)*compressArchive\s*1\s*\n}" - tmp = re.search(rep,conf) - s = 'on' - if not tmp: - s = 'off' - rep = "\n#errorlog(.|\n)*compressArchive\s*1\s*\n#}" - tmp = re.search(rep,conf) - tmp = tmp.group() - result = '' - if s == 'on': - for l in tmp.strip().splitlines(): - result += "\n#"+l - else: - for l in tmp.splitlines(): - result += "\n"+l[1:] - conf = re.sub(rep,"\n"+result.strip(),conf) - public.writeFile(filename,conf) - - - - public.serviceReload() - return public.returnMsg(True, 'SUCCESS') - - #取日志状态 - def GetLogsStatus(self,get): - filename = public.GetConfigValue( - 'setup_path') + '/panel/vhost/' + public.get_webserver() + '/' + get.name + '.conf' - if public.get_webserver() == 'openlitespeed': - filename = public.GetConfigValue( - 'setup_path') + '/panel/vhost/' + public.get_webserver() + '/detail/' + get.name + '.conf' - conf = public.readFile(filename) - if not conf: return True - if conf.find('#ErrorLog') != -1: return False - if conf.find("access_log /dev/null") != -1: return False - if re.search('\n#accesslog',conf): - return False - return True - - #取目录加密状态 - def GetHasPwd(self,get): - if not hasattr(get,'siteName'): - get.siteName = public.M('sites').where('id=?',(get.id,)).getField('name') - get.configFile = self.setupPath + '/panel/vhost/nginx/' + get.siteName + '.conf' - conf = public.readFile(get.configFile) - if type(conf)==bool:return False - if conf.find('#AUTH_START') != -1: return True - return False - - #设置目录加密 - def SetHasPwd(self,get): - if public.get_webserver() == 'openlitespeed': - return public.returnMsg(False,'该功能暂时还不支持OpenLiteSpeed') - if len(get.username.strip()) == 0 or len(get.password.strip()) == 0: return public.returnMsg(False,'LOGIN_USER_EMPTY') - - if not hasattr(get,'siteName'): - get.siteName = public.M('sites').where('id=?',(get.id,)).getField('name') - - self.CloseHasPwd(get) - filename = public.GetConfigValue('setup_path') + '/pass/' + get.siteName + '.pass' - passconf = get.username + ':' + public.hasPwd(get.password) - - if get.siteName == 'phpmyadmin': - get.configFile = self.setupPath + '/nginx/conf/nginx.conf' - if os.path.exists(self.setupPath + '/panel/vhost/nginx/phpmyadmin.conf'): - get.configFile = self.setupPath + '/panel/vhost/nginx/phpmyadmin.conf' - else: - get.configFile = self.setupPath + '/panel/vhost/nginx/' + get.siteName + '.conf' - - #处理Nginx配置 - conf = public.readFile(get.configFile) - if conf: - rep = '#error_page 404 /404.html;' - if conf.find(rep) == -1: rep = '#error_page 404/404.html;' - data = ''' - #AUTH_START - auth_basic "Authorization"; - auth_basic_user_file %s; - #AUTH_END''' % (filename,) - conf = conf.replace(rep,rep + data) - public.writeFile(get.configFile,conf) - - - if get.siteName == 'phpmyadmin': - get.configFile = self.setupPath + '/apache/conf/extra/httpd-vhosts.conf' - if os.path.exists(self.setupPath + '/panel/vhost/apache/phpmyadmin.conf'): - get.configFile = self.setupPath + '/panel/vhost/apache/phpmyadmin.conf' - else: - get.configFile = self.setupPath + '/panel/vhost/apache/' + get.siteName + '.conf' - - conf = public.readFile(get.configFile) - if conf: - #处理Apache配置 - rep = 'SetOutputFilter' - if conf.find(rep) != -1: - data = '''#AUTH_START - AuthType basic - AuthName "Authorization " - AuthUserFile %s - Require user %s - #AUTH_END - ''' % (filename,get.username) - conf = conf.replace(rep,data + rep) - conf = conf.replace(' Require all granted'," #Require all granted") - public.writeFile(get.configFile,conf) - - #写密码配置 - passDir = public.GetConfigValue('setup_path') + '/pass' - if not os.path.exists(passDir): public.ExecShell('mkdir -p ' + passDir) - public.writeFile(filename,passconf) - public.serviceReload() - public.WriteLog("TYPE_SITE","SITE_AUTH_OPEN_SUCCESS",(get.siteName,)) - return public.returnMsg(True,'SET_SUCCESS') - - #取消目录加密 - def CloseHasPwd(self,get): - if not hasattr(get,'siteName'): - get.siteName = public.M('sites').where('id=?',(get.id,)).getField('name') - - if get.siteName == 'phpmyadmin': - get.configFile = self.setupPath + '/nginx/conf/nginx.conf' - else: - get.configFile = self.setupPath + '/panel/vhost/nginx/' + get.siteName + '.conf' - - if os.path.exists(get.configFile): - conf = public.readFile(get.configFile) - rep = "\n\s*#AUTH_START(.|\n){1,200}#AUTH_END" - conf = re.sub(rep,'',conf) - public.writeFile(get.configFile,conf) - - if get.siteName == 'phpmyadmin': - get.configFile = self.setupPath + '/apache/conf/extra/httpd-vhosts.conf' - else: - get.configFile = self.setupPath + '/panel/vhost/apache/' + get.siteName + '.conf' - - if os.path.exists(get.configFile): - conf = public.readFile(get.configFile) - rep = "\n\s*#AUTH_START(.|\n){1,200}#AUTH_END" - conf = re.sub(rep,'',conf) - conf = conf.replace(' #Require all granted'," Require all granted") - public.writeFile(get.configFile,conf) - public.serviceReload() - public.WriteLog("TYPE_SITE","SITE_AUTH_CLOSE_SUCCESS",(get.siteName,)) - return public.returnMsg(True,'SET_SUCCESS') - - #启用tomcat支持 - def SetTomcat(self,get): - siteName = get.siteName - name = siteName.replace('.','_') - - rep = "^(\d{1,3}\.){3,3}\d{1,3}$" - if re.match(rep,siteName): return public.returnMsg(False,'TOMCAT_IP') - - #nginx - filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' - if os.path.exists(filename): - conf = public.readFile(filename) - if conf.find('#TOMCAT-START') != -1: return self.CloseTomcat(get) - tomcatConf = '''#TOMCAT-START - location / - { - proxy_pass "http://%s:8080"; - proxy_set_header Host %s; - proxy_set_header X-Forwarded-For $remote_addr; - } - location ~ .*\.(gif|jpg|jpeg|bmp|png|ico|txt|js|css)$ - { - expires 12h; - } - - location ~ .*\.war$ - { - return 404; - } - #TOMCAT-END - ''' % (siteName,siteName) - rep = 'include enable-php' - conf = conf.replace(rep,tomcatConf + rep) - public.writeFile(filename,conf) - - #apache - filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' - if os.path.exists(filename): - conf = public.readFile(filename) - if conf.find('#TOMCAT-START') != -1: return self.CloseTomcat(get) - tomcatConf = '''#TOMCAT-START - - ProxyRequests Off - SSLProxyEngine on - ProxyPass / http://%s:8080/ - ProxyPassReverse / http://%s:8080/ - RequestHeader unset Accept-Encoding - ExtFilterDefine fixtext mode=output intype=text/html cmd="/bin/sed 's,:8080,,g'" - SetOutputFilter fixtext - - #TOMCAT-END - ''' % (siteName,siteName) - - rep = '#PATH' - conf = conf.replace(rep,tomcatConf + rep) - public.writeFile(filename,conf) - path = public.M('sites').where("name=?",(siteName,)).getField('path') - import tomcat - tomcat.tomcat().AddVhost(path,siteName) - public.serviceReload() - public.ExecShell('/etc/init.d/tomcat stop') - public.ExecShell('/etc/init.d/tomcat start') - public.ExecShell('echo "127.0.0.1 '+siteName + '" >> /etc/hosts') - public.WriteLog('TYPE_SITE','SITE_TOMCAT_OPEN',(siteName,)) - return public.returnMsg(True,'SITE_TOMCAT_OPEN') - - #关闭tomcat支持 - def CloseTomcat(self,get): - if not os.path.exists('/etc/init.d/tomcat'): return False - siteName = get.siteName - name = siteName.replace('.','_') - - #nginx - filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' - if os.path.exists(filename): - conf = public.readFile(filename) - rep = "\s*#TOMCAT-START(.|\n)+#TOMCAT-END" - conf = re.sub(rep,'',conf) - public.writeFile(filename,conf) - - #apache - filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' - if os.path.exists(filename): - conf = public.readFile(filename) - rep = "\s*#TOMCAT-START(.|\n)+#TOMCAT-END" - conf = re.sub(rep,'',conf) - public.writeFile(filename,conf) - public.ExecShell('rm -rf ' + self.setupPath + '/panel/vhost/tomcat/' + name) - try: - import tomcat - tomcat.tomcat().DelVhost(siteName) - except: - pass - public.serviceReload() - public.ExecShell('/etc/init.d/tomcat restart') - public.ExecShell("sed -i '/"+siteName+"/d' /etc/hosts") - public.WriteLog('TYPE_SITE','SITE_TOMCAT_CLOSE',(siteName,)) - return public.returnMsg(True,'SITE_TOMCAT_CLOSE') - - #取当站点前运行目录 - def GetSiteRunPath(self,get): - siteName = public.M('sites').where('id=?',(get.id,)).getField('name') - sitePath = public.M('sites').where('id=?',(get.id,)).getField('path') - path = sitePath - if public.get_webserver() == 'nginx': - filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' - if os.path.exists(filename): - conf = public.readFile(filename) - rep = '\s*root\s+(.+);' - tmp1 = re.search(rep,conf) - if tmp1: path = tmp1.groups()[0] - elif public.get_webserver() == 'apache': - filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' - if os.path.exists(filename): - conf = public.readFile(filename) - rep = '\s*DocumentRoot\s*"(.+)"\s*\n' - tmp1 = re.search(rep,conf) - if tmp1: path = tmp1.groups()[0] - else: - filename = self.setupPath + '/panel/vhost/openlitespeed/' + siteName + '.conf' - if os.path.exists(filename): - conf = public.readFile(filename) - rep = "vhRoot\s*(.*)" - path = re.search(rep,conf) - if not path: - return public.returnMsg(False, "Get Site run path false") - path = path.groups()[0] - data = {} - if sitePath == path: - data['runPath'] = '/' - else: - data['runPath'] = path.replace(sitePath,'') - - dirnames = [] - dirnames.append('/') - if not os.path.exists(sitePath): os.makedirs(sitePath) - for filename in os.listdir(sitePath): - try: - json.dumps(filename) - if sys.version_info[0] == 2: - filename = filename.encode('utf-8') - else: - filename.encode('utf-8') - filePath = sitePath + '/' + filename - if not os.path.exists(filePath): continue - if os.path.islink(filePath): continue - if os.path.isdir(filePath): - dirnames.append('/' + filename) - except: - pass - - data['dirs'] = dirnames - return data - - #设置当前站点运行目录 - def SetSiteRunPath(self,get): - siteName = public.M('sites').where('id=?',(get.id,)).getField('name') - sitePath = public.M('sites').where('id=?',(get.id,)).getField('path') - old_run_path = self.GetRunPath(get) - #处理Nginx - filename = self.setupPath + '/panel/vhost/nginx/' + siteName + '.conf' - if os.path.exists(filename): - conf = public.readFile(filename) - rep = '\s*root\s+(.+);' - path = re.search(rep,conf).groups()[0] - conf = conf.replace(path,sitePath + get.runPath) - public.writeFile(filename,conf) - - #处理Apache - filename = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf' - if os.path.exists(filename): - conf = public.readFile(filename) - rep = '\s*DocumentRoot\s*"(.+)"\s*\n' - path = re.search(rep,conf).groups()[0] - conf = conf.replace(path,sitePath + get.runPath) - public.writeFile(filename,conf) - # 处理OLS - self._set_ols_run_path(sitePath,get.runPath,siteName) - # self.DelUserInI(sitePath) - # get.path = sitePath; - # self.SetDirUserINI(get) - s_path = sitePath+old_run_path+"/.user.ini" - d_path = sitePath + get.runPath+"/.user.ini" - if s_path != d_path: - public.ExecShell("chattr -i {}".format(s_path)) - public.ExecShell("mv {} {}".format(s_path,d_path)) - public.ExecShell("chattr +i {}".format(d_path)) - - public.serviceReload() - return public.returnMsg(True,'SET_SUCCESS') - - def _set_ols_run_path(self,site_path,run_path,sitename): - ols_conf_file = "{}/panel/vhost/openlitespeed/{}.conf".format(self.setupPath,sitename) - ols_conf = public.readFile(ols_conf_file) - if not ols_conf: - return - reg = '#VHOST\s*{s}\s*START(.|\n)+#VHOST\s*{s}\s*END'.format(s=sitename) - tmp = re.search(reg,ols_conf) - if not tmp: - return - reg = "vhRoot\s*(.*)" - # tmp = re.search(reg,tmp.group()) - # if not tmp: - # return - tmp = "vhRoot "+ site_path + run_path - ols_conf = re.sub(reg,tmp,ols_conf) - public.writeFile(ols_conf_file,ols_conf) - - #设置默认站点 - def SetDefaultSite(self,get): - import time - default_site_save = 'data/defaultSite.pl' - #清理旧的 - defaultSite = public.readFile(default_site_save) - http2 = '' - versionStr = public.readFile('/www/server/nginx/version.pl') - if versionStr: - if versionStr.find('1.8.1') == -1: http2 = ' http2' - if defaultSite: - path = self.setupPath + '/panel/vhost/nginx/' + defaultSite + '.conf' - if os.path.exists(path): - conf = public.readFile(path) - rep = "listen\s+80.+;" - conf = re.sub(rep,'listen 80;',conf,1) - rep = "listen\s+\[::\]:80.+;" - conf = re.sub(rep,'listen [::]:80;',conf,1) - rep = "listen\s+443.+;" - conf = re.sub(rep,'listen 443 ssl'+http2+';',conf,1) - rep = "listen\s+\[::\]:443.+;" - conf = re.sub(rep,'listen [::]:443 ssl'+http2+';',conf,1) - public.writeFile(path,conf) - - path = self.setupPath + '/apache/htdocs/.htaccess' - if os.path.exists(path): os.remove(path) - - if get.name == '0': - if os.path.exists(default_site_save): os.remove(default_site_save) - public.serviceReload() - return public.returnMsg(True,'设置成功!') - - #处理新的 - path = self.setupPath + '/apache/htdocs' - if os.path.exists(path): - conf = ''' - RewriteEngine on - RewriteCond %{HTTP_HOST} !^127.0.0.1 [NC] - RewriteRule (.*) http://%s/$1 [L] -''' - conf = conf.replace("%s",get.name) - if get.name == 'off': conf = '' - public.writeFile(path + '/.htaccess',conf) - - - path = self.setupPath + '/panel/vhost/nginx/' + get.name + '.conf' - if os.path.exists(path): - conf = public.readFile(path) - rep = "listen\s+80\s*;" - conf = re.sub(rep,'listen 80 default_server;',conf,1) - rep = "listen\s+\[::\]:80\s*;" - conf = re.sub(rep,'listen [::]:80 default_server;',conf,1) - rep = "listen\s+443\s*ssl\s*\w*\s*;" - conf = re.sub(rep,'listen 443 ssl'+http2+' default_server;',conf,1) - rep = "listen\s+\[::\]:443\s*ssl\s*\w*\s*;" - conf = re.sub(rep,'listen [::]:443 ssl'+http2+' default_server;',conf,1) - public.writeFile(path,conf) - - path = self.setupPath + '/panel/vhost/nginx/default.conf' - if os.path.exists(path): public.ExecShell('rm -f ' + path) - public.writeFile(default_site_save,get.name) - public.serviceReload() - return public.returnMsg(True,'SET_SUCCESS') - - #取默认站点 - def GetDefaultSite(self,get): - data = {} - data['sites'] = public.M('sites').field('name').order('id desc').select() - data['defaultSite'] = public.readFile('data/defaultSite.pl') - return data - - #扫描站点 - def CheckSafe(self,get): - import db,time - isTask = '/tmp/panelTask.pl' - if os.path.exists(self.setupPath + '/panel/class/panelSafe.py'): - import py_compile - py_compile.compile(self.setupPath + '/panel/class/panelSafe.py') - get.path = public.M('sites').where('id=?',(get.id,)).getField('path') - execstr = "cd " + public.GetConfigValue('setup_path') + "/panel/class && "+public.get_python_bin() +" panelSafe.pyc " + get.path - sql = db.Sql() - sql.table('tasks').add('id,name,type,status,addtime,execstr',(None,'扫描目录 ['+get.path+']','execshell','0',time.strftime('%Y-%m-%d %H:%M:%S'),execstr)) - public.writeFile(isTask,'True') - public.WriteLog('TYPE_SETUP','SITE_SCAN_ADD',(get.path,)) - return public.returnMsg(True,'SITE_SCAN_ADD') - - #获取结果信息 - def GetCheckSafe(self,get): - get.path = public.M('sites').where('id=?',(get.id,)).getField('path') - path = get.path + '/scan.pl' - result = {} - result['data'] = [] - result['phpini'] = [] - result['userini'] = result['sshd'] = True - result['scan'] = False - result['outime'] = result['count'] = result['error'] = 0 - if not os.path.exists(path): return result - import json - return json.loads(public.readFile(path)) - - #更新病毒库 - def UpdateRulelist(self,get): - try: - conf = public.httpGet(public.getUrl()+'/install/ruleList.conf') - if conf: - public.writeFile(self.setupPath + '/panel/data/ruleList.conf',conf) - return public.returnMsg(True,'UPDATE_SUCCESS') - return public.returnMsg(False,'CONNECT_ERR') - except: - return public.returnMsg(False,'CONNECT_ERR') - - def set_site_etime_multiple(self,get): - ''' - @name 批量网站到期时间 - @author zhwen<2020-11-17> - @param sites_id "1,2" - @param edate 2020-11-18 - ''' - sites_id = get.sites_id.split(',') - set_edate_successfully = [] - set_edate_failed = {} - for site_id in sites_id: - get.id = site_id - site_name = public.M('sites').where("id=?", (site_id,)).getField('name') - if not site_name: - continue - try: - self.SetEdate(get) - set_edate_successfully.append(site_name) - except: - set_edate_failed[site_name] = '设置时错误了,请再试一次' - pass - return {'status': True, 'msg': '设置网站 [ {} ] 到期时间成功'.format(','.join(set_edate_successfully)), 'error': set_edate_failed, - 'success': set_edate_successfully} - - #设置到期时间 - def SetEdate(self,get): - result = public.M('sites').where('id=?',(get.id,)).setField('edate',get.edate) - siteName = public.M('sites').where('id=?',(get.id,)).getField('name') - public.WriteLog('TYPE_SITE','SITE_EXPIRE_SUCCESS',(siteName,get.edate)) - return public.returnMsg(True,'SITE_EXPIRE_SUCCESS') - - #获取防盗链状态 - def GetSecurity(self,get): - file = '/www/server/panel/vhost/nginx/' + get.name + '.conf' - conf = public.readFile(file) - data = {} - if type(conf)==bool:return public.returnMsg(False,'读取配置文件失败!') - if conf.find('SECURITY-START') != -1: - rep = "#SECURITY-START(\n|.)+#SECURITY-END" - tmp = re.search(rep,conf).group() - data['fix'] = re.search("\(.+\)\$",tmp).group().replace('(','').replace(')$','').replace('|',',') - try: - data['domains'] = ','.join(list(set(re.search("valid_referers\s+none\s+blocked\s+(.+);\n",tmp).groups()[0].split()))) - except: - data['domains'] = ','.join(list(set(re.search("valid_referers\s+(.+);\n",tmp).groups()[0].split()))) - data['status'] = True - data['none'] = tmp.find('none blocked') != -1 - try: - data['return_rule'] = re.findall(r'(return|rewrite)\s+.*(\d{3}|(/.+)\s+(break|last));',conf)[0][1].replace('break','').strip() - except: data['return_rule'] = '404' - else: - data['fix'] = 'jpg,jpeg,gif,png,js,css' - domains = public.M('domain').where('pid=?',(get.id,)).field('name').select() - tmp = [] - for domain in domains: - tmp.append(domain['name']) - - data['return_rule'] = '404' - data['domains'] = ','.join(tmp) - data['status'] = False - data['none'] = False - return data - - #设置防盗链 - def SetSecurity(self,get): - if len(get.fix) < 2: return public.returnMsg(False,'URL后缀不能为空!') - if len(get.domains) < 3: return public.returnMsg(False,'防盗链域名不能为空!') - file = '/www/server/panel/vhost/nginx/' + get.name + '.conf' - if os.path.exists(file): - conf = public.readFile(file) - if get.status == '1': - r_key = 'valid_referers none blocked' - d_key = 'valid_referers' - if conf.find(r_key) == -1: - conf = conf.replace(d_key,r_key) - else: - if conf.find('SECURITY-START') == -1: return public.returnMsg(False,'请先开启防盗链!') - conf = conf.replace(r_key,d_key) - else: - - if conf.find('SECURITY-START') != -1: - rep = "\s{0,4}#SECURITY-START(\n|.){1,500}#SECURITY-END\n?" - conf = re.sub(rep,'',conf) - public.WriteLog('网站管理','站点['+get.name+']已关闭防盗链设置!') - else: - return_rule = 'return 404' - if 'return_rule' in get: - get.return_rule = get.return_rule.strip() - if get.return_rule in ['404','403','200','301','302','401','201']: - return_rule = 'return {}'.format(get.return_rule) - else: - if get.return_rule[0] != '/': - return public.returnMsg(False,"响应资源应使用URI路径或HTTP状态码,如:/test.png 或 404") - return_rule = 'rewrite /.* {} break'.format(get.return_rule) - rconf = '''#SECURITY-START 防盗链配置 - location ~ .*\.(%s)$ - { - expires 30d; - access_log /dev/null; - valid_referers %s; - if ($invalid_referer){ - %s; - } - } - #SECURITY-END - include enable-php-''' % (get.fix.strip().replace(',','|'),get.domains.strip().replace(',',' '),return_rule) - conf = re.sub("include\s+enable-php-",rconf,conf) - public.WriteLog('网站管理','站点['+get.name+']已开启防盗链!') - public.writeFile(file,conf) - - file = '/www/server/panel/vhost/apache/' + get.name + '.conf' - if os.path.exists(file): - conf = public.readFile(file) - if get.status == '1': - r_key = '#SECURITY-START 防盗链配置\n RewriteEngine on\n RewriteCond %{HTTP_REFERER} !^$ [NC]\n' - d_key = '#SECURITY-START 防盗链配置\n RewriteEngine on\n' - if conf.find(r_key) == -1: - conf = conf.replace(d_key,r_key) - else: - if conf.find('SECURITY-START') == -1: return public.returnMsg(False,'请先开启防盗链!') - conf = conf.replace(r_key,d_key) - else: - if conf.find('SECURITY-START') != -1: - rep = "#SECURITY-START(\n|.){1,500}#SECURITY-END\n" - conf = re.sub(rep,'',conf) - else: - return_rule = '/404.html [R=404,NC,L]' - if 'return_rule' in get: - get.return_rule = get.return_rule.strip() - if get.return_rule in ['404','403','200','301','302','401','201']: - return_rule = '/{s}.html [R={s},NC,L]'.format(s=get.return_rule) - else: - if get.return_rule[0] != '/': - return public.returnMsg(False,"响应资源应使用URI路径或HTTP状态码,如:/test.png 或 404") - return_rule = '{}'.format(get.return_rule) - - tmp = " RewriteCond %{HTTP_REFERER} !{DOMAIN} [NC]" - tmps = [] - for d in get.domains.split(','): - tmps.append(tmp.replace('{DOMAIN}',d)) - domains = "\n".join(tmps) - rconf = "combined\n #SECURITY-START 防盗链配置\n RewriteEngine on\n" + domains + "\n RewriteRule .("+get.fix.strip().replace(',','|')+") "+return_rule+"\n #SECURITY-END" - conf = conf.replace('combined',rconf) - public.writeFile(file,conf) - # OLS - cond_dir = '/www/server/panel/vhost/openlitespeed/prevent_hotlink/' - if not os.path.exists(cond_dir): - os.makedirs(cond_dir) - file = cond_dir + get.name + '.conf' - if get.status == '1': - conf = """ -RewriteCond %{HTTP_REFERER} !^$ -RewriteCond %{HTTP_REFERER} !BTDOMAIN_NAME [NC] -RewriteRule \.(BTPFILE)$ /404.html [R,NC] -""" - conf = conf.replace('BTDOMAIN_NAME',get.domains.replace(',',' ')).replace('BTPFILE',get.fix.replace(',','|')) - else: - conf = """ -RewriteCond %{HTTP_REFERER} !BTDOMAIN_NAME [NC] -RewriteRule \.(BTPFILE)$ /404.html [R,NC] -""" - conf = conf.replace('BTDOMAIN_NAME', get.domains.replace(',', ' ')).replace('BTPFILE',get.fix.replace(',', '|')) - public.writeFile(file, conf) - if get.status == "false": - public.ExecShell('rm -f {}'.format(file)) - public.serviceReload() - return public.returnMsg(True,'SET_SUCCESS') - - #取网站日志 - def GetSiteLogs(self,get): - serverType = public.get_webserver() - if serverType == "nginx": - logPath = '/www/wwwlogs/' + get.siteName + '.log' - elif serverType == 'apache': - logPath = '/www/wwwlogs/' + get.siteName + '-access_log' - else: - logPath = '/www/wwwlogs/' + get.siteName + '_ols.access_log' - if not os.path.exists(logPath): return public.returnMsg(False,'日志为空') - return public.returnMsg(True,public.GetNumLines(logPath,1000)) - - #取网站错误日志 - def get_site_errlog(self,get): - serverType = public.get_webserver() - if serverType == "nginx": - logPath = '/www/wwwlogs/' + get.siteName + '.error.log' - elif serverType == 'apache': - logPath = '/www/wwwlogs/' + get.siteName + '-error_log' - else: - logPath = '/www/wwwlogs/' + get.siteName + '_ols.error_log' - if not os.path.exists(logPath): return public.returnMsg(False,'日志为空') - return public.returnMsg(True,public.GetNumLines(logPath,1000)) - - #取网站分类 - def get_site_types(self,get): - data = public.M("site_types").field("id,name").order("id asc").select() - data.insert(0,{"id":0,"name":"默认分类"}) - return data - - #添加网站分类 - def add_site_type(self,get): - get.name = get.name.strip() - if not get.name: return public.returnMsg(False,"分类名称不能为空") - if len(get.name) > 18: return public.returnMsg(False,"分类名称长度不能超过6个汉字或18位字母") - type_sql = public.M('site_types') - if type_sql.count() >= 10: return public.returnMsg(False,'最多添加10个分类!') - if type_sql.where('name=?',(get.name,)).count()>0: return public.returnMsg(False,"指定分类名称已存在!") - type_sql.add("name",(get.name,)) - return public.returnMsg(True,'添加成功!') - - #删除网站分类 - def remove_site_type(self,get): - type_sql = public.M('site_types') - if type_sql.where('id=?',(get.id,)).count()==0: return public.returnMsg(False,"指定分类不存在!") - type_sql.where('id=?',(get.id,)).delete() - public.M("sites").where("type_id=?",(get.id,)).save("type_id",(0,)) - return public.returnMsg(True,"分类已删除!") - - #修改网站分类名称 - def modify_site_type_name(self,get): - get.name = get.name.strip() - if not get.name: return public.returnMsg(False,"分类名称不能为空") - if len(get.name) > 18: return public.returnMsg(False,"分类名称长度不能超过6个汉字或18位字母") - type_sql = public.M('site_types') - if type_sql.where('id=?',(get.id,)).count()==0: return public.returnMsg(False,"指定分类不存在!") - type_sql.where('id=?',(get.id,)).setField('name',get.name) - return public.returnMsg(True,"修改成功!") - - #设置指定站点的分类 - def set_site_type(self,get): - site_ids = json.loads(get.site_ids) - site_sql = public.M("sites") - for s_id in site_ids: - site_sql.where("id=?",(s_id,)).setField("type_id",get.id) - return public.returnMsg(True,"设置成功!") - - # 设置目录保护 - def set_dir_auth(self,get): - sd = site_dir_auth.SiteDirAuth() - return sd.set_dir_auth(get) - - def delete_dir_auth_multiple(self,get): - ''' - @name 批量目录保护 - @author zhwen<2020-11-17> - @param site_id 1 - @param names test,baohu - ''' - names = get.names.split(',') - del_successfully = [] - del_failed = {} - for name in names: - get.name = name - get.id = get.site_id - try: - get.multiple = 1 - result = self.delete_dir_auth(get) - if not result['status']: - del_failed[name] = result['msg'] - continue - del_successfully.append(name) - except: - del_failed[name]='删除时错误了,请再试一次' - public.serviceReload() - return {'status': True, 'msg': '删除目录保护 [ {} ] 成功'.format(','.join(del_successfully)), 'error': del_failed, - 'success': del_successfully} - - # 删除目录保护 - def delete_dir_auth(self,get): - sd = site_dir_auth.SiteDirAuth() - return sd.delete_dir_auth(get) - - # 获取目录保护列表 - def get_dir_auth(self,get): - sd = site_dir_auth.SiteDirAuth() - return sd.get_dir_auth(get) - - # 修改目录保护密码 - def modify_dir_auth_pass(self,get): - sd = site_dir_auth.SiteDirAuth() - return sd.modify_dir_auth_pass(get) \ No newline at end of file diff --git a/class/public.py b/class/public.py index 19eaa216..f4dc5f1e 100644 --- a/class/public.py +++ b/class/public.py @@ -456,7 +456,7 @@ def getMsg(key,args = ()): #获取Web服务器 def GetWebServer(): - if os.path.exists('/www/server/apache/bin/apachectl'): + if os.path.exists('{}/apache/bin/apachectl'.format(get_setup_path())): webserver = 'apache' elif os.path.exists('/usr/local/lsws/bin/lswsctrl'): webserver = 'openlitespeed' @@ -469,30 +469,65 @@ def get_webserver(): def ServiceReload(): #重载Web服务配置 - if os.path.exists('/www/server/nginx/sbin/nginx'): + if os.path.exists('{}/nginx/sbin/nginx'.format(get_setup_path())): result = ExecShell('/etc/init.d/nginx reload') if result[1].find('nginx.pid') != -1: ExecShell('pkill -9 nginx && sleep 1') ExecShell('/etc/init.d/nginx start') - elif os.path.exists('/www/server/apache/bin/apachectl'): + elif os.path.exists('{}/apache/bin/apachectl'.format(get_setup_path())): result = ExecShell('/etc/init.d/httpd reload') else: result = ExecShell('rm -f /tmp/lshttpd/*.sock* && /usr/local/lsws/bin/lswsctrl restart') return result + def serviceReload(): return ServiceReload() -def ExecShell(cmdstring, timeout=None, shell=True): + +def get_preexec_fn(run_user): + ''' + @name 获取指定执行用户预处理函数 + @author hwliang<2021-08-19> + @param run_user 运行用户 + @return 预处理函数 + ''' + import pwd + pid = pwd.getpwnam(run_user) + uid = pid.pw_uid + gid = pid.pw_gid + + def _exec_rn(): + os.setgid(gid) + os.setuid(uid) + return _exec_rn + + +def ExecShell(cmdstring, timeout=None, shell=True,cwd=None,env=None,user = None): + ''' + @name 执行命令 + @author hwliang<2021-08-19> + @param cmdstring 命令 [必传] + @param timeout 超时时间 + @param shell 是否通过shell运行 + @param cwd 进入的目录 + @param env 环境变量 + @param user 执行用户名 + @return 命令执行结果 + ''' a = '' e = '' import subprocess,tempfile - + preexec_fn = None + tmp_dir = '/dev/shm' + if user: + preexec_fn = get_preexec_fn(user) + tmp_dir = '/tmp' try: rx = md5(cmdstring) - succ_f = tempfile.SpooledTemporaryFile(max_size=4096,mode='wb+',suffix='_succ',prefix='btex_' + rx ,dir='/dev/shm') - err_f = tempfile.SpooledTemporaryFile(max_size=4096,mode='wb+',suffix='_err',prefix='btex_' + rx ,dir='/dev/shm') - sub = subprocess.Popen(cmdstring, close_fds=True, shell=shell,bufsize=128,stdout=succ_f,stderr=err_f) + succ_f = tempfile.SpooledTemporaryFile(max_size=4096,mode='wb+',suffix='_succ',prefix='btex_' + rx ,dir=tmp_dir) + err_f = tempfile.SpooledTemporaryFile(max_size=4096,mode='wb+',suffix='_err',prefix='btex_' + rx ,dir=tmp_dir) + sub = subprocess.Popen(cmdstring, close_fds=True, shell=shell,bufsize=128,stdout=succ_f,stderr=err_f,cwd=cwd,env=env,preexec_fn=preexec_fn) if timeout: s = 0 d = 0.01 @@ -588,10 +623,13 @@ def GetClientIp(): from flask import request return request.remote_addr.replace('::ffff:','') +def get_client_ip(): + return GetClientIp() + def phpReload(version): #重载PHP配置 import os - if os.path.exists('/www/server/php/' + version + '/libphp5.so'): + if os.path.exists(get_setup_path()+'/php/' + version + '/libphp5.so'): ExecShell('/etc/init.d/httpd reload') else: ExecShell('/etc/init.d/php-fpm-'+version+' reload') @@ -881,7 +919,7 @@ def get_plugin_title(plugin_name): @return string ''' - info_file = '/www/server/panel/plugin/{}/info.json'.format(plugin_name) + info_file = '{}/{}/info.json'.format(get_plugin_path(),plugin_name) try: return json.loads(readFile(info_file))['title'] except: @@ -908,6 +946,8 @@ def get_error_object(plugin_title = None,plugin_name = None): During handling of the above exception, another exception occurred:''' error_info = get_error_info().strip().split(ss)[-1].strip() request_info = '''REQUEST_DATE: {request_date} + PAN_VERSION: {panel_version} + OS_VERSION: {os_version} REMOTE_ADDR: {remote_addr} REQUEST_URI: {method} {full_path} REQUEST_FORM: {request_form} @@ -917,10 +957,12 @@ def get_error_object(plugin_title = None,plugin_name = None): method = request.method, full_path = request.full_path, request_form = request.form.to_dict(), - user_agent = request.headers.get('User-Agent') + user_agent = request.headers.get('User-Agent'), + panel_version = version(), + os_version = get_os_version() ) - result =readFile('/www/server/panel/BTPanel/templates/default/plugin_error.html').format( + result =readFile('{}/BTPanel/templates/default/plugin_error.html'.format(get_panel_path())).format( plugin_name=plugin_title, request_info=request_info, error_title=error_info.split("\n")[-1], @@ -934,7 +976,7 @@ def get_error_object(plugin_title = None,plugin_name = None): def submit_error(err_msg = None): try: - if os.path.exists('/www/server/panel/not_submit_errinfo.pl'): return False + if os.path.exists('{}/not_submit_errinfo.pl'.format(get_panel_path())): return False from BTPanel import request import system if not err_msg: err_msg = get_error_info() @@ -944,7 +986,7 @@ def submit_error(err_msg = None): pdata['version'] = 'Linux-Panel-%s' % version() pdata['os'] = system.system().GetSystemVersion() pdata['py_version'] = sys.version - pdata['install_date'] = int(os.stat('/www/server/panel/class/common.py').st_mtime) + pdata['install_date'] = int(os.stat('{}/common.py'.format(get_class_path())).st_mtime) httpPost(GetConfigValue('home') + "/api/panel/s_error",pdata,timeout=3) except: pass @@ -962,13 +1004,14 @@ def inArray(arrays,searchStr): def format_date(format="%Y-%m-%d %H:%M:%S",times = None): if not times: times = int(time.time()) time_local = time.localtime(times) - return time.strftime(format, time_local) + return time.strftime(format, time_local) #检查Web服务器配置文件是否有错误 def checkWebConfig(): - f1 = '/www/server/panel/vhost/' - f2 = '/www/server/panel/plugin/' + f1 = '{}/'.format(get_vhost_path()) + f2 = '{}/'.format(get_plugin_path()) + setup_path = get_setup_path() if not os.path.exists(f2 + 'btwaf'): f3 = f1 + 'nginx/btwaf.conf' if os.path.exists(f3): os.remove(f3) @@ -982,19 +1025,19 @@ def checkWebConfig(): f3 = f1 + 'nginx/total.conf' if os.path.exists(f3): os.remove(f3) else: - if os.path.exists('/www/server/apache/modules/mod_lua.so'): + if os.path.exists(setup_path + '/apache/modules/mod_lua.so'): writeFile(f1 + 'apache/btwaf.conf','LoadModule lua_module modules/mod_lua.so') - writeFile(f1 + 'apache/total.conf','LuaHookLog /www/server/total/httpd_log.lua run_logs') + writeFile(f1 + 'apache/total.conf','LuaHookLog {}/total/httpd_log.lua run_logs'.format(setup_path)) else: f3 = f1 + 'apache/total.conf' if os.path.exists(f3): os.remove(f3) if get_webserver() == 'nginx': - result = ExecShell("ulimit -n 8192 ; /www/server/nginx/sbin/nginx -t -c /www/server/nginx/conf/nginx.conf") + result = ExecShell("ulimit -n 8192 ; {setup_path}/nginx/sbin/nginx -t -c {setup_path}/nginx/conf/nginx.conf".format(setup_path = setup_path)) searchStr = 'successful' elif get_webserver() == 'apache': # else: - result = ExecShell("ulimit -n 8192 ; /www/server/apache/bin/apachectl -t") + result = ExecShell("ulimit -n 8192 ; {setup_path}/apache/bin/apachectl -t".format(setup_path = setup_path)) searchStr = 'Syntax OK' else: result = ["1","1"] @@ -1069,7 +1112,7 @@ def CheckMyCnf(): if os.path.exists(confFile): conf = readFile(confFile) if conf.find('[mysqld]') != -1: return True - versionFile = '/www/server/mysql/version.pl' + versionFile = get_setup_path() + '/mysql/version.pl' if not os.path.exists(versionFile): return False versions = ['5.1','5.5','5.6','5.7','8.0','AliSQL'] @@ -1466,17 +1509,33 @@ def get_page(count,p=1,rows=12,callback='',result='1,2,3,4,5,8'): # 取面板版本 def version(): try: - comm = ReadFile('/www/server/panel/class/common.py') + comm = ReadFile('{}/common.py'.format(get_class_path())) return re.search("g\.version\s*=\s*'(\d+\.\d+\.\d+)'",comm).groups()[0] except: return get_panel_version() def get_panel_version(): - comm = ReadFile('/www/server/panel/class/common.py') + comm = ReadFile('{}/common.py'.format(get_class_path())) s_key = 'g.version = ' s_len = len(s_key) s_leff = comm.find(s_key) + s_len - version = comm[s_leff:s_leff+6].strip().strip("'") + version = comm[s_leff:s_leff+10].strip().strip("'") + return version + + +def get_os_version(): + ''' + @name 取操作系统版本 + @author hwliang<2021-08-07> + @return string + ''' + version = readFile('/etc/redhat-release') + if not version: + version = readFile('/etc/issue').strip().split("\n")[0].replace('\\n','').replace('\l','').strip() + else: + version = version.replace('release ','').replace('Linux','').replace('(Core)','').strip() + v_info = sys.version_info + version = "{} {}(Py{}.{}.{})".format(version,os.uname().machine,v_info.major,v_info.minor,v_info.micro) return version #取文件或目录大小 @@ -1546,7 +1605,7 @@ def write_request_log(reques = None): if request.path in ['/service_status','/favicon.ico','/task','/system','/ajax','/control','/data','/ssl']: return False - log_path = '/www/server/panel/logs/request' + log_path = '{}/logs/request'.format(get_panel_path()) log_file = getDate(format='%Y-%m-%d') + '.json' if not os.path.exists(log_path): os.makedirs(log_path) @@ -1630,6 +1689,32 @@ def get_database_character(db_name): except: return 'utf8' +def get_database_codestr(codeing): + wheres = { + 'utf8' : 'utf8_general_ci', + 'utf8mb4' : 'utf8mb4_general_ci', + 'gbk' : 'gbk_chinese_ci', + 'big5' : 'big5_chinese_ci' + } + return wheres[codeing] + + +def get_database_size(): + """ + @获取数据库大小 + """ + data = {} + try: + import panelMysql + tables = panelMysql.panelMysql().query("select table_schema, sum(DATA_LENGTH) as data from information_schema.TABLES group by table_schema") + if type(tables) == list: + for x in tables: + if len(x) < 2:continue + if x[1] == None:continue + data[x[0]] = int(x[1]) + except: return data + return data + def en_punycode(domain): if sys.version_info[0] == 2: domain = domain.encode('utf8') @@ -1763,17 +1848,17 @@ def check_domain_panel(): #是否离线模式 def is_local(): - s_file = '/www/server/panel/data/not_network.pl' + s_file = '{}/data/not_network.pl'.format(get_panel_path()) return os.path.exists(s_file) #自动备份面板数据 def auto_backup_panel(): try: - panel_paeh = '/www/server/panel' + panel_paeh = get_panel_path() paths = panel_paeh + '/data/not_auto_backup.pl' if os.path.exists(paths): return False - b_path = '/www/backup/panel' + b_path = '{}/panel'.format(get_backup_path()) backup_path = b_path + '/' + format_date('%Y-%m-%d') if os.path.exists(backup_path): return True if os.path.getsize(panel_paeh + '/data/default.db') > 104857600 * 2: return False @@ -1928,7 +2013,7 @@ def get_fpm_address(php_version,bind=False): @return tuple or string ''' fpm_address = '/tmp/php-cgi-{}.sock'.format(php_version) - php_fpm_file = '/www/server/php/{}/etc/php-fpm.conf'.format(php_version) + php_fpm_file = '{}/php/{}/etc/php-fpm.conf'.format(get_setup_path(),php_version) try: fpm_conf = readFile(php_fpm_file) tmp = re.findall(r"listen\s*=\s*(.+)",fpm_conf) @@ -2002,9 +2087,10 @@ def get_site_php_version(siteName): @return string ''' web_server = get_webserver() - conf = readFile('/www/server/panel/vhost/'+web_server+'/'+siteName+'.conf') + vhost_path = get_vhost_path() + conf = readFile(vhost_path + '/'+web_server+'/'+siteName+'.conf') if web_server == 'openlitespeed': - conf = readFile('/www/server/panel/vhost/' + web_server + '/detail/' + siteName + '.conf') + conf = readFile(vhost_path + '/' + web_server + '/detail/' + siteName + '.conf') return get_php_version_conf(conf) @@ -2069,7 +2155,7 @@ def sync_php_address(php_version): @param php_version string PHP版本 @return void ''' - if not os.path.exists('/www/server/php/{}/bin/php'.format(php_version)): # 指定PHP版本是否安装 + if not os.path.exists('{}/php/{}/bin/php'.format(get_setup_path(),php_version)): # 指定PHP版本是否安装 return False ngx_rep = r"(unix:/tmp/php-cgi.*\.sock|127.0.0.1:\d+)" apa_rep = r"(unix:/tmp/php-cgi.*\.sock\|fcgi://localhost|fcgi://127.0.0.1:\d+)" @@ -2078,7 +2164,7 @@ def sync_php_address(php_version): is_write = False #nginx的PHP配置文件 - nginx_conf_path = '/www/server/nginx/conf' + nginx_conf_path = '{}/nginx/conf'.format(get_setup_path()) if os.path.exists(nginx_conf_path): for f_name in os.listdir(nginx_conf_path): @@ -2093,14 +2179,14 @@ def sync_php_address(php_version): # is_write = True #apache的网站配置文件 - apache_conf_path = '/www/server/panel/vhost/apache' + apache_conf_path = '{}/apache'.format(get_vhost_path()) if os.path.exists(apache_conf_path): for f_name in os.listdir(apache_conf_path): conf_file = '/'.join((apache_conf_path,f_name)) if sub_php_address(conf_file,apa_rep,apa_proxy,php_version): is_write = True #apache的phpmyadmin - conf_file = '/www/server/apache/conf/extra/httpd-vhosts.conf' + conf_file = '{}/apache/conf/extra/httpd-vhosts.conf'.format(get_setup_path()) if os.path.exists(conf_file): if sub_php_address(conf_file,apa_rep,apa_proxy,php_version): is_write = True @@ -2191,7 +2277,7 @@ def set_cdn_url(cdn_url): return True def get_python_bin(): - bin_file = '/www/server/panel/pyenv/bin/python' + bin_file = '{}/pyenv/bin/python'.format(get_panel_path()) if os.path.exists(bin_file): return bin_file return '/usr/bin/python' @@ -2300,19 +2386,19 @@ def get_debug_log(): #获取sessionid def get_session_id(): - from BTPanel import request - session_id = request.cookies.get('SESSIONID','') + from BTPanel import request,app + session_id = request.cookies.get(app.config['SESSION_COOKIE_NAME'],'') if not re.findall(r"^([\w\.-]{64,64})$",session_id): return GetRandomString(64) return session_id #尝试自动恢复面板数据库 def rep_default_db(): - db_path = '/www/server/panel/data/' + db_path = '{}/data/'.format(get_panel_path()) db_file = db_path + 'default.db' db_tmp_backup = db_path + 'default_' + format_date("%Y%m%d_%H%M%S") + ".db" - panel_backup = '/www/backup/panel' + panel_backup = '{}/panel'.format(get_backup_path()) bak_list = os.listdir(panel_backup) if not bak_list: return False bak_list = sorted(bak_list,reverse=True) @@ -2356,7 +2442,7 @@ def chdck_salt(): def get_login_token(): - token_s = readFile('/www/server/panel/data/login_token.pl') + token_s = readFile('{}/data/login_token.pl'.format(get_panel_path())) if not token_s: return GetRandomString(32) return token_s @@ -2499,7 +2585,7 @@ def get_curl_bin(): #设置防跨站配置 def set_open_basedir(): try: - fastcgi_file = '/www/server/nginx/conf/fastcgi.conf' + fastcgi_file = '{}/nginx/conf/fastcgi.conf'.format(get_setup_path()) if os.path.exists(fastcgi_file): fastcgi_body = readFile(fastcgi_file) @@ -2507,7 +2593,7 @@ def set_open_basedir(): fastcgi_body = fastcgi_body + "\n"+'fastcgi_param PHP_ADMIN_VALUE "$bt_safe_dir=$bt_safe_open";' writeFile(fastcgi_file,fastcgi_body) - proxy_file = '/www/server/nginx/conf/proxy.conf' + proxy_file = '{}/nginx/conf/proxy.conf'.format(get_setup_path()) if os.path.exists(proxy_file): proxy_body = readFile(proxy_file) if proxy_body.find('bt_safe_dir') == -1: @@ -2519,7 +2605,7 @@ def set_open_basedir(): } ''' writeFile(proxy_file,proxy_body) - open_basedir_path = '/www/server/panel/vhost/open_basedir/nginx' + open_basedir_path = '{}/open_basedir/nginx'.format(get_vhost_path()) if not os.path.exists(open_basedir_path): os.makedirs(open_basedir_path,384) @@ -2595,7 +2681,7 @@ def cloud_check_domain(domain): @return void ''' try: - check_domain_path = '/www/server/panel/data/check_domain/' + check_domain_path = '{}/data/check_domain/'.format(get_panel_path()) if not os.path.exists(check_domain_path): os.makedirs(check_domain_path,384) pdata = get_user_info() @@ -2606,23 +2692,31 @@ def cloud_check_domain(domain): except: pass +def get_mac_address(): + import uuid + mac=uuid.UUID(int = uuid.getnode()).hex[-12:] + return ":".join([mac[e:e+2] for e in range(0,11,2)]) def get_user_info(): - user_file = '/www/server/panel/data/userInfo.json' + user_file = '{}/data/userInfo.json'.format(get_panel_path()) if not os.path.exists(user_file): return {} userInfo = {} try: userTmp = json.loads(readFile(user_file)) + if not 'serverid' in userTmp or len(userTmp['serverid']) != 64: + import panelAuth + userTmp = panelAuth.panelAuth().create_serverid(None) userInfo['uid'] = userTmp['uid'] userInfo['username'] = userTmp['username'] userInfo['serverid'] = userTmp['serverid'] userInfo['oem'] = get_oem_name() userInfo['o'] = userInfo['oem'] + userInfo['mac'] = get_mac_address() except: pass return userInfo def is_bind(): - if not os.path.exists('data/bind.pl'): return True + # if not os.path.exists('{}/data/bind.pl'.format(get_panel_path())): return True return not not get_user_info() @@ -2682,7 +2776,7 @@ def get_oem_name(): @return string ''' oem = '' - oem_file = '/www/server/panel/data/o.pl' + oem_file = '{}/data/o.pl'.format(get_panel_path()) if os.path.exists(oem_file): oem = readFile(oem_file) if oem: oem = oem.strip() @@ -2793,7 +2887,7 @@ def get(self,key,default='',format='',limit = []): @name 获取指定参数 @param key 参数名称,允许在/后面限制参数格式,请参考参数值格式(format) @param default 默认值,默认空字符串 - @param format 参数值格式(int|str|float|json|xss|path|url|ip|ipv4|ipv6|letter|mail|phone|正则表达式|>1|<1|=1),默认为空 + @param format 参数值格式(int|str|port|float|json|xss|path|url|ip|ipv4|ipv6|letter|mail|phone|正则表达式|>1|<1|=1),默认为空 @param limit 限制参数值内容 @param return mixed ''' @@ -2846,13 +2940,18 @@ def get(self,key,default='',format='',limit = []): raise ValueError('参数:{},要求正确的ipv4/ipv6地址'.format(key)) elif format in ['w','letter']: if not re.match(r'^\w+$',result): - raise ValueError('参数:{},要求只能是英文字母组成'.format(key)) + raise ValueError('参数:{},要求只能是英文字母或数据组成'.format(key)) elif format in ['email','mail','m']: if not re.match(r"^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$",result): raise ValueError("参数:{},要求正确的邮箱地址格式".format(key)) elif format in ['phone','mobile','p']: if not re.match("^[0-9]{11,11}$",result): raise ValueError("参数:{},要求手机号码格式".format(key)) + elif format in ['port']: + result_port = int(result) + if result_port > 65535 or result_port < 0: + raise ValueError("参数:{},要求端口号为0-65535".format(key)) + result = result_port elif re.match(r"^[<>=]\d+$",result): operator = format[0] length = int(format[1:].strip()) @@ -2875,13 +2974,6 @@ def get(self,key,default='',format='',limit = []): if not result in limit: raise ValueError("指定参数值范围不正确, {}:{}".format(key,limit)) return result - - - - - - - #实例化定目录下的所有模块 class get_modules: @@ -2936,7 +3028,7 @@ def __init__(self,path = "class",limit = None): else: print(p.__dict__) ''' - os.chdir('/www/server/panel') + os.chdir(get_panel_path()) exp_files = ['__init__.py','__pycache__'] if not path in sys.path: sys.path.insert(0,path) @@ -2962,7 +3054,7 @@ def __init__(self,path = "class",limit = None): #检查App和小程序的绑定 def check_app(check='app'): - path='/www/server/panel/' + path=get_panel_path() + '/' if check=='app': try: if not os.path.exists(path+'data/user.json') and os.path.exists(path+'config/api.json') and not os.path.exists(path+'plugin/app/user.json'):return False @@ -3052,8 +3144,9 @@ def send_dingding(body,is_logs=False,is_type="堡塔登录提醒"): #获取服务器IP def get_ip(): - if os.path.exists('/www/server/panel/data/iplist.txt'): - data=ReadFile('/www/server/panel/data/iplist.txt') + iplist_file = '{}/data/iplist.txt'.format(get_panel_path()) + if os.path.exists(iplist_file): + data=ReadFile(iplist_file) return data.strip() else:return '127.0.0.1' @@ -3102,11 +3195,14 @@ def check_ip_white(path,ip): #登陆告警 def login_send_body(is_type,username,login_ip,port): - if os.path.exists("/www/server/panel/data/login_send_mail.pl"): - if check_ip_white('/www/server/panel/data/send_login_white.json',login_ip):return False + login_send_mail = "{}/data/login_send_mail.pl".format(get_panel_path()) + send_login_white = '{}/data/send_login_white.json'.format(get_panel_path()) + login_send_dingding = "{}/data/login_send_dingding.pl".format(get_panel_path()) + if os.path.exists(login_send_mail): + if check_ip_white(send_login_white,login_ip):return False send_mail("堡塔登录提醒","堡塔登录提醒:您的服务器"+get_ip()+"通过"+is_type+"登录成功,账号:"+username+",登录IP:"+login_ip+":"+port+",登录时间:"+time.strftime('%Y-%m-%d %X',time.localtime()), True) - if os.path.exists("/www/server/panel/data/login_send_dingding.pl"): - if check_ip_white('/www/server/panel/data/send_login_white.json',login_ip):return False + if os.path.exists(login_send_dingding): + if check_ip_white(send_login_white,login_ip):return False send_dingding("#### 堡塔登录提醒\n\n > 服务器 :"+get_ip()+"\n\n > 登录方式:"+is_type+"\n\n > 登录账号:"+username+"\n\n > 登录IP:"+login_ip+":"+port+"\n\n > 登录时间:"+time.strftime('%Y-%m-%d %X',time.localtime())+'\n\n > 登录状态: 成功', True) @@ -3116,11 +3212,14 @@ def login_send_body(is_type,username,login_ip,port): #is_logs= 是否记录日志 #is_type=发送告警的类型 def send_to_body(title,body,is_logs=False,is_type="堡塔邮件告警"): - if os.path.exists("/www/server/panel/data/login_send_mail.pl"): + login_send_mail = "{}/data/login_send_mail.pl".format(get_panel_path()) + login_send_dingding = "{}/data/login_send_dingding.pl".format(get_panel_path()) + if os.path.exists(login_send_mail): if is_logs: send_mail(title, body,True,is_type) send_mail(title,body) - if os.path.exists("/www/server/panel/data/login_send_dingding.pl"): + + if os.path.exists(login_send_dingding): if is_logs: send_dingding(body,True,is_type) send_dingding(body) @@ -3177,7 +3276,8 @@ def check_site_path(site_path): return True def is_debug(): - return os.path.exists('/www/server/panel/data/debug.pl') + debug_file = "{}/data/debug.pl".format(get_panel_path()) + return os.path.exists(debug_file) class PanelError(Exception): @@ -3208,6 +3308,8 @@ def get_plugin_find(upgrade_plugin_name = None): upgrade_plugin_name = p_data_info['name'] return p_data_info + return get_plugin_info(upgrade_plugin_name) + def get_plugin_info(upgrade_plugin_name): ''' @@ -3216,7 +3318,7 @@ def get_plugin_info(upgrade_plugin_name): @param upgrade_plugin_name 插件名称 @return dict ''' - plugin_path = '/www/server/panel/plugin' + plugin_path = get_plugin_path() plugin_info_file = '{}/{}/info.json'.format(plugin_path,upgrade_plugin_name) if not os.path.exists(plugin_info_file): return {} info_body = readFile(plugin_info_file) @@ -3233,8 +3335,8 @@ def download_main(upgrade_plugin_name,upgrade_version): @return void ''' import requests,shutil - plugin_path = '/www/server/panel/plugin' - tmp_path = '/www/server/panel/temp' + plugin_path = get_plugin_path() + tmp_path = '{}/temp'.format(get_panel_path()) download_d_main_url = 'https://api.bt.cn/down/download_plugin_main' pdata = get_user_info() pdata['name'] = upgrade_plugin_name @@ -3255,9 +3357,10 @@ def download_main(upgrade_plugin_name,upgrade_version): def get_plugin_bin(plugin_save_file,plugin_timeout): list_body = None + if not os.path.exists(plugin_save_file): return list_body s_time = time.time() m_time = os.stat(plugin_save_file).st_mtime - if s_time - m_time < plugin_timeout: + if (s_time - m_time) < plugin_timeout or not plugin_timeout: list_body = readFile(plugin_save_file,'rb') return list_body @@ -3308,6 +3411,13 @@ def get_sysbit(): import struct return struct.calcsize('P') * 8 +def get_setup_path(): + ''' + @name 获取安装路径 + @author hwliang<2021-07-22> + @return string + ''' + return '/www/server' def get_panel_path(): ''' @@ -3315,7 +3425,7 @@ def get_panel_path(): @author hwliang<2021-07-14> @return string ''' - return '/www/server/panel' + return '{}/panel'.format(get_setup_path()) def get_plugin_path(plugin_name = None): ''' @@ -3337,6 +3447,47 @@ def get_class_path(): ''' return "{}/class".format(get_panel_path()) +def get_logs_path(): + ''' + @name 取日志目录 + @author hwliang<2021-07-14> + @return string + ''' + return '/www/wwwlogs' + +def get_vhost_path(): + ''' + @name 取虚拟主机目录 + @author hwliang<2021-08-14> + @return string + ''' + return '{}/vhost'.format(get_panel_path()) + + +def get_backup_path(): + ''' + @name 取备份目录 + @author hwliang<2021-07-14> + @return string + ''' + default_backup_path = '/www/backup' + backup_path = M('config').where("id=?",(1,)).getField('backup_path') + if not backup_path: return default_backup_path + if os.path.exists(backup_path): return backup_path + return default_backup_path + +def get_site_path(): + ''' + @name 取站点默认存储目录 + @author hwliang<2021-07-14> + @return string + ''' + default_site_path = '/www/wwwroot' + site_path = M('config').where("id=?",(1,)).getField('sites_path') + if not site_path: return default_site_path + if os.path.exists(site_path): return site_path + return default_site_path + def read_config(config_name,ext_name = 'json'): ''' @@ -3406,7 +3557,7 @@ def set_config_value(config_name,key,value,ext_name='json'): return save_config(config_name,config_data,ext_name) -def return_data(status,data,status_code=None,error_msg = None): +def return_data(status,data = {},status_code=None,error_msg = None): ''' @name 格式化响应内容 @author hwliang<2021-07-14> @@ -3443,6 +3594,28 @@ def return_error(error_msg,status_code = -1,data = []): return return_data(False,data,status_code,str(error_msg)) +def error(error_msg,status_code = -1,data = []): + ''' + @name 格式化错误响应内容 + @author hwliang<2021-07-15> + @param error_msg 错误消息 + @param status_code 状态码,默认为-1 + @param data 响应数据 + @return dict + ''' + return return_error(error_msg,status_code,data) + +def success(data = [],status_code = 1,error_msg = ''): + ''' + @name 格式化成功响应内容 + @author hwliang<2021-07-15> + @param data 响应数据 + @param status_code 状态码,默认为0 + @return dict + ''' + return return_data(True,data,status_code,error_msg) + + def return_status_code(status_code,format_body,data = []): ''' @name 按状态码返回 @@ -3471,13 +3644,15 @@ def to_dict_obj(data): pdata[key] = data[key] return pdata -def get_sctipt_object(filename): +def get_script_object(filename): ''' @name 从脚本文件获取对像 @author hwliang<2021-07-19> @param filename 文件名 @return object ''' + _obj = sys.modules.get(filename,None) + if _obj: return _obj from types import ModuleType _obj = sys.modules.setdefault(filename, ModuleType(filename)) _code = readFile(filename) @@ -3499,7 +3674,7 @@ def check_hooks(): for hook_name in os.listdir(hooks_path): if hook_name[-3:] != '.py': continue filename = os.path.join(hooks_path,hook_name) - _obj = get_sctipt_object(filename) + _obj = get_script_object(filename) _main = getattr(_obj,'main',None) if not _main: continue _main() @@ -3533,7 +3708,7 @@ def exec_hook(hook_index,data): if not hook_index in hook_keys: return data - for hook_def in hook_keys[hook_index]: + for hook_def in hooks[hook_index]: data = hook_def(data) return data @@ -3550,3 +3725,125 @@ def get_hook_index(mod_name,def_name): last_index = '{}_{}_LAST'.format(mod_name,def_name) end_index = '{}_{}_END'.format(mod_name,def_name) return last_index,end_index + + +def flush_plugin_list(): + ''' + @name 刷新插件列表 + @author hwliang<2021-07-22> + @return bool + ''' + skey = 'TNaMJdG3mDHKRS6Y' + from BTPanel import cache + from pluginAuth import Plugin + if cache.get(skey): cache.delete(skey) + Plugin(False).get_plugin_list(True) + return True + + +def get_session_timeout(): + ''' + @name 获取session过期时间 + @author hwliang<2021-07-28> + @return int + ''' + from BTPanel import cache + skey = 'session_timeout' + session_timeout = cache.get(skey) + if not session_timeout is None: return session_timeout + + sess_out_path = '{}/data/session_timeout.pl'.format(get_panel_path()) + session_timeout = 86400 + if not os.path.exists(sess_out_path): + return session_timeout + session_timeout = int(readFile(sess_out_path)) + cache.set(skey,session_timeout,3600) + return session_timeout + + +def get_login_token_auth(): + ''' + @name 获取登录token + @author hwliang<2021-07-28> + @return string + ''' + from BTPanel import cache + skey = 'login_token' + login_token = cache.get(skey) + if not login_token is None: return login_token + + login_token_file = '{}/data/login_token.pl'.format(get_panel_path()) + login_token = '1234567890' + if not os.path.exists(login_token_file): + return login_token + login_token = readFile(login_token_file) + cache.set(skey,login_token,3600) + return login_token + + +def listen_ipv6(): + ''' + @name 是否监听ipv6 + @author hwliang<2021-08-12> + @return bool + ''' + ipv6_file = '{}/data/ipv6.pl'.format(get_panel_path()) + return os.path.exists(ipv6_file) + +def get_panel_log_file(): + ''' + @name 获取panel日志文件 + @author hwliang<2021-08-12> + @return string + ''' + return "{}/logs/error.log".format(get_panel_path()) + + +def print_log(_info,_level = 'DEBUG'): + ''' + @name 写入日志 + @author hwliang<2021-08-12> + @param _info 要写入到日志文件的信息 + @param _level 日志级别 + @return void + ''' + log_body = "[{}][{}] - {}\n".format(format_date(),_level.upper(),_info) + return WriteFile(get_panel_log_file(),log_body,'a+') + + + +def to_date(format = "%Y-%m-%d %H:%M:%S",times = None): + ''' + @name 格式时间转时间戳 + @author hwliang<2021-08-17> + @param format 时间格式 + @param times 时间 + @return int + ''' + ts = time.strptime(times, "%Y-%m-%d %H:%M:%S") + return time.mktime(ts) + + +def get_glibc_version(): + ''' + @name 获取glibc版本 + @author hwliang<2021-08-17> + @return string + ''' + try: + cmd_result = ExecShell("ldd --version")[0] + if not cmd_result: return '' + glibc_version = cmd_result.split("\n")[0].split()[-1] + except: + return '' + return glibc_version + + +def is_apache_nginx(): + ''' + @name 是否是apache或nginx + @author hwliang<2021-08-17> + @return bool + ''' + setup_path = get_setup_path() + return os.path.exists(setup_path + '/apache') or os.path.exists(setup_path + '/nginx') diff --git a/class/pyotp/__init__.py b/class/pyotp/__init__.py new file mode 100644 index 00000000..b400ff4f --- /dev/null +++ b/class/pyotp/__init__.py @@ -0,0 +1,23 @@ +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from pyotp.hotp import HOTP # noqa +from pyotp.otp import OTP # noqa +from pyotp.totp import TOTP # noqa +from . import utils # noqa + +def random_base32(length=16, random=None, + chars=list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')): + + # Use secrets module if available (Python version >= 3.6) per PEP 506 + try: + import secrets + random = secrets.SystemRandom() + except ImportError: + import random as _random + random = _random.SystemRandom() + + return ''.join( + random.choice(chars) + for _ in range(length) + ) diff --git a/class/pyotp/compat.py b/class/pyotp/compat.py new file mode 100644 index 00000000..56499098 --- /dev/null +++ b/class/pyotp/compat.py @@ -0,0 +1,10 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +import sys + +USING_PYTHON2 = True if sys.version_info < (3, 0) else False + +if USING_PYTHON2: + str = unicode # noqa +else: + str = str diff --git a/class/pyotp/hotp.py b/class/pyotp/hotp.py new file mode 100644 index 00000000..bb9e3f1d --- /dev/null +++ b/class/pyotp/hotp.py @@ -0,0 +1,58 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +from . import utils +from .otp import OTP +from .compat import str + +class HOTP(OTP): + """ + Handler for HMAC-based OTP counters. + """ + def at(self, count): + """ + Generates the OTP for the given count. + + :param count: the OTP HMAC counter + :type count: int + :returns: OTP + :rtype: str + """ + return self.generate_otp(count) + + def verify(self, otp, counter): + """ + Verifies the OTP passed in against the current counter OTP. + + :param otp: the OTP to check against + :type otp: str + :param count: the OTP HMAC counter + :type count: int + """ + return utils.strings_equal(str(otp), str(self.at(counter))) + + def provisioning_uri(self, name, initial_count=0, issuer_name=None): + """ + Returns the provisioning URI for the OTP. This can then be + encoded in a QR Code and used to provision an OTP app like + Google Authenticator. + + See also: + https://github.com/google/google-authenticator/wiki/Key-Uri-Format + + :param name: name of the user account + :type name: str + :param initial_count: starting HMAC counter value, defaults to 0 + :type initial_count: int + :param issuer_name: the name of the OTP issuer; this will be the + organization title of the OTP entry in Authenticator + :returns: provisioning URI + :rtype: str + """ + return utils.build_uri( + self.secret, + name, + initial_count=initial_count, + issuer_name=issuer_name, + algorithm=self.digest().name, + digits=self.digits + ) diff --git a/class/pyotp/otp.py b/class/pyotp/otp.py new file mode 100644 index 00000000..45afcf3d --- /dev/null +++ b/class/pyotp/otp.py @@ -0,0 +1,66 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +import base64 +import hashlib +import hmac +from .compat import str + +class OTP(object): + """ + Base class for OTP handlers. + """ + def __init__(self, s, digits=6, digest=hashlib.sha1): + """ + :param s: secret in base32 format + :type s: str + :param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more. + :type digits: int + :param digest: digest function to use in the HMAC (expected to be sha1) + :type digest: callable + """ + self.digits = digits + self.digest = digest + self.secret = s + + def generate_otp(self, input): + """ + :param input: the HMAC counter value to use as the OTP input. + Usually either the counter, or the computed integer based on the Unix timestamp + :type input: int + """ + if input < 0: + raise ValueError('input must be positive integer') + hasher = hmac.new(self.byte_secret(), self.int_to_bytestring(input), self.digest) + hmac_hash = bytearray(hasher.digest()) + offset = hmac_hash[-1] & 0xf + code = ((hmac_hash[offset] & 0x7f) << 24 | + (hmac_hash[offset + 1] & 0xff) << 16 | + (hmac_hash[offset + 2] & 0xff) << 8 | + (hmac_hash[offset + 3] & 0xff)) + str_code = str(code % 10 ** self.digits) + while len(str_code) < self.digits: + str_code = '0' + str_code + + return str_code + + def byte_secret(self): + missing_padding = len(self.secret) % 8 + if missing_padding != 0: + self.secret += '=' * (8 - missing_padding) + return base64.b32decode(self.secret, casefold=True) + + @staticmethod + def int_to_bytestring(i, padding=8): + """ + Turns an integer to the OATH specified + bytestring, which is fed to the HMAC + along with the secret + """ + result = bytearray() + while i != 0: + result.append(i & 0xFF) + i >>= 8 + # It's necessary to convert the final result from bytearray to bytes + # because the hmac functions in python 2.6 and 3.3 don't work with + # bytearray + return bytes(bytearray(reversed(result)).rjust(padding, b'\0')) diff --git a/class/pyotp/totp.py b/class/pyotp/totp.py new file mode 100644 index 00000000..50faf8bd --- /dev/null +++ b/class/pyotp/totp.py @@ -0,0 +1,92 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +import datetime +import time + +from . import utils +from .otp import OTP +from .compat import str + +class TOTP(OTP): + """ + Handler for time-based OTP counters. + """ + def __init__(self, *args, **kwargs): + """ + :param interval: the time interval in seconds + for OTP. This defaults to 30. + :type interval: int + """ + self.interval = kwargs.pop('interval', 30) + super(TOTP, self).__init__(*args, **kwargs) + + def at(self, for_time, counter_offset=0): + """ + Accepts either a Unix timestamp integer or a datetime object. + + :param for_time: the time to generate an OTP for + :type for_time: int or datetime + :param counter_offset: the amount of ticks to add to the time counter + :returns: OTP value + :rtype: str + """ + if not isinstance(for_time, datetime.datetime): + for_time = datetime.datetime.fromtimestamp(int(for_time)) + return self.generate_otp(self.timecode(for_time) + counter_offset) + + def now(self): + """ + Generate the current time OTP + + :returns: OTP value + :rtype: str + """ + return self.generate_otp(self.timecode(datetime.datetime.now())) + + def verify(self, otp, for_time=None, valid_window=0): + """ + Verifies the OTP passed in against the current time OTP. + + :param otp: the OTP to check against + :type otp: str + :param for_time: Time to check OTP at (defaults to now) + :type for_time: int or datetime + :param valid_window: extends the validity to this many counter ticks before and after the current one + :type valid_window: int + :returns: True if verification succeeded, False otherwise + :rtype: bool + """ + if for_time is None: + for_time = datetime.datetime.now() + + if valid_window: + for i in range(-valid_window, valid_window + 1): + if utils.strings_equal(str(otp), str(self.at(for_time, i))): + return True + return False + + return utils.strings_equal(str(otp), str(self.at(for_time))) + + def provisioning_uri(self, name, issuer_name=None): + """ + Returns the provisioning URI for the OTP. This can then be + encoded in a QR Code and used to provision an OTP app like + Google Authenticator. + + See also: + https://github.com/google/google-authenticator/wiki/Key-Uri-Format + + :param name: name of the user account + :type name: str + :param issuer_name: the name of the OTP issuer; this will be the + organization title of the OTP entry in Authenticator + :returns: provisioning URI + :rtype: str + """ + return utils.build_uri(self.secret, name, issuer_name=issuer_name, + algorithm=self.digest().name, + digits=self.digits, period=self.interval) + + def timecode(self, for_time): + i = time.mktime(for_time.timetuple()) + return int(i / self.interval) diff --git a/class/pyotp/utils.py b/class/pyotp/utils.py new file mode 100644 index 00000000..ebfe3adb --- /dev/null +++ b/class/pyotp/utils.py @@ -0,0 +1,109 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +import unicodedata +try: + from itertools import izip_longest +except ImportError: + from itertools import zip_longest as izip_longest + +try: + from urllib.parse import quote, urlencode +except ImportError: + from urllib import quote, urlencode + + +def build_uri(secret, name, initial_count=None, issuer_name=None, + algorithm=None, digits=None, period=None): + """ + Returns the provisioning URI for the OTP; works for either TOTP or HOTP. + + This can then be encoded in a QR Code and used to provision the Google + Authenticator app. + + For module-internal use. + + See also: + https://github.com/google/google-authenticator/wiki/Key-Uri-Format + + :param secret: the hotp/totp secret used to generate the URI + :type secret: str + :param name: name of the account + :type name: str + :param initial_count: starting counter value, defaults to None. + If none, the OTP type will be assumed as TOTP. + :type initial_count: int + :param issuer_name: the name of the OTP issuer; this will be the + organization title of the OTP entry in Authenticator + :type issuer_name: str + :param algorithm: the algorithm used in the OTP generation. + :type algorithm: str + :param digits: the length of the OTP generated code. + :type digits: int + :param period: the number of seconds the OTP generator is set to + expire every code. + :type period: int + :returns: provisioning uri + :rtype: str + """ + # initial_count may be 0 as a valid param + is_initial_count_present = (initial_count is not None) + + # Handling values different from defaults + is_algorithm_set = (algorithm is not None and algorithm != 'sha1') + is_digits_set = (digits is not None and digits != 6) + is_period_set = (period is not None and period != 30) + + otp_type = 'hotp' if is_initial_count_present else 'totp' + base_uri = 'otpauth://{0}/{1}?{2}' + + url_args = {'secret': secret} + + label = quote(name) + if issuer_name is not None: + label = quote(issuer_name) + ':' + label + url_args['issuer'] = issuer_name + + if is_initial_count_present: + url_args['counter'] = initial_count + if is_algorithm_set: + url_args['algorithm'] = algorithm.upper() + if is_digits_set: + url_args['digits'] = digits + if is_period_set: + url_args['period'] = period + + uri = base_uri.format(otp_type, label, urlencode(url_args).replace("+", "%20")) + return uri + + +def _compare_digest(s1, s2): + differences = 0 + for c1, c2 in izip_longest(s1, s2): + if c1 is None or c2 is None: + differences = 1 + continue + differences |= ord(c1) ^ ord(c2) + return differences == 0 + + +try: + # Python 3.3+ and 2.7.7+ include a timing-attack-resistant + # comparison function, which is probably more reliable than ours. + # Use it if available. + from hmac import compare_digest +except ImportError: + compare_digest = _compare_digest + + +def strings_equal(s1, s2): + """ + Timing-attack resistant string comparison. + + Normal comparison using == will short-circuit on the first mismatching + character. This avoids that by scanning the whole string, though we + still reveal to a timing attack whether the strings are the same + length. + """ + s1 = unicodedata.normalize('NFKC', s1) + s2 = unicodedata.normalize('NFKC', s2) + return compare_digest(s1.encode("utf-8"), s2.encode("utf-8")) diff --git a/class/sewer/__init__.py b/class/sewer/__init__.py new file mode 100644 index 00000000..f908c2fb --- /dev/null +++ b/class/sewer/__init__.py @@ -0,0 +1,11 @@ +from .client import Client # noqa: F401 + +from .dns_providers import BaseDns # noqa: F401 +from .dns_providers import AuroraDns # noqa: F401 +from .dns_providers import CloudFlareDns # noqa: F401 +from .dns_providers import AcmeDnsDns # noqa: F401 +from .dns_providers import AliyunDns # noqa:F401 +from .dns_providers import HurricaneDns # noqa:F401 +from .dns_providers import RackspaceDns # noqa:F401 +from .dns_providers import DNSPodDns +from .dns_providers import DuckDNSDns diff --git a/class/sewer/__version__.py b/class/sewer/__version__.py new file mode 100644 index 00000000..df18c0ac --- /dev/null +++ b/class/sewer/__version__.py @@ -0,0 +1,7 @@ +__title__ = "sewer" +__description__ = "Sewer is a programmatic Lets Encrypt(ACME) client" +__url__ = "https://github.com/komuw/sewer" +__version__ = "0.7.2" +__author__ = "komuW" +__author_email__ = "komuw05@gmail.com" +__license__ = "MIT" diff --git a/class/sewer/cli.py b/class/sewer/cli.py new file mode 100644 index 00000000..84947fa9 --- /dev/null +++ b/class/sewer/cli.py @@ -0,0 +1,333 @@ +import os +import logging +import argparse + +from . import Client +from . import __version__ as sewer_version +from .config import ACME_DIRECTORY_URL_STAGING, ACME_DIRECTORY_URL_PRODUCTION + + +def main(): + """ + Usage: + 1. To get a new certificate: + CLOUDFLARE_EMAIL=example@example.com \ + CLOUDFLARE_API_KEY=api-key \ + sewer \ + --dns cloudflare \ + --domain example.com \ + --action run + + 2. To renew a certificate: + CLOUDFLARE_EMAIL=example@example.com \ + CLOUDFLARE_API_KEY=api-key \ + sewer \ + --account_key /path/to/your/account.key \ + --dns cloudflare \ + --domain example.com \ + --action renew + """ + parser = argparse.ArgumentParser( + prog="sewer", + description="""Sewer is a Let's Encrypt(ACME) client. + Example usage:: + CLOUDFLARE_EMAIL=example@example.com \ + CLOUDFLARE_API_KEY=api-key \ + sewer \ + --dns cloudflare \ + --domain example.com \ + --action run""", + ) + parser.add_argument( + "--version", + action="version", + version="%(prog)s {version}".format(version=sewer_version.__version__), + help="The currently installed sewer version.", + ) + parser.add_argument( + "--account_key", + type=argparse.FileType("r"), + required=False, + help="The path to your letsencrypt/acme account key. \ + eg: --account_key /home/myaccount.key", + ) + parser.add_argument( + "--certificate_key", + type=argparse.FileType("r"), + required=False, + help="The path to your certificate key. \ + eg: --certificate_key /home/mycertificate.key", + ) + parser.add_argument( + "--dns", + type=str, + required=True, + choices=[ + "cloudflare", + "aurora", + "acmedns", + "aliyun", + "hurricane", + "rackspace", + "dnspod", + "duckdns", + ], + help="The name of the dns provider that you want to use.", + ) + parser.add_argument( + "--domain", + type=str, + required=True, + help="The domain/subdomain name for which \ + you want to get/renew certificate for. \ + wildcards are also supported \ + eg: --domain example.com", + ) + parser.add_argument( + "--alt_domains", + type=str, + required=False, + default=[], + nargs="*", + help="A list of alternative domain/subdomain name/s(if any) for which \ + you want to get/renew certificate for. \ + eg: --alt_domains www.example.com blog.example.com", + ) + parser.add_argument( + "--bundle_name", + type=str, + required=False, + help="The name to use for certificate \ + certificate key and account key. Default is name of domain.", + ) + parser.add_argument( + "--endpoint", + type=str, + required=False, + default="production", + choices=["production", "staging"], + help="Whether to use letsencrypt/acme production/live endpoints \ + or staging endpoints. production endpoints are used by default. \ + eg: --endpoint staging", + ) + parser.add_argument( + "--email", + type=str, + required=False, + help="Email to be used for registration and recovery. \ + eg: --email me@example.com", + ) + parser.add_argument( + "--action", + type=str, + required=True, + choices=["run", "renew"], + help="The action that you want to perform. \ + Either run (get a new certificate) or renew (renew a certificate). \ + eg: --action run", + ) + parser.add_argument( + "--out_dir", + type=str, + required=False, + default=os.getcwd(), + help="""The dir where the certificate and keys file will be stored. + default: The directory you run sewer command. + eg: --out_dir /data/ssl/ + """, + ) + parser.add_argument( + "--loglevel", + type=str, + required=False, + default="INFO", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + help="The log level to output log messages at. \ + eg: --loglevel DEBUG", + ) + + args = parser.parse_args() + + dns_provider = args.dns + domain = args.domain + alt_domains = args.alt_domains + action = args.action + account_key = args.account_key + certificate_key = args.certificate_key + bundle_name = args.bundle_name + endpoint = args.endpoint + email = args.email + loglevel = args.loglevel + out_dir = args.out_dir + + # Make sure the output dir user specified is writable + if not os.access(out_dir, os.W_OK): + raise OSError("The dir '{0}' is not writable".format(out_dir)) + + logger = logging.getLogger() + handler = logging.StreamHandler() + formatter = logging.Formatter("%(message)s") + handler.setFormatter(formatter) + if not logger.handlers: + logger.addHandler(handler) + logger.setLevel(loglevel) + + if account_key: + account_key = account_key.read() + if certificate_key: + certificate_key = certificate_key.read() + if bundle_name: + file_name = bundle_name + else: + file_name = "{0}".format(domain) + if endpoint == "staging": + ACME_DIRECTORY_URL = ACME_DIRECTORY_URL_STAGING + else: + ACME_DIRECTORY_URL = ACME_DIRECTORY_URL_PRODUCTION + + if dns_provider == "cloudflare": + from . import CloudFlareDns + + try: + CLOUDFLARE_EMAIL = os.environ["CLOUDFLARE_EMAIL"] + CLOUDFLARE_API_KEY = os.environ["CLOUDFLARE_API_KEY"] + + dns_class = CloudFlareDns( + CLOUDFLARE_EMAIL=CLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY=CLOUDFLARE_API_KEY + ) + logger.info("chosen_dns_provider. Using {0} as dns provider.".format(dns_provider)) + except KeyError as e: + logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e))) + raise + + elif dns_provider == "aurora": + from . import AuroraDns + + try: + AURORA_API_KEY = os.environ["AURORA_API_KEY"] + AURORA_SECRET_KEY = os.environ["AURORA_SECRET_KEY"] + + dns_class = AuroraDns( + AURORA_API_KEY=AURORA_API_KEY, AURORA_SECRET_KEY=AURORA_SECRET_KEY + ) + logger.info("chosen_dns_provider. Using {0} as dns provider.".format(dns_provider)) + except KeyError as e: + logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e))) + raise + + elif dns_provider == "acmedns": + from . import AcmeDnsDns + + try: + ACME_DNS_API_USER = os.environ["ACME_DNS_API_USER"] + ACME_DNS_API_KEY = os.environ["ACME_DNS_API_KEY"] + ACME_DNS_API_BASE_URL = os.environ["ACME_DNS_API_BASE_URL"] + + dns_class = AcmeDnsDns( + ACME_DNS_API_USER=ACME_DNS_API_USER, + ACME_DNS_API_KEY=ACME_DNS_API_KEY, + ACME_DNS_API_BASE_URL=ACME_DNS_API_BASE_URL, + ) + logger.info("chosen_dns_provider. Using {0} as dns provider.".format(dns_provider)) + except KeyError as e: + logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e))) + raise + elif dns_provider == "aliyun": + from . import AliyunDns + + try: + aliyun_ak = os.environ["ALIYUN_AK_ID"] + aliyun_secret = os.environ["ALIYUN_AK_SECRET"] + aliyun_endpoint = os.environ.get("ALIYUN_ENDPOINT", "cn-beijing") + dns_class = AliyunDns(aliyun_ak, aliyun_secret, aliyun_endpoint) + logger.info("chosen_dns_provider. Using {0} as dns provider.".format(dns_provider)) + except KeyError as e: + logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e))) + raise + elif dns_provider == "hurricane": + from . import HurricaneDns + + try: + he_username = os.environ["HURRICANE_USERNAME"] + he_password = os.environ["HURRICANE_PASSWORD"] + dns_class = HurricaneDns(he_username, he_password) + logger.info("chosen_dns_provider. Using {0} as dns provider.".format(dns_provider)) + except KeyError as e: + logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e))) + raise + elif dns_provider == "rackspace": + from . import RackspaceDns + + try: + RACKSPACE_USERNAME = os.environ["RACKSPACE_USERNAME"] + RACKSPACE_API_KEY = os.environ["RACKSPACE_API_KEY"] + dns_class = RackspaceDns(RACKSPACE_USERNAME, RACKSPACE_API_KEY) + logger.info("chosen_dns_prover. Using {0} as dns provider. ".format(dns_provider)) + except KeyError as e: + logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e))) + raise + elif dns_provider == "dnspod": + from . import DNSPodDns + + try: + DNSPOD_ID = os.environ["DNSPOD_ID"] + DNSPOD_API_KEY = os.environ["DNSPOD_API_KEY"] + dns_class = DNSPodDns(DNSPOD_ID, DNSPOD_API_KEY) + logger.info("chosen_dns_prover. Using {0} as dns provider. ".format(dns_provider)) + except KeyError as e: + logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e))) + raise + elif dns_provider == "duckdns": + from . import DuckDNSDns + + try: + duckdns_token = os.environ["DUCKDNS_TOKEN"] + + dns_class = DuckDNSDns(duckdns_token=duckdns_token) + logger.info("chosen_dns_provider. Using {0} as dns provider.".format(dns_provider)) + except KeyError as e: + logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e))) + raise + else: + raise ValueError("The dns provider {0} is not recognised.".format(dns_provider)) + + client = Client( + domain_name=domain, + dns_class=dns_class, + domain_alt_names=alt_domains, + contact_email=email, + account_key=account_key, + certificate_key=certificate_key, + ACME_DIRECTORY_URL=ACME_DIRECTORY_URL, + LOG_LEVEL=loglevel, + ) + certificate_key = client.certificate_key + account_key = client.account_key + + # prepare file path + account_key_file_path = os.path.join(out_dir, "{0}.account.key".format(file_name)) + crt_file_path = os.path.join(out_dir, "{0}.crt".format(file_name)) + crt_key_file_path = os.path.join(out_dir, "{0}.key".format(file_name)) + + # write out account_key in out_dir directory + with open(account_key_file_path, "w") as account_file: + account_file.write(account_key) + logger.info("account key succesfully written to {0}.".format(account_key_file_path)) + + if action == "renew": + message = "Certificate Succesfully renewed. The certificate, certificate key and account key have been saved in the current directory" + certificate = client.renew() + else: + message = "Certificate Succesfully issued. The certificate, certificate key and account key have been saved in the current directory" + certificate = client.cert() + + # write out certificate and certificate key in out_dir directory + with open(crt_file_path, "w") as certificate_file: + certificate_file.write(certificate) + with open(crt_key_file_path, "w") as certificate_key_file: + certificate_key_file.write(certificate_key) + + logger.info("certificate succesfully written to {0}.".format(crt_file_path)) + logger.info("certificate key succesfully written to {0}.".format(crt_key_file_path)) + + logger.info("the_end. {0}".format(message)) diff --git a/class/sewer/client.py b/class/sewer/client.py new file mode 100644 index 00000000..433daf7a --- /dev/null +++ b/class/sewer/client.py @@ -0,0 +1,634 @@ +import time +import copy +import json +import base64 +import hashlib +import logging +import binascii +import platform +import sys +import http_requests as requests +import OpenSSL +import cryptography +from . import __version__ as sewer_version +from .config import ACME_DIRECTORY_URL_PRODUCTION + + + +class Client(object): + """ + todo: improve documentation. + + usage: + import sewer + dns_class = sewer.CloudFlareDns(CLOUDFLARE_EMAIL='example@example.com', + CLOUDFLARE_API_KEY='nsa-grade-api-key') + + 1. to create a new certificate. + client = sewer.Client(domain_name='example.com', + dns_class=dns_class) + certificate = client.cert() + certificate_key = client.certificate_key + account_key = client.account_key + + with open('certificate.crt', 'w') as certificate_file: + certificate_file.write(certificate) + + with open('certificate.key', 'w') as certificate_key_file: + certificate_key_file.write(certificate_key) + + + 2. to renew a certificate: + with open('account_key.key', 'r') as account_key_file: + account_key = account_key_file.read() + + client = sewer.Client(domain_name='example.com', + dns_class=dns_class, + account_key=account_key) + certificate = client.renew() + certificate_key = client.certificate_key + + todo: + - handle more exceptions + """ + + def __init__( + self, + domain_name, + dns_class, + domain_alt_names=None, + contact_email=None, + account_key=None, + certificate_key=None, + bits=2048, + digest="sha256", + ACME_REQUEST_TIMEOUT=7, + ACME_AUTH_STATUS_WAIT_PERIOD=8, + ACME_AUTH_STATUS_MAX_CHECKS=3, + ACME_DIRECTORY_URL=ACME_DIRECTORY_URL_PRODUCTION, + LOG_LEVEL="INFO", + ): + + self.domain_name = domain_name + self.dns_class = dns_class + if not domain_alt_names: + domain_alt_names = [] + self.domain_alt_names = domain_alt_names + self.domain_alt_names = list(set(self.domain_alt_names)) + self.contact_email = contact_email + self.bits = bits + self.digest = digest + self.ACME_REQUEST_TIMEOUT = ACME_REQUEST_TIMEOUT + self.ACME_AUTH_STATUS_WAIT_PERIOD = ACME_AUTH_STATUS_WAIT_PERIOD + self.ACME_AUTH_STATUS_MAX_CHECKS = ACME_AUTH_STATUS_MAX_CHECKS + self.ACME_DIRECTORY_URL = ACME_DIRECTORY_URL + self.LOG_LEVEL = LOG_LEVEL.upper() + + try: + self.all_domain_names = copy.copy(self.domain_alt_names) + self.all_domain_names.insert(0, self.domain_name) + self.domain_alt_names = list(set(self.domain_alt_names)) + + self.User_Agent = self.get_user_agent() + acme_endpoints = self.get_acme_endpoints().json() + self.ACME_GET_NONCE_URL = acme_endpoints["newNonce"] + self.ACME_TOS_URL = acme_endpoints["meta"]["termsOfService"] + self.ACME_KEY_CHANGE_URL = acme_endpoints["keyChange"] + self.ACME_NEW_ACCOUNT_URL = acme_endpoints["newAccount"] + self.ACME_NEW_ORDER_URL = acme_endpoints["newOrder"] + self.ACME_REVOKE_CERT_URL = acme_endpoints["revokeCert"] + + # unique account identifier + # https://tools.ietf.org/html/draft-ietf-acme-acme#section-6.2 + self.kid = None + + self.certificate_key = certificate_key or self.create_certificate_key() + self.csr = self.create_csr() + + if not account_key: + self.account_key = self.create_account_key() + self.PRIOR_REGISTERED = False + else: + self.account_key = account_key + self.PRIOR_REGISTERED = True + + except Exception as e: + raise e + + @staticmethod + def log_response(response): + """ + renders response as json or as a string + """ + # TODO: use this to handle all response logs. + try: + log_body = response.json() + except ValueError: + log_body = response.content[:30] + return log_body + + @staticmethod + def get_user_agent(): + return "python-requests/{requests_version} ({system}: {machine}) sewer {sewer_version} ({sewer_url})".format( + requests_version=requests.__version__, + system=platform.system(), + machine=platform.machine(), + sewer_version=sewer_version.__version__, + sewer_url=sewer_version.__url__, + ) + + def get_acme_endpoints(self): + headers = {"User-Agent": self.User_Agent} + get_acme_endpoints = requests.get( + self.ACME_DIRECTORY_URL, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False + ) + if get_acme_endpoints.status_code not in [200, 201]: + raise ValueError( + "Error while getting Acme endpoints: status_code={status_code} response={response}".format( + status_code=get_acme_endpoints.status_code, + response=self.log_response(get_acme_endpoints), + ) + ) + return get_acme_endpoints + + def create_certificate_key(self): + return self.create_key().decode() + + def create_account_key(self): + return self.create_key().decode() + + def create_key(self, key_type=OpenSSL.crypto.TYPE_RSA): + key = OpenSSL.crypto.PKey() + key.generate_key(key_type, self.bits) + private_key = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) + return private_key + + def create_csr(self): + """ + https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.4 + The CSR is sent in the base64url-encoded version of the DER format. (NB: this + field uses base64url, and does not include headers, it is different from PEM.) + """ + X509Req = OpenSSL.crypto.X509Req() + X509Req.get_subject().CN = self.domain_name + + if self.domain_alt_names: + SAN = "DNS:{0}, ".format(self.domain_name).encode("utf8") + ", ".join( + "DNS:" + i for i in self.domain_alt_names + ).encode("utf8") + else: + SAN = "DNS:{0}".format(self.domain_name).encode("utf8") + + X509Req.add_extensions( + [ + OpenSSL.crypto.X509Extension( + "subjectAltName".encode("utf8"), critical=False, value=SAN + ) + ] + ) + pk = OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, self.certificate_key.encode() + ) + X509Req.set_pubkey(pk) + X509Req.set_version(2) + X509Req.sign(pk, self.digest) + return OpenSSL.crypto.dump_certificate_request(OpenSSL.crypto.FILETYPE_ASN1, X509Req) + + def acme_register(self): + """ + https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.3 + The server creates an account and stores the public key used to + verify the JWS (i.e., the "jwk" element of the JWS header) to + authenticate future requests from the account. + The server returns this account object in a 201 (Created) response, with the account URL + in a Location header field. + This account URL will be used in subsequest requests to ACME, as the "kid" value in the acme header. + If the server already has an account registered with the provided + account key, then it MUST return a response with a 200 (OK) status + code and provide the URL of that account in the Location header field. + If there is an existing account with the new key + provided, then the server SHOULD use status code 409 (Conflict) and + provide the URL of that account in the Location header field + """ + if self.PRIOR_REGISTERED: + payload = {"onlyReturnExisting": True} + elif self.contact_email: + payload = { + "termsOfServiceAgreed": True, + "contact": ["mailto:{0}".format(self.contact_email)], + } + else: + payload = {"termsOfServiceAgreed": True} + + url = self.ACME_NEW_ACCOUNT_URL + acme_register_response = self.make_signed_acme_request(url=url, payload=payload) + + if acme_register_response.status_code not in [201, 200, 409]: + raise ValueError( + "Error while registering: status_code={status_code} response={response}".format( + status_code=acme_register_response.status_code, + response=self.log_response(acme_register_response), + ) + ) + + kid = acme_register_response.headers["Location"] + setattr(self, "kid", kid) + + return acme_register_response + + def apply_for_cert_issuance(self): + """ + https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.4 + The order object returned by the server represents a promise that if + the client fulfills the server's requirements before the "expires" + time, then the server will be willing to finalize the order upon + request and issue the requested certificate. In the order object, + any authorization referenced in the "authorizations" array whose + status is "pending" represents an authorization transaction that the + client must complete before the server will issue the certificate. + + Once the client believes it has fulfilled the server's requirements, + it should send a POST request to the order resource's finalize URL. + The POST body MUST include a CSR: + + The date values seem to be ignored by LetsEncrypt although they are + in the ACME draft spec; https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.4 + """ + identifiers = [] + for domain_name in self.all_domain_names: + identifiers.append({"type": "dns", "value": domain_name}) + + payload = {"identifiers": identifiers} + url = self.ACME_NEW_ORDER_URL + apply_for_cert_issuance_response = self.make_signed_acme_request(url=url, payload=payload) + + if apply_for_cert_issuance_response.status_code != 201: + raise ValueError( + "Error applying for certificate issuance: status_code={status_code} response={response}".format( + status_code=apply_for_cert_issuance_response.status_code, + response=self.log_response(apply_for_cert_issuance_response), + ) + ) + + apply_for_cert_issuance_response_json = apply_for_cert_issuance_response.json() + finalize_url = apply_for_cert_issuance_response_json["finalize"] + authorizations = apply_for_cert_issuance_response_json["authorizations"] + + return authorizations, finalize_url + + def get_identifier_authorization(self, url): + """ + https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.5 + When a client receives an order from the server it downloads the + authorization resources by sending GET requests to the indicated + URLs. If the client initiates authorization using a request to the + new authorization resource, it will have already received the pending + authorization object in the response to that request. + + This is also where we get the challenges/tokens. + """ + headers = {"User-Agent": self.User_Agent} + get_identifier_authorization_response = requests.get( + url, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False + ) + if get_identifier_authorization_response.status_code not in [200, 201]: + raise ValueError( + "Error getting identifier authorization: status_code={status_code} response={response}".format( + status_code=get_identifier_authorization_response.status_code, + response=self.log_response(get_identifier_authorization_response), + ) + ) + res = get_identifier_authorization_response.json() + domain = res["identifier"]["value"] + wildcard = res.get("wildcard") + if wildcard: + domain = "*." + domain + + for i in res["challenges"]: + if i["type"] == "dns-01": + dns_challenge = i + dns_token = dns_challenge["token"] + dns_challenge_url = dns_challenge["url"] + identifier_auth = { + "domain": domain, + "url": url, + "wildcard": wildcard, + "dns_token": dns_token, + "dns_challenge_url": dns_challenge_url, + } + + + return identifier_auth + + def get_keyauthorization(self, dns_token): + acme_header_jwk_json = json.dumps( + self.get_acme_header("GET_THUMBPRINT")["jwk"], sort_keys=True, separators=(",", ":") + ) + acme_thumbprint = self.calculate_safe_base64( + hashlib.sha256(acme_header_jwk_json.encode("utf8")).digest() + ) + acme_keyauthorization = "{0}.{1}".format(dns_token, acme_thumbprint) + base64_of_acme_keyauthorization = self.calculate_safe_base64( + hashlib.sha256(acme_keyauthorization.encode("utf8")).digest() + ) + + return acme_keyauthorization, base64_of_acme_keyauthorization + + def check_authorization_status(self, authorization_url, desired_status=None): + """ + https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.5.1 + To check on the status of an authorization, the client sends a GET(polling) + request to the authorization URL, and the server responds with the + current authorization object. + + https://tools.ietf.org/html/draft-ietf-acme-acme#section-8.2 + Clients SHOULD NOT respond to challenges until they believe that the + server's queries will succeed. If a server's initial validation + query fails, the server SHOULD retry[intended to address things like propagation delays in + HTTP/DNS provisioning] the query after some time. + The server MUST provide information about its retry state to the + client via the "errors" field in the challenge and the Retry-After + """ + desired_status = desired_status or ["pending", "valid","invalid"] + number_of_checks = 0 + while True: + headers = {"User-Agent": self.User_Agent} + check_authorization_status_response = requests.get( + authorization_url, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False + ) + a_auth = check_authorization_status_response.json() + authorization_status = a_auth["status"] + number_of_checks = number_of_checks + 1 + if number_of_checks == self.ACME_AUTH_STATUS_MAX_CHECKS: + raise StopIteration( + "Checks done={0}. Max checks allowed={1}. Interval between checks={2}seconds. >>>> {3}".format( + number_of_checks, + self.ACME_AUTH_STATUS_MAX_CHECKS, + self.ACME_AUTH_STATUS_WAIT_PERIOD, + json.dumps(a_auth) + ) + ) + if authorization_status in desired_status: + if authorization_status == "invalid": + try: + import panelLets + if 'error' in a_auth['challenges'][0]: + ret_title = a_auth['challenges'][0]['error']['detail'] + elif 'error' in a_auth['challenges'][1]: + ret_title = a_auth['challenges'][1]['error']['detail'] + elif 'error' in a_auth['challenges'][2]: + ret_title = a_auth['challenges'][2]['error']['detail'] + else: + ret_title = str(a_auth) + ret_title = panelLets.panelLets().get_error(ret_title) + except: + ret_title = str(a_auth) + raise StopIteration( + "{0} >>>> {1}".format( + ret_title, + json.dumps(a_auth) + ) + ) + break + else: + # for any other status, sleep then retry + time.sleep(self.ACME_AUTH_STATUS_WAIT_PERIOD) + + return check_authorization_status_response + + def respond_to_challenge(self, acme_keyauthorization, dns_challenge_url): + """ + https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.5.1 + To prove control of the identifier and receive authorization, the + client needs to respond with information to complete the challenges. + The server is said to "finalize" the authorization when it has + completed one of the validations, by assigning the authorization a + status of "valid" or "invalid". + + Usually, the validation process will take some time, so the client + will need to poll the authorization resource to see when it is finalized. + To check on the status of an authorization, the client sends a GET(polling) + request to the authorization URL, and the server responds with the + current authorization object. + """ + payload = {"keyAuthorization": "{0}".format(acme_keyauthorization)} + respond_to_challenge_response = self.make_signed_acme_request(dns_challenge_url, payload) + + return respond_to_challenge_response + + def send_csr(self, finalize_url): + """ + https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.4 + Once the client believes it has fulfilled the server's requirements, + it should send a POST request(include a CSR) to the order resource's finalize URL. + A request to finalize an order will result in error if the order indicated does not have status "pending", + if the CSR and order identifiers differ, or if the account is not authorized for the identifiers indicated in the CSR. + The CSR is sent in the base64url-encoded version of the DER format(OpenSSL.crypto.FILETYPE_ASN1) + + A valid request to finalize an order will return the order to be finalized. + The client should begin polling the order by sending a + GET request to the order resource to obtain its current state. + """ + payload = {"csr": self.calculate_safe_base64(self.csr)} + send_csr_response = self.make_signed_acme_request(url=finalize_url, payload=payload) + + if send_csr_response.status_code not in [200, 201]: + raise ValueError( + "Error sending csr: status_code={status_code} response={response}".format( + status_code=send_csr_response.status_code, + response=self.log_response(send_csr_response), + ) + ) + send_csr_response_json = send_csr_response.json() + certificate_url = send_csr_response_json["certificate"] + return certificate_url + + def download_certificate(self, certificate_url): + download_certificate_response = self.make_signed_acme_request( + certificate_url, payload="DOWNLOAD_Z_CERTIFICATE" + ) + + if download_certificate_response.status_code not in [200, 201]: + raise ValueError( + "Error fetching signed certificate: status_code={status_code} response={response}".format( + status_code=download_certificate_response.status_code, + response=self.log_response(download_certificate_response), + ) + ) + + + pem_certificate = download_certificate_response.content + if type(pem_certificate) == bytes: pem_certificate = pem_certificate.decode('utf-8') + + return pem_certificate + + def sign_message(self, message): + pk = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, self.account_key.encode()) + return OpenSSL.crypto.sign(pk, message.encode("utf8"), self.digest) + + def get_nonce(self): + """ + https://tools.ietf.org/html/draft-ietf-acme-acme#section-6.4 + Each request to an ACME server must include a fresh unused nonce + in order to protect against replay attacks. + """ + headers = {"User-Agent": self.User_Agent} + response = requests.get( + self.ACME_GET_NONCE_URL, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False + ) + nonce = response.headers["Replay-Nonce"] + return nonce + + @staticmethod + def stringfy_items(payload): + """ + method that takes a dictionary and then converts any keys or values + in that are of type bytes into unicode strings. + This is necessary esp if you want to then turn that dict into a json string. + """ + if isinstance(payload, str): + return payload + + for k, v in payload.items(): + if isinstance(k, bytes): + k = k.decode("utf-8") + if isinstance(v, bytes): + v = v.decode("utf-8") + payload[k] = v + return payload + + @staticmethod + def calculate_safe_base64(un_encoded_data): + """ + takes in a string or bytes + returns a string + """ + if sys.version_info[0] == 3: + if isinstance(un_encoded_data, str): + un_encoded_data = un_encoded_data.encode("utf8") + r = base64.urlsafe_b64encode(un_encoded_data).rstrip(b"=") + return r.decode("utf8") + + def get_acme_header(self, url): + """ + https://tools.ietf.org/html/draft-ietf-acme-acme#section-6.2 + The JWS Protected Header MUST include the following fields: + - "alg" (Algorithm) + - "jwk" (JSON Web Key, only for requests to new-account and revoke-cert resources) + - "kid" (Key ID, for all other requests). gotten from self.ACME_NEW_ACCOUNT_URL + - "nonce". gotten from self.ACME_GET_NONCE_URL + - "url" + """ + header = {"alg": "RS256", "nonce": self.get_nonce(), "url": url} + + if url in [self.ACME_NEW_ACCOUNT_URL, self.ACME_REVOKE_CERT_URL, "GET_THUMBPRINT"]: + private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key( + self.account_key.encode(), + password=None, + backend=cryptography.hazmat.backends.default_backend(), + ) + public_key_public_numbers = private_key.public_key().public_numbers() + # private key public exponent in hex format + exponent = "{0:x}".format(public_key_public_numbers.e) + exponent = "0{0}".format(exponent) if len(exponent) % 2 else exponent + # private key modulus in hex format + modulus = "{0:x}".format(public_key_public_numbers.n) + jwk = { + "kty": "RSA", + "e": self.calculate_safe_base64(binascii.unhexlify(exponent)), + "n": self.calculate_safe_base64(binascii.unhexlify(modulus)), + } + header["jwk"] = jwk + else: + header["kid"] = self.kid + return header + + def make_signed_acme_request(self, url, payload): + headers = {"User-Agent": self.User_Agent} + payload = self.stringfy_items(payload) + + if payload in ["GET_Z_CHALLENGE", "DOWNLOAD_Z_CERTIFICATE"]: + response = requests.get(url, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False) + else: + payload64 = self.calculate_safe_base64(json.dumps(payload)) + protected = self.get_acme_header(url) + protected64 = self.calculate_safe_base64(json.dumps(protected)) + signature = self.sign_message(message="{0}.{1}".format(protected64, payload64)) # bytes + signature64 = self.calculate_safe_base64(signature) # str + data = json.dumps( + {"protected": protected64, "payload": payload64, "signature": signature64} + ) + headers.update({"Content-Type": "application/jose+json"}) + response = requests.post( + url, data=data.encode("utf8"), timeout=self.ACME_REQUEST_TIMEOUT, headers=headers ,verify=False + ) + return response + + def get_certificate(self): + domain_dns_value = "placeholder" + dns_names_to_delete = [] + try: + self.acme_register() + authorizations, finalize_url = self.apply_for_cert_issuance() + responders = [] + for url in authorizations: + identifier_auth = self.get_identifier_authorization(url) + authorization_url = identifier_auth["url"] + dns_name = identifier_auth["domain"] + dns_token = identifier_auth["dns_token"] + dns_challenge_url = identifier_auth["dns_challenge_url"] + + acme_keyauthorization, domain_dns_value = self.get_keyauthorization(dns_token) + self.dns_class.create_dns_record(dns_name, domain_dns_value) + dns_names_to_delete.append( + {"dns_name": dns_name, "domain_dns_value": domain_dns_value} + ) + responders.append( + { + "authorization_url": authorization_url, + "acme_keyauthorization": acme_keyauthorization, + "dns_challenge_url": dns_challenge_url, + } + ) + + # for a case where you want certificates for *.example.com and example.com + # you have to create both dns records AND then respond to the challenge. + # see issues/83 + for i in responders: + # Make sure the authorization is in a status where we can submit a challenge + # response. The authorization can be in the "valid" state before submitting + # a challenge response if there was a previous authorization for these hosts + # that was successfully validated, still cached by the server. + auth_status_response = self.check_authorization_status(i["authorization_url"]) + if auth_status_response.json()["status"] == "pending": + self.respond_to_challenge(i["acme_keyauthorization"], i["dns_challenge_url"]) + + for i in responders: + # Before sending a CSR, we need to make sure the server has completed the + # validation for all the authorizations + self.check_authorization_status(i["authorization_url"], ["valid"]) + + certificate_url = self.send_csr(finalize_url) + certificate = self.download_certificate(certificate_url) + except Exception as e: + raise e + finally: + for i in dns_names_to_delete: + self.dns_class.delete_dns_record(i["dns_name"], i["domain_dns_value"]) + + return certificate + + def cert(self): + """ + convenience method to get a certificate without much hassle + """ + return self.get_certificate() + + def renew(self): + """ + renews a certificate. + A renewal is actually just getting a new certificate. + An issuance request counts as a renewal if it contains the exact same set of hostnames as a previously issued certificate. + https://letsencrypt.org/docs/rate-limits/ + """ + return self.cert() diff --git a/class/sewer/config.py b/class/sewer/config.py new file mode 100644 index 00000000..1c4952df --- /dev/null +++ b/class/sewer/config.py @@ -0,0 +1,2 @@ +ACME_DIRECTORY_URL_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory" +ACME_DIRECTORY_URL_PRODUCTION = "https://acme-v02.api.letsencrypt.org/directory" diff --git a/class/sewer/dns_providers/__init__.py b/class/sewer/dns_providers/__init__.py new file mode 100644 index 00000000..2ff525d1 --- /dev/null +++ b/class/sewer/dns_providers/__init__.py @@ -0,0 +1,9 @@ +from .common import BaseDns # noqa: F401 +from .auroradns import AuroraDns # noqa: F401 +from .cloudflare import CloudFlareDns # noqa: F401 +from .acmedns import AcmeDnsDns # noqa: F401 +from .aliyundns import AliyunDns # noqa: F401 +from .hurricane import HurricaneDns # noqa: F401 +from .rackspace import RackspaceDns # noqa: F401 +from .dnspod import DNSPodDns +from .duckdns import DuckDNSDns diff --git a/class/sewer/dns_providers/acmedns.py b/class/sewer/dns_providers/acmedns.py new file mode 100644 index 00000000..98d0415d --- /dev/null +++ b/class/sewer/dns_providers/acmedns.py @@ -0,0 +1,65 @@ +try: + import urllib.parse as urlparse +except: + import urlparse + +try: + acmedns_dependencies = True + from dns.resolver import Resolver +except ImportError: + acmedns_dependencies = False +import requests + +from . import common + + +class AcmeDnsDns(common.BaseDns): + """ + """ + + dns_provider_name = "acmedns" + + def __init__(self, ACME_DNS_API_USER, ACME_DNS_API_KEY, ACME_DNS_API_BASE_URL): + + if not acmedns_dependencies: + raise ImportError( + """You need to install AcmeDnsDns dependencies. run; pip3 install sewer[acmedns]""" + ) + + self.ACME_DNS_API_USER = ACME_DNS_API_USER + self.ACME_DNS_API_KEY = ACME_DNS_API_KEY + self.HTTP_TIMEOUT = 65 # seconds + + if ACME_DNS_API_BASE_URL[-1] != "/": + self.ACME_DNS_API_BASE_URL = ACME_DNS_API_BASE_URL + "/" + else: + self.ACME_DNS_API_BASE_URL = ACME_DNS_API_BASE_URL + super(AcmeDnsDns, self).__init__() + + def create_dns_record(self, domain_name, domain_dns_value): + # if we have been given a wildcard name, strip wildcard + domain_name = domain_name.lstrip("*.") + + resolver = Resolver(configure=False) + resolver.nameservers = ["8.8.8.8"] + answer = resolver.query("_acme-challenge.{0}.".format(domain_name), "TXT") + subdomain, _ = str(answer.canonical_name).split(".", 1) + + url = urlparse.urljoin(self.ACME_DNS_API_BASE_URL, "update") + headers = {"X-Api-User": self.ACME_DNS_API_USER, "X-Api-Key": self.ACME_DNS_API_KEY} + body = {"subdomain": subdomain, "txt": domain_dns_value} + update_acmedns_dns_record_response = requests.post( + url, headers=headers, json=body, timeout=self.HTTP_TIMEOUT + ) + if update_acmedns_dns_record_response.status_code != 200: + # raise error so that we do not continue to make calls to ACME + # server + raise ValueError( + "Error creating acme-dns dns record: status_code={status_code} response={response}".format( + status_code=update_acmedns_dns_record_response.status_code, + response=self.log_response(update_acmedns_dns_record_response), + ) + ) + + def delete_dns_record(self, domain_name, domain_dns_value): + pass diff --git a/class/sewer/dns_providers/aliyundns.py b/class/sewer/dns_providers/aliyundns.py new file mode 100644 index 00000000..63816b09 --- /dev/null +++ b/class/sewer/dns_providers/aliyundns.py @@ -0,0 +1,199 @@ +import json + +try: + aliyun_dependencies = True + from aliyunsdkcore import client + from aliyunsdkalidns.request.v20150109 import DescribeDomainRecordsRequest + from aliyunsdkalidns.request.v20150109 import AddDomainRecordRequest + from aliyunsdkalidns.request.v20150109 import DeleteDomainRecordRequest +except ImportError: + aliyun_dependencies = False + +from . import common + + +class _ResponseForAliyun(object): + """ + wrapper aliyun resp to the format sewer wanted. + """ + + def __init__(self, status_code=200, content=None, headers=None): + self.status_code = status_code + self.headers = headers or {} + self.content = content or {} + self.content = json.dumps(content) + super(_ResponseForAliyun, self).__init__() + + def json(self): + return json.loads(self.content) + + +class AliyunDns(common.BaseDns): + def __init__(self, key, secret, endpoint="cn-beijing", debug=False): + """ + aliyun dns client + :param str key: access key + :param str secret: access sceret + :param str endpoint: endpoint + :param bool debug: if debug? + """ + super(AliyunDns, self).__init__() + if not aliyun_dependencies: + raise ImportError( + """You need to install aliyunDns dependencies. run; pip3 install sewer[aliyun]""" + ) + self._key = key + self._secret = secret + self._endpoint = endpoint + self._debug = debug + self.clt = client.AcsClient(self._key, self._secret, self._endpoint, debug=self._debug) + + def _send_reqeust(self, request): + """ + send request to aliyun + """ + request.set_accept_format("json") + try: + status, headers, result = self.clt.implementation_of_do_action(request) + result = json.loads(result) + if "Message" in result or "Code" in result: + result["Success"] = False + except Exception as exc: + status, headers, result = 502, {}, '{"Success": false}' + result = json.loads(result) + + return _ResponseForAliyun(status, result, headers) + + def query_recored_items(self, host, zone=None, tipe=None, page=1, psize=200): + request = DescribeDomainRecordsRequest.DescribeDomainRecordsRequest() + request.get_action_name() + request.set_DomainName(host) + request.set_PageNumber(page) + request.set_PageSize(psize) + if zone: + request.set_RRKeyWord(zone) + if tipe: + request.set_TypeKeyWord(tipe) + resp = self._send_reqeust(request) + body = resp.json() + return body + + def query_recored_id(self, root, zone, tipe="TXT"): + """ + find recored + :param str root: root host, like example.com + :param str zone: sub zone, like menduo.example.com + :param str tipe: record tipe, TXT, CNAME, IP. we use TXT + :return str: + """ + record_id = None + recoreds = self.query_recored_items(root, zone, tipe=tipe) + recored_list = recoreds.get("DomainRecords", {}).get("Record", []) + recored_item_list = [i for i in recored_list if i["RR"] == zone] + if len(recored_item_list): + record_id = recored_item_list[0]["RecordId"] + return record_id + + @staticmethod + def extract_zone(domain_name): + """ + extract domain to root, sub, acme_txt + :param str domain_name: the value sewer client passed in, like *.menduo.example.com + :return tuple: root, zone, acme_txt + """ + # if we have been given a wildcard name, strip wildcard + domain_name = domain_name.lstrip("*.") + if domain_name.count(".") > 1: + zone, middle, last = str(domain_name).rsplit(".", 2) + root = ".".join([middle, last]) + acme_txt = "_acme-challenge.%s" % zone + else: + zone = "" + root = domain_name + acme_txt = "_acme-challenge" + return root, zone, acme_txt + + def create_dns_record(self, domain_name, domain_dns_value): + """ + create a dns record + :param str domain_name: the value sewer client passed in, like *.menduo.example.com + :param str domain_dns_value: the value sewer client passed in. + :return _ResponseForAliyun: + """ + root, _, acme_txt = self.extract_zone(domain_name) + + request = AddDomainRecordRequest.AddDomainRecordRequest() + request.set_DomainName(root) + request.set_TTL(600) + request.set_RR(acme_txt) + request.set_Type("TXT") + request.set_Value(domain_dns_value) + resp = self._send_reqeust(request) + + try: + request = AddDomainRecordRequest.AddDomainRecordRequest() + request.set_DomainName(root) + request.set_TTL(600) + request.set_RR('@') + request.set_Type("CAA") + request.set_Value('1 issue letsencrypt.org') + resp = self._send_reqeust(request) + except: + pass + + try: + tmp = acme_txt.split('.') + if len(tmp) > 1: + request = AddDomainRecordRequest.AddDomainRecordRequest() + request.set_DomainName(root) + request.set_TTL(600) + request.set_RR(tmp[-1]) + request.set_Type("CAA") + request.set_Value('1 issue letsencrypt.org') + resp = self._send_reqeust(request) + except: + pass + + + return resp + + def delete_dns_record(self, domain_name, domain_dns_value): + """ + delete a txt record we created just now. + :param str domain_name: the value sewer client passed in, like *.menduo.example.com + :param str domain_dns_value: the value sewer client passed in. we do not use this. + :return _ResponseForAliyun: + :return: + """ + root, _, acme_txt = self.extract_zone(domain_name) + + record_id = self.query_recored_id(root, acme_txt) + if not record_id: + return + + + request = DeleteDomainRecordRequest.DeleteDomainRecordRequest() + request.set_RecordId(record_id) + resp = self._send_reqeust(request) + + try: + record_id = self.query_recored_id(root, '@','CAA') + if record_id: + request = DeleteDomainRecordRequest.DeleteDomainRecordRequest() + request.set_RecordId(record_id) + self._send_reqeust(request) + except: + pass + + try: + tmp = acme_txt.split('.') + if len(tmp) > 1: + record_id = self.query_recored_id(root, tmp[-1],'CAA') + if record_id: + request = DeleteDomainRecordRequest.DeleteDomainRecordRequest() + request.set_RecordId(record_id) + self._send_reqeust(request) + except: + pass + + return resp diff --git a/class/sewer/dns_providers/auroradns.py b/class/sewer/dns_providers/auroradns.py new file mode 100644 index 00000000..d6a6db36 --- /dev/null +++ b/class/sewer/dns_providers/auroradns.py @@ -0,0 +1,73 @@ +# DNS Provider for AuroRa DNS from the dutch hosting provider pcextreme +# https://www.pcextreme.nl/aurora/dns +# Aurora uses libcloud from apache +# https://libcloud.apache.org/ +try: + aurora_dependencies = True + from libcloud.dns.providers import get_driver + from libcloud.dns.types import Provider, RecordType + import tldextract +except ImportError: + aurora_dependencies = False +from . import common + + +class AuroraDns(common.BaseDns): + """ + Todo: re-organize this class so that we make it easier to mock things out to + facilitate better tests. + """ + + dns_provider_name = "aurora" + + def __init__(self, AURORA_API_KEY, AURORA_SECRET_KEY): + + if not aurora_dependencies: + raise ImportError( + """You need to install AuroraDns dependencies. run; pip3 install sewer[aurora]""" + ) + + self.AURORA_API_KEY = AURORA_API_KEY + self.AURORA_SECRET_KEY = AURORA_SECRET_KEY + super(AuroraDns, self).__init__() + + def create_dns_record(self, domain_name, domain_dns_value): + # if we have been given a wildcard name, strip wildcard + domain_name = domain_name.lstrip("*.") + + extractedDomain = tldextract.extract(domain_name) + domainSuffix = extractedDomain.domain + "." + extractedDomain.suffix + + if extractedDomain.subdomain is "": + subDomain = "_acme-challenge" + else: + subDomain = "_acme-challenge." + extractedDomain.subdomain + + cls = get_driver(Provider.AURORADNS) + driver = cls(key=self.AURORA_API_KEY, secret=self.AURORA_SECRET_KEY) + zone = driver.get_zone(domainSuffix) + zone.create_record(name=subDomain, type=RecordType.TXT, data=domain_dns_value) + + return + + def delete_dns_record(self, domain_name, domain_dns_value): + + extractedDomain = tldextract.extract(domain_name) + domainSuffix = extractedDomain.domain + "." + extractedDomain.suffix + + if extractedDomain.subdomain is "": + subDomain = "_acme-challenge" + else: + subDomain = "_acme-challenge." + extractedDomain.subdomain + + cls = get_driver(Provider.AURORADNS) + driver = cls(key=self.AURORA_API_KEY, secret=self.AURORA_SECRET_KEY) + zone = driver.get_zone(domainSuffix) + + records = driver.list_records(zone) + for x in records: + if x.name == subDomain and x.type == "TXT": + record_id = x.id + record = driver.get_record(zone_id=zone.id, record_id=record_id) + driver.delete_record(record) + return diff --git a/class/sewer/dns_providers/cloudflare.py b/class/sewer/dns_providers/cloudflare.py new file mode 100644 index 00000000..8648284a --- /dev/null +++ b/class/sewer/dns_providers/cloudflare.py @@ -0,0 +1,122 @@ +try: + import urllib.parse as urlparse +except: + import urlparse + +import requests + +from . import common + + +class CloudFlareDns(common.BaseDns): + """ + """ + + dns_provider_name = "cloudflare" + + def __init__( + self, + CLOUDFLARE_EMAIL, + CLOUDFLARE_API_KEY, + CLOUDFLARE_API_BASE_URL="https://api.cloudflare.com/client/v4/", + ): + self.CLOUDFLARE_DNS_ZONE_ID = None + self.CLOUDFLARE_EMAIL = CLOUDFLARE_EMAIL + self.CLOUDFLARE_API_KEY = CLOUDFLARE_API_KEY + self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL + self.HTTP_TIMEOUT = 65 # seconds + + if CLOUDFLARE_API_BASE_URL[-1] != "/": + self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL + "/" + else: + self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL + super(CloudFlareDns, self).__init__() + + def find_dns_zone(self, domain_name): + url = urlparse.urljoin(self.CLOUDFLARE_API_BASE_URL, "zones?status=active") + headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY} + find_dns_zone_response = requests.get(url, headers=headers, timeout=self.HTTP_TIMEOUT) + if find_dns_zone_response.status_code != 200: + raise ValueError( + "Error creating cloudflare dns record: status_code={status_code} response={response}".format( + status_code=find_dns_zone_response.status_code, + response=self.log_response(find_dns_zone_response), + ) + ) + + result = find_dns_zone_response.json()["result"] + for i in result: + if i["name"] in domain_name: + setattr(self, "CLOUDFLARE_DNS_ZONE_ID", i["id"]) + if isinstance(self.CLOUDFLARE_DNS_ZONE_ID, type(None)): + raise ValueError( + "Error unable to get DNS zone for domain_name={domain_name}: status_code={status_code} response={response}".format( + domain_name=domain_name, + status_code=find_dns_zone_response.status_code, + response=self.log_response(find_dns_zone_response), + ) + ) + + + def create_dns_record(self, domain_name, domain_dns_value): + # if we have been given a wildcard name, strip wildcard + domain_name = domain_name.lstrip("*.") + self.find_dns_zone(domain_name) + + url = urllib.parse.urljoin( + self.CLOUDFLARE_API_BASE_URL, + "zones/{0}/dns_records".format(self.CLOUDFLARE_DNS_ZONE_ID), + ) + headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY} + body = { + "type": "TXT", + "name": "_acme-challenge" + "." + domain_name + ".", + "content": "{0}".format(domain_dns_value), + } + create_cloudflare_dns_record_response = requests.post( + url, headers=headers, json=body, timeout=self.HTTP_TIMEOUT + ) + if create_cloudflare_dns_record_response.status_code != 200: + # raise error so that we do not continue to make calls to ACME + # server + raise ValueError( + "Error creating cloudflare dns record: status_code={status_code} response={response}".format( + status_code=create_cloudflare_dns_record_response.status_code, + response=self.log_response(create_cloudflare_dns_record_response), + ) + ) + def delete_dns_record(self, domain_name, domain_dns_value): + class MockResponse(object): + def __init__(self, status_code=200, content="mock-response"): + self.status_code = status_code + self.content = content + super(MockResponse, self).__init__() + + def json(self): + return {} + + delete_dns_record_response = MockResponse() + headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY} + + dns_name = "_acme-challenge" + "." + domain_name + list_dns_payload = {"type": "TXT", "name": dns_name} + list_dns_url = urllib.parse.urljoin( + self.CLOUDFLARE_API_BASE_URL, + "zones/{0}/dns_records".format(self.CLOUDFLARE_DNS_ZONE_ID), + ) + + list_dns_response = requests.get( + list_dns_url, params=list_dns_payload, headers=headers, timeout=self.HTTP_TIMEOUT + ) + + for i in range(0, len(list_dns_response.json()["result"])): + dns_record_id = list_dns_response.json()["result"][i]["id"] + url = urllib.parse.urljoin( + self.CLOUDFLARE_API_BASE_URL, + "zones/{0}/dns_records/{1}".format(self.CLOUDFLARE_DNS_ZONE_ID, dns_record_id), + ) + headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY} + delete_dns_record_response = requests.delete( + url, headers=headers, timeout=self.HTTP_TIMEOUT + ) + diff --git a/class/sewer/dns_providers/common.py b/class/sewer/dns_providers/common.py new file mode 100644 index 00000000..cc26cbac --- /dev/null +++ b/class/sewer/dns_providers/common.py @@ -0,0 +1,65 @@ + +class BaseDns(object): + """ + """ + + def __init__(self, LOG_LEVEL="INFO"): + self.LOG_LEVEL = LOG_LEVEL + self.dns_provider_name = self.__class__.__name__ + + def log_response(self, response): + """ + renders a python-requests response as json or as a string + """ + try: + log_body = response.json() + except ValueError: + log_body = response.content + return log_body + + def create_dns_record(self, domain_name, domain_dns_value): + """ + Method that creates/adds a dns TXT record for a domain/subdomain name on + a chosen DNS provider. + + :param domain_name: :string: The domain/subdomain name whose dns record ought to be + created/added on a chosen DNS provider. + :param domain_dns_value: :string: The value/content of the TXT record that will be + created/added for the given domain/subdomain + + This method should return None + + Basic Usage: + If the value of the `domain_name` variable is example.com and the value of + `domain_dns_value` is HAJA_4MkowIFByHhFaP8u035skaM91lTKplKld + Then, your implementation of this method ought to create a DNS TXT record + whose name is '_acme-challenge' + '.' + domain_name + '.' (ie: _acme-challenge.example.com. ) + and whose value/content is HAJA_4MkowIFByHhFaP8u035skaM91lTKplKld + + Using a dns client like dig(https://linux.die.net/man/1/dig) to do a dns lookup should result + in something like: + dig TXT _acme-challenge.example.com + ... + ;; ANSWER SECTION: + _acme-challenge.example.com. 120 IN TXT "HAJA_4MkowIFByHhFaP8u035skaM91lTKplKld" + _acme-challenge.singularity.brandur.org. 120 IN TXT "9C0DqKC_4MkowIFByHhFaP8u0Zv4z7Wz2IHM91lTKec" + Optionally, you may also use an online dns client like: https://toolbox.googleapps.com/apps/dig/#TXT/ + + Please consult your dns provider on how/format of their DNS TXT records. + You may also want to consult the cloudflare DNS implementation that is found in this repository. + """ + raise NotImplementedError("create_dns_record method must be implemented.") + + def delete_dns_record(self, domain_name, domain_dns_value): + """ + Method that deletes/removes a dns TXT record for a domain/subdomain name on + a chosen DNS provider. + + :param domain_name: :string: The domain/subdomain name whose dns record ought to be + deleted/removed on a chosen DNS provider. + :param domain_dns_value: :string: The value/content of the TXT record that will be + deleted/removed for the given domain/subdomain + + This method should return None + """ + raise NotImplementedError("delete_dns_record method must be implemented.") diff --git a/class/sewer/dns_providers/dnspod.py b/class/sewer/dns_providers/dnspod.py new file mode 100644 index 00000000..522cbb06 --- /dev/null +++ b/class/sewer/dns_providers/dnspod.py @@ -0,0 +1,130 @@ +#coding: utf-8 +try: + import urllib.parse as urlparse +except: + import urlparse + +import requests + +from . import common + + +class DNSPodDns(common.BaseDns): + """ + """ + + dns_provider_name = "dnspod" + + def __init__(self, DNSPOD_ID, DNSPOD_API_KEY, DNSPOD_API_BASE_URL="https://dnsapi.cn/"): + self.DNSPOD_ID = DNSPOD_ID + self.DNSPOD_API_KEY = DNSPOD_API_KEY + self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL + self.HTTP_TIMEOUT = 65 # seconds + self.DNSPOD_LOGIN = "{0},{1}".format(self.DNSPOD_ID, self.DNSPOD_API_KEY) + + if DNSPOD_API_BASE_URL[-1] != "/": + self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL + "/" + else: + self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL + super(DNSPodDns, self).__init__() + + def extract_zone(self,domain_name): + domain_name = domain_name.lstrip("*.") + top_domain_list = ['.ac.cn', '.ah.cn', '.bj.cn', '.com.cn', '.cq.cn', '.fj.cn', '.gd.cn', + '.gov.cn', '.gs.cn', '.gx.cn', '.gz.cn', '.ha.cn', '.hb.cn', '.he.cn', + '.hi.cn', '.hk.cn', '.hl.cn', '.hn.cn', '.jl.cn', '.js.cn', '.jx.cn', + '.ln.cn', '.mo.cn', '.net.cn', '.nm.cn', '.nx.cn', '.org.cn'] + old_domain_name = domain_name + m_count = domain_name.count(".") + top_domain = "."+".".join(domain_name.rsplit('.')[-2:]) + new_top_domain = "." + top_domain.replace(".","") + is_tow_top = False + if top_domain in top_domain_list: + is_tow_top = True + domain_name = domain_name[:-len(top_domain)] + new_top_domain + + if domain_name.count(".") > 1: + zone, middle, last = domain_name.rsplit(".", 2) + acme_txt = "_acme-challenge.%s" % zone + if is_tow_top: last = top_domain[1:] + root = ".".join([middle, last]) + else: + zone = "" + root = old_domain_name + acme_txt = "_acme-challenge" + return root, zone, acme_txt + + def create_dns_record(self, domain_name, domain_dns_value): + # if we have been given a wildcard name, strip wildcard + #domain_name = domain_name.lstrip("*.") + #subd = "" + #if domain_name.count(".") != 1: # not top level domain + # pos = domain_name.rfind(".", 0, domain_name.rfind(".")) + # subd = domain_name[:pos] + # domain_name = domain_name[pos + 1 :] + # if subd != "": + # subd = "." + subd + + domain_name,_,subd = self.extract_zone(domain_name) + url = urlparse.urljoin(self.DNSPOD_API_BASE_URL, "Record.Create") + body = { + "record_type": "TXT", + "domain": domain_name, + "sub_domain": subd, + "value": domain_dns_value, + "record_line_id": "0", + "format": "json", + "login_token": self.DNSPOD_LOGIN, + } + print(body) + create_dnspod_dns_record_response = requests.post( + url, data=body, timeout=self.HTTP_TIMEOUT + ).json() + if create_dnspod_dns_record_response["status"]["code"] != "1": + # raise error so that we do not continue to make calls to ACME + # server + raise ValueError( + "Error creating dnspod dns record: status_code={status_code} response={response}".format( + status_code=create_dnspod_dns_record_response["status"]["code"], + response=create_dnspod_dns_record_response["status"]["message"], + ) + ) + + def delete_dns_record(self, domain_name, domain_dns_value): + #domain_name = domain_name.lstrip("*.") + #subd = "" + #if domain_name.count(".") != 1: # not top level domain + # pos = domain_name.rfind(".", 0, domain_name.rfind(".")) + # subd = domain_name[:pos] + # domain_name = domain_name[pos + 1 :] + # if subd != "": + # subd = "." + subd + + domain_name,_,subd = self.extract_zone(domain_name) + url = urllib.parse.urljoin(self.DNSPOD_API_BASE_URL, "Record.List") + # pos = domain_name.rfind(".",0, domain_name.rfind(".")) + subdomain = subd + rootdomain = domain_name + body = { + "login_token": self.DNSPOD_LOGIN, + "format": "json", + "domain": rootdomain, + "subdomain": subdomain, + "record_type": "TXT", + } + print(body) + list_dns_response = requests.post(url, data=body, timeout=self.HTTP_TIMEOUT).json() + for i in range(0, len(list_dns_response["records"])): + rid = list_dns_response["records"][i]["id"] + urlr = urllib.parse.urljoin(self.DNSPOD_API_BASE_URL, "Record.Remove") + bodyr = { + "login_token": self.DNSPOD_LOGIN, + "format": "json", + "domain": rootdomain, + "record_id": rid, + } + delete_dns_record_response = requests.post( + urlr, data=bodyr, timeout=self.HTTP_TIMEOUT + ).json() + + diff --git a/class/sewer/dns_providers/duckdns.py b/class/sewer/dns_providers/duckdns.py new file mode 100644 index 00000000..4be811c6 --- /dev/null +++ b/class/sewer/dns_providers/duckdns.py @@ -0,0 +1,56 @@ +try: + import urllib.parse as urlparse +except: + import urlparse +import requests + +from . import common + + +class DuckDNSDns(common.BaseDns): + + dns_provider_name = "duckdns" + + def __init__(self, duckdns_token, DUCKDNS_API_BASE_URL="https://www.duckdns.org"): + + self.duckdns_token = duckdns_token + self.HTTP_TIMEOUT = 65 # seconds + + if DUCKDNS_API_BASE_URL[-1] != "/": + self.DUCKDNS_API_BASE_URL = DUCKDNS_API_BASE_URL + "/" + else: + self.DUCKDNS_API_BASE_URL = DUCKDNS_API_BASE_URL + super(DuckDNSDns, self).__init__() + + def _common_dns_record(self, logger_info, domain_name, payload_end_arg): + # if we have been given a wildcard name, strip wildcard + domain_name = domain_name.lstrip("*.") + # add provider domain to the domain name if not present + provider_domain = ".duckdns.org" + if domain_name.rfind(provider_domain) == -1: + "".join((domain_name, provider_domain)) + + url = urlparse.urljoin(self.DUCKDNS_API_BASE_URL, "update") + + payload = dict([("domains", domain_name), ("token", self.duckdns_token), payload_end_arg]) + update_duckdns_dns_record_response = requests.get( + url, params=payload, timeout=self.HTTP_TIMEOUT + ) + + normalized_response = update_duckdns_dns_record_response.text + + if update_duckdns_dns_record_response.status_code != 200 or normalized_response != "OK": + # raise error so that we do not continue to make calls to DuckDNS + # server + raise ValueError( + "Error creating DuckDNS dns record: status_code={status_code} response={response}".format( + status_code=update_duckdns_dns_record_response.status_code, + response=normalized_response, + ) + ) + + def create_dns_record(self, domain_name, domain_dns_value): + self._common_dns_record("create_dns_record", domain_name, ("txt", domain_dns_value)) + + def delete_dns_record(self, domain_name, domain_dns_value): + self._common_dns_record("delete_dns_record", domain_name, ("clear", "true")) diff --git a/class/sewer/dns_providers/hurricane.py b/class/sewer/dns_providers/hurricane.py new file mode 100644 index 00000000..76dff4b9 --- /dev/null +++ b/class/sewer/dns_providers/hurricane.py @@ -0,0 +1,73 @@ +""" +Hurricane Electric DNS Support +""" +import json + +try: + hedns_dependencies = True + import HurricaneDNS as _hurricanedns +except ImportError: + hedns_dependencies = False + +from . import common + + +class _Response(object): + """ + wrapper aliyun resp to the format sewer wanted. + """ + + def __init__(self, status_code=200, content=None, headers=None): + self.status_code = status_code + self.headers = headers or {} + self.content = content or {} + self.content = json.dumps(content) + super(_Response, self).__init__() + + def json(self): + return json.loads(self.content) + + +class HurricaneDns(common.BaseDns): + def __init__(self, username, password): + super(HurricaneDns, self).__init__() + if not hedns_dependencies: + raise ImportError( + """You need to install HurricaneDns dependencies. run: pip3 install sewer[hurricane]""" + ) + + self.clt = _hurricanedns.HurricaneDNS(username, password) + + @staticmethod + def extract_zone(domain_name): + """ + extract domain to root, sub, acme_txt + :param str domain_name: the value sewer client passed in, like *.menduo.example.com + :return tuple: root, zone, acme_txt + """ + # if we have been given a wildcard name, strip wildcard + domain_name = domain_name.lstrip("*.") + if domain_name.count(".") > 1: + zone, middle, last = str(domain_name).rsplit(".", 2) + root = ".".join([middle, last]) + acme_txt = "_acme-challenge.%s" % zone + else: + zone = "" + root = domain_name + acme_txt = "_acme-challenge" + return root, zone, acme_txt + + def create_dns_record(self, domain_name, domain_dns_value): + + root, _, acme_txt = self.extract_zone(domain_name) + self.clt.add_record(root, acme_txt, "TXT", domain_dns_value, ttl=300) + + + def delete_dns_record(self, domain_name, domain_dns_value): + root, _, acme_txt = self.extract_zone(domain_name) + host = "%s.%s" % (acme_txt, root) + + recored_list = self.clt.get_records(root, host, "TXT") + + for i in recored_list: + self.clt.del_record(root, i["id"]) diff --git a/class/sewer/dns_providers/rackspace.py b/class/sewer/dns_providers/rackspace.py new file mode 100644 index 00000000..c28ec918 --- /dev/null +++ b/class/sewer/dns_providers/rackspace.py @@ -0,0 +1,202 @@ +try: + import urllib.parse as urlparse +except: + import urlparse +import requests +from . import common + +try: + rackspace_dependencies = True + import tldextract +except ImportError: + rackspace_dependencies = False + +import time + + +class RackspaceDns(common.BaseDns): + """ + """ + + dns_providername = "rackspace" + + def get_rackspace_credentials(self): + RACKSPACE_IDENTITY_URL = "https://identity.api.rackspacecloud.com/v2.0/tokens" + payload = { + "auth": { + "RAX-KSKEY:apiKeyCredentials": { + "username": self.RACKSPACE_USERNAME, + "apiKey": self.RACKSPACE_API_KEY, + } + } + } + find_rackspace_api_details_response = requests.post(RACKSPACE_IDENTITY_URL, json=payload) + if find_rackspace_api_details_response.status_code != 200: + raise ValueError( + "Error getting token and URL details from rackspace identity server: status_code={status_code} response={response}".format( + status_code=find_rackspace_api_details_response.status_code, + response=self.log_response(find_rackspace_api_details_response), + ) + ) + data = find_rackspace_api_details_response.json() + api_token = data["access"]["token"]["id"] + url_data = next( + (item for item in data["access"]["serviceCatalog"] if item["type"] == "rax:dns"), None + ) + if url_data is None: + raise ValueError( + "Error finding url data for the rackspace dns api in the response from the identity server" + ) + else: + api_base_url = url_data["endpoints"][0]["publicURL"] + "/" + return (api_token, api_base_url) + + def __init__(self, RACKSPACE_USERNAME, RACKSPACE_API_KEY): + + if not rackspace_dependencies: + raise ImportError( + """You need to install RackspaceDns dependencies. run; pip3 install sewer[rackspace]""" + ) + self.RACKSPACE_DNS_ZONE_ID = None + self.RACKSPACE_USERNAME = RACKSPACE_USERNAME + self.RACKSPACE_API_KEY = RACKSPACE_API_KEY + self.HTTP_TIMEOUT = 65 # seconds + super(RackspaceDns, self).__init__() + self.RACKSPACE_API_TOKEN, self.RACKSPACE_API_BASE_URL = self.get_rackspace_credentials() + self.RACKSPACE_HEADERS = { + "X-Auth-Token": self.RACKSPACE_API_TOKEN, + "Content-Type": "application/json", + } + + def get_dns_zone(self, domain_name): + extracted_domain = tldextract.extract(domain_name) + self.RACKSPACE_DNS_ZONE = ".".join([extracted_domain.domain, extracted_domain.suffix]) + + def find_dns_zone_id(self, domain_name): + self.get_dns_zone(domain_name) + url = self.RACKSPACE_API_BASE_URL + "domains" + find_dns_zone_id_response = requests.get(url, headers=self.RACKSPACE_HEADERS) + if find_dns_zone_id_response.status_code != 200: + raise ValueError( + "Error getting rackspace dns domain info: status_code={status_code} response={response}".format( + status_code=find_dns_zone_id_response.status_code, + response=self.log_response(find_dns_zone_id_response), + ) + ) + result = find_dns_zone_id_response.json() + domain_data = next( + (item for item in result["domains"] if item["name"] == self.RACKSPACE_DNS_ZONE), None + ) + if domain_data is None: + raise ValueError( + "Error finding information for {dns_zone} in dns response data:\n{response_data})".format( + dns_zone=self.RACKSPACE_DNS_ZONE, + response_data=self.log_response(find_dns_zone_id_response), + ) + ) + dns_zone_id = domain_data["id"] + return dns_zone_id + + def find_dns_record_id(self, domain_name, domain_dns_value): + self.RACKSPACE_DNS_ZONE_ID = self.find_dns_zone_id(domain_name) + url = self.RACKSPACE_API_BASE_URL + "domains/{0}/records".format(self.RACKSPACE_DNS_ZONE_ID) + find_dns_record_id_response = requests.get(url, headers=self.RACKSPACE_HEADERS) + if find_dns_record_id_response.status_code != 200: + raise ValueError( + "Error finding dns records for {dns_zone}: status_code={status_code} response={response}".format( + dns_zone=self.RACKSPACE_DNS_ZONE, + status_code=find_dns_record_id_response.status_code, + response=self.log_response(find_dns_record_id_response), + ) + ) + records = find_dns_record_id_response.json()["records"] + RACKSPACE_RECORD_DATA = next( + (item for item in records if item["data"] == domain_dns_value), None + ) + if RACKSPACE_RECORD_DATA is None: + raise ValueError( + "Couldn't find record with name {domain_name}\ncontaining data: {domain_dns_value}\nin the response data:{response_data}".format( + domain_name=domain_name, + domain_dns_value=domain_dns_value, + response_data=self.log_response(find_dns_record_id_response), + ) + ) + record_id = RACKSPACE_RECORD_DATA["id"] + return record_id + + def poll_callback_url(self, callback_url): + start_time = time.time() + while True: + callback_url_response = requests.get(callback_url, headers=self.RACKSPACE_HEADERS) + if time.time() > start_time + self.HTTP_TIMEOUT: + raise ValueError( + "Timed out polling callbackurl for dns record status. Last status_code={status_code} last response={response}".format( + status_code=callback_url_response.status_code, + response=self.log_response(callback_url_response), + ) + ) + if callback_url_response.status_code != 200: + raise Exception( + "Could not get dns record status from callback url. Status code ={status_code}. response={response}".format( + status_code=callback_url_response.status_code, + response=self.log_response(callback_url_response), + ) + ) + if callback_url_response.json()["status"] == "ERROR": + raise Exception( + "Error in creating/deleting dns record: status_Code={status_code}. response={response}".format( + status_code=callback_url_response.status_code, + response=self.log_response(callback_url_response), + ) + ) + if callback_url_response.json()["status"] == "COMPLETED": + break + + def create_dns_record(self, domain_name, domain_dns_value): + # strip wildcard if present + domain_name = domain_name.lstrip("*.") + self.RACKSPACE_DNS_ZONE_ID = self.find_dns_zone_id(domain_name) + record_name = "_acme-challenge." + domain_name + url = urlparse.urljoin( + self.RACKSPACE_API_BASE_URL, "domains/{0}/records".format(self.RACKSPACE_DNS_ZONE_ID) + ) + body = { + "records": [{"name": record_name, "type": "TXT", "data": domain_dns_value, "ttl": 3600}] + } + create_rackspace_dns_record_response = requests.post( + url, headers=self.RACKSPACE_HEADERS, json=body, timeout=self.HTTP_TIMEOUT + ) + + if create_rackspace_dns_record_response.status_code != 202: + raise ValueError( + "Error creating rackspace dns record: status_code={status_code} response={response}".format( + status_code=create_rackspace_dns_record_response.status_code, + response=create_rackspace_dns_record_response.text, + ) + ) + # response=self.log_response(create_rackspace_dns_record_response))) + # After posting the dns record we want created, the response gives us a url to check that will + # update when the job is done + callback_url = create_rackspace_dns_record_response.json()["callbackUrl"] + self.poll_callback_url(callback_url) + + + def delete_dns_record(self, domain_name, domain_dns_value): + record_name = "_acme-challenge." + domain_name + self.RACKSPACE_DNS_ZONE_ID = self.find_dns_zone_id(domain_name) + self.RACKSPACE_RECORD_ID = self.find_dns_record_id(domain_name, domain_dns_value) + url = self.RACKSPACE_API_BASE_URL + "domains/{domain_id}/records/?id={record_id}".format( + domain_id=self.RACKSPACE_DNS_ZONE_ID, record_id=self.RACKSPACE_RECORD_ID + ) + delete_dns_record_response = requests.delete(url, headers=self.RACKSPACE_HEADERS) + # After sending a delete request, if all goes well, we get a 202 from the server and a URL that we can poll + # to see when the job is done + if delete_dns_record_response.status_code != 202: + raise ValueError( + "Error deleting rackspace dns record: status_code={status_code} response={response}".format( + status_code=delete_dns_record_response.status_code, + response=self.log_response(delete_dns_record_response), + ) + ) + callback_url = delete_dns_record_response.json()["callbackUrl"] + self.poll_callback_url(callback_url) diff --git a/class/ssh_authentication.py b/class/ssh_authentication.py new file mode 100644 index 00000000..d07ded64 --- /dev/null +++ b/class/ssh_authentication.py @@ -0,0 +1,337 @@ +# coding: utf-8 +# +------------------------------------------------------------------- +# | version :1.0 +# +------------------------------------------------------------------- +# | Author: 梁凯强 <1249648969@qq.com> +# +------------------------------------------------------------------- +# | SSH 双因子认证 +# +-------------------------------------------------------------------- +import public,re,os +import platform,time + +class ssh_authentication: + __SSH_CONFIG='/etc/ssh/sshd_config' + __PAM_CONFIG='/etc/pam.d/sshd' + __python_pam='/usr/pam_python_so' + __config_pl='/www/server/panel/data/pam_btssh_authentication.pl' + + + def __init__(self): + '''检查pam_python目录是否存在''' + if not os.path.exists(self.__python_pam): + public.ExecShell("mkdir -p " + self.__python_pam) + public.ExecShell("chmod 600 " + self.__python_pam) + if not os.path.exists(self.__config_pl): + public.ExecShell("echo '%s' >>%s"%(public.GetRandomString(32),self.__config_pl)) + public.ExecShell("chmod 600 " + self.__config_pl) + + def wirte(self, file, ret): + result = public.writeFile(file, ret) + return result + + #重启SSH + def restart_ssh(self): + act = 'restart' + if os.path.exists('/etc/redhat-release'): + version = public.readFile('/etc/redhat-release') + if isinstance(version, str): + if version.find(' 7.') != -1 or version.find(' 8.') != -1: + public.ExecShell("systemctl " + act + " sshd.service") + else: + public.ExecShell("/etc/init.d/sshd " + act) + else: + public.ExecShell("/etc/init.d/sshd " + act) + else: + public.ExecShell("/etc/init.d/sshd " + act) + + #查找PAM目录 + def get_pam_dir(self): + #Centos 系列 + if os.path.exists('/etc/redhat-release'): + version = public.readFile('/etc/redhat-release') + if isinstance(version, str): + if version.find(' 7.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + elif version.find(' 8.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + else: + return False + #Ubuntu + elif os.path.exists('/etc/lsb-release'): + version = public.readFile('/etc/lsb-release') + if isinstance(version, str): + if version.find('16.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + elif version.find('20.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + elif version.find('18.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + else: + return False + #debian + elif os.path.exists('/etc/debian_version'): + version = public.readFile('/etc/debian_version') + if isinstance(version, str): + if version.find('9.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + elif version.find('10.') != -1: + return 'auth requisite %s/pam_btssh_authentication.so'%(self.__python_pam) + else: + return False + return False + + #判断PAMSO文件是否存在 + def isPamSoExists(self): + check2=self.get_pam_dir() + if not check2: return False + check=check2.split() + if len(check)<3: return False + if os.path.exists(check[2]): + #判断文件大小 + if os.path.getsize(check[2])<10240: + self.install_pam_python(check) + return self.isPamSoExists() + return check2 + else: + self.install_pam_python(check) + return self.isPamSoExists() + + #安装pam_python + def install_pam_python(self,check): + so_path=check[2] + so_name=check[2].split('/')[-1] + public.ExecShell('/usr/local/curl/bin/curl -o %s http://download.bt.cn/btwaf_rule/pam_python_so/%s'%(so_path,so_name)) + public.ExecShell("chmod 600 " + so_path) + return True + + #开启双因子认证 + def start_ssh_authentication(self): + check=self.isPamSoExists() + if not check:return False + if os.path.exists(self.__PAM_CONFIG): + auth_data=public.readFile(self.__PAM_CONFIG) + if isinstance(auth_data, str): + if auth_data.find("\n"+check) != -1: + return True + else: + auth_data=auth_data+"\n"+check + public.writeFile(self.__PAM_CONFIG,auth_data) + return True + return False + + #关闭双因子认证 + def stop_ssh_authentication(self): + check=self.isPamSoExists() + if not check:return False + if os.path.exists(self.__PAM_CONFIG): + auth_data=public.readFile(self.__PAM_CONFIG) + if isinstance(auth_data, str): + if auth_data.find("\n"+check) != -1: + auth_data=auth_data.replace("\n"+check,'') + public.writeFile(self.__PAM_CONFIG,auth_data) + return True + else: + return False + return False + + #检查是否开启双因子认证 + def check_ssh_authentication(self): + check=self.isPamSoExists() + if not check:return False + if os.path.exists(self.__PAM_CONFIG): + auth_data=public.readFile(self.__PAM_CONFIG) + if isinstance(auth_data, str): + if auth_data.find("\n"+check) != -1: + return True + else: + return False + return False + + + #设置SSH应答模式 + def set_ssh_login_user(self): + ssh_password = '\nChallengeResponseAuthentication\s\w+' + file = public.readFile(self.__SSH_CONFIG) + if isinstance(file, str): + if len(re.findall(ssh_password, file)) == 0: + file_result = file + '\nChallengeResponseAuthentication yes' + else: + file_result = re.sub(ssh_password, '\nChallengeResponseAuthentication yes', file) + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + return public.returnMsg(True, '开启成功') + + #关闭SSH应答模式 + def close_ssh_login_user(self): + file = public.readFile(self.__SSH_CONFIG) + ssh_password = '\nChallengeResponseAuthentication\s\w+' + if isinstance(file, str): + file_result = re.sub(ssh_password, '\nChallengeResponseAuthentication no', file) + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + return public.returnMsg(True, '关闭成功') + + #查看SSH应答模式 + def check_ssh_login_user(self): + file = public.readFile(self.__SSH_CONFIG) + ssh_password = '\nChallengeResponseAuthentication\s\w+' + if isinstance(file, str): + ret = re.findall(ssh_password, file) + if not ret: + return False + else: + if ret[-1].split()[-1] == 'yes': + return True + else: + return False + return False + + #关闭密码访问 + def stop_password(self): + ''' + 关闭密码访问 + 无参数传递 + ''' + file = public.readFile(self.__SSH_CONFIG) + if isinstance(file, str): + if file.find('PasswordAuthentication') != -1: + file_result = file.replace('\nPasswordAuthentication yes', '\nPasswordAuthentication no') + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + return public.returnMsg(True, '关闭密码认证成功') + else: + return public.returnMsg(False, '没有密码认证') + return public.returnMsg(False, '没有密码认证') + + #开启密码登录 + def start_password(self): + ''' + 开启密码登陆 + get: 无需传递参数 + ''' + file = public.readFile(self.__SSH_CONFIG) + if isinstance(file, str): + if file.find('PasswordAuthentication') != -1: + file_result = file.replace('\nPasswordAuthentication no', '\nPasswordAuthentication yes') + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + return public.returnMsg(True, '开启密码认证成功') + else: + file_result = file + '\nPasswordAuthentication yes' + self.wirte(self.__SSH_CONFIG, file_result) + self.restart_ssh() + return public.returnMsg(True, '开启密码认证成功') + return public.returnMsg(False, '没有密码认证') + + #查看密码登录状态 + def check_password(self): + ''' + 查看密码登录状态 + 无参数传递 + ''' + file = public.readFile(self.__SSH_CONFIG) + ssh_password = '\nPasswordAuthentication\s\w+' + if isinstance(file, str): + ret = re.findall(ssh_password, file) + if not ret: + return False + else: + if ret[-1].split()[-1] == 'yes': + return True + else: + return False + return False + + + #开启SSH 双因子认证 + def start_ssh_authentication_two_factors(self): + if not self.get_pam_dir():return public.returnMsg(False,'不支持该系统') + check=self.isPamSoExists() + if not check:return 'False' + if not self.check_ssh_login_user(): + self.set_ssh_login_user() + if not self.check_ssh_authentication(): + self.start_ssh_authentication() + #如果开启的话,就关闭密码认证 + # if self.check_password(): + # self.stop_password() + #检查是否开启双因子认证 + if self.check_ssh_authentication() and self.check_ssh_login_user(): + return public.returnMsg(True,'开启成功') + return public.returnMsg(True,'开启失败') + + + #关闭SSH 双因子认证 + def close_ssh_authentication_two_factors(self): + if not self.get_pam_dir():return public.returnMsg(False,'不支持该系统') + check=self.isPamSoExists() + if not check:return False + if self.check_ssh_authentication(): + self.stop_ssh_authentication() + #检查是否关闭双因子认证 + #如果是关闭的SSH,那么就开启 + # if not self.check_password(): + # self.start_password() + if not self.check_ssh_authentication(): + return public.returnMsg(True,'已关闭') + if self.stop_ssh_authentication(): + return public.returnMsg(True,'已关闭') + + + #检查是否开启双因子认证 + def check_ssh_authentication_two_factors(self): + if not self.get_pam_dir():return public.returnMsg(False,'不支持该系统') + check=self.isPamSoExists() + if not check:return False + if not self.check_ssh_login_user(): + return public.returnMsg(False,'未开启') + if not self.check_ssh_authentication(): + return public.returnMsg(False,'未开启') + return public.returnMsg(True,'已开启') + + def is_check_so(self): + '''判断SO文件是否存在''' + if not self.get_pam_dir():return public.returnMsg(False,'不支持该系统') + config_data=self.get_pam_dir() + if not config_data:return False + config_data2=config_data.split() + ret={} + ret['so_path']=config_data2[2].split('/')[-1] + if os.path.exists(config_data2[2]): + ret['so_status']=True + else: + ret['so_status']=False + return public.returnMsg(True,ret) + + def download_so(self): + '''下载so文件''' + if not self.get_pam_dir():return public.returnMsg(False,'不支持该系统') + config_data=self.get_pam_dir() + if not config_data:return False + config_data=config_data.split() + self.install_pam_python(config_data) + #判断下载的文件大小 + if os.path.exists(config_data[2]) : + if os.path.getsize(config_data[2])>10240: + return public.returnMsg(True,"下载文件成功") + return public.returnMsg(False,"下载失败") + + #获取Linux系统的主机名 + def get_pin(self): + import platform,time + data=platform.uname() + tme_data=time.strftime('%Y-%m-%d%H:%M',time.localtime(time.time())) + #获取秒 + tis_data=time.strftime('%S',time.localtime(time.time())) + ip_list=public.ReadFile('/www/server/panel/data/pam_btssh_authentication.pl') + ret={} + if isinstance(ip_list,str): + info=data[0]+data[1]+data[2]+tme_data+ip_list + md5_info=public.Md5(info) + ret['pin']=md5_info[:6] + ret['time']=60-int(tis_data) + return ret + else: + ret['pin']='error' + ret['time']=60 + return ret \ No newline at end of file diff --git a/class/ssh_security.py b/class/ssh_security.py index 2db1e191..4ce0684e 100644 --- a/class/ssh_security.py +++ b/class/ssh_security.py @@ -430,7 +430,82 @@ def restart_ssh(self): public.ExecShell("systemctl " + act + " sshd.service") else: public.ExecShell("/etc/init.d/sshd " + act) + #检查是否设置了钉钉 + def check_dingding(self, get): + ''' + 检查是否设置了钉钉 + ''' + #检查文件是否存在 + if not os.path.exists('/www/server/panel/data/dingding.json'):return False + dingding_config=public.ReadFile('/www/server/panel/data/dingding.json') + if not dingding_config:return False + #解析json + try: + dingding=json.loads(dingding_config) + if dingding['dingding_url']: + return True + except: + return False + + #开启SSH双因子认证 + def start_auth_method(self, get): + ''' + 开启SSH双因子认证 + ''' + #检查是否设置了钉钉 + #if not self.check_dingding(get): return public.returnMsg(False, '钉钉未设置') + import ssh_authentication + ssh_class=ssh_authentication.ssh_authentication() + return ssh_class.start_ssh_authentication_two_factors() + + #关闭SSH双因子认证 + def stop_auth_method(self, get): + ''' + 关闭SSH双因子认证 + ''' + #检查是否设置了钉钉 + #if not self.check_dingding(get): return public.returnMsg(False, '钉钉未设置') + import ssh_authentication + ssh_class=ssh_authentication.ssh_authentication() + return ssh_class.close_ssh_authentication_two_factors() + + #获取SSH双因子认证状态 + def get_auth_method(self, get): + ''' + 获取SSH双因子认证状态 + ''' + #检查是否设置了钉钉 + #if not self.check_dingding(get): return public.returnMsg(False, '钉钉未设置') + import ssh_authentication + ssh_class=ssh_authentication.ssh_authentication() + return ssh_class.check_ssh_authentication_two_factors() + + #判断so文件是否存在 + def check_so_file(self, get): + ''' + 判断so文件是否存在 + ''' + import ssh_authentication + ssh_class=ssh_authentication.ssh_authentication() + return ssh_class.is_check_so() + #下载so文件 + def get_so_file(self, get): + ''' + 下载so文件 + ''' + import ssh_authentication + ssh_class=ssh_authentication.ssh_authentication() + return ssh_class.download_so() + + #获取pin + def get_pin(self, get): + ''' + 获取pin + ''' + import ssh_authentication + ssh_class=ssh_authentication.ssh_authentication() + return public.returnMsg(True, ssh_class.get_pin()) if __name__ == '__main__': import sys diff --git a/class/ssh_terminal.py b/class/ssh_terminal.py index ec7ae69f..cc16cc94 100644 --- a/class/ssh_terminal.py +++ b/class/ssh_terminal.py @@ -45,6 +45,8 @@ class ssh_terminal: _tp = None _old_conf = None _debug_file = 'logs/terminal.log' + _s_code = None + _last_num = 0 def connect(self): ''' @@ -123,8 +125,16 @@ def connect(self): self._tp.auth_publickey(username=self._user, key=pkey) else: - self.debug('正在认证密码') - self._tp.auth_password(username=self._user, password=self._pass) + try: + self._tp.auth_none(self._user) + except Exception as e: + e = str(e) + if e.find('keyboard-interactive') >= 0: + self._auth_interactive() + else: + self.debug('正在认证密码') + self._tp.auth_password(username=self._user, password=self._pass) + # self._tp.auth_password(username=self._user, password=self._pass) except Exception as e: if self._old_conf: s_file = '/www/server/panel/config/t_info.json' @@ -132,6 +142,11 @@ def connect(self): self.set_sshd_config(True) self._tp.close() e = str(e) + if e.find('websocket error!') != -1: + return returnMsg(True,'连接成功') + if e.find('Authentication timeout') != -1: + self.debug("认证超时{}".format(e)) + return returnMsg(False,'认证超时,请按回车重试!{}'.format(e)) if e.find('Authentication failed') != -1: self.debug('认证失败{}'.format(e)) return returnMsg(False,'帐号或密码错误: {}'.format(e + "," + self._user + "@" + self._host + ":" +str(self._port))) @@ -168,6 +183,41 @@ def connect(self): return returnMsg(True,'连接成功') + def _auth_interactive(self): + self.debug('正在二次认证 Verification Code') + + self.brk = False + def handler(title, instructions, prompt_list): + if not self._ws: raise public.PanelError('websocket error!') + if instructions: + self._ws.send(instructions) + if title: + self._ws.send(title) + resp = [] + for pr in prompt_list: + if str(pr[0]).strip() == "Password:": + resp.append(self._pass) + elif str(pr[0]).strip() == "Verification code:": + #获取前段传入的验证码 + self._ws.send("Verification code# ") + self._s_code = True + code = "" + while True: + data = self._ws.receive() + if data.find('"resize":1') != -1: + self.resize(data) + continue + self._ws.send(data) + if data in ["\n","\r"]: break + code += data + resp.append(code) + self._ws.send("\n") + self._s_code = None + return tuple(resp) + self._tp.auth_interactive(self._user, handler) + + + def get_login_user(self): ''' @name 获取本地登录用户 @@ -491,8 +541,12 @@ def send(self): ''' try: while not self._ws.closed: + if self._s_code: + time.sleep(0.1) + continue client_data = self._ws.receive() if not client_data: continue + if client_data is '{}': continue if len(client_data) > 10: if client_data.find('{"host":"') != -1: continue @@ -528,7 +582,7 @@ def history_recv(self,recv_data): #处理TAB补登 if self._last_cmd_tip == 1: if not recv_data.startswith('\r\n'): - self._last_cmd += recv_data.replace('\u0007','').strip() + self._last_cmd += recv_data.replace('\u0007','').replace("\x07","").strip() self._last_cmd_tip = 0 #上下切换命令 @@ -552,20 +606,34 @@ def history_send(self,send_data): if send_data in ["\x1b[A","\x1b[B"]: self._last_cmd_tip = 2 return + + #左移光标 + if send_data in ["\x1b[C"]: + self._last_num -= 1 + return + + # 右移光标 + if send_data in ["\x1b[D"]: + self._last_num += 1 + return #退格 if send_data == "\x7f": self._last_cmd = self._last_cmd[:-1] return + #过滤特殊符号 - if send_data in ["\x1b[C","\x1b[D","\x1b[K","\x07","\x08","\x03","\x01","\x02","\x04","\x05","\x06","\u0007"]: + if send_data in ["\x1b[C","\x1b[D","\x1b[K","\x07","\x08","\x03","\x01","\x02","\x04","\x05","\x06","\x1bOB","\x1bOA","\x1b[8P","\x1b","\x1b[4P","\x1b[6P","\x1b[5P"]: return #Tab补全处理 - if send_data == '\t': + if send_data == "\t": self._last_cmd_tip = 1 return + + if str(send_data).find("\x1b") != -1: + return if send_data[-1] in ['\r','\n']: if not self._last_cmd: return @@ -573,12 +641,15 @@ def history_send(self,send_data): public.writeFile(his_file, json.dumps(his_shell) + "\n","a+") self._last_cmd = "" - #超过5M则保留最新的200行 - if os.stat(his_file).st_size > 5242880: - his_tmp = public.GetNumLines(his_file,200) + #超过50M则保留最新的20000行 + if os.stat(his_file).st_size > 52428800: + his_tmp = public.GetNumLines(his_file,20000) public.writeFile(his_file, his_tmp) else: - self._last_cmd += send_data + if self._last_num >= 0: + self._last_cmd += send_data + else: + self._last_cmd.insert(len(self._last_cmd) + self._last_num, send_data) def close(self): @@ -612,8 +683,11 @@ def set_attr(self,ssh_info): self._pkey = ssh_info['pkey'] if 'password' in ssh_info: self._pass = ssh_info['password'] - - result = self.connect() + try: + result = self.connect() + except Exception as ex: + if str(ex).find("NoneType") == -1: + raise public.PanelError(ex) return result @@ -641,6 +715,7 @@ def debug(self,msg): @return void ''' msg = "{} - {}:{} => {} \n".format(public.format_date(),self._host,self._port,msg) + self.history_send(msg) public.writeFile(self._debug_file,msg,'a+') def run(self,web_socket, ssh_info=None): diff --git a/class/ssh_weixin_send.py b/class/ssh_weixin_send.py new file mode 100644 index 00000000..6fb439b2 --- /dev/null +++ b/class/ssh_weixin_send.py @@ -0,0 +1,164 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +import pwd +import json +import string +import random +import hashlib +import datetime +import platform +import httplib +import os,sys,time + +def auth_log(msg): + """写入日志""" + f=open('/tmp/1.json','a+') + f.write(json.dumps(msg)+"\n") + f.close() + +def ReadFile(filename,mode = 'r'): + import os + if not os.path.exists(filename): return False + try: + fp = open(filename, mode) + f_body = fp.read() + fp.close() + except Exception as ex: + if sys.version_info[0] != 2: + try: + fp = open(filename, mode,encoding="utf-8") + f_body = fp.read() + fp.close() + except: + fp = open(filename, mode,encoding="GBK") + f_body = fp.read() + fp.close() + else: + return False + return f_body + +#获取服务器IP +def get_ip(): + if os.path.exists('/www/server/panel/data/iplist.txt'): + data=ReadFile('/www/server/panel/data/iplist.txt') + if data: + return data.strip() + else: + return 'None' + else: + return 'None' + +def dingding_send(config_url,url, content): + host=url.replace('https://','').replace('http://','').split('/')[0] + send_url=url.replace('https://','').replace('http://','').replace(host,'') + if 'weixin.qq.com' in host: + data = {"msgtype": "markdown","markdown": {"content": content}} + elif 'dingtalk.com' in host: + if config_url['isAtAll']: + data = {"msgtype": "markdown","markdown": {"title": "SSH二次认证","text": content},"at": {"atMobiles": ["1"],"isAtAll":True}} + else: + data = {"msgtype": "markdown","markdown": {"title": "SSH二次认证","text": content},"at": {"atMobiles": ["1"],"isAtAll":False}} + else: + return False + headers = {'Content-Type': 'application/json'} + try: + httpClient = httplib.HTTPSConnection(host, timeout=10) + httpClient.request("POST", send_url, json.dumps(data), headers=headers) + response = httpClient.getresponse() + result = json.loads(response.read()) + if result["errcode"] == 0: + return True + else: + return False + except: + cmd='/usr/local/curl/bin/curl -H "Content-Type:application/json" -X POST --data \'%s\' %s'%(json.dumps(data),url) + try: + data=json.loads(os.popen(cmd).read()) + if data["errcode"] == 0: + return True + except: + return False + return False + +def action_wechat(config_url,url,content): + """微信通知""" + return dingding_send(config_url,url, content) + +def get_user_comment(user): + try: + comments = pwd.getpwnam(user).pw_gecos + except: + comments = '' + return comments + +def get_hash(plain_text): + key_hash = hashlib.sha512() + key_hash.update(plain_text) + return key_hash.digest() + +def gen_key(config_url,url,pamh, user, length): + pin = ''.join(random.choice(string.digits) for i in range(length)) + hostname = platform.node().split('.')[0] + content = "####SSH动态密码 \n\n >客户端IP: %s\n\n >登录的账户: %s\n\n >服务器外网IP: %s \n\n >主机名:%s \n\n>验证码:【%s】\n\n >发送时间: %s\n\n >有效期: 2分钟" % (pamh.rhost, user,get_ip(),hostname, pin,time.strftime('%Y-%m-%d %X', time.localtime())) + is_send=action_wechat(config_url,url,content) + pin_time = datetime.datetime.now() + return get_hash(pin), pin_time,is_send + +#检查配置文件是否存在 +def is_config(): + if not os.path.exists('/www/server/panel/data/dingding.json'): + return False + else:return True + +def pam_sm_authenticate(pamh, flags, argv): + if not os.path.exists('/www/server/panel/data/dingding.json'):return pamh.PAM_SUCCESS + try: + config=ReadFile('/www/server/panel/data/dingding.json') + config_url=json.loads(config) + url=config_url['dingding_url'] + except: + return pamh.PAM_SUCCESS + PIN_LENGTH = 6 + PIN_LIVE = 120 + PIN_LIMIT = 3 + try: + user = pamh.get_user() + except pamh.exception as e: + return e.pam_result + pin, pin_time,is_send = gen_key(config_url,url,pamh, user, PIN_LENGTH) + if not is_send: + msg = pamh.Message(pamh.PAM_ERROR_MSG, "[Warning] Failed to send verification code, please check the configuration file") + pamh.conversation(msg) + return pamh.PAM_SUCCESS + for attempt in range(0, PIN_LIMIT): + msg = pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "Verification code:") + resp = pamh.conversation(msg) + resp_time = datetime.datetime.now() + input_interval = resp_time - pin_time + if input_interval.seconds > PIN_LIVE: + msg = pamh.Message(pamh.PAM_ERROR_MSG, "[Warning] Time limit exceeded.") + pamh.conversation(msg) + return pamh.PAM_ABORT + resp_hash = get_hash(resp.resp) + if resp_hash == pin: + return pamh.PAM_SUCCESS + else: + continue + msg = pamh.Message(pamh.PAM_ERROR_MSG, "[Warning] Too many authentication failures.") + pamh.conversation(msg) + return pamh.PAM_AUTH_ERR + +def pam_sm_setcred(pamh, flags, argv): + return pamh.PAM_SUCCESS + +def pam_sm_acct_mgmt(pamh, flags, argv): + return pamh.PAM_SUCCESS + +def pam_sm_open_session(pamh, flags, argv): + return pamh.PAM_SUCCESS + +def pam_sm_close_session(pamh, flags, argv): + return pamh.PAM_SUCCESS + +def pam_sm_chauthtok(pamh, flags, argv): + return pamh.PAM_SUCCESS \ No newline at end of file diff --git a/class/system.py b/class/system.py index 2298fc8e..149a8087 100644 --- a/class/system.py +++ b/class/system.py @@ -282,14 +282,7 @@ def GetSystemVersion(self): key = 'sys_version' version = cache.get(key) if version: return version - import public - version = public.readFile('/etc/redhat-release') - if not version: - version = public.readFile('/etc/issue').strip().split("\n")[0].replace('\\n','').replace('\l','').strip() - else: - version = version.replace('release ','').replace('Linux','').replace('(Core)','').strip() - v_info = sys.version_info - version = version + '(Py' + str(v_info.major) + '.' + str(v_info.minor) + '.' + str(v_info.micro) + ')' + version = public.get_os_version() cache.set(key,version,600) return version diff --git a/class/userlogin.py b/class/userlogin.py index 67838a93..954634a3 100644 --- a/class/userlogin.py +++ b/class/userlogin.py @@ -108,8 +108,7 @@ def request_tmp(self,get): self.limit_address('-') cache.delete('panelNum') cache.delete('dologin') - sess_input_path = 'data/session_last.pl' - public.writeFile(sess_input_path,str(int(time.time()))) + session['session_timeout'] = time.time() + public.get_session_timeout() del(data['tmp_token']) del(data['tmp_time']) public.writeFile(save_path,json.dumps(data)) @@ -160,8 +159,7 @@ def request_temp(self,get): self.limit_address('-') cache.delete('panelNum') cache.delete('dologin') - sess_input_path = 'data/session_last.pl' - public.writeFile(sess_input_path,str(int(time.time()))) + session['session_timeout'] = time.time() + public.get_session_timeout() self.set_request_token() self.login_token() self.set_cdn_host(get) @@ -285,8 +283,7 @@ def _set_login_session(self,userInfo): self.limit_address('-') cache.delete('panelNum') cache.delete('dologin') - sess_input_path = 'data/session_last.pl' - public.writeFile(sess_input_path,str(int(time.time()))) + session['session_timeout'] = time.time() + public.get_session_timeout() self.set_request_token() self.login_token() login_type = 'data/app_login.pl' diff --git a/class/wxapp.py b/class/wxapp.py index fa71bfd1..29ac3e44 100644 --- a/class/wxapp.py +++ b/class/wxapp.py @@ -107,8 +107,7 @@ def check_app_login(self,get): public.WriteLog('TYPE_LOGIN','APP扫码登录,帐号:{},登录IP:{}'.format(userInfo['username'],public.GetClientIp()+ ":" + str(request.environ.get('REMOTE_PORT')))) cache.delete('panelNum') cache.delete('dologin') - sess_input_path = 'data/session_last.pl' - public.writeFile(sess_input_path,str(int(time.time()))) + session['session_timeout'] = time.time() + public.get_session_timeout() login_type = 'data/app_login.pl' self.set_request_token() import config diff --git a/install/install_soft.sh b/install/install_soft.sh index 080c2625..d6761a56 100644 --- a/install/install_soft.sh +++ b/install/install_soft.sh @@ -1,9 +1,6 @@ #!/bin/bash PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH -pyenv_bin=/www/server/panel/pyenv/bin -rep_path=${pyenv_bin}:$PATH - mtype=$1 actionType=$2 name=$3 @@ -20,14 +17,9 @@ if [ "$libNull" == '' ];then wget -O lib.sh $serverUrl/$mtype/lib.sh fi -if [ -d "$pyenv_bin" ];then - PATH=$rep_path -fi - -wget -O $name.sh $serverUrl/$mtype/${name}.sh +wget -O $name.sh $serverUrl/$mtype/$name.sh if [ "$actionType" == 'install' ];then - sed -i "s#PATH=.*#PATH=$PATH#" lib.sh bash lib.sh fi -sed -i "s#PATH=.*#PATH=$PATH#" ${name}.sh -bash ${name}.sh $actionType $version +bash $name.sh $actionType $version +echo '|-Successify --- 命令已执行! ---' diff --git a/script/run_script.py b/script/run_script.py new file mode 100644 index 00000000..331f0ed8 --- /dev/null +++ b/script/run_script.py @@ -0,0 +1,60 @@ +#coding: utf-8 +#------------------------------------------------------------------- +# 宝塔Linux面板 +#------------------------------------------------------------------- +# Copyright (c) 2015-2099 宝塔软件(http:#bt.cn) All rights reserved. +#------------------------------------------------------------------- +# Author: hwliang +#------------------------------------------------------------------- + +#------------------------------ +# 项目开机自启调用脚本 +#------------------------------ +import os,sys +panel_path = '/www/server/panel' +os.chdir(panel_path) +if not 'class/' in sys.path: sys.path.insert(0,'class/') +import public,time,psutil + + +def project_model_auto_run(): + ''' + @name 项目模型自启调用 + @author hwliang<2021-08-09> + @return bool + ''' + project_model_path = '{}/projectModel'.format(public.get_class_path()) + if not os.path.exists(project_model_path): return False + for mod_name in os.listdir(project_model_path): + if mod_name in ['base.py','__init__.py']: continue + mod_file = "{}/{}".format(project_model_path,mod_name) + if not os.path.exists(mod_file): continue + if not os.path.isfile(mod_file): continue + + tmp_mod = public.get_script_object(mod_file) + if not hasattr(tmp_mod,'main'): continue + + run_object = getattr(tmp_mod.main(),'auto_run',None) + if run_object: run_object() + + +def start(): + run_tips = '/dev/shm/bt_auto_run.pl' + boot_time = psutil.boot_time() + stime = time.time() + if os.path.exists(run_tips): + last_time = int(public.readFile(run_tips)) + if boot_time < last_time: return False + if stime - 3600 > boot_time: return False + + # --------------------- 调用自启动程序 --------------------- + + project_model_auto_run() + + # --------------------- 结束调用 --------------------- + + public.writeFile(run_tips,str(int(stime))) + + +if __name__ == '__main__': + start() \ No newline at end of file diff --git a/task.py b/task.py index 12cb5c68..57467224 100644 --- a/task.py +++ b/task.py @@ -113,7 +113,7 @@ def ExecShell(cmdstring, cwd=None, timeout=None, shell=True): # 任务队列 def startTask(): - global isTask + global isTask,logPath try: while True: try: @@ -311,19 +311,13 @@ def systemTask(): data = (cpuInfo['used'], cpuInfo['mem'], addtime) sql.table('cpuio').add('pro,mem,addtime', data) sql.table('cpuio').where("addtime 100: lpro = 100 - sql.table('load_average').add('pro,one,five,fifteen,addtime', ( - lpro, load_average['one'], load_average['five'], load_average['fifteen'], addtime)) + sql.table('load_average').add('pro,one,five,fifteen,addtime', (lpro, load_average['one'], load_average['five'], load_average['fifteen'], addtime)) + sql.table('load_average').where("addtime 2: - clinum = int(sys.argv[2]) if sys.argv[2][:6] not in ['instal','update'] else sys.argv[2] + try: + if len(sys.argv) > 2: + clinum = int(sys.argv[2]) if sys.argv[2][:6] not in ['instal','update'] else sys.argv[2] + except: + clinum = sys.argv[2] bt_cli(clinum) else: print('ERROR: Parameter error') diff --git a/vhost/template/apache/node_http.conf b/vhost/template/apache/node_http.conf new file mode 100644 index 00000000..4e5ed076 --- /dev/null +++ b/vhost/template/apache/node_http.conf @@ -0,0 +1,26 @@ + + ServerAdmin {server_admin} + DocumentRoot "{site_path}" + ServerName {server_name} + ServerAlias {domains} + #errorDocument 404 /404.html + ErrorLog "{log_path}/{project_name}-error_log" + CustomLog "{log_path}/{project_name}-access_log" combined + + {ssl_config} + + #DENY FILES + + Order allow,deny + Deny from all + + + # HTTP反向代理相关配置开始 >>> + + ProxyRequests Off + SSLProxyEngine on + ProxyPass / {url}/ + ProxyPassReverse / {url}/ + + # HTTP反向代理相关配置结束 <<< + \ No newline at end of file diff --git a/vhost/template/nginx/node_http.conf b/vhost/template/nginx/node_http.conf new file mode 100644 index 00000000..1e2e7145 --- /dev/null +++ b/vhost/template/nginx/node_http.conf @@ -0,0 +1,57 @@ +server +{{ + {listen_ports} + server_name {domains}; + index index.html index.htm default.htm default.html; + # root {site_path}; + + #SSL-START SSL相关配置 + #error_page 404/404.html; + {ssl_config} + #SSL-END + + #ERROR-PAGE-START 错误页相关配置 + #error_page 404 /404.html; + #error_page 502 /502.html; + #ERROR-PAGE-END + + + #REWRITE-START 伪静态相关配置 + include {panel_path}/vhost/rewrite/node_{project_name}.conf; + #REWRITE-END + + #禁止访问的文件或目录 + location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md|package.json|package-lock.json|\.env|node_modules) {{ + return 404; + }} + + #一键申请SSL证书验证目录相关设置 + location ~ \.well-known {{ + allow all; + }} + + # HTTP反向代理相关配置开始 >>> + location ~ /purge(/.*) {{ + proxy_cache_purge cache_one {host}$request_uri$is_args$args; + }} + + location / {{ + proxy_pass {url}; + proxy_set_header Host {host}; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + add_header X-Cache $upstream_cache_status; + + proxy_connect_timeout 30s; + proxy_read_timeout 86400s; + proxy_send_timeout 30s; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + }} + # HTTP反向代理相关配置结束 <<< + + access_log {log_path}/{project_name}.log; + error_log {log_path}/{project_name}.error.log; +}} \ No newline at end of file diff --git a/vhost/template/nginx/site.conf b/vhost/template/nginx/site.conf new file mode 100644 index 00000000..e69de29b