API 的插件框架#413
Conversation
|
尝试修复一些安全性的bug以及优化了Web Error的前端显示 TODO: 更新所有旧的API到v1中 |
|
迁移旧 API 我来吧,编码和时间相关的都写好了 有一个想要讨论的点,现在注册 API 的方式是通过 |
怎么简单怎么直观怎么来,不用太受旧版约束,未来大概率新旧版api会同时存在很长一段时间 |
| if escape_html: | ||
| data = escape(json.dumps(data, ensure_ascii=ensure_ascii, indent=indent), quote=escape_quote) | ||
| else: | ||
| data = json.dumps(data, ensure_ascii=ensure_ascii, indent=indent).replace('</', '<\\/') |
There was a problem hiding this comment.
这里应该没必要进行转义,因为 Content-Type 已经设置为 application/json 了
更进一步,API 的返回内容默认为 text/plain 吧,避免 API 作者忘记转义(比如我);bytes 类型则 application/octet-stream;JSON application/json。
应该没有 API 需要使用其他类型的需求,所以就不用允许 API 自己设置 Content-Type 了
There was a problem hiding this comment.
我的建议是默认返回JSON格式的code,message,data信息
There was a problem hiding this comment.
这样设计的话,我觉得对应的系统对 JSON respond 的处理能力需要增强,目前系统对返回内容好像只支持正则匹配。
思考了一下好像不难实现,提供一个 respond content 的魔法变量和一个 JSON 函数应该就可以了,比如 {{ json_parse(content)['code'] }}
There was a problem hiding this comment.
There was a problem hiding this comment.
在写了.jpg,先把方案写下来讨论一下
2f56b2f to
e208928
Compare
Signed-off-by: a76yyyy <56478790+a76yyyy@users.noreply.github.com>
| # 使用原生 tornado API,详细参考 tornado 文档 | ||
| text = self.get_argument("text", "") | ||
| self.write( | ||
| escape(text) |
Check warning
Code scanning / CodeQL
Reflected server-side cross-site scripting
| traceback.print_exception(*kwargs["exc_info"]) | ||
| if len(kwargs["exc_info"]) > 1: | ||
| logger_Web_Handler.debug(str(kwargs["exc_info"][1])) | ||
| self.write(data) |
Check warning
Code scanning / CodeQL
Information exposure through an exception
|
有个问题,less 还在用吗 |
我对前端学习并不多,没有学过less, 如果可以的话,你可以考虑一下用less或者直接用css哪个合适一些,并且将合适的方案重新创建一个PR |
Signed-off-by: a76yyyy <56478790+a76yyyy@users.noreply.github.com>
qd 的前端部分太抽象了,用了一些重复的、停止维护的包,结构也有点乱。 因为前后端分离的设计也需要新 API,所以下面有两种方案:
|
|
其实如果可以的话, 我的想法是在保留框架所有功能的基础上采用第二种方案, 直接做一个全新的前端或者UI 旧的前端过于冗余、陈旧和复杂了
|
|
3000 预算进图吧系列:本来我只想加个 API,现在我想直接来个 qd2( YAML 可能是最适合表示流程格式,CI/CD 基本都是使用 YAML 格式。 task.yaml 草案 # task.yaml
# code 类型是我自己定义的,该类型的值会被解释为 python 表达式进行执行,比如 1+1 会被解释为 2, '1'+'1' 会被解释为 '11'
# 使用 YAML 在 code 类型声明纯字符串是有点麻烦的, "xxx" 外围的双引号在 YAML 中会被解释为字符串的定界符,解析之后会丢掉引号。
# 在 code 中声明纯字符串有三种方法:
# 1. 双重引号:"'xxx'"
# 2. 多行字符串:
# key: |
# "xxx"
# 这得到的结果是 {'key': '"xxx"\n' },虽然最后多了一个换行,但是对最终值没有影响
# 3. 在 vars 中预先定义,vars 的 default 是 string 类型。
# 当一个字符串需要多次使用时,推荐这种方法。
# 4. 让引号不在最外围就可以了,str('xxx')、u'xxx'、f'xxx'
# 推荐用 u'',因为在 Python3 中 '' 和 u'' 完全没有区别
require:
- string # 依赖的模块/插件
'on': # 不加引号会被自动转换为 bool(True),这是 YAML 特性
corn: string # cron 风格定时,min hour day month dayOfWeek
timer: int # 倒计时定时
retry: # 失败后重试
delay: int # 选项1: 固定等待时间
delay: # 选项2: 随机等待时间
max: int # 最大等待时间
min: int # 最小等待时间
max: int # 最多重试次数
delay: int # 执行延时区间。前后范围内延迟,比如设定为 60,那么范围就是 -60~60s
# 为什么 retry 的 delay 有两种格式,此处只有一种?因为固定的执行延迟似乎没有必要,如果能找到合适的场景,再加上
vars:
- name: string # 变量名
type: string # 变量类型,可选 string, int, float, bool, list, dict。默认 string
display: bool # 是否在前端设置界面显示。默认 True
default: string # 默认值,可选
description: string # 描述,可选
process:
# type: statement 时
- type: string('statement') # 类型,默认是 statement,下面会有 if 和 loop 的格式
name: string # 任务名
id: string # step id,可选。其他 step 可以通过 id 访问此 step 的输出
if: code # 条件,可选。如果设置了 if,那么只有满足条件才会执行
url: code # url。有此项时才会执行请求、断言(assert)和输出(output),未给出时则直接执行输出(output)
method: string # 请求方法,默认 GET。这个应该没必要设为 code 吧
assert-success: # 成功断言,满足一条即为成功
- status: int # 检查状态码,完全相等为真
- match: string # 检查 response body,包含为真
- regex: string # 正则匹配 response body
- code: code # 代码
assert-failure: # 失败断言,满足一条即为失败
headers: # 请求头
- string: code
output: # 输出
key: code # 输出的 key
log: # 日志输出
debug: code
info: code
warning: code
error: code
summary: code # 概括性输出,会在前端突出显示
delay: int # 延迟执行,单位毫秒
# loop
- type: string('loop')
# 等价为
# for `iter` in `in`:
# then()
iter: string # 循环
in: string # 迭代对象
then: # 循环体
if: code # 条件,可选。如果不满足条件,整个语句块都不会执行
# if
- type: string('if')
# 等价为
# if `if`:
# then()
# else:
# else()
if: code # 条件
then: # if 语句块
else: # else 语句块
# while
- type: string('while')
# 等价为
# while `while`:
# then()
if: code # 条件
then: # while 语句块
一个简单的例子 # 场景:用户提供账号、密码。
# 首先判断 cookie 是否存在且有效,如果有效,直接跳过,否则执行登录;
# 访问 example.com/api/get-task 获取任务列表(格式:{"tasks": [1,2,3...]};
# 根据 task id 选择访问 url,完成任务。
'on': # 执行任务
cron: '0 12 * * *' # 每天 12:00 执行
timer: 86400 # 每过 24 小时执行一次
retry:
delay: # 重试前等待 1800~3600s
max: 3600
min: 1800
max: 8 # 最多重试 8 次
delay: 60 # 随机延迟 -60~60s 执行
vars: # 变量
- name: username
type: string
display: True
default: xyz
description: 用户名
- name: password
type: string
display: True
default: "a very strong password"
description: 密码
# 隐藏变量,不会在前端显示
- name: cookie
type: string
display: False
default: ""
description: Cookie 存储
- name: taskMap
type: dict
display: False
default:
1: 'https://example.com/api/task/1'
2: 'https://example.com/api/task/3'
3: 'https://example.com/api/task/9'
- name: success-sum
type: int
display: False
default: 0
description: 成功次数,用于生成报告
- name: urlLogin
type: string
display: False
default: 'https://example.com/api/login'
# 一些系统内置变量
- name: __taskid__
type: int
description: 本任务的 ID
- name: __proxy__
type: string
display: True
description: |
本任务使用的代理,所有除了框架 API 访问之外的请求都会使用此代理。
如果有复杂的代理策略,可以自行声明变量,然后在代码中使用。
- name: __token__
type: string
description: 本任务的临时 token,用于访问框架 API,仅对本任务有完全读写权限。
# “临时”是一次性还是短有效期呢?
# 一次性需要额外的数据库
# 短有效期的短是多短呢
# 要不要保证 token 不泄露呢(a. 只能用于访问框架 API; b. 不能被输出到日志中),还是将安全责任交给作者和管理员呢
- name: __retry__
type: int
description: 本次运行是第几次重试
# - name: __log__
# type: string
# description: 本次运行的日志,支持有限的 HTML
_36541de69: # 并不在定义中的 key,可以在这随便写引用的模板
# 数据引用是 YAML 的特性
- headers: &UA
user-agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) HEICORE/49.1.2623.213 Safari/537.36
X-User-Agent: HEICORE/49.1.2623.213
- x: &assert
assert-success:
- content: {"code": 0}
- status: 200
- header: {"Content-Type": "application/json"}
assert-fail:
- code: response.status != 200
process:
- name: 检测 cookie 有效性
type: statement # 类型,默认是 statement,下面会有 if 和 loop 的例子
id: cookie-check
if: cookie != ""
url: u'https://example.com/api/user/me'
method: GET
<<: *assert # 引用模板
assert-success: # 覆盖前面 assert 模板中的部分内容
- code: respose.status == 200 and 'username' in response.json()
headers:
<<: *UA
cookie: cookie
output:
valid: | # YAML 中,开头的引号不能再字符串中间闭合,两种解决方法:1. 外层加上引号;2. 使用 | 或 > 多行字符串;3. u'''
'username' in response.json()
log:
debug: response.json()
- name: 登录
id: login
if: cookie == "" or not cookie-check.valid
url: u'https://example.com/api/login'
method: POST
headers:
<<: *UA
content-type: application/json
body: |
{
"username": username,
"password": password
}
assert-success:
- code: response.status == 200 and 'success' in response.json()
output:
cookie: response.cookie() # 会覆盖全局的 cookie,也可以通过 login.cookie 访问
- name: 保存 cookie
id: cookie-save
if: login.success
url: u'api://v1/var/save'
method: POST
headers:
Authorization: __token__ # 和所有 API 的认证方法保持一致
body: >
{
"task": __taskid__,
"name": "cookie",
"value": json.dump(cookie)
}
assert-success:
- status: 200
- name: 获取任务
id: get-task
url: u'https://example.com/api/get-task'
headers:
<<: *UA
cookie: cookie
<<: *assert
output:
tasks: response.json()['tasks']
log:
info: >
"获取到任务:" + response.json()['tasks']
- type: loop
iter: taskId
in: get-task.tasks
then:
- name: 执行任务
id: task
url: taskMap[taskId]
headers:
<<: *UA
cookie: cookie
<<: *assert
log:
info: f"任务 { taskId } 执行结果:{ response.json() }"
- if: task.success
output:
success-sum: success-sum + 1
- name: 生成日志
log:
summary: f'共有 {len(get-task.tasks)} 个任务,成功执行 {success-sum} 个任务'
|
如果是这样的话, 需要重写一个har2yaml的前端函数, 这个可以作为一个v3的长期任务, v2的任务在于前后端分离和api重构 |
|
去掉了网络访问,速度更快,错误处理更加简单,同时免去了有副作用 API 的认证问题(虽然现在还有没有副作用的 API) 完整定义(非终稿)
## code 类型是我自己定义的,该类型的值会被解释为 python 表达式进行执行,比如 1+1 会被解释为 2, '1'+'1' 会被解释为 '11'
## 使用 YAML 在 code 类型声明纯字符串是有点麻烦的, "xxx" 外围的双引号在 YAML 中会被解释为字符串的定界符,解析之后会丢掉引号。
## 'xxx' in xxx,也会因为最开头的引号在字符串中间闭合而导致 YAML 报错。
## 所以定义了 meta.ingnorePrefix,如果字符串以此开头,则会忽略此前缀,将剩余部分作为 code。
## 例如 meta.ignorePrefix: '$',那么 $1+1 会被解释为 1+1,也就是 int(2);$'1'+'1' 会被解释为 '1'+'1',也就是 str('11')
## 也可以在在 variables 中预先定义需要用到的字符串,当一个字符串需要多次使用时,推荐这种方法。
# hint: YAML 关键字:
# c-sequence-entry # '-'
# | c-mapping-key # '?'
# | c-mapping-value # ':'
# | c-collect-entry # ','
# | c-sequence-start # '['
# | c-sequence-end # ']'
# | c-mapping-start # '{'
# | c-mapping-end # '}'
# | c-comment # '#'
# | c-anchor # '&'
# | c-alias # '*'
# | c-tag # '!'
# | c-literal # '|'
# | c-folded # '>'
# | c-single-quote # "'"
# | c-double-quote # '"'
# | c-directive # '%'
# | c-reserved # '@' '`'
# 这么一看,同时不是 YAML 和 Python 关键字就没几个
# 最终定稿可能会把 meta.ignorePrefix 固定为 $
version: integer # 版本号,用于兼容性检查
meta: # 模块的 meta 信息
name: string # 模板名(网站名)
author: string # 作者
version: string # 版本
url:
release: string # 发布页
update: string # 更新地址
homepage: string # 作者主页
description: string # 备注、描述
ignorePrefix: string # 忽略前缀,默认为 '~'
require:
- string # 依赖的模块
schedule: # 什么时候应该执行
interval: # 固定间隔执行
seconds: integer
minutes: integer
hours: integer
days: integer
weeks: integer
months: integer
years: integer
cron: # cron 风格
second: string
minute: string
hour: string
day: string
dayOfWeek: string
week: string
month: string
year: string
retry: # 失败后重试
delay: integer # 选项1: 固定等待时间
delay: # 选项2: 随机等待时间
max: integer # 最大等待时间(要使用和 interval 一样的类型吗?
min: integer # 最小等待时间
max: integer # 最多重试次数
delayRange: integer # 执行延时区间。前后范围内延迟,比如设定为 60,那么范围就是 -60~60s
# 为什么 retry 的 delay 有两种格式,此处只有一种?因为固定的执行延迟似乎没有必要,如果能找到合适的场景,再加上
variables:
# 内置变量,替代旧的变量存储和 API
# 格式为 namespace.symbol
# process[].output 中创建的变量的完整访问路径是 process.id.name,也可以省略 process、通过 id.name 访问(注意,优先级是 namespace > id,例如:id=global,那么通过 process.global.name 是可以正常访问的,而通过 global.name 则会访问到全局变量)
# global 变量是全局的,访问时可省略 namespace
# 根据定义不同,每次读变量得到的值可能不同:
# 如每个任务的 context.taskid 都不同,不同时间读取 time.stamp 得到的值不同
# 向 namespace 写值会将 namespace 覆盖为 variable;
# 根据定义不同,向 namespace.symbol 写值可能会有副作用:
# 如向 time.stamp 写值不会有任何影响,下次读 time.stamp 得到的还是当前时间戳
# 如向 global.varname 写值会更新全局变量,下次任务启动时,读取 global.varname 得到的是新值(省略 namespace,则只会覆盖本次运行时变量,下次运行时变量仍然是旧值)
#
# context.taskid
# context.proxy
# context.retry
# time.
- name: string # 变量名
# 命名空间,在template.yaml variables 下声明的变量的命名空间强制为 global
# 在 process[].output 中创建的变量命名空间默认为空。
# namespace: string
type: string # 变量类型,可选 string, integer, float, boolean, array, map。默认 string
display: boolean # 是否在前端设置界面显示。默认 True
default: string # 默认值,可选
description: string # 描述,可选
cookies:
- name: string
default: string
domain: string
path: string
# expires: string
process:
# type: fetch
- type: string('fetch') # 类型。类型是 fetch、simple 时可省略,下面会有 if 和 loop 的格式
name: string # 任务名
id: string # step id,可选。其他 step 可以通过 id 访问此 step 的输出
when: code # 条件,可选。如果设置了 when,那么只有满足条件才会执行
url: code # url。有此项时才会执行请求、断言(assert)和输出(output),未给出时则直接执行输出(output)
method: string # 请求方法,默认 GET。这个应该没必要设为 code 吧
asserts:
success: # 成功断言,满足一条即为成功
- status: integer # 检查状态码,完全相等为真
- match: string # 检查 response body,包含为真
- regex: string # 正则匹配 response body
- code: code # 代码
failure: # 失败断言,满足一条即为失败
headers: # 请求头
string: code # HTTP Header 里应该没有同个 key 多次出现的情形吧
cookies:
name: string # 仅用于此条请求的 cookie,会被全局 cookie 覆盖
body: code # 请求体
output: # 输出
key: code # 输出的 key
log: # 日志输出
debug: code
info: code
warning: code
error: code
summary: code # 概括性输出,会在前端突出显示
delay: integer # 延迟执行,单位毫秒
# simple
- type: string('simple') # 可省略
name: string
id: string
when: string
assert-success: ...
assert-failure: ...
output: ...
log: ...
# loop
- type: string('loop')
# 等价为
# for `iter` in `in`:
# then()
iter: string # 循环
of: string # 迭代对象
then: # 循环体
# while
- type: string('while')
# 等价为
# while `while`:
# then()
when: code # 条件
then: # while 语句块
# if
- type: string('if')
# 等价为
# if `if`:
# then()
# else:
# else()
_if: code # 条件。
# when 的行为是不满足时不执行该条语句,包括 else 块
# 加下划线是为了避开 Python 的关键字。不加也可以,但会多很多麻烦
then: # if 语句块
_else: # else 语句块。下划线同样是为了避开关键字(otherwise 有点长
when:
例子
# 例子:登录
# 场景:
# 网站使用 HTTP Header Authorization token 进行认证。
# 访问 example.com/api/get-task 获取任务列表(格式:{"tasks": [1,2,3...]};
# 根据 task id 选择访问 url,完成任务。
#
# 用户提供账号、密码。
# 首先判断是否已有 Authorization token 且是否有效,如果有效,直接跳过,否则执行登录,并保存;
# 首先判断 cookie 是否存在且有效,如果有效,直接跳过,否则执行登录;
#
#
version: 1
meta:
name: 测试模板
author: Cirn09
version: canary#1
url:
release: https://github.com/cirn09/qd2
update: https://github.com/
homepage: https://github.com/cirn09
description: |
测试模板
ignorePrefix: '$'
require:
schedule: # 执行任务
interval:
seconds: 1
minutes: 0
hours: 1
cron:
hour: "*/1"
retry:
delay: # 重试前等待 1800~3600s
max: 3600
min: 1800
max: 8 # 最多重试 8 次
delayRange: 60 # 随机延迟 -60~60s 执行
variables: # 变量
- name: username
# type: string
# display: True
default: xyz
description: 用户名
- name: password
default: "aVeryStr0ngPassword!"
description: 密码
# 隐藏变量,不会在前端显示
- name: token
display: False
- name: taskMap
type: dict
display: False
default:
1: 'https://example.com/api/task/1'
2: 'https://example.com/api/task/3'
3: 'https://example.com/api/task/9'
- name: success_sum # 通过 variables 创建,下面有通过 simple 语句动态创建的例子
type: int
display: False
default: 0
description: 成功次数,用于生成报告
- name: urlLogin
type: string
display: False
default: 'https://example.com/api/login'
# 一些系统内置变量
- name: context.taskid
type: int
description: 本任务的 ID
- name: context.proxy
type: string
display: True
description: |
本任务使用的代理,所有除了框架 API 访问之外的请求都会使用此代理。
如果有复杂的代理策略,可以自行声明变量,然后在代码中使用。
# - name: context.token
# type: string
# description: 本任务的临时 token,用于访问框架 API,仅对本任务有完全读写权限。
# 设计token的本意是为了方便访问 API 保存一些数据,后续设计了新的不使用 API fetch 保存方法
# 所以取消了内置 token,也就不用考虑下面的“临时”问题了。
# “临时”是一次性还是短有效期呢?
# 一次性需要额外的数据库
# 短有效期的短是多短呢
# 要不要保证 token 不泄露呢(a. 只能用于访问框架 API; b. 不能被输出到日志中),还是将安全责任交给作者和管理员呢
- name: context.retry
type: int
description: 本次运行是第几次重试
_36541de69: # 并不在定义中的 key,可以在这随便写引用的模板
# 数据引用是 YAML 的特性
- headers: &UA
user-agent: $'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) HEICORE/49.1.2623.213 Safari/537.36'
X-User-Agent: $'HEICORE/49.1.2623.213'
- x: &assert
asserts:
success:
- match: {"code": 0}
- status: 200
failure:
- code: response.status != 200
process:
- name: 检测 token 有效性
type: fetch # 类型,默认是 statement,下面会有 if 和 loop 的例子
id: token_check
when: token != ""
url: $'https://example.com/api/user/me'
method: GET
<<: *assert # 引用模板
asserts:
success: # 覆盖前面 assert 模板中的部分内容
- code: respose.status == 200
headers:
<<: *UA
Authorization: token
output:
valid: $'username' in response.json()
log:
debug: response.json()
- name: 登录
id: login
when: token == "" or not valid # 完整路径为 process.token_check.valid
url: $'https://example.com/api/login'
method: POST
headers:
<<: *UA
content-type: application/json
body: |
{
"username": username,
"password": password
}
asserts:
success:
- code: response.status == 200 and 'success' in response.json()
output:
global.token: response.json()['token'] # 向 global.token 输出,即会覆盖 token,也会将 token 保存到数据库,下次运行时的 token 也会是这个值
- name: 获取任务
id: get_task
url: $'https://example.com/api/get-task'
headers:
<<: *UA
Authorization: token
<<: *assert
output:
tasks: response.json()['tasks']
log:
info: >
"获取到任务:" + response.json()['tasks']
- output:
failure_sum: 0 # 动态创建,上面有静态创建的例子
# type: simple # 未提供 url 时默认为 simple
- type: loop
iter: taskId
of: get_task.tasks
then:
- name: 执行任务
id: task
url: taskMap[taskId]
headers:
<<: *UA
cookie: cookie
<<: *assert
output:
success: response.json()['success']
log:
info: f"任务 { taskId } 执行结果:{ response.json() }"
- _if: process.task.success
then:
output:
success_sum: success_sum + 1
_else:
output:
failure_sum: failure_sum + 1
- name: 生成日志
log:
summary: f'共有 {len(get_task.tasks)} 个任务,成功执行 {success-sum} 个任务,失败 {failure_sum} 个任务。'
|

相关:#354
现在这个是第三版设计,废弃的第二版中已经实现了几个 API
https://github.com/Cirn09/qiandao/tree/api-v2