mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-11-14 06:04:22 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7483897d3c | |||
| d979b74784 | |||
| 5e86f63c46 | |||
| d1978a8571 | |||
| 4e584d2e2d | |||
| fd0e9acc2f | |||
| e49e0ad2bf | |||
| 28b7f7d796 | |||
| 7ad25f61aa | |||
| 0bebca2990 | |||
| 2960a4ddeb | |||
| 6c15c11e35 | |||
| 1db4c0683c | |||
| 04cda06045 | |||
| 588298c520 | |||
| 0946978052 | |||
| 04615e244e | |||
| de793010ae | |||
| 5471a63360 | |||
| 737b3ae760 |
@@ -10,6 +10,27 @@ sidebar: false
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v3.5.7] - 2026-10-07
|
||||
|
||||
### ✨ 新功能
|
||||
|
||||
- Cloudflare 指向分组新增自定义 DNS 目标,可直接填写域名或 IP;原有节点绑定方式仍为默认选项。
|
||||
- Cloudflare 节点分组会在主节点离线或应用失败时自动切换至备用节点,并在主节点恢复后切回;分组详情支持批量开启橙云。
|
||||
- 代理路由详情支持单独关闭 HTTP/2,默认仍启用;关闭后该路由生成的 HTTPS 配置不再启用 HTTP/2。
|
||||
|
||||
### 🛠 修复
|
||||
|
||||
- 节点配置应用失败后,Agent 会按指数退避自动强制重试,最长间隔为 5 分钟;成功后停止重试,重启后也会恢复未完成的重试。
|
||||
|
||||
### ⚡️ 优化与改进
|
||||
|
||||
- 优化常见代理访问的响应压缩与上传处理效率,同时保留流式上传的兼容选项。
|
||||
|
||||
### 💄 其他/体验
|
||||
|
||||
- Cloudflare 分组成员操作改为图标按钮,减少操作栏占用空间。
|
||||
- 性能设置的代理选项新增请求体缓冲开关,默认开启;流式上传场景可关闭。
|
||||
|
||||
## [v3.5.6] - 2026-10-01
|
||||
|
||||
### ✨ 新功能
|
||||
|
||||
@@ -6419,6 +6419,63 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/d/cloudflare/groups/{id}/members/batch-proxy": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"SessionCookie": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"openflare-cloudflare"
|
||||
],
|
||||
"summary": "批量开启 Cloudflare 橙云",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "分组 ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "批量开启橙云参数",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/cloudflare.MemberBatchProxyInput"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Any"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Any"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Any"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/d/cloudflare/groups/{id}/members/batch-remove": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -15285,6 +15342,15 @@ const docTemplate = `{
|
||||
},
|
||||
"primary_node_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"record_content": {
|
||||
"type": "string"
|
||||
},
|
||||
"record_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"target_mode": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -15318,6 +15384,15 @@ const docTemplate = `{
|
||||
"primary_node": {
|
||||
"$ref": "#/definitions/cloudflare.NodeOption"
|
||||
},
|
||||
"record_content": {
|
||||
"type": "string"
|
||||
},
|
||||
"record_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"target_mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -15337,6 +15412,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"cloudflare.MemberBatchProxyInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"member_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cloudflare.MemberBatchRemoveInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -19426,6 +19512,9 @@ const docTemplate = `{
|
||||
"$ref": "#/definitions/proxy_route.CustomHeaderInput"
|
||||
}
|
||||
},
|
||||
"enable_http2": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable_https": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -19542,6 +19631,9 @@ const docTemplate = `{
|
||||
"custom_headers": {
|
||||
"type": "string"
|
||||
},
|
||||
"enable_http2": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable_https": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
Generated
+22
-8
@@ -283,66 +283,79 @@ packages:
|
||||
resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.60.3':
|
||||
resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.60.3':
|
||||
resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.60.3':
|
||||
resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.60.3':
|
||||
resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.60.3':
|
||||
resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.60.3':
|
||||
resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.60.3':
|
||||
resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.60.3':
|
||||
resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.60.3':
|
||||
resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.60.3':
|
||||
resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.60.3':
|
||||
resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.60.3':
|
||||
resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.60.3':
|
||||
resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==}
|
||||
@@ -543,6 +556,7 @@ packages:
|
||||
'@xmldom/xmldom@0.9.10':
|
||||
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
|
||||
engines: {node: '>=14.6'}
|
||||
deprecated: this version has critical issues, please update to the latest version
|
||||
|
||||
abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
@@ -897,8 +911,8 @@ packages:
|
||||
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
linkify-it@5.0.0:
|
||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||
linkify-it@5.0.2:
|
||||
resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==}
|
||||
|
||||
longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
@@ -912,8 +926,8 @@ packages:
|
||||
markdown-it-mathjax3@4.3.2:
|
||||
resolution: {integrity: sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==}
|
||||
|
||||
markdown-it@14.1.1:
|
||||
resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
|
||||
markdown-it@14.3.2:
|
||||
resolution: {integrity: sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==}
|
||||
hasBin: true
|
||||
|
||||
markdown-title@1.0.2:
|
||||
@@ -2189,7 +2203,7 @@ snapshots:
|
||||
|
||||
kind-of@6.0.3: {}
|
||||
|
||||
linkify-it@5.0.0:
|
||||
linkify-it@5.0.2:
|
||||
dependencies:
|
||||
uc.micro: 2.1.0
|
||||
|
||||
@@ -2208,11 +2222,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
markdown-it@14.1.1:
|
||||
markdown-it@14.3.2:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
entities: 4.5.0
|
||||
linkify-it: 5.0.0
|
||||
linkify-it: 5.0.2
|
||||
mdurl: 2.0.0
|
||||
punycode.js: 2.3.1
|
||||
uc.micro: 2.1.0
|
||||
@@ -2818,7 +2832,7 @@ snapshots:
|
||||
vitepress-plugin-llms@1.12.2:
|
||||
dependencies:
|
||||
gray-matter: 4.0.3
|
||||
markdown-it: 14.1.1
|
||||
markdown-it: 14.3.2
|
||||
markdown-title: 1.0.2
|
||||
mdast-util-from-markdown: 2.0.3
|
||||
millify: 6.1.0
|
||||
|
||||
@@ -237,7 +237,7 @@ Server 的所有核心基础配置定义在 `config.yaml` 中,且均支持环
|
||||
| `openresty_proxy_read_timeout` | `int` | 源站收到请求后返回数据,Agent 最大的等待数据返回间隔时长(秒) | `60` |
|
||||
| `openresty_websocket_enabled` | `bool` | 是否在 HTTP 段中自动载入渲染支持 WebSocket 协议的全局变量及头信息 | `true` |
|
||||
| `openresty_http3_enabled` | `bool` | 是否在生成 nginx 监听描述中渲染支持 HTTP/3 QUIC 双栈监听能力 | `true` |
|
||||
| `openresty_proxy_request_buffering_enabled`| `bool` | 是否将客户端 Request Body 先在网关做完全部读取缓存再向源站递交 | `false` |
|
||||
| `openresty_proxy_request_buffering_enabled`| `bool` | 是否将客户端 Request Body 先在网关做完全部读取缓存再向源站递交;普通代理建议开启,流式上传或实时转发场景可关闭 | `true` |
|
||||
| `openresty_proxy_buffering_enabled` | `bool` | 是否允许网关暂存源站的大量 Response 数据待全部解析后再转发给用户 | `true` |
|
||||
| `openresty_proxy_buffers` | `string` | nginx 反代响应缓冲区的分配数量与单缓存大大小(如 `16 16k`) | `16 16k` |
|
||||
| `openresty_proxy_buffer_size` | `string` | 存放源站返回 Response Header 头部信息的专属缓冲区限制 | `8k` |
|
||||
|
||||
@@ -6412,6 +6412,63 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/d/cloudflare/groups/{id}/members/batch-proxy": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"SessionCookie": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"openflare-cloudflare"
|
||||
],
|
||||
"summary": "批量开启 Cloudflare 橙云",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "分组 ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "批量开启橙云参数",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/cloudflare.MemberBatchProxyInput"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Any"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Any"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Any"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/d/cloudflare/groups/{id}/members/batch-remove": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -15278,6 +15335,15 @@
|
||||
},
|
||||
"primary_node_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"record_content": {
|
||||
"type": "string"
|
||||
},
|
||||
"record_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"target_mode": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -15311,6 +15377,15 @@
|
||||
"primary_node": {
|
||||
"$ref": "#/definitions/cloudflare.NodeOption"
|
||||
},
|
||||
"record_content": {
|
||||
"type": "string"
|
||||
},
|
||||
"record_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"target_mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -15330,6 +15405,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"cloudflare.MemberBatchProxyInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"member_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cloudflare.MemberBatchRemoveInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -19419,6 +19505,9 @@
|
||||
"$ref": "#/definitions/proxy_route.CustomHeaderInput"
|
||||
}
|
||||
},
|
||||
"enable_http2": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable_https": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -19535,6 +19624,9 @@
|
||||
"custom_headers": {
|
||||
"type": "string"
|
||||
},
|
||||
"enable_http2": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable_https": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -330,6 +330,12 @@ definitions:
|
||||
type: string
|
||||
primary_node_id:
|
||||
type: integer
|
||||
record_content:
|
||||
type: string
|
||||
record_type:
|
||||
type: string
|
||||
target_mode:
|
||||
type: string
|
||||
type: object
|
||||
cloudflare.GroupItem:
|
||||
properties:
|
||||
@@ -351,6 +357,12 @@ definitions:
|
||||
type: string
|
||||
primary_node:
|
||||
$ref: '#/definitions/cloudflare.NodeOption'
|
||||
record_content:
|
||||
type: string
|
||||
record_type:
|
||||
type: string
|
||||
target_mode:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
@@ -363,6 +375,13 @@ definitions:
|
||||
target_group_id:
|
||||
type: integer
|
||||
type: object
|
||||
cloudflare.MemberBatchProxyInput:
|
||||
properties:
|
||||
member_ids:
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
type: object
|
||||
cloudflare.MemberBatchRemoveInput:
|
||||
properties:
|
||||
member_ids:
|
||||
@@ -3063,6 +3082,8 @@ definitions:
|
||||
items:
|
||||
$ref: '#/definitions/proxy_route.CustomHeaderInput'
|
||||
type: array
|
||||
enable_http2:
|
||||
type: boolean
|
||||
enable_https:
|
||||
type: boolean
|
||||
enabled:
|
||||
@@ -3140,6 +3161,8 @@ definitions:
|
||||
type: array
|
||||
custom_headers:
|
||||
type: string
|
||||
enable_http2:
|
||||
type: boolean
|
||||
enable_https:
|
||||
type: boolean
|
||||
enabled:
|
||||
@@ -8331,6 +8354,42 @@ paths:
|
||||
summary: 批量移动 Cloudflare 指向成员
|
||||
tags:
|
||||
- openflare-cloudflare
|
||||
/api/v1/d/cloudflare/groups/{id}/members/batch-proxy:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: 分组 ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
- description: 批量开启橙云参数
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/cloudflare.MemberBatchProxyInput'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Any'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/response.Any'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/response.Any'
|
||||
security:
|
||||
- SessionCookie: []
|
||||
summary: 批量开启 Cloudflare 橙云
|
||||
tags:
|
||||
- openflare-cloudflare
|
||||
/api/v1/d/cloudflare/groups/{id}/members/batch-remove:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import type {
|
||||
CloudflareGroup,
|
||||
CloudflareGroupPayload,
|
||||
@@ -50,7 +51,15 @@ export function GroupDialog({
|
||||
() => nodes.filter((node) => node.node_type === 'edge_node'),
|
||||
[nodes],
|
||||
);
|
||||
const recordContentPlaceholders = {
|
||||
CNAME: t('recordContentPlaceholder.CNAME'),
|
||||
A: t('recordContentPlaceholder.A'),
|
||||
AAAA: t('recordContentPlaceholder.AAAA'),
|
||||
};
|
||||
const [name, setName] = useState('');
|
||||
const [targetMode, setTargetMode] = useState<'node' | 'custom'>('node');
|
||||
const [recordType, setRecordType] = useState<'CNAME' | 'A' | 'AAAA'>('A');
|
||||
const [recordContent, setRecordContent] = useState('');
|
||||
const [primaryNodeID, setPrimaryNodeID] = useState('');
|
||||
const [backupNodeID, setBackupNodeID] = useState('none');
|
||||
const [defaultProxied, setDefaultProxied] = useState(true);
|
||||
@@ -59,8 +68,19 @@ export function GroupDialog({
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setName(group?.name ?? '');
|
||||
setPrimaryNodeID(group ? String(group.primary_node.id) : '');
|
||||
setBackupNodeID(group?.backup_node ? String(group.backup_node.id) : 'none');
|
||||
setTargetMode(group?.target_mode === 'custom' ? 'custom' : 'node');
|
||||
setRecordType(group?.record_type ?? 'A');
|
||||
setRecordContent(group?.record_content ?? '');
|
||||
setPrimaryNodeID(
|
||||
group?.target_mode !== 'custom' && group
|
||||
? String(group.primary_node.id)
|
||||
: '',
|
||||
);
|
||||
setBackupNodeID(
|
||||
group?.target_mode !== 'custom' && group?.backup_node
|
||||
? String(group.backup_node.id)
|
||||
: 'none',
|
||||
);
|
||||
setDefaultProxied(group?.default_proxied ?? true);
|
||||
setEnabled(group?.enabled ?? true);
|
||||
}, [group, open]);
|
||||
@@ -82,46 +102,113 @@ export function GroupDialog({
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-primary-node'>{t('primary')}</FieldLabel>
|
||||
<Select value={primaryNodeID} onValueChange={setPrimaryNodeID}>
|
||||
<SelectTrigger id='cf-primary-node' className='w-full'>
|
||||
<SelectValue placeholder={t('primaryPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{edgeNodes.map((node) => (
|
||||
<SelectItem
|
||||
key={node.id}
|
||||
value={String(node.id)}
|
||||
disabled={!node.ip}
|
||||
>
|
||||
{node.name} · {node.ip || t('noIp')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-backup-node'>{t('backup')}</FieldLabel>
|
||||
<Select value={backupNodeID} onValueChange={setBackupNodeID}>
|
||||
<SelectTrigger id='cf-backup-node' className='w-full'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value='none'>{t('none')}</SelectItem>
|
||||
{edgeNodes
|
||||
.filter((node) => String(node.id) !== primaryNodeID)
|
||||
.map((node) => (
|
||||
<SelectItem key={node.id} value={String(node.id)}>
|
||||
{node.name} · {node.ip || t('noIp')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldLabel id='cf-target-mode-label'>{t('targetMode')}</FieldLabel>
|
||||
<ToggleGroup
|
||||
type='single'
|
||||
variant='outline'
|
||||
value={targetMode}
|
||||
aria-labelledby='cf-target-mode-label'
|
||||
className='grid w-full grid-cols-2'
|
||||
onValueChange={(value) => {
|
||||
if (value === 'node' || value === 'custom') {
|
||||
setTargetMode(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleGroupItem value='node' className='w-full'>
|
||||
{t('bindNode')}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value='custom' className='w-full'>
|
||||
{t('customTarget')}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</Field>
|
||||
{targetMode === 'node' ? (
|
||||
<>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-primary-node'>
|
||||
{t('primary')}
|
||||
</FieldLabel>
|
||||
<Select value={primaryNodeID} onValueChange={setPrimaryNodeID}>
|
||||
<SelectTrigger id='cf-primary-node' className='w-full'>
|
||||
<SelectValue placeholder={t('primaryPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{edgeNodes.map((node) => (
|
||||
<SelectItem
|
||||
key={node.id}
|
||||
value={String(node.id)}
|
||||
disabled={!node.ip}
|
||||
>
|
||||
{node.name} · {node.ip || t('noIp')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-backup-node'>{t('backup')}</FieldLabel>
|
||||
<Select value={backupNodeID} onValueChange={setBackupNodeID}>
|
||||
<SelectTrigger id='cf-backup-node' className='w-full'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value='none'>{t('none')}</SelectItem>
|
||||
{edgeNodes
|
||||
.filter((node) => String(node.id) !== primaryNodeID)
|
||||
.map((node) => (
|
||||
<SelectItem key={node.id} value={String(node.id)}>
|
||||
{node.name} · {node.ip || t('noIp')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-record-type'>
|
||||
{t('recordType')}
|
||||
</FieldLabel>
|
||||
<Select
|
||||
value={recordType}
|
||||
onValueChange={(value: 'CNAME' | 'A' | 'AAAA') =>
|
||||
setRecordType(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id='cf-record-type' className='w-full'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value='CNAME'>CNAME</SelectItem>
|
||||
<SelectItem value='A'>A</SelectItem>
|
||||
<SelectItem value='AAAA'>AAAA</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-record-content'>
|
||||
{t('recordContent')}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id='cf-record-content'
|
||||
value={recordContent}
|
||||
placeholder={recordContentPlaceholders[recordType]}
|
||||
onChange={(event) => setRecordContent(event.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
{t('recordContentHint')}
|
||||
</p>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<Field orientation='horizontal'>
|
||||
<FieldLabel htmlFor='cf-default-proxied'>
|
||||
{t('defaultProxied')}
|
||||
@@ -146,13 +233,23 @@ export function GroupDialog({
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={pending || !name.trim() || !primaryNodeID}
|
||||
disabled={
|
||||
pending ||
|
||||
!name.trim() ||
|
||||
(targetMode === 'node' ? !primaryNodeID : !recordContent.trim())
|
||||
}
|
||||
onClick={() =>
|
||||
onSubmit({
|
||||
name: name.trim(),
|
||||
primary_node_id: Number(primaryNodeID),
|
||||
target_mode: targetMode,
|
||||
record_type: recordType,
|
||||
record_content: recordContent.trim(),
|
||||
primary_node_id:
|
||||
targetMode === 'node' ? Number(primaryNodeID) : 0,
|
||||
backup_node_id:
|
||||
backupNodeID === 'none' ? null : Number(backupNodeID),
|
||||
targetMode === 'node' && backupNodeID !== 'none'
|
||||
? Number(backupNodeID)
|
||||
: null,
|
||||
default_proxied: defaultProxied,
|
||||
enabled,
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ArrowLeft,
|
||||
ArrowRightLeft,
|
||||
Cloud,
|
||||
CloudCog,
|
||||
Loader2,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
@@ -239,6 +240,16 @@ export function CloudflareGroupDetailPageClient() {
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
});
|
||||
const batchEnableProxyMutation = useMutation({
|
||||
mutationFn: (memberIds: number[]) =>
|
||||
CloudflareService.batchEnableProxy(groupID, memberIds),
|
||||
onSuccess: async (_, memberIds) => {
|
||||
toast.success(t('batchProxyEnabled', { count: memberIds.length }));
|
||||
setSelectedMemberIDs(new Set());
|
||||
await invalidate();
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
});
|
||||
|
||||
const handleSingleMove = (member: CloudflareMember) => {
|
||||
setMovingMembers([member]);
|
||||
@@ -324,23 +335,39 @@ export function CloudflareGroupDetailPageClient() {
|
||||
<CardHeader>
|
||||
<CardTitle className='text-base'>{t('currentPoint')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('activeNode', {
|
||||
name: group.active_node.name,
|
||||
ip: group.active_node.ip,
|
||||
})}
|
||||
{group.target_mode === 'custom'
|
||||
? t('customTargetSummary', {
|
||||
type: group.record_type,
|
||||
content: group.record_content,
|
||||
})
|
||||
: t('activeNode', {
|
||||
name: group.active_node.name,
|
||||
ip: group.active_node.ip,
|
||||
})}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='flex flex-wrap gap-2'>
|
||||
<Badge variant={group.enabled ? 'default' : 'secondary'}>
|
||||
{group.enabled ? t('syncEnabled') : t('syncDisabled')}
|
||||
</Badge>
|
||||
{group.target_mode === 'node' ? (
|
||||
<>
|
||||
<Badge variant='outline'>
|
||||
{t('primaryNode', { name: group.primary_node.name })}
|
||||
</Badge>
|
||||
<Badge variant='outline'>
|
||||
{t('backupNode', {
|
||||
name: group.backup_node?.name ?? t('backupUnset'),
|
||||
})}
|
||||
</Badge>
|
||||
</>
|
||||
) : null}
|
||||
<Badge variant='outline'>
|
||||
{t('primaryNode', { name: group.primary_node.name })}
|
||||
</Badge>
|
||||
<Badge variant='outline'>
|
||||
{t('backupNode', {
|
||||
name: group.backup_node?.name ?? t('backupUnset'),
|
||||
})}
|
||||
{t(
|
||||
group.target_mode === 'custom'
|
||||
? 'customTargetMode'
|
||||
: 'nodeTargetMode',
|
||||
)}
|
||||
</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -356,6 +383,19 @@ export function CloudflareGroupDetailPageClient() {
|
||||
<Badge variant='secondary' className='text-xs'>
|
||||
{t('selectedCount', { count: selectedMembers.length })}
|
||||
</Badge>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
batchEnableProxyMutation.mutate(
|
||||
selectedMembers.map((member) => member.id),
|
||||
)
|
||||
}
|
||||
disabled={batchEnableProxyMutation.isPending}
|
||||
>
|
||||
<CloudCog data-icon='inline-start' />
|
||||
{t('batchEnableProxy')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
@@ -485,27 +525,30 @@ export function CloudflareGroupDetailPageClient() {
|
||||
<div className='flex justify-end gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
size='icon'
|
||||
title={t('move')}
|
||||
aria-label={t('move')}
|
||||
onClick={() => handleSingleMove(member)}
|
||||
>
|
||||
<ArrowRightLeft data-icon='inline-start' />
|
||||
{t('move')}
|
||||
<ArrowRightLeft />
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
size='icon'
|
||||
title={t('sync')}
|
||||
aria-label={t('sync')}
|
||||
onClick={() => syncMutation.mutate(member.id)}
|
||||
>
|
||||
<RefreshCw data-icon='inline-start' />
|
||||
{t('sync')}
|
||||
<RefreshCw />
|
||||
</Button>
|
||||
<Button
|
||||
variant='destructive'
|
||||
size='sm'
|
||||
size='icon'
|
||||
title={t('remove')}
|
||||
aria-label={t('remove')}
|
||||
onClick={() => removeMutation.mutate(member.id)}
|
||||
>
|
||||
<Trash2 data-icon='inline-start' />
|
||||
{t('remove')}
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -182,7 +182,12 @@ export default function CloudflarePage() {
|
||||
<div>
|
||||
<CardTitle className='text-base'>{group.name}</CardTitle>
|
||||
<CardDescription>
|
||||
{group.active_node.name} · {group.active_node.ip}
|
||||
{group.target_mode === 'custom'
|
||||
? t('customTargetSummary', {
|
||||
type: group.record_type,
|
||||
content: group.record_content,
|
||||
})
|
||||
: `${group.active_node.name} · ${group.active_node.ip}`}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant={group.enabled ? 'default' : 'secondary'}>
|
||||
|
||||
@@ -56,7 +56,7 @@ export const defaultPerformanceFields: PerformanceFields = {
|
||||
openresty_proxy_read_timeout: '60',
|
||||
openresty_websocket_enabled: true,
|
||||
openresty_http3_enabled: true,
|
||||
openresty_proxy_request_buffering_enabled: false,
|
||||
openresty_proxy_request_buffering_enabled: true,
|
||||
openresty_proxy_buffering_enabled: true,
|
||||
openresty_proxy_buffers: '16 16k',
|
||||
openresty_proxy_buffer_size: '8k',
|
||||
@@ -133,7 +133,7 @@ export function mapOptionsToFields(
|
||||
),
|
||||
openresty_proxy_request_buffering_enabled: toBoolean(
|
||||
optionMap.openresty_proxy_request_buffering_enabled,
|
||||
false,
|
||||
true,
|
||||
),
|
||||
openresty_proxy_buffering_enabled: toBoolean(
|
||||
optionMap.openresty_proxy_buffering_enabled,
|
||||
|
||||
@@ -345,6 +345,13 @@ export default function PerformancePage() {
|
||||
updateField('openresty_proxy_buffering_enabled', v)
|
||||
}
|
||||
/>
|
||||
<ToggleRow
|
||||
label={t('proxyRequestBuffering')}
|
||||
checked={fields.openresty_proxy_request_buffering_enabled}
|
||||
onChange={(v) =>
|
||||
updateField('openresty_proxy_request_buffering_enabled', v)
|
||||
}
|
||||
/>
|
||||
<FieldInput
|
||||
label='proxy_buffers'
|
||||
value={fields.openresty_proxy_buffers}
|
||||
@@ -499,7 +506,7 @@ function ToggleRow({
|
||||
return (
|
||||
<div className='flex items-center justify-between rounded-lg border border-dashed px-3 py-2'>
|
||||
<Label className='text-xs'>{label}</Label>
|
||||
<Switch checked={checked} onCheckedChange={onChange} />
|
||||
<Switch checked={checked} onCheckedChange={onChange} aria-label={label} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -380,6 +380,7 @@ export function buildPayloadFromRoute(
|
||||
upstreams: (route.upstream_list ?? []).slice(1),
|
||||
enabled: route.enabled,
|
||||
enable_https: route.enable_https,
|
||||
enable_http2: route.enable_http2,
|
||||
redirect_http: route.redirect_http,
|
||||
limit_conn_per_server: route.limit_conn_per_server,
|
||||
limit_conn_per_ip: route.limit_conn_per_ip,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import type { ProxyRouteItem } from '@/lib/services/openflare';
|
||||
import { NodeService, PagesService } from '@/lib/services/openflare';
|
||||
@@ -50,6 +51,7 @@ type ReverseProxyValues = {
|
||||
tunnel_target_protocol?: 'http' | 'https';
|
||||
pages_project_id?: string;
|
||||
custom_headers_text: string;
|
||||
enable_http2: boolean;
|
||||
};
|
||||
|
||||
interface ProxySectionProps {
|
||||
@@ -74,6 +76,7 @@ export function ProxySection({
|
||||
tunnel_target_protocol: z.enum(['http', 'https']).optional(),
|
||||
pages_project_id: z.string().optional(),
|
||||
custom_headers_text: z.string(),
|
||||
enable_http2: z.boolean(),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
if (value.upstream_type === 'direct') {
|
||||
@@ -174,6 +177,7 @@ export function ProxySection({
|
||||
? String(route.pages_project_id)
|
||||
: '',
|
||||
custom_headers_text: customHeadersToText(route.custom_header_list),
|
||||
enable_http2: route.enable_http2 ?? true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -190,6 +194,7 @@ export function ProxySection({
|
||||
? String(route.pages_project_id)
|
||||
: '',
|
||||
custom_headers_text: customHeadersToText(route.custom_header_list),
|
||||
enable_http2: route.enable_http2 ?? true,
|
||||
});
|
||||
}, [form, route]);
|
||||
|
||||
@@ -267,6 +272,7 @@ export function ProxySection({
|
||||
values.upstream_type === 'pages' && values.pages_project_id
|
||||
? Number(values.pages_project_id)
|
||||
: null,
|
||||
enable_http2: values.enable_http2,
|
||||
},
|
||||
t('proxySaved'),
|
||||
);
|
||||
@@ -306,6 +312,25 @@ export function ProxySection({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_http2'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between rounded-lg border p-3'>
|
||||
<div className='space-y-1'>
|
||||
<FormLabel>{t('enableHttp2')}</FormLabel>
|
||||
<FormDescription>{t('enableHttp2Desc')}</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{upstreamType === 'direct' ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -133,4 +133,10 @@ export class CloudflareService extends OpenFlareBaseService {
|
||||
member_ids: memberIds,
|
||||
});
|
||||
}
|
||||
|
||||
static batchEnableProxy(groupId: number, memberIds: number[]): Promise<void> {
|
||||
return this.post<void>(`/groups/${groupId}/members/batch-proxy`, {
|
||||
member_ids: memberIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@ export interface ProxyRouteItem {
|
||||
upstream_list: string[];
|
||||
enabled: boolean;
|
||||
enable_https: boolean;
|
||||
enable_http2: boolean;
|
||||
redirect_http: boolean;
|
||||
limit_conn_per_server: number;
|
||||
limit_conn_per_ip: number;
|
||||
@@ -271,6 +272,7 @@ export interface ProxyRouteMutationPayload {
|
||||
upstreams: string[];
|
||||
enabled: boolean;
|
||||
enable_https: boolean;
|
||||
enable_http2?: boolean;
|
||||
redirect_http: boolean;
|
||||
limit_conn_per_server?: number;
|
||||
limit_conn_per_ip?: number;
|
||||
@@ -1367,6 +1369,9 @@ export interface CloudflareNodeOption {
|
||||
export interface CloudflareGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
target_mode: 'node' | 'custom';
|
||||
record_type: 'CNAME' | 'A' | 'AAAA';
|
||||
record_content: string;
|
||||
primary_node: CloudflareNodeOption;
|
||||
backup_node: CloudflareNodeOption | null;
|
||||
active_node: CloudflareNodeOption;
|
||||
@@ -1379,6 +1384,9 @@ export interface CloudflareGroup {
|
||||
|
||||
export interface CloudflareGroupPayload {
|
||||
name: string;
|
||||
target_mode: 'node' | 'custom';
|
||||
record_type: 'CNAME' | 'A' | 'AAAA';
|
||||
record_content: string;
|
||||
primary_node_id: number;
|
||||
backup_node_id: number | null;
|
||||
default_proxied: boolean;
|
||||
|
||||
@@ -509,7 +509,8 @@
|
||||
"runtimeTitle": "Connections and events",
|
||||
"runtimeDesc": "Workers, timeouts, and request body size",
|
||||
"proxyTitle": "Proxy buffers and timeouts",
|
||||
"proxyDesc": "Upstream connections, buffers, and WebSocket/HTTP3 switches",
|
||||
"proxyDesc": "Upstream connections, request/response buffering, and WebSocket/HTTP3 switches",
|
||||
"proxyRequestBuffering": "Request body buffering (turn off for streaming uploads)",
|
||||
"gzipTitle": "Compression",
|
||||
"cacheTitle": "Cache",
|
||||
"cacheDesc": "Single-node reverse-proxy cache",
|
||||
|
||||
@@ -509,7 +509,8 @@
|
||||
"runtimeTitle": "连接与事件",
|
||||
"runtimeDesc": "worker、超时、请求体大小等运行参数",
|
||||
"proxyTitle": "反代缓冲与超时",
|
||||
"proxyDesc": "upstream 连接、缓冲与 WebSocket/HTTP3 开关",
|
||||
"proxyDesc": "upstream 连接、请求与响应缓冲,以及 WebSocket/HTTP3 开关",
|
||||
"proxyRequestBuffering": "请求体缓冲(流式上传时关闭)",
|
||||
"gzipTitle": "压缩",
|
||||
"cacheTitle": "缓存",
|
||||
"cacheDesc": "单节点反代缓存优化场景",
|
||||
|
||||
@@ -772,10 +772,10 @@
|
||||
"notReadyDesc": "Import an existing Cloudflare DNS account, or enter a standalone API token and test the connection.",
|
||||
"configure": "Configure connection",
|
||||
"groupsTitle": "Pointing groups",
|
||||
"groupsDesc": "Manage the nodes and domain members behind Cloudflare A records.",
|
||||
"groupsDesc": "Manage Cloudflare DNS targets and domain members.",
|
||||
"loadingGroups": "Loading pointing groups...",
|
||||
"emptyTitle": "No pointing groups",
|
||||
"emptyDesc": "After creating a group, you can sync domain members to selected edge nodes.",
|
||||
"emptyDesc": "Create a group to bind domain members to an edge node or a custom DNS target.",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"memberSummary": "{count} members · new members default orange-cloud {proxy}",
|
||||
@@ -788,7 +788,7 @@
|
||||
"syncQueued": "Group sync queued",
|
||||
"deleted": "Pointing group and remote records deleted",
|
||||
"deleteTitle": "Delete pointing group",
|
||||
"deleteDesc": "This deletes all members of {name} and the remote A records managed by this module. This cannot be undone.",
|
||||
"deleteDesc": "This deletes all members of {name} and the remote DNS records managed by this module. This cannot be undone.",
|
||||
"confirmDelete": "Delete",
|
||||
"loadingDetail": "Loading group details...",
|
||||
"groupUpdated": "Group settings updated",
|
||||
@@ -798,13 +798,18 @@
|
||||
"memberPartial": "Partial success: {succeeded} added, {failed} failed",
|
||||
"proxyUpdated": "Orange-cloud setting updated and queued for sync",
|
||||
"memberSyncQueued": "Member sync queued",
|
||||
"memberDeleted": "Member and remote A record deleted",
|
||||
"memberDeleted": "Member and remote DNS record deleted",
|
||||
"batchEnableProxy": "Enable orange cloud",
|
||||
"batchProxyEnabled": "Enabled orange cloud for {count} members and queued sync",
|
||||
"back": "Back to list",
|
||||
"refresh": "Refresh",
|
||||
"edit": "Edit",
|
||||
"addDomain": "Add domain",
|
||||
"currentPoint": "Current target",
|
||||
"activeNode": "Active node {name} · {ip}",
|
||||
"customTargetSummary": "Custom target {type} · {content}",
|
||||
"nodeTargetMode": "Node binding",
|
||||
"customTargetMode": "Custom record",
|
||||
"syncEnabled": "Sync enabled",
|
||||
"syncDisabled": "Sync disabled",
|
||||
"primaryNode": "Primary {name}",
|
||||
@@ -815,7 +820,7 @@
|
||||
"noMembers": "No domain members yet.",
|
||||
"columns": {
|
||||
"domain": "Domain",
|
||||
"desiredIp": "Desired IP",
|
||||
"desiredIp": "Desired value",
|
||||
"status": "Status",
|
||||
"proxied": "Orange cloud",
|
||||
"actions": "Actions"
|
||||
@@ -825,8 +830,19 @@
|
||||
"groupDialog": {
|
||||
"editTitle": "Edit pointing group",
|
||||
"createTitle": "New pointing group",
|
||||
"description": "Phase 1 uses the primary node as the active node. Backup nodes are saved only and do not fail over automatically.",
|
||||
"description": "Bind members to an edge node or assign a custom DNS record target.",
|
||||
"name": "Group name",
|
||||
"targetMode": "Target mode",
|
||||
"bindNode": "Bind node",
|
||||
"customTarget": "Custom",
|
||||
"recordType": "Record type",
|
||||
"recordContent": "Record value",
|
||||
"recordContentHint": "Enter an IPv4 for A, IPv6 for AAAA, or a target hostname for CNAME.",
|
||||
"recordContentPlaceholder": {
|
||||
"CNAME": "e.g. origin.example.com",
|
||||
"A": "e.g. 203.0.113.10",
|
||||
"AAAA": "e.g. 2001:db8::10"
|
||||
},
|
||||
"primary": "Primary node",
|
||||
"primaryPlaceholder": "Select an edge node with IPv4",
|
||||
"noIp": "No IP configured",
|
||||
@@ -838,7 +854,7 @@
|
||||
},
|
||||
"memberDialog": {
|
||||
"title": "Add domain members",
|
||||
"description": "Search and select in bulk. Joining creates or takes over the unique same-name A record. Multiple same-name A records are rejected.",
|
||||
"description": "Search and select in bulk. Joining creates or takes over the unique same-name DNS record used by this group.",
|
||||
"zoneDomain": "Zone domain",
|
||||
"searchPlaceholder": "Search domains or zones…",
|
||||
"selected": "{count} selected",
|
||||
|
||||
@@ -772,10 +772,10 @@
|
||||
"notReadyDesc": "请先导入现有 Cloudflare DNS 账号,或录入独立 API Token 并完成连接测试。",
|
||||
"configure": "配置连接",
|
||||
"groupsTitle": "指向分组",
|
||||
"groupsDesc": "管理 Cloudflare A 记录对应的节点与域名成员。",
|
||||
"groupsDesc": "管理 Cloudflare DNS 记录的指向方式与域名成员。",
|
||||
"loadingGroups": "加载指向分组中...",
|
||||
"emptyTitle": "暂无指向分组",
|
||||
"emptyDesc": "创建分组后,可将域名成员同步到指定的边缘节点。",
|
||||
"emptyDesc": "创建分组后,可将域名成员绑定到边缘节点,或同步到自定义 DNS 目标。",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已停用",
|
||||
"memberSummary": "成员 {count} 个 · 新成员默认{proxy}橙云",
|
||||
@@ -788,7 +788,7 @@
|
||||
"syncQueued": "分组同步任务已入队",
|
||||
"deleted": "指向分组及远端记录已删除",
|
||||
"deleteTitle": "删除指向分组",
|
||||
"deleteDesc": "将删除 {name} 的全部成员及本模块管理的远端 A 记录。此操作不可撤销。",
|
||||
"deleteDesc": "将删除 {name} 的全部成员及本模块管理的远端 DNS 记录。此操作不可撤销。",
|
||||
"confirmDelete": "确认删除",
|
||||
"loadingDetail": "加载分组详情中...",
|
||||
"groupUpdated": "分组配置已更新",
|
||||
@@ -798,13 +798,18 @@
|
||||
"memberPartial": "部分成功:{succeeded} 个已加入,{failed} 个失败",
|
||||
"proxyUpdated": "橙云设置已更新并排队同步",
|
||||
"memberSyncQueued": "成员同步任务已入队",
|
||||
"memberDeleted": "成员及远端 A 记录已删除",
|
||||
"memberDeleted": "成员及远端 DNS 记录已删除",
|
||||
"batchEnableProxy": "批量开启橙云",
|
||||
"batchProxyEnabled": "已为 {count} 个成员开启橙云并排队同步",
|
||||
"back": "返回列表",
|
||||
"refresh": "刷新",
|
||||
"edit": "编辑",
|
||||
"addDomain": "添加域名",
|
||||
"currentPoint": "当前指向",
|
||||
"activeNode": "生效节点 {name} · {ip}",
|
||||
"customTargetSummary": "自定义目标 {type} · {content}",
|
||||
"nodeTargetMode": "绑定节点",
|
||||
"customTargetMode": "自定义记录",
|
||||
"syncEnabled": "同步已启用",
|
||||
"syncDisabled": "同步已停用",
|
||||
"primaryNode": "主节点 {name}",
|
||||
@@ -815,7 +820,7 @@
|
||||
"noMembers": "暂无域名成员。",
|
||||
"columns": {
|
||||
"domain": "域名",
|
||||
"desiredIp": "期望 IP",
|
||||
"desiredIp": "期望值",
|
||||
"status": "状态",
|
||||
"proxied": "橙云",
|
||||
"actions": "操作"
|
||||
@@ -825,8 +830,19 @@
|
||||
"groupDialog": {
|
||||
"editTitle": "编辑指向分组",
|
||||
"createTitle": "新增指向分组",
|
||||
"description": "一期使用主节点作为生效节点;备用节点仅保存,不会自动切换。",
|
||||
"description": "选择绑定边缘节点,或为成员统一指定自定义 DNS 记录目标。",
|
||||
"name": "分组名称",
|
||||
"targetMode": "指向方式",
|
||||
"bindNode": "绑定节点",
|
||||
"customTarget": "自定义",
|
||||
"recordType": "记录类型",
|
||||
"recordContent": "记录值",
|
||||
"recordContentHint": "A 填 IPv4,AAAA 填 IPv6,CNAME 填目标域名。",
|
||||
"recordContentPlaceholder": {
|
||||
"CNAME": "例如:origin.example.com",
|
||||
"A": "例如:203.0.113.10",
|
||||
"AAAA": "例如:2001:db8::10"
|
||||
},
|
||||
"primary": "主节点",
|
||||
"primaryPlaceholder": "选择带 IPv4 的边缘节点",
|
||||
"noIp": "未配置 IP",
|
||||
@@ -838,7 +854,7 @@
|
||||
},
|
||||
"memberDialog": {
|
||||
"title": "添加域名成员",
|
||||
"description": "支持搜索筛选与批量勾选;加入后会创建或接管唯一同名 A 记录,多条同名 A 会拒绝同步。",
|
||||
"description": "支持搜索筛选与批量勾选;加入后会创建或接管当前分组指向所用的唯一同名 DNS 记录。",
|
||||
"zoneDomain": "Zone 域名",
|
||||
"searchPlaceholder": "搜索域名或顶级域…",
|
||||
"selected": "已选 {count}",
|
||||
|
||||
@@ -380,6 +380,8 @@
|
||||
"redirectHttpNeedsCert": "At least one selected domain must have a bound certificate before this can be enabled.",
|
||||
"reverseProxy": "Reverse proxy",
|
||||
"reverseProxyDesc": "Configure the origin strategy and upstream addresses.",
|
||||
"enableHttp2": "Enable HTTP/2",
|
||||
"enableHttp2Desc": "Enabled by default. When disabled, HTTPS traffic will not use HTTP/2.",
|
||||
"proxySaved": "Reverse proxy settings saved",
|
||||
"originHostHint": "If left empty, the request hostname $host is passed through by default.",
|
||||
"customHeaders": "Custom request headers",
|
||||
|
||||
@@ -380,6 +380,8 @@
|
||||
"redirectHttpNeedsCert": "所选域名至少绑定一张证书后才能启用。",
|
||||
"reverseProxy": "反向代理",
|
||||
"reverseProxyDesc": "配置请求回源上游的策略与地址。",
|
||||
"enableHttp2": "启用 HTTP/2",
|
||||
"enableHttp2Desc": "默认开启。关闭后,HTTPS 访问将不再使用 HTTP/2。",
|
||||
"proxySaved": "反向代理设置已保存",
|
||||
"originHostHint": "留空时默认透传访问域名 $host。",
|
||||
"customHeaders": "自定义请求头",
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"@tanstack/react-virtual": "^3.14.9",
|
||||
"@uiw/react-codemirror": "^4.25.11",
|
||||
"@xyflow/react": "^12.11.2",
|
||||
"axios": "^1.19.0",
|
||||
"axios": "^1.20.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -61,7 +61,7 @@
|
||||
"jspdf": "^4.2.1",
|
||||
"lucide-react": "^1.28.0",
|
||||
"motion": "^12.43.0",
|
||||
"next": "16.3.0",
|
||||
"next": "16.3.6",
|
||||
"next-intl": "^4.13.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"pinyin-match": "^1.2.10",
|
||||
|
||||
Generated
+194
-193
@@ -111,8 +111,8 @@ importers:
|
||||
specifier: ^12.11.2
|
||||
version: 12.11.2(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(immer@11.1.15)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
axios:
|
||||
specifier: ^1.19.0
|
||||
version: 1.19.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
specifier: ^1.20.0
|
||||
version: 1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
babel-plugin-react-compiler:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
@@ -153,11 +153,11 @@ importers:
|
||||
specifier: ^12.43.0
|
||||
version: 12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
next:
|
||||
specifier: 16.3.0
|
||||
version: 16.3.0(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.1.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
specifier: 16.3.6
|
||||
version: 16.3.6(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.1.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
next-intl:
|
||||
specifier: ^4.13.6
|
||||
version: 4.13.6(next@16.3.0(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.1.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@6.0.3)
|
||||
version: 4.13.6(@swc/helpers@0.5.23)(next@16.3.6(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.1.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@6.0.3)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
@@ -729,160 +729,160 @@ packages:
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
|
||||
'@img/sharp-darwin-arm64@0.35.5':
|
||||
resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.35.4':
|
||||
resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
|
||||
'@img/sharp-darwin-x64@0.35.5':
|
||||
resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-freebsd-wasm32@0.35.4':
|
||||
resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
|
||||
'@img/sharp-freebsd-wasm32@0.35.5':
|
||||
resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
os: [freebsd]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.3':
|
||||
resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.4':
|
||||
resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.3.3':
|
||||
resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
|
||||
'@img/sharp-libvips-darwin-x64@1.3.4':
|
||||
resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.3.3':
|
||||
resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
|
||||
'@img/sharp-libvips-linux-arm64@1.3.4':
|
||||
resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.3.3':
|
||||
resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
|
||||
'@img/sharp-libvips-linux-arm@1.3.4':
|
||||
resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.3':
|
||||
resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.4':
|
||||
resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.3':
|
||||
resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.4':
|
||||
resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.3.3':
|
||||
resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
|
||||
'@img/sharp-libvips-linux-s390x@1.3.4':
|
||||
resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.3.3':
|
||||
resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
|
||||
'@img/sharp-libvips-linux-x64@1.3.4':
|
||||
resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
|
||||
resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.4':
|
||||
resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
|
||||
resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.4':
|
||||
resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
|
||||
'@img/sharp-linux-arm64@0.35.5':
|
||||
resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.35.4':
|
||||
resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
|
||||
'@img/sharp-linux-arm@0.35.5':
|
||||
resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.35.4':
|
||||
resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
|
||||
'@img/sharp-linux-ppc64@0.35.5':
|
||||
resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.35.4':
|
||||
resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
|
||||
'@img/sharp-linux-riscv64@0.35.5':
|
||||
resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.35.4':
|
||||
resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
|
||||
'@img/sharp-linux-s390x@0.35.5':
|
||||
resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.35.4':
|
||||
resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
|
||||
'@img/sharp-linux-x64@0.35.5':
|
||||
resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
|
||||
'@img/sharp-linuxmusl-arm64@0.35.5':
|
||||
resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.35.4':
|
||||
resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
|
||||
'@img/sharp-linuxmusl-x64@0.35.5':
|
||||
resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.35.4':
|
||||
resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
|
||||
'@img/sharp-wasm32@0.35.5':
|
||||
resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
|
||||
'@img/sharp-webcontainers-wasm32@0.35.4':
|
||||
resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
|
||||
'@img/sharp-webcontainers-wasm32@0.35.5':
|
||||
resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
|
||||
'@img/sharp-win32-arm64@0.35.5':
|
||||
resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.35.4':
|
||||
resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
|
||||
'@img/sharp-win32-ia32@0.35.5':
|
||||
resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==}
|
||||
engines: {node: ^20.9.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.35.4':
|
||||
resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
|
||||
'@img/sharp-win32-x64@0.35.5':
|
||||
resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -934,60 +934,60 @@ packages:
|
||||
'@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3
|
||||
'@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3
|
||||
|
||||
'@next/env@16.3.0':
|
||||
resolution: {integrity: sha512-o9r1S0BNiNreHP9Vs+Qnqd9kviDkJh8xIACY7UFZSmiGbbQRzPBBosvHzAU4TULHOIuOj/18RSsyz2qrREmIFw==}
|
||||
'@next/env@16.3.6':
|
||||
resolution: {integrity: sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ==}
|
||||
|
||||
'@next/eslint-plugin-next@16.3.0':
|
||||
resolution: {integrity: sha512-OqgJ8PN0d04KcPhDX/PTY5tJUJZxlbrt7O7FBsm4XE0XW2JDrKnDXsc9uo9WUimJGPoo2j+JRGhyXApC//mvbw==}
|
||||
|
||||
'@next/swc-darwin-arm64@16.3.0':
|
||||
resolution: {integrity: sha512-55hpqq18bEVAlxedlTt3tFqZmKg2nUXT1kn1G/BGEy0R13h3LwtwHPVzzjG6P4LLeOHE32PFDQUVaJEWvBEZBw==}
|
||||
'@next/swc-darwin-arm64@16.3.6':
|
||||
resolution: {integrity: sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@16.3.0':
|
||||
resolution: {integrity: sha512-SOi96kSaF5T+0wW4koiM1bWzSPwjzTesC1p3df+FjdOi5LIQkBK/blxh7HdoKnNuI4PURF1OO7TZqtfnbWDSgw==}
|
||||
'@next/swc-darwin-x64@16.3.6':
|
||||
resolution: {integrity: sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.3.0':
|
||||
resolution: {integrity: sha512-P0gZAoPMF4dyTRzhmkV4PrqVzSOB6t4mC1oI3c4dqijJ+OVEVx5clIXAKR4/uQpsqw2KKM/0D5tVumcR2r5blg==}
|
||||
'@next/swc-linux-arm64-gnu@16.3.6':
|
||||
resolution: {integrity: sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.3.0':
|
||||
resolution: {integrity: sha512-tXXGKJw0m37O0eKJARVTX/TheKPhz0QFVtVVZXmOig+9YKLQOSP6hvf2pxv5DO7CLEJyTHx3Pg043CDQkv1G4Q==}
|
||||
'@next/swc-linux-arm64-musl@16.3.6':
|
||||
resolution: {integrity: sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.3.0':
|
||||
resolution: {integrity: sha512-pjGxK5EY7yWml78ALejFkWmgHsU7wbFQrISiugpH6FbUJhgEvw3xFZ/EBAtLl7QtL0WdQKiG9eWJ3mOKGTukHw==}
|
||||
'@next/swc-linux-x64-gnu@16.3.6':
|
||||
resolution: {integrity: sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.3.0':
|
||||
resolution: {integrity: sha512-sjo++Xx+lomlPs3HRsHWhVDyGG6ms1kGW5EtHLERdII8AyG1i+f6aq68xHREO6AEMlhjTNEWBSmfJfqm9orf7g==}
|
||||
'@next/swc-linux-x64-musl@16.3.6':
|
||||
resolution: {integrity: sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.3.0':
|
||||
resolution: {integrity: sha512-C5JSgiO54wURdaxdEUIXqkz04uMqC9UmPX1gtDrV/5Tf1UowdWYI8uA5hfFbPolTlp0q4KZ60xlHePNibf0VIw==}
|
||||
'@next/swc-win32-arm64-msvc@16.3.6':
|
||||
resolution: {integrity: sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.3.0':
|
||||
resolution: {integrity: sha512-fDOggsweNb5SSw0ZKVk6U+gxSyGFFlIBY/LBc1r8GUj4u/6t6oArL+Pmkg0MBnsgR+KkdsURilVH4F3GXUGepA==}
|
||||
'@next/swc-win32-x64-msvc@16.3.6':
|
||||
resolution: {integrity: sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1994,8 +1994,8 @@ packages:
|
||||
'@swc/counter@0.1.3':
|
||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
'@swc/helpers@0.5.23':
|
||||
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
|
||||
|
||||
'@swc/types@0.1.28':
|
||||
resolution: {integrity: sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==}
|
||||
@@ -2625,8 +2625,8 @@ packages:
|
||||
resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
axios@1.19.0:
|
||||
resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==}
|
||||
axios@1.20.0:
|
||||
resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==}
|
||||
|
||||
axobject-query@4.1.0:
|
||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||
@@ -2657,8 +2657,8 @@ packages:
|
||||
bidi-js@1.0.3:
|
||||
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
||||
|
||||
brace-expansion@1.1.18:
|
||||
resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==}
|
||||
brace-expansion@1.1.21:
|
||||
resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==}
|
||||
|
||||
brace-expansion@5.0.9:
|
||||
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
|
||||
@@ -2951,8 +2951,8 @@ packages:
|
||||
dom-accessibility-api@0.6.3:
|
||||
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
||||
|
||||
dompurify@3.4.13:
|
||||
resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==}
|
||||
dompurify@3.4.16:
|
||||
resolution: {integrity: sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
@@ -4026,8 +4026,8 @@ packages:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next@16.3.0:
|
||||
resolution: {integrity: sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A==}
|
||||
next@16.3.6:
|
||||
resolution: {integrity: sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -4447,8 +4447,8 @@ packages:
|
||||
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
sharp@0.35.4:
|
||||
resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
|
||||
sharp@0.35.5:
|
||||
resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '*'
|
||||
@@ -4492,8 +4492,8 @@ packages:
|
||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
source-map-js@1.2.2:
|
||||
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
source-map-support@0.5.21:
|
||||
@@ -4716,8 +4716,8 @@ packages:
|
||||
undici-types@8.3.0:
|
||||
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
|
||||
|
||||
undici@8.10.0:
|
||||
resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==}
|
||||
undici@8.11.2:
|
||||
resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
unified@11.0.5:
|
||||
@@ -5445,108 +5445,108 @@ snapshots:
|
||||
'@img/colour@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-arm64@0.35.4':
|
||||
'@img/sharp-darwin-arm64@0.35.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.3
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.35.4':
|
||||
'@img/sharp-darwin-x64@0.35.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.3
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-freebsd-wasm32@0.35.4':
|
||||
'@img/sharp-freebsd-wasm32@0.35.5':
|
||||
dependencies:
|
||||
'@img/sharp-wasm32': 0.35.4
|
||||
'@img/sharp-wasm32': 0.35.5
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.3':
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.3.3':
|
||||
'@img/sharp-libvips-darwin-x64@1.3.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.3.3':
|
||||
'@img/sharp-libvips-linux-arm64@1.3.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.3.3':
|
||||
'@img/sharp-libvips-linux-arm@1.3.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.3':
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.3':
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.3.3':
|
||||
'@img/sharp-libvips-linux-s390x@1.3.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.3.3':
|
||||
'@img/sharp-libvips-linux-x64@1.3.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.35.4':
|
||||
'@img/sharp-linux-arm64@0.35.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.35.4':
|
||||
'@img/sharp-linux-arm@0.35.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.35.4':
|
||||
'@img/sharp-linux-ppc64@0.35.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.3
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.35.4':
|
||||
'@img/sharp-linux-riscv64@0.35.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.3
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.35.4':
|
||||
'@img/sharp-linux-s390x@0.35.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.3
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.35.4':
|
||||
'@img/sharp-linux-x64@0.35.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.3.3
|
||||
'@img/sharp-libvips-linux-x64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.35.4':
|
||||
'@img/sharp-linuxmusl-arm64@0.35.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.35.4':
|
||||
'@img/sharp-linuxmusl-x64@0.35.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.35.4':
|
||||
'@img/sharp-wasm32@0.35.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.11.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-webcontainers-wasm32@0.35.4':
|
||||
'@img/sharp-webcontainers-wasm32@0.35.5':
|
||||
dependencies:
|
||||
'@img/sharp-wasm32': 0.35.4
|
||||
'@img/sharp-wasm32': 0.35.5
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.35.4':
|
||||
'@img/sharp-win32-arm64@0.35.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.35.4':
|
||||
'@img/sharp-win32-ia32@0.35.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.35.4':
|
||||
'@img/sharp-win32-x64@0.35.5':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
@@ -5618,7 +5618,7 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.3
|
||||
optional: true
|
||||
|
||||
'@next/env@16.3.0': {}
|
||||
'@next/env@16.3.6': {}
|
||||
|
||||
'@next/eslint-plugin-next@16.3.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))':
|
||||
dependencies:
|
||||
@@ -5627,28 +5627,28 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- eslint
|
||||
|
||||
'@next/swc-darwin-arm64@16.3.0':
|
||||
'@next/swc-darwin-arm64@16.3.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@16.3.0':
|
||||
'@next/swc-darwin-x64@16.3.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.3.0':
|
||||
'@next/swc-linux-arm64-gnu@16.3.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.3.0':
|
||||
'@next/swc-linux-arm64-musl@16.3.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.3.0':
|
||||
'@next/swc-linux-x64-gnu@16.3.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@16.3.0':
|
||||
'@next/swc-linux-x64-musl@16.3.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.3.0':
|
||||
'@next/swc-win32-arm64-msvc@16.3.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.3.0':
|
||||
'@next/swc-win32-x64-msvc@16.3.6':
|
||||
optional: true
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
@@ -6577,7 +6577,7 @@ snapshots:
|
||||
'@swc/core-win32-x64-msvc@1.16.0':
|
||||
optional: true
|
||||
|
||||
'@swc/core@1.16.0':
|
||||
'@swc/core@1.16.0(@swc/helpers@0.5.23)':
|
||||
dependencies:
|
||||
'@swc/counter': 0.1.3
|
||||
'@swc/types': 0.1.28
|
||||
@@ -6594,10 +6594,11 @@ snapshots:
|
||||
'@swc/core-win32-arm64-msvc': 1.16.0
|
||||
'@swc/core-win32-ia32-msvc': 1.16.0
|
||||
'@swc/core-win32-x64-msvc': 1.16.0
|
||||
'@swc/helpers': 0.5.23
|
||||
|
||||
'@swc/counter@0.1.3': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
'@swc/helpers@0.5.23':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
@@ -6619,7 +6620,7 @@ snapshots:
|
||||
jiti: 2.7.0
|
||||
lightningcss: 1.32.0
|
||||
magic-string: 0.30.21
|
||||
source-map-js: 1.2.1
|
||||
source-map-js: 1.2.2
|
||||
tailwindcss: 4.3.3
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.3.3':
|
||||
@@ -7238,7 +7239,7 @@ snapshots:
|
||||
|
||||
axe-core@4.13.0: {}
|
||||
|
||||
axios@1.19.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0):
|
||||
axios@1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0):
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
|
||||
form-data: 4.0.6
|
||||
@@ -7269,7 +7270,7 @@ snapshots:
|
||||
dependencies:
|
||||
require-from-string: 2.0.2
|
||||
|
||||
brace-expansion@1.1.18:
|
||||
brace-expansion@1.1.21:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
@@ -7415,7 +7416,7 @@ snapshots:
|
||||
css-tree@3.2.1:
|
||||
dependencies:
|
||||
mdn-data: 2.27.1
|
||||
source-map-js: 1.2.1
|
||||
source-map-js: 1.2.2
|
||||
|
||||
css.escape@1.5.1: {}
|
||||
|
||||
@@ -7570,7 +7571,7 @@ snapshots:
|
||||
|
||||
dom-accessibility-api@0.6.3: {}
|
||||
|
||||
dompurify@3.4.13:
|
||||
dompurify@3.4.16:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
optional: true
|
||||
@@ -8437,7 +8438,7 @@ snapshots:
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 6.0.2
|
||||
undici: 8.10.0
|
||||
undici: 8.11.2
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 8.0.1
|
||||
whatwg-mimetype: 5.0.0
|
||||
@@ -8468,7 +8469,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
canvg: 3.0.11
|
||||
core-js: 3.50.0
|
||||
dompurify: 3.4.13
|
||||
dompurify: 3.4.16
|
||||
html2canvas: 1.4.1
|
||||
|
||||
jsx-ast-utils@3.3.5:
|
||||
@@ -8952,7 +8953,7 @@ snapshots:
|
||||
|
||||
minimatch@3.1.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.18
|
||||
brace-expansion: 1.1.21
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
@@ -8982,14 +8983,14 @@ snapshots:
|
||||
|
||||
next-intl-swc-plugin-extractor@4.13.6: {}
|
||||
|
||||
next-intl@4.13.6(next@16.3.0(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.1.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@6.0.3):
|
||||
next-intl@4.13.6(@swc/helpers@0.5.23)(next@16.3.6(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.1.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.8.13
|
||||
'@parcel/watcher': 2.6.0
|
||||
'@swc/core': 1.16.0
|
||||
'@swc/core': 1.16.0(@swc/helpers@0.5.23)
|
||||
icu-minify: 4.13.6
|
||||
negotiator: 1.0.0
|
||||
next: 16.3.0(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.1.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
next: 16.3.6(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.1.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
next-intl-swc-plugin-extractor: 4.13.6
|
||||
po-parser: 2.2.0
|
||||
react: 19.2.8
|
||||
@@ -9004,10 +9005,10 @@ snapshots:
|
||||
react: 19.2.8
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
|
||||
next@16.3.0(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.1.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||
next@16.3.6(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.1.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||
dependencies:
|
||||
'@next/env': 16.3.0
|
||||
'@swc/helpers': 0.5.15
|
||||
'@next/env': 16.3.6
|
||||
'@swc/helpers': 0.5.23
|
||||
baseline-browser-mapping: 2.11.12
|
||||
caniuse-lite: 1.0.30001806
|
||||
postcss: 8.5.23
|
||||
@@ -9015,16 +9016,16 @@ snapshots:
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.8)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.3.0
|
||||
'@next/swc-darwin-x64': 16.3.0
|
||||
'@next/swc-linux-arm64-gnu': 16.3.0
|
||||
'@next/swc-linux-arm64-musl': 16.3.0
|
||||
'@next/swc-linux-x64-gnu': 16.3.0
|
||||
'@next/swc-linux-x64-musl': 16.3.0
|
||||
'@next/swc-win32-arm64-msvc': 16.3.0
|
||||
'@next/swc-win32-x64-msvc': 16.3.0
|
||||
'@next/swc-darwin-arm64': 16.3.6
|
||||
'@next/swc-darwin-x64': 16.3.6
|
||||
'@next/swc-linux-arm64-gnu': 16.3.6
|
||||
'@next/swc-linux-arm64-musl': 16.3.6
|
||||
'@next/swc-linux-x64-gnu': 16.3.6
|
||||
'@next/swc-linux-x64-musl': 16.3.6
|
||||
'@next/swc-win32-arm64-msvc': 16.3.6
|
||||
'@next/swc-win32-x64-msvc': 16.3.6
|
||||
babel-plugin-react-compiler: 1.0.0
|
||||
sharp: 0.35.4(@types/node@26.1.2)
|
||||
sharp: 0.35.5(@types/node@26.1.2)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@types/node'
|
||||
@@ -9163,13 +9164,13 @@ snapshots:
|
||||
dependencies:
|
||||
nanoid: 3.3.17
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
source-map-js: 1.2.2
|
||||
|
||||
postcss@8.5.25:
|
||||
dependencies:
|
||||
nanoid: 3.3.17
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
source-map-js: 1.2.2
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
@@ -9571,37 +9572,37 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.2
|
||||
|
||||
sharp@0.35.4(@types/node@26.1.2):
|
||||
sharp@0.35.5(@types/node@26.1.2):
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.8.5
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.35.4
|
||||
'@img/sharp-darwin-x64': 0.35.4
|
||||
'@img/sharp-freebsd-wasm32': 0.35.4
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.3
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.3
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.3
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.3
|
||||
'@img/sharp-libvips-linux-x64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
|
||||
'@img/sharp-linux-arm': 0.35.4
|
||||
'@img/sharp-linux-arm64': 0.35.4
|
||||
'@img/sharp-linux-ppc64': 0.35.4
|
||||
'@img/sharp-linux-riscv64': 0.35.4
|
||||
'@img/sharp-linux-s390x': 0.35.4
|
||||
'@img/sharp-linux-x64': 0.35.4
|
||||
'@img/sharp-linuxmusl-arm64': 0.35.4
|
||||
'@img/sharp-linuxmusl-x64': 0.35.4
|
||||
'@img/sharp-webcontainers-wasm32': 0.35.4
|
||||
'@img/sharp-win32-arm64': 0.35.4
|
||||
'@img/sharp-win32-ia32': 0.35.4
|
||||
'@img/sharp-win32-x64': 0.35.4
|
||||
'@img/sharp-darwin-arm64': 0.35.5
|
||||
'@img/sharp-darwin-x64': 0.35.5
|
||||
'@img/sharp-freebsd-wasm32': 0.35.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.4
|
||||
'@img/sharp-libvips-linux-arm': 1.3.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.4
|
||||
'@img/sharp-libvips-linux-x64': 1.3.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.4
|
||||
'@img/sharp-linux-arm': 0.35.5
|
||||
'@img/sharp-linux-arm64': 0.35.5
|
||||
'@img/sharp-linux-ppc64': 0.35.5
|
||||
'@img/sharp-linux-riscv64': 0.35.5
|
||||
'@img/sharp-linux-s390x': 0.35.5
|
||||
'@img/sharp-linux-x64': 0.35.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.35.5
|
||||
'@img/sharp-linuxmusl-x64': 0.35.5
|
||||
'@img/sharp-webcontainers-wasm32': 0.35.5
|
||||
'@img/sharp-win32-arm64': 0.35.5
|
||||
'@img/sharp-win32-ia32': 0.35.5
|
||||
'@img/sharp-win32-x64': 0.35.5
|
||||
'@types/node': 26.1.2
|
||||
optional: true
|
||||
|
||||
@@ -9648,7 +9649,7 @@ snapshots:
|
||||
react: 19.2.8
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
source-map-js@1.2.2: {}
|
||||
|
||||
source-map-support@0.5.21:
|
||||
dependencies:
|
||||
@@ -9897,7 +9898,7 @@ snapshots:
|
||||
|
||||
undici-types@8.3.0: {}
|
||||
|
||||
undici@8.10.0: {}
|
||||
undici@8.11.2: {}
|
||||
|
||||
unified@11.0.5:
|
||||
dependencies:
|
||||
|
||||
@@ -57,6 +57,9 @@ vi.mock('@/lib/services/openflare', async (importOriginal) => {
|
||||
const mockGroup = {
|
||||
id: 7,
|
||||
name: '生产节点',
|
||||
target_mode: 'node' as const,
|
||||
record_type: 'A' as const,
|
||||
record_content: '',
|
||||
primary_node: { id: 1, name: '主节点', ip: '192.0.2.1' },
|
||||
backup_node: null,
|
||||
active_node: { id: 1, name: '主节点', ip: '192.0.2.1' },
|
||||
@@ -70,6 +73,9 @@ const mockGroup = {
|
||||
const mockTargetGroup = {
|
||||
id: 8,
|
||||
name: '备用分组',
|
||||
target_mode: 'node' as const,
|
||||
record_type: 'A' as const,
|
||||
record_content: '',
|
||||
primary_node: { id: 2, name: '备用节点', ip: '192.0.2.2' },
|
||||
backup_node: null,
|
||||
active_node: { id: 2, name: '备用节点', ip: '192.0.2.2' },
|
||||
|
||||
@@ -109,6 +109,9 @@ describe('Cloudflare overview', () => {
|
||||
{
|
||||
id: 7,
|
||||
name: '生产节点',
|
||||
target_mode: 'node',
|
||||
record_type: 'A',
|
||||
record_content: '',
|
||||
primary_node: { id: 1, name: '主节点', ip: '192.0.2.1' },
|
||||
backup_node: null,
|
||||
active_node: { id: 1, name: '主节点', ip: '192.0.2.1' },
|
||||
|
||||
@@ -62,14 +62,28 @@ type Runner struct {
|
||||
RuntimeManager RuntimeManager
|
||||
WebSocketService WebSocketService
|
||||
|
||||
configSyncRetry *configSyncRetry
|
||||
restartOpenrestyNow bool
|
||||
websocketUpgradeEnabled bool
|
||||
}
|
||||
|
||||
// Run starts the agent's main loop, performing heartbeats and upgrading to WebSocket when available.
|
||||
func (r *Runner) Run(ctx context.Context) error {
|
||||
if r.SyncService != nil {
|
||||
r.configSyncRetry = newConfigSyncRetry(r.SyncService, r.recordSyncError)
|
||||
go r.configSyncRetry.run(ctx)
|
||||
if snapshot, loadErr := r.StateStore.Load(); loadErr != nil {
|
||||
slog.Error("load state before scheduling config sync retry failed", "error", loadErr)
|
||||
} else if strings.TrimSpace(snapshot.BlockedVersion) != "" || strings.TrimSpace(snapshot.BlockedChecksum) != "" {
|
||||
r.configSyncRetry.schedule(&protocol.ActiveConfigMeta{
|
||||
Version: snapshot.BlockedVersion,
|
||||
Checksum: snapshot.BlockedChecksum,
|
||||
})
|
||||
}
|
||||
}
|
||||
if r.HeartbeatCycle != nil {
|
||||
r.HeartbeatCycle.RecordSyncError = r.recordSyncError
|
||||
r.HeartbeatCycle.ConfigSyncResult = r.handleConfigSyncResult
|
||||
}
|
||||
nodeID, err := r.StateStore.EnsureNodeID()
|
||||
if err != nil {
|
||||
@@ -313,10 +327,11 @@ func (r *Runner) handleWebSocketMessage(ctx context.Context, message protocol.WS
|
||||
return false, nil
|
||||
}
|
||||
slog.Debug("agent ws active config received", "version", target.Version, "checksum", target.Checksum, "trigger_sync", true)
|
||||
if err := r.SyncService.SyncOnce(ctx, &target); err != nil {
|
||||
r.recordSyncError(err)
|
||||
err := r.SyncService.SyncOnce(ctx, &target)
|
||||
if err != nil {
|
||||
slog.Error("agent ws triggered sync failed", "version", target.Version, "error", err)
|
||||
}
|
||||
r.handleConfigSyncResult(&target, err)
|
||||
return false, nil
|
||||
case protocol.WSMessageTypeForceSyncConfig:
|
||||
var target protocol.ActiveConfigMeta
|
||||
@@ -325,10 +340,11 @@ func (r *Runner) handleWebSocketMessage(ctx context.Context, message protocol.WS
|
||||
return false, nil
|
||||
}
|
||||
slog.Debug("agent ws force sync config received", "version", target.Version, "checksum", target.Checksum, "trigger_sync", true)
|
||||
if err := r.SyncService.ForceSyncOnce(ctx, &target); err != nil {
|
||||
r.recordSyncError(err)
|
||||
err := r.SyncService.ForceSyncOnce(ctx, &target)
|
||||
if err != nil {
|
||||
slog.Error("agent ws triggered force sync failed", "version", target.Version, "error", err)
|
||||
}
|
||||
r.handleConfigSyncResult(&target, err)
|
||||
return false, nil
|
||||
case protocol.WSMessageTypeWAFIPGroups:
|
||||
var groups []protocol.WAFIPGroup
|
||||
@@ -350,6 +366,27 @@ func (r *Runner) handleWebSocketMessage(ctx context.Context, message protocol.WS
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) handleConfigSyncResult(target *protocol.ActiveConfigMeta, err error) {
|
||||
if err != nil {
|
||||
r.recordSyncError(err)
|
||||
if r.configSyncRetry != nil {
|
||||
r.configSyncRetry.schedule(target)
|
||||
}
|
||||
return
|
||||
}
|
||||
if r.configSyncRetry == nil || r.StateStore == nil {
|
||||
return
|
||||
}
|
||||
snapshot, loadErr := r.StateStore.Load()
|
||||
if loadErr != nil {
|
||||
slog.Error("load state after config sync failed", "error", loadErr)
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(snapshot.BlockedVersion) == "" && strings.TrimSpace(snapshot.BlockedChecksum) == "" {
|
||||
r.configSyncRetry.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
type webSocketBackoff struct {
|
||||
delays []time.Duration
|
||||
index int
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
configSyncRetryInitialDelay = 10 * time.Second
|
||||
configSyncRetryMaxDelay = 5 * time.Minute
|
||||
)
|
||||
|
||||
type configSyncRetryEvent struct {
|
||||
target *protocol.ActiveConfigMeta
|
||||
resolve bool
|
||||
}
|
||||
|
||||
type configSyncRetry struct {
|
||||
syncService SyncService
|
||||
recordError func(error)
|
||||
mu sync.Mutex
|
||||
pending *configSyncRetryEvent
|
||||
wake chan struct{}
|
||||
}
|
||||
|
||||
func newConfigSyncRetry(syncService SyncService, recordError func(error)) *configSyncRetry {
|
||||
return &configSyncRetry{
|
||||
syncService: syncService,
|
||||
recordError: recordError,
|
||||
wake: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *configSyncRetry) schedule(target *protocol.ActiveConfigMeta) {
|
||||
r.notify(configSyncRetryEvent{target: cloneActiveConfigMeta(target)})
|
||||
}
|
||||
|
||||
func (r *configSyncRetry) resolve() {
|
||||
r.notify(configSyncRetryEvent{resolve: true})
|
||||
}
|
||||
|
||||
func (r *configSyncRetry) notify(event configSyncRetryEvent) {
|
||||
r.mu.Lock()
|
||||
r.pending = &event
|
||||
r.mu.Unlock()
|
||||
select {
|
||||
case r.wake <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (r *configSyncRetry) takePending() (configSyncRetryEvent, bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.pending == nil {
|
||||
return configSyncRetryEvent{}, false
|
||||
}
|
||||
event := *r.pending
|
||||
r.pending = nil
|
||||
return event, true
|
||||
}
|
||||
|
||||
func (r *configSyncRetry) run(ctx context.Context) {
|
||||
var target *protocol.ActiveConfigMeta
|
||||
var timer *time.Timer
|
||||
var timerC <-chan time.Time
|
||||
attempt := 0
|
||||
pending := false
|
||||
defer func() {
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-r.wake:
|
||||
event, ok := r.takePending()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if event.resolve {
|
||||
target = nil
|
||||
attempt = 0
|
||||
pending = false
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
timerC = nil
|
||||
continue
|
||||
}
|
||||
if !pending || !sameConfigTarget(target, event.target) {
|
||||
target = event.target
|
||||
attempt = 0
|
||||
pending = true
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
timer = time.NewTimer(configSyncRetryDelay(attempt))
|
||||
timerC = timer.C
|
||||
}
|
||||
case <-timerC:
|
||||
if err := r.syncService.ForceSyncOnce(ctx, target); err != nil {
|
||||
attempt++
|
||||
delay := configSyncRetryDelay(attempt)
|
||||
if r.recordError != nil {
|
||||
r.recordError(err)
|
||||
}
|
||||
slog.Warn("agent forced config sync retry failed", "attempt", attempt, "retry_after", delay, "error", err)
|
||||
timer.Reset(delay)
|
||||
timerC = timer.C
|
||||
continue
|
||||
}
|
||||
slog.Info("agent forced config sync retry succeeded", "attempt", attempt+1)
|
||||
target = nil
|
||||
attempt = 0
|
||||
pending = false
|
||||
timerC = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func configSyncRetryDelay(attempt int) time.Duration {
|
||||
delay := configSyncRetryInitialDelay
|
||||
for i := 0; i < attempt && delay < configSyncRetryMaxDelay; i++ {
|
||||
delay *= 2
|
||||
if delay > configSyncRetryMaxDelay {
|
||||
return configSyncRetryMaxDelay
|
||||
}
|
||||
}
|
||||
return delay
|
||||
}
|
||||
|
||||
func sameConfigTarget(left, right *protocol.ActiveConfigMeta) bool {
|
||||
if left == nil || right == nil {
|
||||
return left == nil && right == nil
|
||||
}
|
||||
return strings.TrimSpace(left.Version) == strings.TrimSpace(right.Version) &&
|
||||
strings.TrimSpace(left.Checksum) == strings.TrimSpace(right.Checksum)
|
||||
}
|
||||
|
||||
func cloneActiveConfigMeta(target *protocol.ActiveConfigMeta) *protocol.ActiveConfigMeta {
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := *target
|
||||
return &cloned
|
||||
}
|
||||
@@ -43,6 +43,7 @@ type Cycle struct {
|
||||
Sync SyncService
|
||||
Updater *updater.Service
|
||||
RecordSyncError func(err error)
|
||||
ConfigSyncResult func(target *protocol.ActiveConfigMeta, err error)
|
||||
}
|
||||
|
||||
// Perform executes one complete heartbeat cycle: sends the heartbeat, syncs config, and applies settings.
|
||||
@@ -69,14 +70,16 @@ func (c *Cycle) Perform(ctx context.Context, nodeID string, startup bool, settin
|
||||
c.ApplyWAFIPGroups(ctx, heartbeatResult.WAFIPGroups)
|
||||
if startup {
|
||||
if err = c.Sync.SyncOnStartup(ctx, heartbeatResult.ActiveConfig); err != nil {
|
||||
c.recordSyncError(err)
|
||||
slog.Error("agent startup sync failed", "error", err)
|
||||
} else {
|
||||
slog.Debug("agent startup sync completed")
|
||||
}
|
||||
c.reportConfigSyncResult(heartbeatResult.ActiveConfig, err)
|
||||
} else if err = c.Sync.SyncOnce(ctx, heartbeatResult.ActiveConfig); err != nil {
|
||||
c.recordSyncError(err)
|
||||
slog.Error("agent sync failed", "error", err)
|
||||
c.reportConfigSyncResult(heartbeatResult.ActiveConfig, err)
|
||||
} else {
|
||||
c.reportConfigSyncResult(heartbeatResult.ActiveConfig, nil)
|
||||
}
|
||||
if settings != nil {
|
||||
settings.RestartOpenrestyIfNeeded(ctx)
|
||||
@@ -85,6 +88,16 @@ func (c *Cycle) Perform(ctx context.Context, nodeID string, startup bool, settin
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
func (c *Cycle) reportConfigSyncResult(target *protocol.ActiveConfigMeta, err error) {
|
||||
if c.ConfigSyncResult != nil {
|
||||
c.ConfigSyncResult(target, err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.recordSyncError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// NodePayload builds and returns the full NodePayload to be sent in a heartbeat request.
|
||||
func (c *Cycle) NodePayload(ctx context.Context, nodeID string) protocol.NodePayload {
|
||||
snapshot, _ := c.StateStore.Load()
|
||||
|
||||
@@ -59,9 +59,9 @@ type Client interface {
|
||||
VerifyToken(context.Context) error
|
||||
FindZone(context.Context, string) (*Zone, error)
|
||||
GetRecord(context.Context, string, string) (*DNSRecord, error)
|
||||
ListARecords(context.Context, string, string) ([]DNSRecord, error)
|
||||
CreateARecord(context.Context, string, RecordInput) (*DNSRecord, error)
|
||||
UpdateARecord(context.Context, string, string, RecordInput) (*DNSRecord, error)
|
||||
ListRecords(context.Context, string, string, string) ([]DNSRecord, error)
|
||||
CreateRecord(context.Context, string, RecordInput) (*DNSRecord, error)
|
||||
UpdateRecord(context.Context, string, string, RecordInput) (*DNSRecord, error)
|
||||
DeleteRecord(context.Context, string, string) error
|
||||
}
|
||||
|
||||
@@ -146,9 +146,9 @@ func (client *HTTPClient) GetRecord(ctx context.Context, zoneID, recordID string
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// ListARecords lists exact-name A records.
|
||||
func (client *HTTPClient) ListARecords(ctx context.Context, zoneID, name string) ([]DNSRecord, error) {
|
||||
query := url.Values{"type": {"A"}, "name": {strings.TrimSpace(name)}, "per_page": {"100"}}
|
||||
// ListRecords lists exact-name DNS records of a type.
|
||||
func (client *HTTPClient) ListRecords(ctx context.Context, zoneID, name, recordType string) ([]DNSRecord, error) {
|
||||
query := url.Values{"type": {strings.TrimSpace(recordType)}, "name": {strings.TrimSpace(name)}, "per_page": {"100"}}
|
||||
var records []DNSRecord
|
||||
path := "/zones/" + url.PathEscape(zoneID) + "/dns_records"
|
||||
if err := client.do(ctx, http.MethodGet, path, query, nil, &records); err != nil {
|
||||
@@ -157,8 +157,8 @@ func (client *HTTPClient) ListARecords(ctx context.Context, zoneID, name string)
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// CreateARecord creates an A record.
|
||||
func (client *HTTPClient) CreateARecord(ctx context.Context, zoneID string, input RecordInput) (*DNSRecord, error) {
|
||||
// CreateRecord creates a DNS record.
|
||||
func (client *HTTPClient) CreateRecord(ctx context.Context, zoneID string, input RecordInput) (*DNSRecord, error) {
|
||||
var record DNSRecord
|
||||
path := "/zones/" + url.PathEscape(zoneID) + "/dns_records"
|
||||
if err := client.do(ctx, http.MethodPost, path, nil, input, &record); err != nil {
|
||||
@@ -167,8 +167,8 @@ func (client *HTTPClient) CreateARecord(ctx context.Context, zoneID string, inpu
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// UpdateARecord replaces an A record.
|
||||
func (client *HTTPClient) UpdateARecord(ctx context.Context, zoneID, recordID string, input RecordInput) (*DNSRecord, error) {
|
||||
// UpdateRecord replaces a DNS record.
|
||||
func (client *HTTPClient) UpdateRecord(ctx context.Context, zoneID, recordID string, input RecordInput) (*DNSRecord, error) {
|
||||
var record DNSRecord
|
||||
path := "/zones/" + url.PathEscape(zoneID) + "/dns_records/" + url.PathEscape(recordID)
|
||||
if err := client.do(ctx, http.MethodPut, path, nil, input, &record); err != nil {
|
||||
@@ -177,6 +177,21 @@ func (client *HTTPClient) UpdateARecord(ctx context.Context, zoneID, recordID st
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// ListARecords lists exact-name A records.
|
||||
func (client *HTTPClient) ListARecords(ctx context.Context, zoneID, name string) ([]DNSRecord, error) {
|
||||
return client.ListRecords(ctx, zoneID, name, "A")
|
||||
}
|
||||
|
||||
// CreateARecord creates an A record.
|
||||
func (client *HTTPClient) CreateARecord(ctx context.Context, zoneID string, input RecordInput) (*DNSRecord, error) {
|
||||
return client.CreateRecord(ctx, zoneID, input)
|
||||
}
|
||||
|
||||
// UpdateARecord replaces an A record.
|
||||
func (client *HTTPClient) UpdateARecord(ctx context.Context, zoneID, recordID string, input RecordInput) (*DNSRecord, error) {
|
||||
return client.UpdateRecord(ctx, zoneID, recordID, input)
|
||||
}
|
||||
|
||||
// DeleteRecord deletes a DNS record.
|
||||
func (client *HTTPClient) DeleteRecord(ctx context.Context, zoneID, recordID string) error {
|
||||
var result struct {
|
||||
|
||||
@@ -11,11 +11,14 @@ const (
|
||||
errDNSAccountInvalid = "请选择有效的 Cloudflare DNS 账号"
|
||||
errGroupNameRequired = "分组名称不能为空"
|
||||
errGroupNodeSame = "主节点和备用节点不能相同"
|
||||
errGroupTargetModeInvalid = "请选择有效的指向方式"
|
||||
errRecordTypeInvalid = "请选择 CNAME、A 或 AAAA 记录类型"
|
||||
errRecordContentInvalid = "DNS 记录值无效,请检查域名或 IP 地址"
|
||||
errNodeInvalid = "请选择有效的边缘节点"
|
||||
errNodeIPv4Required = "生效节点必须配置合法 IPv4"
|
||||
errGroupDisabled = "指向分组已停用"
|
||||
errMemberExists = "该域名已加入其他指向分组"
|
||||
errMultipleARecords = "检测到 Cloudflare 中存在多条同名 A 记录,请先手动清理"
|
||||
errMultipleDNSRecords = "检测到 Cloudflare 中存在多条同名 DNS 记录,请先手动清理"
|
||||
errSyncFailed = "Cloudflare DNS 同步失败"
|
||||
errDeleteRemoteFailed = "删除 Cloudflare DNS 记录失败"
|
||||
errTaskDispatchFailed = "无法投递 Cloudflare 同步任务"
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -219,30 +220,110 @@ func groupFromInput(ctx context.Context, existing *model.CFPointingGroup, input
|
||||
if name == "" {
|
||||
return nil, errors.New(errGroupNameRequired)
|
||||
}
|
||||
if input.BackupNodeID != nil && *input.BackupNodeID == input.PrimaryNodeID {
|
||||
return nil, errors.New(errGroupNodeSame)
|
||||
targetMode := strings.TrimSpace(input.TargetMode)
|
||||
if targetMode == "" {
|
||||
targetMode = model.CFPointingTargetModeNode
|
||||
}
|
||||
primary, err := validEdgeNode(ctx, input.PrimaryNodeID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if targetMode != model.CFPointingTargetModeNode && targetMode != model.CFPointingTargetModeCustom {
|
||||
return nil, errors.New(errGroupTargetModeInvalid)
|
||||
}
|
||||
if input.BackupNodeID != nil {
|
||||
if _, err = validEdgeNode(ctx, *input.BackupNodeID, false); err != nil {
|
||||
primaryNodeID := uint(0)
|
||||
var backupNodeID *uint
|
||||
recordType := strings.ToUpper(strings.TrimSpace(input.RecordType))
|
||||
if recordType == "" {
|
||||
recordType = "A"
|
||||
}
|
||||
recordContent := strings.TrimSpace(input.RecordContent)
|
||||
if targetMode == model.CFPointingTargetModeCustom && !validCustomRecordType(recordType) {
|
||||
return nil, errors.New(errRecordTypeInvalid)
|
||||
}
|
||||
if targetMode == model.CFPointingTargetModeCustom && !validCustomRecordContent(recordType, recordContent) {
|
||||
return nil, errors.New(errRecordContentInvalid)
|
||||
}
|
||||
if targetMode == model.CFPointingTargetModeNode {
|
||||
resolvedPrimary, resolvedBackup, err := resolveGroupNodes(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
primaryNodeID = resolvedPrimary
|
||||
backupNodeID = resolvedBackup
|
||||
}
|
||||
activeNodeID := primaryNodeID
|
||||
if existing != nil && targetMode == model.CFPointingTargetModeNode && existing.TargetMode == targetMode && existing.PrimaryNodeID == primaryNodeID && equalOptionalUint(existing.BackupNodeID, backupNodeID) {
|
||||
activeNodeID = existing.ActiveNodeID
|
||||
}
|
||||
if existing == nil {
|
||||
existing = &model.CFPointingGroup{}
|
||||
}
|
||||
existing.Name = name
|
||||
existing.PrimaryNodeID = primary.ID
|
||||
existing.ActiveNodeID = primary.ID
|
||||
existing.BackupNodeID = input.BackupNodeID
|
||||
existing.TargetMode = targetMode
|
||||
existing.RecordType = recordType
|
||||
existing.RecordContent = recordContent
|
||||
existing.PrimaryNodeID = primaryNodeID
|
||||
existing.ActiveNodeID = activeNodeID
|
||||
existing.BackupNodeID = backupNodeID
|
||||
existing.DefaultProxied = input.DefaultProxied
|
||||
existing.Enabled = input.Enabled
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
func equalOptionalUint(left, right *uint) bool {
|
||||
if left == nil || right == nil {
|
||||
return left == right
|
||||
}
|
||||
return *left == *right
|
||||
}
|
||||
|
||||
func resolveGroupNodes(ctx context.Context, input GroupInput) (uint, *uint, error) {
|
||||
if input.BackupNodeID != nil && *input.BackupNodeID == input.PrimaryNodeID {
|
||||
return 0, nil, errors.New(errGroupNodeSame)
|
||||
}
|
||||
primary, err := validEdgeNode(ctx, input.PrimaryNodeID, true)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if input.BackupNodeID != nil {
|
||||
if _, err = validEdgeNode(ctx, *input.BackupNodeID, false); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
}
|
||||
return primary.ID, input.BackupNodeID, nil
|
||||
}
|
||||
|
||||
func validCustomRecordType(recordType string) bool {
|
||||
return recordType == "CNAME" || recordType == "A" || recordType == "AAAA"
|
||||
}
|
||||
|
||||
func validCustomRecordContent(recordType, content string) bool {
|
||||
switch recordType {
|
||||
case "A":
|
||||
return isIPv4(content)
|
||||
case "AAAA":
|
||||
return isIPv6(content)
|
||||
case "CNAME":
|
||||
return validCNAMEContent(content)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var cnameHostPattern = regexp.MustCompile(`^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$`)
|
||||
|
||||
func isIPv4(content string) bool {
|
||||
ip := net.ParseIP(content)
|
||||
return ip != nil && ip.To4() != nil
|
||||
}
|
||||
|
||||
func isIPv6(content string) bool {
|
||||
ip := net.ParseIP(content)
|
||||
return ip != nil && ip.To4() == nil
|
||||
}
|
||||
|
||||
func validCNAMEContent(content string) bool {
|
||||
host := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(content)), ".")
|
||||
return len(host) > 0 && len(host) <= 253 && net.ParseIP(host) == nil && cnameHostPattern.MatchString(host)
|
||||
}
|
||||
|
||||
func validEdgeNode(ctx context.Context, id uint, requireIPv4 bool) (*model.OpenFlareNode, error) {
|
||||
node, err := repository.GetOpenFlareNodeByID(ctx, id)
|
||||
if err != nil || node.NodeType != "edge_node" {
|
||||
@@ -254,20 +335,90 @@ func validEdgeNode(ctx context.Context, id uint, requireIPv4 bool) (*model.OpenF
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func buildGroupItem(ctx context.Context, group *model.CFPointingGroup) (*GroupItem, error) {
|
||||
primary, err := lookupGroupNode(ctx, group.ID, group.PrimaryNodeID)
|
||||
// CheckNodeFailover switches enabled node groups to healthy backup nodes and fails back after primary recovery.
|
||||
func CheckNodeFailover(ctx context.Context) (int, error) {
|
||||
groups, err := repository.ListCFPointingGroups(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
active, err := lookupGroupNode(ctx, group.ID, group.ActiveNodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
now := time.Now()
|
||||
changed := 0
|
||||
var dispatchErrors []error
|
||||
for i := range groups {
|
||||
group := &groups[i]
|
||||
if !group.Enabled || group.TargetMode == model.CFPointingTargetModeCustom || group.BackupNodeID == nil {
|
||||
continue
|
||||
}
|
||||
primary, primaryErr := repository.GetOpenFlareNodeByID(ctx, group.PrimaryNodeID)
|
||||
if primaryErr != nil && !errors.Is(primaryErr, gorm.ErrRecordNotFound) {
|
||||
return changed, primaryErr
|
||||
}
|
||||
primaryHealthy := primaryErr == nil && isNodeHealthyForFailover(primary, now)
|
||||
targetNodeID := group.ActiveNodeID
|
||||
if primaryHealthy {
|
||||
targetNodeID = group.PrimaryNodeID
|
||||
} else {
|
||||
backup, backupErr := repository.GetOpenFlareNodeByID(ctx, *group.BackupNodeID)
|
||||
if backupErr != nil && !errors.Is(backupErr, gorm.ErrRecordNotFound) {
|
||||
return changed, backupErr
|
||||
}
|
||||
if backupErr == nil && isNodeHealthyForFailover(backup, now) {
|
||||
targetNodeID = *group.BackupNodeID
|
||||
}
|
||||
}
|
||||
if targetNodeID != group.ActiveNodeID {
|
||||
if err = repository.UpdateCFPointingGroupTarget(ctx, group.ID, map[string]any{"active_node_id": targetNodeID}); err != nil {
|
||||
return changed, err
|
||||
}
|
||||
if err = repository.MarkCFPointingGroupMembersPending(ctx, group.ID); err != nil {
|
||||
return changed, err
|
||||
}
|
||||
changed++
|
||||
}
|
||||
pending, pendingErr := repository.HasPendingCFPointingGroupMembers(ctx, group.ID)
|
||||
if pendingErr != nil {
|
||||
return changed, pendingErr
|
||||
}
|
||||
if pending {
|
||||
if _, err = DispatchGroupSync(ctx, group.ID, "cloudflare_node_failover"); err != nil {
|
||||
logger.WarnF(ctx, "[Cloudflare] dispatch failover sync failed: group_id=%d error=%v", group.ID, err)
|
||||
dispatchErrors = append(dispatchErrors, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed, errors.Join(dispatchErrors...)
|
||||
}
|
||||
|
||||
func isNodeHealthyForFailover(node *model.OpenFlareNode, now time.Time) bool {
|
||||
if node == nil || node.Status == "offline" || node.OpenrestyStatus == "unhealthy" || strings.TrimSpace(node.LastError) != "" {
|
||||
return false
|
||||
}
|
||||
return node.LastSeenAt != nil && now.Sub(*node.LastSeenAt) <= 60*time.Second
|
||||
}
|
||||
|
||||
func buildGroupItem(ctx context.Context, group *model.CFPointingGroup) (*GroupItem, error) {
|
||||
targetMode := group.TargetMode
|
||||
if targetMode == "" {
|
||||
targetMode = model.CFPointingTargetModeNode
|
||||
}
|
||||
primary := (*model.OpenFlareNode)(nil)
|
||||
active := (*model.OpenFlareNode)(nil)
|
||||
var err error
|
||||
if targetMode == model.CFPointingTargetModeNode {
|
||||
primary, err = lookupGroupNode(ctx, group.ID, group.PrimaryNodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
active, err = lookupGroupNode(ctx, group.ID, group.ActiveNodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
count, err := repository.CountCFPointingMembersByGroupID(ctx, group.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := &GroupItem{ID: group.ID, Name: group.Name, PrimaryNode: nodeOptionForID(group.PrimaryNodeID, primary), ActiveNode: nodeOptionForID(group.ActiveNodeID, active), DefaultProxied: group.DefaultProxied, Enabled: group.Enabled, MemberCount: count, CreatedAt: group.CreatedAt, UpdatedAt: group.UpdatedAt}
|
||||
item := &GroupItem{ID: group.ID, Name: group.Name, TargetMode: targetMode, RecordType: group.RecordType, RecordContent: group.RecordContent, PrimaryNode: nodeOptionForID(group.PrimaryNodeID, primary), ActiveNode: nodeOptionForID(group.ActiveNodeID, active), DefaultProxied: group.DefaultProxied, Enabled: group.Enabled, MemberCount: count, CreatedAt: group.CreatedAt, UpdatedAt: group.UpdatedAt}
|
||||
if group.BackupNodeID != nil {
|
||||
backup, backupErr := lookupGroupNode(ctx, group.ID, *group.BackupNodeID)
|
||||
if backupErr != nil {
|
||||
@@ -375,7 +526,7 @@ func UpdateMember(ctx context.Context, groupID, memberID uint, input MemberUpdat
|
||||
return memberItem(member, domain), nil
|
||||
}
|
||||
|
||||
// RemoveMember deletes the managed remote A record before removing local state.
|
||||
// RemoveMember deletes the managed remote DNS record before removing local state.
|
||||
func RemoveMember(ctx context.Context, groupID, memberID uint) error {
|
||||
member, err := repository.GetCFPointingMember(ctx, groupID, memberID)
|
||||
if err != nil {
|
||||
@@ -457,7 +608,7 @@ func BatchMoveMembers(ctx context.Context, sourceGroupID uint, input MemberBatch
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchRemoveMembers deletes multiple members and their remote A records.
|
||||
// BatchRemoveMembers deletes multiple members and their remote DNS records.
|
||||
func BatchRemoveMembers(ctx context.Context, sourceGroupID uint, input MemberBatchRemoveInput) error {
|
||||
if len(input.MemberIDs) == 0 {
|
||||
return errors.New(errNoMembersSelected)
|
||||
@@ -477,6 +628,46 @@ func BatchRemoveMembers(ctx context.Context, sourceGroupID uint, input MemberBat
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchEnableProxy enables orange-cloud proxy for selected members.
|
||||
func BatchEnableProxy(ctx context.Context, groupID uint, input MemberBatchProxyInput) error {
|
||||
if len(input.MemberIDs) == 0 {
|
||||
return errors.New(errNoMembersSelected)
|
||||
}
|
||||
group, err := repository.GetCFPointingGroup(ctx, groupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var firstErr error
|
||||
for _, memberID := range uniqueIDs(input.MemberIDs) {
|
||||
member, getErr := repository.GetCFPointingMember(ctx, groupID, memberID)
|
||||
if getErr != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = getErr
|
||||
}
|
||||
continue
|
||||
}
|
||||
member.Proxied = true
|
||||
member.SyncStatus = model.CFMemberSyncPending
|
||||
member.LastError = ""
|
||||
if saveErr := repository.SaveCFPointingMember(ctx, member); saveErr != nil {
|
||||
logger.ErrorF(ctx, "[Cloudflare] batch enable proxy save member failed: member_id=%d error=%v", memberID, saveErr)
|
||||
if firstErr == nil {
|
||||
firstErr = saveErr
|
||||
}
|
||||
continue
|
||||
}
|
||||
if group.Enabled {
|
||||
if _, syncErr := DispatchMemberSync(ctx, member.ID, "cloudflare_member_batch_proxy"); syncErr != nil {
|
||||
logger.WarnF(ctx, "[Cloudflare] dispatch batch enable proxy sync failed: member_id=%d error=%v", member.ID, syncErr)
|
||||
if firstErr == nil {
|
||||
firstErr = syncErr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func uniqueIDs(ids []uint) []uint {
|
||||
if len(ids) == 0 {
|
||||
return ids
|
||||
@@ -495,7 +686,7 @@ func uniqueIDs(ids []uint) []uint {
|
||||
return result
|
||||
}
|
||||
|
||||
// DeleteGroup removes every managed remote A record and then local state.
|
||||
// DeleteGroup removes every managed remote DNS record and then local state.
|
||||
func DeleteGroup(ctx context.Context, groupID uint) error {
|
||||
if _, err := repository.GetCFPointingGroup(ctx, groupID); err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,13 +5,153 @@ package cloudflare
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||
)
|
||||
|
||||
func TestCheckNodeFailoverContinuesAfterDispatchError(t *testing.T) {
|
||||
ctx, memberID := setupCloudflareLogicDB(t)
|
||||
member, err := repository.GetCFPointingMemberByID(ctx, memberID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCFPointingMemberByID() error = %v", err)
|
||||
}
|
||||
group, err := repository.GetCFPointingGroup(ctx, member.GroupID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCFPointingGroup() error = %v", err)
|
||||
}
|
||||
|
||||
lastSeen := time.Now()
|
||||
backup := model.OpenFlareNode{Name: "backup", NodeID: "node-backup", NodeType: "edge_node", IP: "203.0.113.11", Status: "online", OpenrestyStatus: "healthy", LastSeenAt: &lastSeen}
|
||||
if err := db.DB(ctx).Create(&backup).Error; err != nil {
|
||||
t.Fatalf("Create(backup) error = %v", err)
|
||||
}
|
||||
if err := db.DB(ctx).Model(group).Update("backup_node_id", backup.ID).Error; err != nil {
|
||||
t.Fatalf("Update(group backup) error = %v", err)
|
||||
}
|
||||
|
||||
zoneDomain := model.ZoneDomain{ZoneID: 1, Domain: "second.example.com"}
|
||||
if err := db.DB(ctx).Create(&zoneDomain).Error; err != nil {
|
||||
t.Fatalf("Create(zoneDomain) error = %v", err)
|
||||
}
|
||||
secondGroup := model.CFPointingGroup{Name: "secondary", PrimaryNodeID: group.PrimaryNodeID, BackupNodeID: &backup.ID, ActiveNodeID: group.PrimaryNodeID, Enabled: true}
|
||||
if err := db.DB(ctx).Create(&secondGroup).Error; err != nil {
|
||||
t.Fatalf("Create(secondGroup) error = %v", err)
|
||||
}
|
||||
secondMember := model.CFPointingMember{GroupID: secondGroup.ID, ZoneDomainID: zoneDomain.ID, SyncStatus: model.CFMemberSyncPending}
|
||||
if err := db.DB(ctx).Create(&secondMember).Error; err != nil {
|
||||
t.Fatalf("Create(secondMember) error = %v", err)
|
||||
}
|
||||
|
||||
dispatchErr := errors.New("dispatch unavailable")
|
||||
dispatchCalls := 0
|
||||
restoreDispatch := SetDispatchTaskForTest(func(context.Context, string, []byte, string) (string, error) {
|
||||
dispatchCalls++
|
||||
if dispatchCalls == 1 {
|
||||
return "", dispatchErr
|
||||
}
|
||||
return "task-id", nil
|
||||
})
|
||||
t.Cleanup(restoreDispatch)
|
||||
|
||||
changed, err := CheckNodeFailover(ctx)
|
||||
if !errors.Is(err, dispatchErr) {
|
||||
t.Fatalf("CheckNodeFailover() error = %v, want dispatch error", err)
|
||||
}
|
||||
if changed != 2 {
|
||||
t.Errorf("CheckNodeFailover() changed = %d, want 2", changed)
|
||||
}
|
||||
if dispatchCalls != 2 {
|
||||
t.Errorf("CheckNodeFailover() dispatch calls = %d, want 2", dispatchCalls)
|
||||
}
|
||||
for _, groupID := range []uint{group.ID, secondGroup.ID} {
|
||||
updated, err := repository.GetCFPointingGroup(ctx, groupID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCFPointingGroup(%d) error = %v", groupID, err)
|
||||
}
|
||||
if updated.ActiveNodeID != backup.ID {
|
||||
t.Errorf("group %d active node = %d, want %d", groupID, updated.ActiveNodeID, backup.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchEnableProxyContinuesAfterDispatchError(t *testing.T) {
|
||||
ctx, firstMemberID := setupCloudflareLogicDB(t)
|
||||
firstMember, err := repository.GetCFPointingMemberByID(ctx, firstMemberID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCFPointingMemberByID() error = %v", err)
|
||||
}
|
||||
zoneDomain := model.ZoneDomain{ZoneID: 1, Domain: "second.example.com"}
|
||||
if err := db.DB(ctx).Create(&zoneDomain).Error; err != nil {
|
||||
t.Fatalf("Create(zoneDomain) error = %v", err)
|
||||
}
|
||||
secondMember := model.CFPointingMember{GroupID: firstMember.GroupID, ZoneDomainID: zoneDomain.ID}
|
||||
if err := db.DB(ctx).Create(&secondMember).Error; err != nil {
|
||||
t.Fatalf("Create(secondMember) error = %v", err)
|
||||
}
|
||||
|
||||
dispatchErr := errors.New("dispatch unavailable")
|
||||
dispatchCalls := 0
|
||||
restoreDispatch := SetDispatchTaskForTest(func(context.Context, string, []byte, string) (string, error) {
|
||||
dispatchCalls++
|
||||
if dispatchCalls == 1 {
|
||||
return "", dispatchErr
|
||||
}
|
||||
return "task-id", nil
|
||||
})
|
||||
t.Cleanup(restoreDispatch)
|
||||
|
||||
err = BatchEnableProxy(ctx, firstMember.GroupID, MemberBatchProxyInput{MemberIDs: []uint{firstMember.ID, secondMember.ID}})
|
||||
if !errors.Is(err, dispatchErr) {
|
||||
t.Fatalf("BatchEnableProxy() error = %v, want dispatch error", err)
|
||||
}
|
||||
if dispatchCalls != 2 {
|
||||
t.Errorf("BatchEnableProxy() dispatch calls = %d, want 2", dispatchCalls)
|
||||
}
|
||||
for _, memberID := range []uint{firstMember.ID, secondMember.ID} {
|
||||
updated, err := repository.GetCFPointingMemberByID(ctx, memberID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCFPointingMemberByID(%d) error = %v", memberID, err)
|
||||
}
|
||||
if !updated.Proxied {
|
||||
t.Errorf("member %d proxied = false, want true", memberID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchEnableProxyContinuesAfterMemberLookupError(t *testing.T) {
|
||||
ctx, memberID := setupCloudflareLogicDB(t)
|
||||
member, err := repository.GetCFPointingMemberByID(ctx, memberID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCFPointingMemberByID() error = %v", err)
|
||||
}
|
||||
dispatchCalls := 0
|
||||
restoreDispatch := SetDispatchTaskForTest(func(context.Context, string, []byte, string) (string, error) {
|
||||
dispatchCalls++
|
||||
return "task-id", nil
|
||||
})
|
||||
t.Cleanup(restoreDispatch)
|
||||
|
||||
err = BatchEnableProxy(ctx, member.GroupID, MemberBatchProxyInput{MemberIDs: []uint{99999, member.ID}})
|
||||
if err == nil {
|
||||
t.Fatal("BatchEnableProxy() error = nil, want member lookup error")
|
||||
}
|
||||
if dispatchCalls != 1 {
|
||||
t.Errorf("BatchEnableProxy() dispatch calls = %d, want 1", dispatchCalls)
|
||||
}
|
||||
updated, err := repository.GetCFPointingMemberByID(ctx, member.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCFPointingMemberByID() error = %v", err)
|
||||
}
|
||||
if !updated.Proxied {
|
||||
t.Errorf("member %d proxied = false, want true", member.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveMemberAndBatchOperations(t *testing.T) {
|
||||
ctx, member1ID := setupCloudflareLogicDB(t)
|
||||
|
||||
|
||||
@@ -23,9 +23,11 @@ const (
|
||||
memberSyncStatusColumn = "sync_status"
|
||||
)
|
||||
|
||||
var errCachedRecordUnavailable = errors.New("cached Cloudflare record unavailable")
|
||||
|
||||
var memberLocks [memberLockStripeCount]sync.Mutex
|
||||
|
||||
// ReconcileMember makes one Cloudflare A record match the local desired state.
|
||||
// ReconcileMember makes one Cloudflare DNS record match the local desired state.
|
||||
func ReconcileMember(ctx context.Context, memberID uint) error {
|
||||
lock := &memberLocks[memberID%memberLockStripeCount]
|
||||
lock.Lock()
|
||||
@@ -51,9 +53,9 @@ func reconcileMember(ctx context.Context, memberID uint) error {
|
||||
if !state.Group.Enabled {
|
||||
return errors.New(errGroupDisabled)
|
||||
}
|
||||
ip := strings.TrimSpace(state.Node.IP)
|
||||
if net.ParseIP(ip).To4() == nil {
|
||||
return errors.New(errNodeIPv4Required)
|
||||
input, err := desiredRecordInput(state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connection, err := repository.GetCFConnection(ctx)
|
||||
if err != nil || connection.Status != model.CFConnectionStatusReady {
|
||||
@@ -72,37 +74,79 @@ func reconcileMember(ctx context.Context, memberID uint) error {
|
||||
}
|
||||
zoneID = zone.ID
|
||||
}
|
||||
input := RecordInput{Type: "A", Name: state.Domain.Domain, Content: ip, Proxied: state.Member.Proxied, TTL: 300}
|
||||
input.Name = state.Domain.Domain
|
||||
input.Proxied = state.Member.Proxied
|
||||
input.TTL = 300
|
||||
if input.Proxied {
|
||||
input.TTL = 1
|
||||
}
|
||||
recordID := state.Member.CFRecordID
|
||||
if recordID != "" {
|
||||
if _, getErr := client.GetRecord(ctx, zoneID, recordID); getErr == nil {
|
||||
record, updateErr := client.UpdateARecord(ctx, zoneID, recordID, input)
|
||||
if updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
return markMemberSynced(ctx, memberID, zoneID, record.ID, ip)
|
||||
completed, cachedErr := reconcileCachedRecord(ctx, client, memberID, zoneID, recordID, input)
|
||||
if errors.Is(cachedErr, errCachedRecordUnavailable) {
|
||||
cachedErr = nil
|
||||
}
|
||||
if cachedErr != nil {
|
||||
return cachedErr
|
||||
}
|
||||
if completed {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
records, err := client.ListARecords(ctx, zoneID, state.Domain.Domain)
|
||||
records, err := client.ListRecords(ctx, zoneID, state.Domain.Domain, input.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var record *DNSRecord
|
||||
switch len(records) {
|
||||
case 0:
|
||||
record, err = client.CreateARecord(ctx, zoneID, input)
|
||||
record, err = client.CreateRecord(ctx, zoneID, input)
|
||||
case 1:
|
||||
record, err = client.UpdateARecord(ctx, zoneID, records[0].ID, input)
|
||||
record, err = client.UpdateRecord(ctx, zoneID, records[0].ID, input)
|
||||
default:
|
||||
return errors.New(errMultipleARecords)
|
||||
return errors.New(errMultipleDNSRecords)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return markMemberSynced(ctx, memberID, zoneID, record.ID, ip)
|
||||
return markMemberSynced(ctx, memberID, zoneID, record.ID, input.Content)
|
||||
}
|
||||
|
||||
func reconcileCachedRecord(
|
||||
ctx context.Context,
|
||||
client Client,
|
||||
memberID uint,
|
||||
zoneID, recordID string,
|
||||
input RecordInput,
|
||||
) (bool, error) {
|
||||
current, err := client.GetRecord(ctx, zoneID, recordID)
|
||||
if err != nil {
|
||||
return false, errors.Join(errCachedRecordUnavailable, err)
|
||||
}
|
||||
if current.Type != input.Type {
|
||||
return false, client.DeleteRecord(ctx, zoneID, recordID)
|
||||
}
|
||||
record, err := client.UpdateRecord(ctx, zoneID, recordID, input)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, markMemberSynced(ctx, memberID, zoneID, record.ID, input.Content)
|
||||
}
|
||||
|
||||
func desiredRecordInput(state *repository.CFPointingMemberContext) (RecordInput, error) {
|
||||
if state.Group.TargetMode == model.CFPointingTargetModeCustom {
|
||||
recordType := strings.ToUpper(strings.TrimSpace(state.Group.RecordType))
|
||||
content := strings.TrimSpace(state.Group.RecordContent)
|
||||
if !validCustomRecordType(recordType) || !validCustomRecordContent(recordType, content) {
|
||||
return RecordInput{}, errors.New(errRecordContentInvalid)
|
||||
}
|
||||
return RecordInput{Type: recordType, Content: content}, nil
|
||||
}
|
||||
ip := strings.TrimSpace(state.Node.IP)
|
||||
if net.ParseIP(ip).To4() == nil {
|
||||
return RecordInput{}, errors.New(errNodeIPv4Required)
|
||||
}
|
||||
return RecordInput{Type: "A", Content: ip}, nil
|
||||
}
|
||||
|
||||
func markMemberSynced(ctx context.Context, memberID uint, zoneID, recordID, ip string) error {
|
||||
@@ -113,7 +157,7 @@ func markMemberSynced(ctx context.Context, memberID uint, zoneID, recordID, ip s
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteManagedRecord deletes the cached or uniquely discoverable A record.
|
||||
// DeleteManagedRecord deletes the cached or uniquely discoverable managed record.
|
||||
func DeleteManagedRecord(ctx context.Context, memberID uint) error {
|
||||
state, err := repository.GetCFPointingMemberContext(ctx, memberID)
|
||||
if err != nil {
|
||||
@@ -144,7 +188,14 @@ func DeleteManagedRecord(ctx context.Context, memberID uint) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
records, err := client.ListARecords(ctx, zoneID, state.Domain.Domain)
|
||||
recordType := "A"
|
||||
if state.Group.TargetMode == model.CFPointingTargetModeCustom {
|
||||
recordType = state.Group.RecordType
|
||||
if recordType == "" {
|
||||
recordType = "A"
|
||||
}
|
||||
}
|
||||
records, err := client.ListRecords(ctx, zoneID, state.Domain.Domain, recordType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -152,7 +203,7 @@ func DeleteManagedRecord(ctx context.Context, memberID uint) error {
|
||||
return nil
|
||||
}
|
||||
if len(records) > 1 {
|
||||
return errors.New(errMultipleARecords)
|
||||
return errors.New(errMultipleDNSRecords)
|
||||
}
|
||||
return client.DeleteRecord(ctx, zoneID, records[0].ID)
|
||||
}
|
||||
|
||||
@@ -31,16 +31,22 @@ func (client *fakeClient) FindZone(context.Context, string) (*Zone, error) {
|
||||
func (client *fakeClient) GetRecord(context.Context, string, string) (*DNSRecord, error) {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
func (client *fakeClient) ListARecords(context.Context, string, string) ([]DNSRecord, error) {
|
||||
return client.records, nil
|
||||
func (client *fakeClient) ListRecords(_ context.Context, _, _ string, recordType string) ([]DNSRecord, error) {
|
||||
items := make([]DNSRecord, 0, len(client.records))
|
||||
for _, record := range client.records {
|
||||
if record.Type == "" || record.Type == recordType {
|
||||
items = append(items, record)
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
func (client *fakeClient) CreateARecord(_ context.Context, _ string, input RecordInput) (*DNSRecord, error) {
|
||||
func (client *fakeClient) CreateRecord(_ context.Context, _ string, input RecordInput) (*DNSRecord, error) {
|
||||
client.created = &input
|
||||
return &DNSRecord{ID: "record-created", Name: input.Name, Content: input.Content, Proxied: input.Proxied}, nil
|
||||
return &DNSRecord{ID: "record-created", Type: input.Type, Name: input.Name, Content: input.Content, Proxied: input.Proxied}, nil
|
||||
}
|
||||
func (client *fakeClient) UpdateARecord(_ context.Context, _, id string, input RecordInput) (*DNSRecord, error) {
|
||||
func (client *fakeClient) UpdateRecord(_ context.Context, _, id string, input RecordInput) (*DNSRecord, error) {
|
||||
client.updated = &input
|
||||
return &DNSRecord{ID: id, Name: input.Name, Content: input.Content, Proxied: input.Proxied}, nil
|
||||
return &DNSRecord{ID: id, Type: input.Type, Name: input.Name, Content: input.Content, Proxied: input.Proxied}, nil
|
||||
}
|
||||
func (client *fakeClient) DeleteRecord(_ context.Context, _, recordID string) error {
|
||||
client.deleted = append(client.deleted, recordID)
|
||||
|
||||
@@ -198,7 +198,7 @@ func UpdateGroupHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.OK(item))
|
||||
}
|
||||
|
||||
// DeleteGroupHandler deletes a pointing group and its managed remote A records.
|
||||
// DeleteGroupHandler deletes a pointing group and its managed remote DNS records.
|
||||
// @Summary 删除 Cloudflare 指向分组
|
||||
// @Tags openflare-cloudflare
|
||||
// @Produce json
|
||||
@@ -316,7 +316,7 @@ func UpdateMemberHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.OK(item))
|
||||
}
|
||||
|
||||
// RemoveMemberHandler removes a member and its managed remote A record.
|
||||
// RemoveMemberHandler removes a member and its managed remote DNS record.
|
||||
// @Summary 移出 Cloudflare 指向成员
|
||||
// @Tags openflare-cloudflare
|
||||
// @Produce json
|
||||
@@ -440,6 +440,33 @@ func BatchRemoveMembersHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// BatchEnableProxyHandler enables orange-cloud proxy for multiple members.
|
||||
// @Summary 批量开启 Cloudflare 橙云
|
||||
// @Tags openflare-cloudflare
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "分组 ID"
|
||||
// @Param body body cloudflare.MemberBatchProxyInput true "批量开启橙云参数"
|
||||
// @Success 200 {object} response.Any
|
||||
// @Failure 400 {object} response.Any
|
||||
// @Failure 404 {object} response.Any
|
||||
// @Router /api/v1/d/cloudflare/groups/{id}/members/batch-proxy [post]
|
||||
func BatchEnableProxyHandler(c *gin.Context) {
|
||||
id, ok := apiutil.IDParam(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var input MemberBatchProxyInput
|
||||
if !apiutil.BindJSON(c, &input) {
|
||||
return
|
||||
}
|
||||
if abortLogic(c, BatchEnableProxy(c.Request.Context(), id, input)) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// ListAvailableDomainsHandler lists ZoneDomains not assigned to another group.
|
||||
// @Summary 获取可加入 Cloudflare 指向的域名
|
||||
// @Tags openflare-cloudflare
|
||||
|
||||
@@ -25,6 +25,8 @@ const (
|
||||
SyncGroupTask = "cloudflare:sync_group"
|
||||
// SyncByNodeTask is the Asynq task type for members targeting one node.
|
||||
SyncByNodeTask = "cloudflare:sync_by_node"
|
||||
// FailoverCheckTask checks node availability and updates active Cloudflare targets.
|
||||
FailoverCheckTask = "cloudflare:failover_check"
|
||||
|
||||
// TaskTypeSyncMember is the task metadata type for member synchronization.
|
||||
TaskTypeSyncMember = "of_cloudflare_sync_member"
|
||||
@@ -32,14 +34,29 @@ const (
|
||||
TaskTypeSyncGroup = "of_cloudflare_sync_group"
|
||||
// TaskTypeSyncByNode is the task metadata type for node-triggered synchronization.
|
||||
TaskTypeSyncByNode = "of_cloudflare_sync_by_node"
|
||||
// TaskTypeFailoverCheck is the schedule metadata type for node failover checks.
|
||||
TaskTypeFailoverCheck = "of_cloudflare_failover_check"
|
||||
)
|
||||
|
||||
// FailoverCheckMeta describes the scheduled node failover reconciliation.
|
||||
var FailoverCheckMeta = task.TaskMeta{
|
||||
Type: TaskTypeFailoverCheck,
|
||||
AsynqTask: FailoverCheckTask,
|
||||
Name: "Cloudflare 节点故障回退检查",
|
||||
Description: "根据节点在线和应用状态切换分组主备节点",
|
||||
SupportsTime: false,
|
||||
MaxRetry: 2,
|
||||
Queue: task.QueueDefault,
|
||||
Retryable: true,
|
||||
InternalOnly: true,
|
||||
}
|
||||
|
||||
// SyncMemberMeta describes one-member reconciliation (admin-dispatchable).
|
||||
var SyncMemberMeta = task.TaskMeta{
|
||||
Type: TaskTypeSyncMember,
|
||||
AsynqTask: SyncMemberTask,
|
||||
Name: "Cloudflare 域名同步",
|
||||
Description: "同步单个域名的 Cloudflare A 记录",
|
||||
Description: "同步单个域名的 Cloudflare DNS 记录",
|
||||
SupportsTime: false,
|
||||
MaxRetry: 3,
|
||||
Queue: task.QueueDefault,
|
||||
@@ -169,14 +186,17 @@ func (handler *SyncMemberTaskHandler) Execute(ctx context.Context, payload []byt
|
||||
if loadErr != nil {
|
||||
task.AppendLog(ctx, "加载成员上下文失败: member_id=%d error=%v", input.MemberID, loadErr)
|
||||
} else {
|
||||
target := fmt.Sprintf("%s(%s)", state.Node.Name, strings.TrimSpace(state.Node.IP))
|
||||
if state.Group.TargetMode == model.CFPointingTargetModeCustom {
|
||||
target = fmt.Sprintf("%s %s", state.Group.RecordType, state.Group.RecordContent)
|
||||
}
|
||||
task.AppendLog(ctx,
|
||||
"开始域名同步: domain=%s zone=%s group=%s(#%d) node=%s(%s) proxied=%v member_id=%d",
|
||||
"开始域名同步: domain=%s zone=%s group=%s(#%d) target=%s proxied=%v member_id=%d",
|
||||
state.Domain.Domain,
|
||||
state.Zone.Domain,
|
||||
state.Group.Name,
|
||||
state.Group.ID,
|
||||
state.Node.Name,
|
||||
strings.TrimSpace(state.Node.IP),
|
||||
target,
|
||||
state.Member.Proxied,
|
||||
input.MemberID,
|
||||
)
|
||||
@@ -194,12 +214,15 @@ func (handler *SyncMemberTaskHandler) Execute(ctx context.Context, payload []byt
|
||||
|
||||
message := "Cloudflare 域名同步成功"
|
||||
if state != nil {
|
||||
ip := strings.TrimSpace(state.Node.IP)
|
||||
target := strings.TrimSpace(state.Node.IP)
|
||||
if state.Group.TargetMode == model.CFPointingTargetModeCustom {
|
||||
target = fmt.Sprintf("%s %s", state.Group.RecordType, state.Group.RecordContent)
|
||||
}
|
||||
message = fmt.Sprintf("Cloudflare 域名同步成功: %s → %s (proxied=%v)",
|
||||
state.Domain.Domain, ip, state.Member.Proxied)
|
||||
state.Domain.Domain, target, state.Member.Proxied)
|
||||
task.AppendLog(ctx,
|
||||
"域名同步成功: domain=%s desired_ip=%s proxied=%v group=%s node=%s",
|
||||
state.Domain.Domain, ip, state.Member.Proxied, state.Group.Name, state.Node.Name,
|
||||
"域名同步成功: domain=%s desired_target=%s proxied=%v group=%s target_mode=%s",
|
||||
state.Domain.Domain, target, state.Member.Proxied, state.Group.Name, state.Group.TargetMode,
|
||||
)
|
||||
} else {
|
||||
task.AppendLog(ctx, "域名同步成功: member_id=%d", input.MemberID)
|
||||
@@ -242,14 +265,9 @@ func (handler *SyncGroupTaskHandler) Execute(ctx context.Context, payload []byte
|
||||
task.AppendLog(ctx, "加载分组失败: group_id=%d error=%v", input.GroupID, groupErr)
|
||||
} else {
|
||||
scopeName = group.Name
|
||||
if node, nodeErr := repository.GetOpenFlareNodeByID(ctx, group.ActiveNodeID); nodeErr != nil {
|
||||
task.AppendLog(ctx, "加载生效节点失败: group=%s active_node_id=%d error=%v",
|
||||
group.Name, group.ActiveNodeID, nodeErr)
|
||||
} else {
|
||||
activeNode = fmt.Sprintf("%s(%s)", node.Name, strings.TrimSpace(node.IP))
|
||||
}
|
||||
activeNode = groupSyncTarget(ctx, group)
|
||||
task.AppendLog(ctx,
|
||||
"准备分组同步: group=%s id=%d enabled=%v active_node=%s default_proxied=%v",
|
||||
"准备分组同步: group=%s id=%d enabled=%v target=%s default_proxied=%v",
|
||||
group.Name, group.ID, group.Enabled, activeNode, group.DefaultProxied,
|
||||
)
|
||||
}
|
||||
@@ -258,6 +276,19 @@ func (handler *SyncGroupTaskHandler) Execute(ctx context.Context, payload []byte
|
||||
return executeBatchSync(ctx, members, err, "分组", scopeName, input.GroupID, activeNode)
|
||||
}
|
||||
|
||||
func groupSyncTarget(ctx context.Context, group *model.CFPointingGroup) string {
|
||||
if group.TargetMode == model.CFPointingTargetModeCustom {
|
||||
return fmt.Sprintf("%s %s", group.RecordType, group.RecordContent)
|
||||
}
|
||||
node, err := repository.GetOpenFlareNodeByID(ctx, group.ActiveNodeID)
|
||||
if err != nil {
|
||||
task.AppendLog(ctx, "加载生效节点失败: group=%s active_node_id=%d error=%v",
|
||||
group.Name, group.ActiveNodeID, err)
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s(%s)", node.Name, strings.TrimSpace(node.IP))
|
||||
}
|
||||
|
||||
// SyncByNodeTaskHandler reconciles every member targeting a node.
|
||||
type SyncByNodeTaskHandler struct{}
|
||||
|
||||
@@ -310,7 +341,7 @@ func executeBatchSync(
|
||||
return nil, listErr
|
||||
}
|
||||
|
||||
task.AppendLog(ctx, "开始%s同步: name=%s id=%d active_node=%s 域名数=%d",
|
||||
task.AppendLog(ctx, "开始%s同步: name=%s id=%d target=%s 域名数=%d",
|
||||
scope, scopeName, scopeID, activeNode, len(members))
|
||||
if len(members) == 0 {
|
||||
message := fmt.Sprintf("Cloudflare %s同步完成: %s 无域名成员", scope, scopeName)
|
||||
@@ -364,3 +395,31 @@ func decodePayload(payload []byte, target any) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FailoverCheckTaskHandler checks enabled node groups for failover and recovery.
|
||||
type FailoverCheckTaskHandler struct{}
|
||||
|
||||
// ValidatePayload validates the empty scheduled-task payload.
|
||||
func (handler *FailoverCheckTaskHandler) ValidatePayload(payload []byte) ([]byte, error) {
|
||||
var input struct{}
|
||||
if err := decodePayload(payload, &input); err != nil {
|
||||
return nil, fmt.Errorf("无效的 Cloudflare 故障回退检查参数: %w", err)
|
||||
}
|
||||
return json.Marshal(input)
|
||||
}
|
||||
|
||||
// Execute switches affected groups and queues DNS reconciliation.
|
||||
func (handler *FailoverCheckTaskHandler) Execute(ctx context.Context, payload []byte) (*task.TaskResult, error) {
|
||||
if _, err := handler.ValidatePayload(payload); err != nil {
|
||||
return nil, task.PermanentError(err.Error())
|
||||
}
|
||||
task.AppendLog(ctx, "开始检查 Cloudflare 节点分组主备状态")
|
||||
changed, err := CheckNodeFailover(ctx)
|
||||
if err != nil {
|
||||
task.AppendLog(ctx, "Cloudflare 节点故障回退检查失败: error=%v", err)
|
||||
return nil, err
|
||||
}
|
||||
message := fmt.Sprintf("Cloudflare 节点故障回退检查完成,切换分组 %d 个", changed)
|
||||
task.AppendLog(ctx, "%s", message)
|
||||
return &task.TaskResult{Message: message}, nil
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ type NodeOption struct {
|
||||
// GroupInput creates or updates a pointing group.
|
||||
type GroupInput struct {
|
||||
Name string `json:"name"`
|
||||
TargetMode string `json:"target_mode"`
|
||||
RecordType string `json:"record_type"`
|
||||
RecordContent string `json:"record_content"`
|
||||
PrimaryNodeID uint `json:"primary_node_id"`
|
||||
BackupNodeID *uint `json:"backup_node_id"`
|
||||
DefaultProxied bool `json:"default_proxied"`
|
||||
@@ -42,6 +45,9 @@ type GroupInput struct {
|
||||
type GroupItem struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
TargetMode string `json:"target_mode"`
|
||||
RecordType string `json:"record_type"`
|
||||
RecordContent string `json:"record_content"`
|
||||
PrimaryNode NodeOption `json:"primary_node"`
|
||||
BackupNode *NodeOption `json:"backup_node"`
|
||||
ActiveNode NodeOption `json:"active_node"`
|
||||
@@ -79,6 +85,11 @@ type MemberBatchRemoveInput struct {
|
||||
MemberIDs []uint `json:"member_ids"`
|
||||
}
|
||||
|
||||
// MemberBatchProxyInput contains the members to enable orange-cloud proxy for.
|
||||
type MemberBatchProxyInput struct {
|
||||
MemberIDs []uint `json:"member_ids"`
|
||||
}
|
||||
|
||||
// MemberItem is the admin-facing member state.
|
||||
type MemberItem struct {
|
||||
ID uint `json:"id"`
|
||||
|
||||
@@ -444,6 +444,7 @@ func snapshotPagesDeploymentEqual(left, right *openrestyrender.PagesDeployment)
|
||||
|
||||
func snapshotRoutePolicyEqual(left, right snapshotRoute) bool {
|
||||
return left.EnableHTTPS == right.EnableHTTPS &&
|
||||
left.EnableHTTP2 == right.EnableHTTP2 &&
|
||||
left.RedirectHTTP == right.RedirectHTTP &&
|
||||
left.LimitConnPerServer == right.LimitConnPerServer &&
|
||||
left.LimitConnPerIP == right.LimitConnPerIP &&
|
||||
|
||||
@@ -50,6 +50,7 @@ type snapshotRoute struct {
|
||||
Upstreams []string `json:"upstreams,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
EnableHTTP2 bool `json:"enable_http2"`
|
||||
DomainCertIDs []uint `json:"domain_cert_ids,omitempty"`
|
||||
RedirectHTTP bool `json:"redirect_http"`
|
||||
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
||||
@@ -288,6 +289,7 @@ func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]sna
|
||||
Upstreams: upstreams,
|
||||
Enabled: route.Enabled,
|
||||
EnableHTTPS: route.EnableHTTPS,
|
||||
EnableHTTP2: route.EnableHTTP2,
|
||||
DomainCertIDs: domainCertIDs,
|
||||
RedirectHTTP: route.RedirectHTTP,
|
||||
LimitConnPerServer: route.LimitConnPerServer,
|
||||
@@ -547,7 +549,7 @@ func buildOpenRestyConfigSnapshot(ctx context.Context) openRestyConfigSnapshot {
|
||||
ProxyReadTimeout: getIntConfig(model.ConfigKeyOpenRestyProxyReadTimeout, defaultOpenRestyProxyTimeout),
|
||||
WebsocketEnabled: getBoolConfig(model.ConfigKeyOpenRestyWebsocketEnabled, true),
|
||||
HTTP3Enabled: getBoolConfig(model.ConfigKeyOpenRestyHTTP3Enabled, true),
|
||||
ProxyRequestBuffering: getBoolConfig(model.ConfigKeyOpenRestyProxyRequestBufferingEnabled, false),
|
||||
ProxyRequestBuffering: getBoolConfig(model.ConfigKeyOpenRestyProxyRequestBufferingEnabled, true),
|
||||
ProxyBufferingEnabled: getBoolConfig(model.ConfigKeyOpenRestyProxyBufferingEnabled, true),
|
||||
ProxyBuffers: getStringConfig(model.ConfigKeyOpenRestyProxyBuffers, "16 16k"),
|
||||
ProxyBufferSize: getStringConfig(model.ConfigKeyOpenRestyProxyBufferSize, "8k"),
|
||||
|
||||
@@ -113,6 +113,14 @@ func populateProxyRouteFields(
|
||||
route.UpstreamType = upstreamType
|
||||
}
|
||||
|
||||
func applyProxyRouteHTTP2(route *model.ProxyRoute, input Input) {
|
||||
if input.EnableHTTP2 != nil {
|
||||
route.EnableHTTP2 = *input.EnableHTTP2
|
||||
} else if route.ID == 0 {
|
||||
route.EnableHTTP2 = true
|
||||
}
|
||||
}
|
||||
|
||||
func applyProxyRouteUpstreamType(ctx context.Context, route *model.ProxyRoute, upstreamType string, input Input) error {
|
||||
switch upstreamType {
|
||||
case proxyRouteUpstreamTypeTunnel:
|
||||
|
||||
@@ -35,6 +35,7 @@ type Input struct {
|
||||
Upstreams []string `json:"upstreams"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
EnableHTTP2 *bool `json:"enable_http2"`
|
||||
RedirectHTTP bool `json:"redirect_http"`
|
||||
LimitConnPerServer int `json:"limit_conn_per_server"`
|
||||
LimitConnPerIP int `json:"limit_conn_per_ip"`
|
||||
@@ -68,6 +69,7 @@ type View struct {
|
||||
UpstreamList []string `json:"upstream_list"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
EnableHTTP2 bool `json:"enable_http2"`
|
||||
RedirectHTTP bool `json:"redirect_http"`
|
||||
LimitConnPerServer int `json:"limit_conn_per_server"`
|
||||
LimitConnPerIP int `json:"limit_conn_per_ip"`
|
||||
@@ -310,6 +312,7 @@ func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input)
|
||||
if route == nil {
|
||||
route = &model.ProxyRoute{}
|
||||
}
|
||||
applyProxyRouteHTTP2(route, input)
|
||||
populateProxyRouteFields(
|
||||
route,
|
||||
input,
|
||||
@@ -381,6 +384,7 @@ func buildProxyRouteView(ctx context.Context, route *model.ProxyRoute) (*View, e
|
||||
UpstreamList: upstreams,
|
||||
Enabled: route.Enabled,
|
||||
EnableHTTPS: route.EnableHTTPS,
|
||||
EnableHTTP2: route.EnableHTTP2,
|
||||
RedirectHTTP: route.RedirectHTTP,
|
||||
LimitConnPerServer: route.LimitConnPerServer,
|
||||
LimitConnPerIP: route.LimitConnPerIP,
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE of_proxy_routes ADD COLUMN enable_http2 BOOLEAN NOT NULL DEFAULT TRUE;
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE of_proxy_routes DROP COLUMN enable_http2;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE of_cf_pointing_groups
|
||||
ADD COLUMN target_mode VARCHAR(16) NOT NULL DEFAULT 'node',
|
||||
ADD COLUMN record_type VARCHAR(8) NOT NULL DEFAULT 'A',
|
||||
ADD COLUMN record_content VARCHAR(255) NOT NULL DEFAULT '';
|
||||
|
||||
INSERT INTO w_schedules (id, name, task_type, cron, payload, is_active, created_at, updated_at)
|
||||
VALUES (105, 'Cloudflare 节点故障回退检查', 'of_cloudflare_failover_check', '* * * * *', '{}', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- +goose Down
|
||||
DELETE FROM w_schedules WHERE id = 105;
|
||||
ALTER TABLE of_cf_pointing_groups
|
||||
DROP COLUMN record_content,
|
||||
DROP COLUMN record_type,
|
||||
DROP COLUMN target_mode;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- +goose Up
|
||||
UPDATE w_system_configs
|
||||
SET value = 'true', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE key = 'openresty_proxy_request_buffering_enabled' AND value = 'false';
|
||||
|
||||
-- +goose Down
|
||||
-- Intentionally left unchanged: restoring the old value could overwrite a later user choice.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- +goose Up
|
||||
INSERT INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at)
|
||||
VALUES ('log_db_migration', '', 'system', 0, '日志数据库切换冻结标记', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- +goose Down
|
||||
-- Keep the runtime migration flag: this row may have existed before this migration.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE of_proxy_routes ADD COLUMN enable_http2 BOOLEAN NOT NULL DEFAULT TRUE;
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE of_proxy_routes DROP COLUMN enable_http2;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE of_cf_pointing_groups ADD COLUMN target_mode VARCHAR(16) NOT NULL DEFAULT 'node';
|
||||
ALTER TABLE of_cf_pointing_groups ADD COLUMN record_type VARCHAR(8) NOT NULL DEFAULT 'A';
|
||||
ALTER TABLE of_cf_pointing_groups ADD COLUMN record_content VARCHAR(255) NOT NULL DEFAULT '';
|
||||
|
||||
INSERT INTO w_schedules (id, name, task_type, cron, payload, is_active, created_at, updated_at)
|
||||
VALUES (105, 'Cloudflare 节点故障回退检查', 'of_cloudflare_failover_check', '* * * * *', '{}', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- +goose Down
|
||||
DELETE FROM w_schedules WHERE id = 105;
|
||||
ALTER TABLE of_cf_pointing_groups DROP COLUMN record_content;
|
||||
ALTER TABLE of_cf_pointing_groups DROP COLUMN record_type;
|
||||
ALTER TABLE of_cf_pointing_groups DROP COLUMN target_mode;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- +goose Up
|
||||
UPDATE w_system_configs
|
||||
SET value = 'true', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE key = 'openresty_proxy_request_buffering_enabled' AND value = 'false';
|
||||
|
||||
-- +goose Down
|
||||
-- Intentionally left unchanged: restoring the old value could overwrite a later user choice.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- +goose Up
|
||||
INSERT OR IGNORE INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at)
|
||||
VALUES ('log_db_migration', '', 'system', 0, '日志数据库切换冻结标记', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
|
||||
|
||||
-- +goose Down
|
||||
-- Keep the runtime migration flag: this row may have existed before this migration.
|
||||
@@ -5,6 +5,7 @@ package migrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -23,7 +24,31 @@ import (
|
||||
// (初始系统配置 + 各期配置迁移/新增 seed:of_options 迁移、文件白名单、磁盘缓存、
|
||||
// 登录会话 TTL、升级源、存储、FRPS Web UI、Pages、OpenResty 限流、单 IP 限频、
|
||||
// 错误页、SW 离线、日志保留期、指标保留期等);新增配置 seed 迁移时需同步更新本常量。
|
||||
const expectedMigratedSystemConfigCount = 95
|
||||
const expectedMigratedSystemConfigCount = 96
|
||||
|
||||
func TestGooseMigrationVersionsAreUniquePerDialect(t *testing.T) {
|
||||
for _, dir := range []string{"goose/postgres", "goose/sqlite"} {
|
||||
files, err := fs.Glob(migrationFS, dir+"/*.sql")
|
||||
if err != nil {
|
||||
t.Fatalf("glob %s migrations: %v", dir, err)
|
||||
}
|
||||
|
||||
versions := make(map[string]string, len(files))
|
||||
for _, file := range files {
|
||||
name := strings.TrimPrefix(file, dir+"/")
|
||||
version, _, ok := strings.Cut(name, "_")
|
||||
if !ok {
|
||||
t.Errorf("migration %q does not include a version prefix", file)
|
||||
continue
|
||||
}
|
||||
if previous, exists := versions[version]; exists {
|
||||
t.Errorf("%s has duplicate migration version %s in %q and %q", dir, version, previous, name)
|
||||
continue
|
||||
}
|
||||
versions[version] = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateInitializesSQLiteDatabase(t *testing.T) {
|
||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
@@ -64,6 +89,14 @@ func TestMigrateInitializesSQLiteDatabase(t *testing.T) {
|
||||
t.Errorf("Migrate() w_system_configs count = %d, want %d", systemConfigCount, expectedMigratedSystemConfigCount)
|
||||
}
|
||||
|
||||
var logMigrationConfig model.SystemConfig
|
||||
if err := sqliteDB.Where("key = ?", model.ConfigKeyLogDBMigration).First(&logMigrationConfig).Error; err != nil {
|
||||
t.Fatalf("Migrate() log migration config error = %v", err)
|
||||
}
|
||||
if logMigrationConfig.Value != "" || logMigrationConfig.Type != "system" || logMigrationConfig.Visibility != model.ConfigVisibilityHidden {
|
||||
t.Errorf("Migrate() log migration config = %+v, want an empty hidden system config", logMigrationConfig)
|
||||
}
|
||||
|
||||
var adminCount int64
|
||||
if err := sqliteDB.Table("w_users").Where("username = ?", "admin").Count(&adminCount).Error; err != nil {
|
||||
t.Fatalf("Migrate() count admin user error = %v", err)
|
||||
|
||||
@@ -60,6 +60,8 @@ func Register() {
|
||||
task.RegisterTaskMeta(cf.SyncGroupMeta)
|
||||
task.RegisterHandler(cf.SyncByNodeTask, &cf.SyncByNodeTaskHandler{})
|
||||
task.RegisterTaskMeta(cf.SyncByNodeMeta)
|
||||
task.RegisterHandler(cf.FailoverCheckTask, &cf.FailoverCheckTaskHandler{})
|
||||
task.RegisterTaskMeta(cf.FailoverCheckMeta)
|
||||
|
||||
// pages source actions are only dispatched by the Pages domain API/scanner.
|
||||
task.RegisterHandler(pages.PagesSourceScanTask, &pages.SourceScanHandler{})
|
||||
|
||||
@@ -24,6 +24,11 @@ const (
|
||||
CFMemberSyncOK = "ok"
|
||||
// CFMemberSyncError indicates the latest reconciliation failed.
|
||||
CFMemberSyncError = "error"
|
||||
|
||||
// CFPointingTargetModeNode binds group members to a managed edge node.
|
||||
CFPointingTargetModeNode = "node"
|
||||
// CFPointingTargetModeCustom binds group members to a custom DNS target.
|
||||
CFPointingTargetModeCustom = "custom"
|
||||
)
|
||||
|
||||
// CFConnection stores the single Cloudflare API credential source.
|
||||
@@ -45,6 +50,9 @@ func (CFConnection) TableName() string { return "of_cf_connections" }
|
||||
type CFPointingGroup struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"size:128;not null"`
|
||||
TargetMode string `json:"target_mode" gorm:"size:16;not null;default:'node'"`
|
||||
RecordType string `json:"record_type" gorm:"size:8;not null;default:'A'"`
|
||||
RecordContent string `json:"record_content" gorm:"size:255;not null;default:''"`
|
||||
PrimaryNodeID uint `json:"primary_node_id" gorm:"not null;index:idx_of_cf_pointing_groups_primary_node_id"`
|
||||
BackupNodeID *uint `json:"backup_node_id" gorm:"index:idx_of_cf_pointing_groups_backup_node_id"`
|
||||
ActiveNodeID uint `json:"active_node_id" gorm:"not null;index:idx_of_cf_pointing_groups_active_node_id"`
|
||||
|
||||
@@ -18,6 +18,7 @@ type ProxyRoute struct {
|
||||
Upstreams string `json:"upstreams" gorm:"type:text;not null;default:'[]'"`
|
||||
Enabled bool `json:"enabled" gorm:"not null;default:true"`
|
||||
EnableHTTPS bool `json:"enable_https" gorm:"column:enable_https;not null;default:false"`
|
||||
EnableHTTP2 bool `json:"enable_http2" gorm:"not null;default:true"`
|
||||
RedirectHTTP bool `json:"redirect_http" gorm:"not null;default:false"`
|
||||
LimitConnPerServer int `json:"limit_conn_per_server" gorm:"not null;default:0"`
|
||||
LimitConnPerIP int `json:"limit_conn_per_ip" gorm:"not null;default:0"`
|
||||
|
||||
@@ -64,6 +64,11 @@ func ListCFPointingGroups(ctx context.Context) ([]model.CFPointingGroup, error)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// UpdateCFPointingGroupTarget updates the active node or custom-record target.
|
||||
func UpdateCFPointingGroupTarget(ctx context.Context, groupID uint, changes map[string]any) error {
|
||||
return db.DB(ctx).Model(&model.CFPointingGroup{}).Where("id = ?", groupID).Updates(changes).Error
|
||||
}
|
||||
|
||||
// GetCFPointingGroup returns a group by ID.
|
||||
func GetCFPointingGroup(ctx context.Context, id uint) (*model.CFPointingGroup, error) {
|
||||
var item model.CFPointingGroup
|
||||
@@ -192,9 +197,12 @@ func GetCFPointingMemberContext(ctx context.Context, memberID uint) (*CFPointing
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node, err := GetOpenFlareNodeByID(ctx, group.ActiveNodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
node := &model.OpenFlareNode{}
|
||||
if group.TargetMode != model.CFPointingTargetModeCustom {
|
||||
node, err = GetOpenFlareNodeByID(ctx, group.ActiveNodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &CFPointingMemberContext{Member: *member, Group: *group, Domain: *domain, Zone: *zone, Node: *node}, nil
|
||||
}
|
||||
@@ -214,6 +222,15 @@ func MarkCFPointingGroupMembersPending(ctx context.Context, groupID uint) error
|
||||
Updates(map[string]any{"sync_status": model.CFMemberSyncPending, "last_error": ""}).Error
|
||||
}
|
||||
|
||||
// HasPendingCFPointingGroupMembers reports whether a group has members awaiting reconciliation.
|
||||
func HasPendingCFPointingGroupMembers(ctx context.Context, groupID uint) (bool, error) {
|
||||
var count int64
|
||||
err := db.DB(ctx).Model(&model.CFPointingMember{}).
|
||||
Where("group_id = ? AND sync_status = ?", groupID, model.CFMemberSyncPending).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// DeleteCFPointingGroupAndMembers removes a group after its remote records are deleted.
|
||||
func DeleteCFPointingGroupAndMembers(ctx context.Context, groupID uint) error {
|
||||
return db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
@@ -33,4 +33,5 @@ func registerCloudflareRoutes(apiGroup *gin.RouterGroup) {
|
||||
route.POST("/groups/:id/members/:memberId/sync", cf.SyncMemberHandler)
|
||||
route.POST("/groups/:id/members/batch-move", cf.BatchMoveMembersHandler)
|
||||
route.POST("/groups/:id/members/batch-remove", cf.BatchRemoveMembersHandler)
|
||||
route.POST("/groups/:id/members/batch-proxy", cf.BatchEnableProxyHandler)
|
||||
}
|
||||
|
||||
@@ -334,34 +334,42 @@ func renderHTTPRedirectServer(serverNames string) string {
|
||||
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n return 301 https://$host$request_uri;\n}\n\n", serverNames)
|
||||
}
|
||||
|
||||
func renderHTTPSServer(serverNames string, siteName string, originURL string, originHost string, certificateID uint, customHeaders []CustomHeader, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, swEnabled bool, cfg ConfigSnapshot) string {
|
||||
func renderHTTPSServer(serverNames string, siteName string, originURL string, originHost string, certificateID uint, customHeaders []CustomHeader, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, http2Enabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, swEnabled bool, cfg ConfigSnapshot) string {
|
||||
certPath := fmt.Sprintf("%s/%d.crt", CertDirPlaceholder, certificateID)
|
||||
keyPath := fmt.Sprintf("%s/%d.key", CertDirPlaceholder, certificateID)
|
||||
var h3Listen string
|
||||
var h3Header string
|
||||
var http2Directive string
|
||||
if http2Enabled {
|
||||
http2Directive = " http2 on;\n"
|
||||
}
|
||||
if cfg.HTTP3Enabled {
|
||||
h3Listen = " listen 443 quic;\n"
|
||||
h3Header = " add_header Alt-Svc 'h3=\":443\"; ma=86400';\n"
|
||||
}
|
||||
if swEnabled {
|
||||
return fmt.Sprintf("server {\n listen 443 ssl;\n%s http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s%s location / {\n%s%s%s%s%s%s }\n%s%s%s}\n\n", h3Listen, serverNames, certPath, keyPath, h3Header, renderAccessBlockWithSW(siteName, powEnabled, cfg), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig, cfg), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderOriginErrorPageIntercept(cfg), renderProxyPassBlock(originURL, upstreamConfig), renderOriginErrorPageServerBits(cfg), renderPowStaticLocationBlock(powEnabled), renderServiceWorkerChallenger(cfg))
|
||||
return fmt.Sprintf("server {\n listen 443 ssl;\n%s%s server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s%s location / {\n%s%s%s%s%s%s }\n%s%s%s}\n\n", h3Listen, http2Directive, serverNames, certPath, keyPath, h3Header, renderAccessBlockWithSW(siteName, powEnabled, cfg), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig, cfg), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderOriginErrorPageIntercept(cfg), renderProxyPassBlock(originURL, upstreamConfig), renderOriginErrorPageServerBits(cfg), renderPowStaticLocationBlock(powEnabled), renderServiceWorkerChallenger(cfg))
|
||||
}
|
||||
return fmt.Sprintf("server {\n listen 443 ssl;\n%s http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s%s location / {\n%s%s%s%s%s%s }\n%s%s}\n\n", h3Listen, serverNames, certPath, keyPath, h3Header, renderAccessBlock(siteName, powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig, cfg), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderOriginErrorPageIntercept(cfg), renderProxyPassBlock(originURL, upstreamConfig), renderOriginErrorPageServerBits(cfg), renderPowStaticLocationBlock(powEnabled))
|
||||
return fmt.Sprintf("server {\n listen 443 ssl;\n%s%s server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s%s location / {\n%s%s%s%s%s%s }\n%s%s}\n\n", h3Listen, http2Directive, serverNames, certPath, keyPath, h3Header, renderAccessBlock(siteName, powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig, cfg), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderOriginErrorPageIntercept(cfg), renderProxyPassBlock(originURL, upstreamConfig), renderOriginErrorPageServerBits(cfg), renderPowStaticLocationBlock(powEnabled))
|
||||
}
|
||||
|
||||
func renderHTTPSPagesServer(serverNames string, siteName string, certificateID uint, deployment *PagesDeployment, limitConfig routeLimitConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, swEnabled bool, cfg ConfigSnapshot) string {
|
||||
func renderHTTPSPagesServer(serverNames string, siteName string, certificateID uint, deployment *PagesDeployment, limitConfig routeLimitConfig, powEnabled bool, http2Enabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, swEnabled bool, cfg ConfigSnapshot) string {
|
||||
certPath := fmt.Sprintf("%s/%d.crt", CertDirPlaceholder, certificateID)
|
||||
keyPath := fmt.Sprintf("%s/%d.key", CertDirPlaceholder, certificateID)
|
||||
var h3Listen string
|
||||
var h3Header string
|
||||
var http2Directive string
|
||||
if http2Enabled {
|
||||
http2Directive = " http2 on;\n"
|
||||
}
|
||||
if cfg.HTTP3Enabled {
|
||||
h3Listen = " listen 443 quic;\n"
|
||||
h3Header = " add_header Alt-Svc 'h3=\":443\"; ma=86400';\n"
|
||||
}
|
||||
if swEnabled {
|
||||
return fmt.Sprintf("server {\n listen 443 ssl;\n%s http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s%s root %s;\n index %s;%s%s\n\n location / {\n%s%s }\n%s%s}\n\n", h3Listen, serverNames, certPath, keyPath, h3Header, renderAccessBlockWithSW(siteName, powEnabled, cfg), renderPowLocationBlocks(powEnabled), quoteNginxStringLiteral(pagesDeploymentRoot(deployment)), quoteNginxStringLiteral(pagesEntryFile(deployment)), renderPagesAPIProxyLocationBlock(deployment), renderPagesRootLocationBlock(deployment, limitConfig, basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderPagesLocationBlock(deployment, limitConfig), renderPowStaticLocationBlock(powEnabled), renderServiceWorkerChallenger(cfg))
|
||||
return fmt.Sprintf("server {\n listen 443 ssl;\n%s%s server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s%s root %s;\n index %s;%s%s\n\n location / {\n%s%s }\n%s%s}\n\n", h3Listen, http2Directive, serverNames, certPath, keyPath, h3Header, renderAccessBlockWithSW(siteName, powEnabled, cfg), renderPowLocationBlocks(powEnabled), quoteNginxStringLiteral(pagesDeploymentRoot(deployment)), quoteNginxStringLiteral(pagesEntryFile(deployment)), renderPagesAPIProxyLocationBlock(deployment), renderPagesRootLocationBlock(deployment, limitConfig, basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderPagesLocationBlock(deployment, limitConfig), renderPowStaticLocationBlock(powEnabled), renderServiceWorkerChallenger(cfg))
|
||||
}
|
||||
return fmt.Sprintf("server {\n listen 443 ssl;\n%s http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s%s root %s;\n index %s;%s%s\n\n location / {\n%s%s }\n%s}\n\n", h3Listen, serverNames, certPath, keyPath, h3Header, renderAccessBlock(siteName, powEnabled), renderPowLocationBlocks(powEnabled), quoteNginxStringLiteral(pagesDeploymentRoot(deployment)), quoteNginxStringLiteral(pagesEntryFile(deployment)), renderPagesAPIProxyLocationBlock(deployment), renderPagesRootLocationBlock(deployment, limitConfig, basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderPagesLocationBlock(deployment, limitConfig), renderPowStaticLocationBlock(powEnabled))
|
||||
return fmt.Sprintf("server {\n listen 443 ssl;\n%s%s server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s%s root %s;\n index %s;%s%s\n\n location / {\n%s%s }\n%s}\n\n", h3Listen, http2Directive, serverNames, certPath, keyPath, h3Header, renderAccessBlock(siteName, powEnabled), renderPowLocationBlocks(powEnabled), quoteNginxStringLiteral(pagesDeploymentRoot(deployment)), quoteNginxStringLiteral(pagesEntryFile(deployment)), renderPagesAPIProxyLocationBlock(deployment), renderPagesRootLocationBlock(deployment, limitConfig, basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderPagesLocationBlock(deployment, limitConfig), renderPowStaticLocationBlock(powEnabled))
|
||||
}
|
||||
|
||||
func renderPagesRootLocationBlock(deployment *PagesDeployment, limitConfig routeLimitConfig, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string) string {
|
||||
|
||||
@@ -70,7 +70,7 @@ func renderPagesRouteHTTPS(
|
||||
}
|
||||
for _, certID := range certIDs {
|
||||
if assignedDomains := partition.domainsByCertID[certID]; len(assignedDomains) > 0 {
|
||||
builder.WriteString(renderHTTPSPagesServer(renderServerNames(assignedDomains), displayName, certID, route.PagesDeployment, limitConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, routeSWEnabled(assignedDomains, cfg), cfg))
|
||||
builder.WriteString(renderHTTPSPagesServer(renderServerNames(assignedDomains), displayName, certID, route.PagesDeployment, limitConfig, powEnabled, route.EnableHTTP2, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, routeSWEnabled(assignedDomains, cfg), cfg))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func renderProxyRouteHTTPS(
|
||||
}
|
||||
for _, certID := range certIDs {
|
||||
if assignedDomains := partition.domainsByCertID[certID]; len(assignedDomains) > 0 {
|
||||
builder.WriteString(renderHTTPSServer(renderServerNames(assignedDomains), displayName, route.OriginURL, route.OriginHost, certID, route.CustomHeaders, cacheConfig, limitConfig, upstreamConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, routeSWEnabled(assignedDomains, cfg), cfg))
|
||||
builder.WriteString(renderHTTPSServer(renderServerNames(assignedDomains), displayName, route.OriginURL, route.OriginHost, certID, route.CustomHeaders, cacheConfig, limitConfig, upstreamConfig, powEnabled, route.EnableHTTP2, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, routeSWEnabled(assignedDomains, cfg), cfg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,8 +130,8 @@ func TestRenderServiceWorkerChallengerHTTPExclusion(t *testing.T) {
|
||||
for name, rendered := range map[string]string{
|
||||
"proxy": renderHTTPProxyServer("example.com", "example.com", "http://127.0.0.1:8080", "", nil, routeCacheConfig{}, routeLimitConfig{}, routeUpstreamConfig{}, false, false, "", "", false, cfg),
|
||||
"pages": renderHTTPPagesServer("example.com", "example.com", nil, routeLimitConfig{}, false, false, "", "", false, cfg),
|
||||
"https": renderHTTPSServer("example.com", "example.com", "http://127.0.0.1:8080", "", 1, nil, routeCacheConfig{}, routeLimitConfig{}, routeUpstreamConfig{}, false, false, "", "", true, cfg),
|
||||
"hpages": renderHTTPSPagesServer("example.com", "example.com", 1, nil, routeLimitConfig{}, false, false, "", "", true, cfg),
|
||||
"https": renderHTTPSServer("example.com", "example.com", "http://127.0.0.1:8080", "", 1, nil, routeCacheConfig{}, routeLimitConfig{}, routeUpstreamConfig{}, false, true, false, "", "", true, cfg),
|
||||
"hpages": renderHTTPSPagesServer("example.com", "example.com", 1, nil, routeLimitConfig{}, false, true, false, "", "", true, cfg),
|
||||
} {
|
||||
if strings.Contains(rendered, "access_by_lua_block") && strings.Count(rendered, "access_by_lua_block") != 1 {
|
||||
t.Fatalf("%s: expected at most one access block, got:\n%s", name, rendered)
|
||||
@@ -145,13 +145,13 @@ func TestRenderServiceWorkerChallengerHTTPExclusion(t *testing.T) {
|
||||
if strings.Contains(httpPages, "sw.runtime") || strings.Contains(httpPages, "openflare_sw_challenge") || strings.Contains(httpPages, "location = /sw.js") {
|
||||
t.Fatalf("HTTP pages server must not carry SW intercept, got:\n%s", httpPages)
|
||||
}
|
||||
httpsProxy := renderHTTPSServer("example.com", "example.com", "http://127.0.0.1:8080", "", 1, nil, routeCacheConfig{}, routeLimitConfig{}, routeUpstreamConfig{}, false, false, "", "", true, cfg)
|
||||
httpsProxy := renderHTTPSServer("example.com", "example.com", "http://127.0.0.1:8080", "", 1, nil, routeCacheConfig{}, routeLimitConfig{}, routeUpstreamConfig{}, false, true, false, "", "", true, cfg)
|
||||
for _, want := range []string{"sw.runtime", "location = /sw.js", "location = /offline.html", "__openflare_sw_challenge"} {
|
||||
if !strings.Contains(httpsProxy, want) {
|
||||
t.Fatalf("HTTPS proxy server missing %q, got:\n%s", want, httpsProxy)
|
||||
}
|
||||
}
|
||||
httpsPages := renderHTTPSPagesServer("example.com", "example.com", 1, nil, routeLimitConfig{}, false, false, "", "", true, cfg)
|
||||
httpsPages := renderHTTPSPagesServer("example.com", "example.com", 1, nil, routeLimitConfig{}, false, true, false, "", "", true, cfg)
|
||||
for _, want := range []string{"sw.runtime", "location = /sw.js", "location = /offline.html", "__openflare_sw_challenge"} {
|
||||
if !strings.Contains(httpsPages, want) {
|
||||
t.Fatalf("HTTPS pages server missing %q, got:\n%s", want, httpsPages)
|
||||
@@ -179,7 +179,7 @@ func TestRouteSWEnabled(t *testing.T) {
|
||||
|
||||
func TestRenderHTTPSServerSWScope(t *testing.T) {
|
||||
render := func(swEnabled bool) string {
|
||||
return renderHTTPSServer("example.com", "example.com", "http://127.0.0.1:8080", "", 1, nil, routeCacheConfig{}, routeLimitConfig{}, routeUpstreamConfig{}, false, false, "", "", swEnabled, ConfigSnapshot{SWOfflineEnabled: true})
|
||||
return renderHTTPSServer("example.com", "example.com", "http://127.0.0.1:8080", "", 1, nil, routeCacheConfig{}, routeLimitConfig{}, routeUpstreamConfig{}, false, true, false, "", "", swEnabled, ConfigSnapshot{SWOfflineEnabled: true})
|
||||
}
|
||||
hit := render(routeSWEnabled([]string{"example.com"}, ConfigSnapshot{SWOfflineEnabled: true, SWOfflineDomains: []string{"example.com"}}))
|
||||
for _, want := range []string{`require("sw.runtime").check()`, "location = /sw.js", "location = /offline.html", "__openflare_sw_challenge"} {
|
||||
@@ -193,7 +193,7 @@ func TestRenderHTTPSServerSWScope(t *testing.T) {
|
||||
t.Fatalf("out-of-scope HTTPS server must not carry %q, got:\n%s", notWant, miss)
|
||||
}
|
||||
}
|
||||
if miss != renderHTTPSServer("example.com", "example.com", "http://127.0.0.1:8080", "", 1, nil, routeCacheConfig{}, routeLimitConfig{}, routeUpstreamConfig{}, false, false, "", "", false, ConfigSnapshot{}) {
|
||||
if miss != renderHTTPSServer("example.com", "example.com", "http://127.0.0.1:8080", "", 1, nil, routeCacheConfig{}, routeLimitConfig{}, routeUpstreamConfig{}, false, true, false, "", "", false, ConfigSnapshot{}) {
|
||||
t.Fatalf("out-of-scope HTTPS server must match pre-feature bytes, got:\n%s", miss)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,8 @@ http {
|
||||
gzip {{OpenRestyGzip}};
|
||||
gzip_min_length {{OpenRestyGzipMinLength}};
|
||||
gzip_comp_level {{OpenRestyGzipCompLevel}};
|
||||
gzip_vary on;
|
||||
gzip_types text/plain text/css text/xml application/javascript application/json application/xml application/rss+xml application/atom+xml image/svg+xml;
|
||||
{{OpenRestyResolverDirective}}{{OpenRestyCacheBlock}} include {{OpenRestyRouteConfigInclude}};
|
||||
}
|
||||
`
|
||||
@@ -165,6 +167,7 @@ type Route struct {
|
||||
Upstreams []string `json:"upstreams,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnableHTTPS bool `json:"enable_https"`
|
||||
EnableHTTP2 bool `json:"enable_http2"`
|
||||
DomainCertIDs []uint `json:"domain_cert_ids,omitempty"`
|
||||
RedirectHTTP bool `json:"redirect_http"`
|
||||
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user