关于文本代码执行,什么意思呢?简单打个比方,你在数据库里建了一个文本字段,在文本字段里存放的内容为一段python代码,系统可以直接运行这段代码,实现想要的功能,从此可以在前端编写一些脚本存放至数据库,不需要小需求都得改后端代码,岂不是无敌?
不得不说的是,这个方法实际效果出奇的好,python3内置exec函数,就是可以用来执行长串的python代码的。但是呢,必须要自己处理一下。
至于处理什么?主要有以下几点,都是出于安全性考虑。这几点处理完毕,自己就能做一个云函数功能出来了。反正我花费了两天时间,终于给肝出来了!
第一点,安全性。
何为安全性?要知道,python3的exec函数是完全支持本地环境的。如果不良用户利用这一点,在代码里加入恶意语句,比如import os,然后os.system执行一条语句 rm -rf ./
后果可想而知,这是干嘛?直接python3代码执行删除服务器所有文件,我勒个去。
所以,考虑到这一点,首先,禁止exec的代码里进行import导入包,我不允许你导入包,只能使用python3自带的保留字,看你能掀起什么浪?
因此,我们删除一些不安全的保留字,例如import,eval,exec之类的
因为open也是保留字,防止用户通过文本代码向服务器恶意写入垃圾文件,比如 with open('1.txt) as f,
f.write之类的,我们也给干掉
关键代码如下:
builtins = __builtins__
builtins = dict(builtins).copy()
for key in ['__import__', 'eval', 'exec', 'globals', 'dir', 'copyright', 'open', 'quit']:
del builtins[key] # 删除不安全的关键字
会了上面的代码,这下好了,危险的关键字无法使用了,python3文本代码里再有这种代码,执行的时候直接报错name undefiend了。
但是你以为这么简单就完事儿了吗?答案是否定的。
python3还有很多__下划线开头的隐私方法,可用同样把其他包导入进来为非作歹。
为此,我们需要干掉私有变量的调用,具体检测代码如下:
import tokenize
def check_unsafe_attributes(string):
"""
安全检测需要exec执行的python代码
:param string:
:return:
"""
g = tokenize.tokenize(io.BytesIO(string.encode('utf-8')).readline)
pre_op = ''
for toktype, tokval, _, _, _ in g:
if toktype == tokenize.NAME and pre_op == '.' and tokval.startswith('_'):
attr = tokval
msg = "access to attribute '{0}' is unsafe.".format(attr)
raise AttributeError(msg)
elif toktype == tokenize.OP:
pre_op = tokval
怎么用呢,在exec(目标代码)代码之前调用一下 check_unsafe_attributes(目标代码)
如果检测到目标代码有私有调用直接就抛错了然后不执行就完事儿。
这两步搞定过后,安全执行代码问题就没了,但是还有第二点问题。
第二点, 恶意死循环。
这一个问题着实恶心,比如有人在代码里写了while True,写了for的几十万次垃圾代码,这可如何是好?干掉while和for关键字?这样确实可以杜绝死循环,但是想实现完整的python功能可就太难了。如果不解决这个问题,用户调用云函数执行死循环,服务器后台那个线程就会一直运行着消耗资源,那如果死循环里有个log打印日志一直在跑,服务器上的日志文件都有可能把磁盘空间撑爆。所以,干掉log和print保留字?不,咱们不采取这个 办法。
综合上面情况,我采用了函数执行超时自动退出办法
什么意思呢?简单理解就是说,执行exec语句,我规定5秒内必须执行完毕,如果exec语句里面有死循环或者恶意循环代码,那5秒内一定执行不完毕,那么系统自动报超时错误,并且结束该线程。听这玩意儿是不是很高级?没错,但是需要引入三方模块,并且仅此一种办法可用,不要看网上的其他办法,我通通试过都不行。
pip install func_timeout
import func_timeout
from func_timeout import func_set_timeout
time_out_sec = 5
class my_exception(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
message = f'函数执行超时: "{self.message}"'
return message
@func_set_timeout(time_out_sec)
def excute(*args):
exec(*args)
这里重定义了一个 excute函数,目的就是超时执行exec函数,exec函数执行超过规定的时间自动被系统干掉并抛错。
核心代码讲解完毕了,接下来是实际关联数据库字段运行文本代码的按理。
环境采用python3,框架为odoo。
def action_task_exec(self,call=None,params=None):
"""
接口调用执行函数
:return:
"""
self.ensure_one()
if not params:
params = []
env = self.env
builtins = __builtins__
builtins = dict(builtins).copy()
for key in ['__import__','eval','exec','globals','dir','copyright','open','quit']:
del builtins[key] # 删除不安全的关键字
# print(builtins)
global_dict = {'__builtins__': builtins,
'requests': requests, 'urljoin':urljoin,'quote':quote,'unquote': unquote,
'log': _logger.info,
'env': env, 'cr': env.cr, 'uid': env.uid, 'error': UserError,
're':re,'etree':etree,'time':time,'datetime':datetime} # 禁用内置函数,不允许导入包
try:
check_unsafe_attributes(self.pycode)
localdict = {'result': None}
# 待解决windows下运行超时的问题
to_run_code = self.pycode.strip()
params_str = ''
for i in range(len(params)):
par = params[i]
if isinstance(par,str):
params_str+=f"'{par}'"
else:
params_str+=f'{par}'
if i != len(params)-1:
params_str+=','
func = f'result={call}({params_str})' if call else False
if func:
_logger.info(f'开始执行:{func}')
to_run_code = to_run_code+'\n'+func
# exec(to_run_code, global_dict, localdict)
try:
excute(to_run_code, global_dict, localdict)
except func_timeout.exceptions.FunctionTimedOut:
raise my_exception(f'函数[{self.name}](id:{self.id})运行时间超过{time_out_sec}秒,疑似死循环,已被系统切断')
except Exception as e:
ret = f'执行报错:{e}'
_logger.info(ret)
return ret
else:
# print(global_dict)
# print(localdict)
ret = localdict['result']
return ret
其中global_dict 是exec执行文本代码默认带的环境,干掉了一些保留字并且内置一些常用的函数比如requests,re,xpath。这样被执行的文本代码里不需要import导入包也能使用该对象
localdict 是存放本地执行exec完毕后得到的结果,里面就有你文本代码里赋值的内容
下面是执行案例:
如图所示,云函数任务脚本里的文本代码为死循环打印日志
点击运行后,服务器后台疯狂飙日志
但是5秒后归于大海
前端执行结果如图:
再看一个正常的函数执行:
大功告成,完美!再也不质疑自己的技术水平了,我就是最棒的!网上没有相关的例子资料,那我自己出这个教程吧,欢迎后来者参考~