-
Notifications
You must be signed in to change notification settings - Fork 577
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
API 的插件框架 #413
base: master
Are you sure you want to change the base?
API 的插件框架 #413
Conversation
尝试修复一些安全性的bug以及优化了Web Error的前端显示 TODO: 更新所有旧的API到v1中 |
迁移旧 API 我来吧,编码和时间相关的都写好了 有一个想要讨论的点,现在注册 API 的方式是通过 |
怎么简单怎么直观怎么来,不用太受旧版约束,未来大概率新旧版api会同时存在很长一段时间 |
web/handlers/api/__init__.py
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里应该没必要进行转义,因为 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
我的建议是默认返回JSON格式的code,message,data
信息
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
既然是api就一步到位,规范一下
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这样设计的话,我觉得对应的系统对 JSON respond 的处理能力需要增强,目前系统对返回内容好像只支持正则匹配。
思考了一下好像不难实现,提供一个 respond content 的魔法变量和一个 JSON 函数应该就可以了,比如 {{ json_parse(content)['code'] }}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
没有看到新UI的界面?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
在写了.jpg,先把方案写下来讨论一下
2f56b2f
to
e208928
Compare
Signed-off-by: a76yyyy <[email protected]>
# 使用原生 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 <[email protected]>
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