Compare commits

...

82 Commits

Author SHA1 Message Date
刘祥超
7e5802dcd1 优化没有网站时的提示 2024-01-21 21:17:42 +08:00
刘祥超
2b6d1c70a8 集群节点列表页增加停用/启用操作 2024-01-21 17:42:56 +08:00
刘祥超
faaef3b353 Dockerfile版本号修改为1.3.3 2024-01-21 16:58:01 +08:00
刘祥超
6ffa1d4a1d 版本号修改为1.3.3 2024-01-21 16:55:55 +08:00
刘祥超
10f0e0dcca 提交components.js 2024-01-21 16:55:31 +08:00
刘祥超
0afad2e192 增加创建本地节点命令 2024-01-21 16:55:22 +08:00
刘祥超
6a1e7266b9 优化缓存文件句柄缓存的文字提示 2024-01-21 14:31:05 +08:00
刘祥超
6ffc552bd8 WAF允许ALLOW动作增加有效范围 2024-01-20 21:23:59 +08:00
刘祥超
8d8245971a WAF策略增加显示页面动作默认设置 2024-01-20 16:17:28 +08:00
刘祥超
a2f730d57e 修复部分内置页面没有<head>标签的问题 2024-01-20 10:15:05 +08:00
刘祥超
bf282c41fd 优化创建策略规则集提示 2024-01-19 11:24:02 +08:00
刘祥超
3e3f7b2cc6 WAF动作默认设置为“显示网页(page)”,减少因错误规则导致的IP封禁 2024-01-19 11:13:01 +08:00
刘祥超
0580e063be 优化网站列表批量操作按钮,防止误操作 2024-01-19 10:56:31 +08:00
刘祥超
2924aad25e 修改版本号为1.3.2.2 2024-01-16 20:59:07 +08:00
刘祥超
39907299cd 提交components.js 2024-01-16 20:58:58 +08:00
刘祥超
77705895d5 优化添加规则界面 2024-01-16 20:42:06 +08:00
刘祥超
3fb85edf0b 提交components.js 2024-01-15 08:51:21 +08:00
刘祥超
0e7a968cac 版本号修改为1.3.2.1 2024-01-15 08:40:14 +08:00
刘祥超
195a9dc771 优化网站设置和路由设置中的左侧菜单 2024-01-14 20:33:07 +08:00
刘祥超
ca227a846a 优化代码 2024-01-13 19:31:52 +08:00
刘祥超
e1057fc76e 字符编码设置增加“强制替换”选项 2024-01-13 16:28:37 +08:00
刘祥超
b5168c3174 优化文字 2024-01-13 15:46:26 +08:00
刘祥超
4ced00de50 修改API节点时提示用户需要重启edge-api进程 2024-01-12 16:44:02 +08:00
刘祥超
750c929506 实现批量删除网站功能 2024-01-11 18:40:29 +08:00
刘祥超
8b46a5a5af 自定义页面跳转支持使用变量 2024-01-11 15:37:31 +08:00
刘祥超
10d606a101 增加带宽相关组件 2024-01-11 15:17:52 +08:00
刘祥超
45d36dadcb 修复“迁移”功能中无法远程修改API节点访问地址的问题 2024-01-09 10:39:09 +08:00
刘祥超
61484d5cb8 增加用户系统文章相关管理 2024-01-09 10:21:05 +08:00
刘祥超
b7775a99ef 优化添加缓存条件和不缓存条件交互 2023-12-28 15:50:19 +08:00
刘祥超
c45ba38ffa 修复参数匹配不区分大小写选项无法保存的问题 2023-12-28 15:31:40 +08:00
刘祥超
e97d9fb8a0 优化可执行文件程序 2023-12-26 09:09:21 +08:00
刘祥超
bb56ec9ec5 修改Dockerfile版本 2023-12-24 17:35:30 +08:00
刘祥超
6d8f659558 版本号修改为1.3.2 2023-12-24 11:14:31 +08:00
刘祥超
7e9047c8fe 优化IP名单管理 2023-12-24 11:05:38 +08:00
刘祥超
b5af560b67 IP名单中可以批量删除最多10000个IP 2023-12-24 10:53:03 +08:00
刘祥超
5622636870 优化请求脚本相关代码 2023-12-23 20:54:37 +08:00
刘祥超
372c276afc 优化英文菜单样式 2023-12-23 09:41:33 +08:00
刘祥超
250b5fdd10 取消“通用设置”菜单 2023-12-22 16:58:16 +08:00
刘祥超
d1fb0bf8b7 优化数据库节点相关文字提示 2023-12-21 17:18:04 +08:00
刘祥超
490c273d36 改进部分文字 2023-12-20 15:06:19 +08:00
刘祥超
0773dbaa28 在网站设置中显示流量相关限制状态 2023-12-19 14:56:05 +08:00
刘祥超
4d04964740 修复缓存条件简单和复杂切换时可能导致复杂条件失效的问题 2023-12-19 09:26:18 +08:00
刘祥超
537e6c9a5e 修复一处编译问题 2023-12-18 10:12:20 +08:00
刘祥超
082a6721f3 版本号修改为1.3.1.2 2023-12-18 08:51:16 +08:00
刘祥超
d515a20129 提升Go模板安全性 2023-12-14 19:57:02 +08:00
刘祥超
a66d3ef61a 更新相关库 2023-12-14 19:53:43 +08:00
刘祥超
3b05b1a933 优化修改网站域名时套餐相关检查 2023-12-13 19:02:12 +08:00
刘祥超
105479ce4b 网站所属用户没有套餐时提示用户 2023-12-13 18:53:08 +08:00
刘祥超
1a950e3aad 非商业版隐藏网站配置复制图标 2023-12-13 18:46:16 +08:00
刘祥超
79fa76e039 缓存设置中可以设置缓存主域名,用来复用多域名下的缓存 2023-12-13 18:33:48 +08:00
刘祥超
9db9a70a68 优化文字 2023-12-13 11:08:35 +08:00
刘祥超
632862c742 优化多语言相关代码 2023-12-13 10:29:50 +08:00
刘祥超
76d0783f6f 切换语言时保存语言设置 2023-12-12 22:39:42 +08:00
刘祥超
fc6a0e9813 切换语言时保存更长时间 2023-12-12 22:28:17 +08:00
刘祥超
038e289057 更新components.js 2023-12-12 22:28:04 +08:00
刘祥超
4b2a4af1e7 优化极验相关代码 2023-12-12 15:00:38 +08:00
刘祥超
0fb2555bea Update en-us.js 2023-12-12 12:11:13 +08:00
刘祥超
bd8a809065 优化英文版样式 2023-12-12 11:54:20 +08:00
刘祥超
fc6fb7dce2 部分菜单实现中英文切换 2023-12-12 11:47:41 +08:00
刘祥超
bacb94abe6 优化WebP相关提示和代码 2023-12-11 11:07:02 +08:00
刘祥超
eb04421412 WebP转换质量转移到WebP策略配置 2023-12-11 10:00:22 +08:00
刘祥超
2fbd5a785e 改进部分表单文字提示 2023-12-10 10:46:50 +08:00
刘祥超
d8ae1525be 管理系统安全设置增加“自定义客户端IP报头” 2023-12-10 10:46:35 +08:00
刘祥超
daaa165d25 优化集群设置--WAF策略选择窗口尺寸 2023-12-10 08:55:12 +08:00
刘祥超
27ea02be5e 优化WAF动作“显示网页”显示 2023-12-09 15:55:17 +08:00
刘祥超
8eac1ef307 优化文字提示 2023-12-08 16:00:48 +08:00
刘祥超
1181974ea5 WAF规则集列表中参数和操作符增加悬停提示 2023-12-08 12:01:36 +08:00
刘祥超
5f57fa0470 WAF规则集列表中参数显示中文 2023-12-08 11:20:32 +08:00
刘祥超
c0ad74ab7b WAF操作符对应的对比值可以不填写 2023-12-07 20:24:26 +08:00
刘祥超
c27905e1cc 优化WAF多行对比值的文字提示 2023-12-07 11:44:37 +08:00
刘祥超
cadb65a85f 业务域名CNAME检查DNS主机地址增加Google公共DNS 2023-12-06 09:42:15 +08:00
刘祥超
b3c152685a 优化检查域名解析文字提示 2023-12-05 11:33:32 +08:00
刘祥超
16ce0726f8 将部分MB、GB...改成MiB、GiB... 2023-12-03 11:41:04 +08:00
刘祥超
5be306e087 将部分MB、GB...改成MiB、GiB... 2023-12-03 11:30:02 +08:00
刘祥超
0d21fc27ab 增加“极验-行为验”验证码集成支持 2023-11-29 16:57:58 +08:00
刘祥超
7deaa678bc 创建集群时默认生成子域名 2023-11-27 11:28:01 +08:00
刘祥超
0d230a9237 <domain-box>组件自动分割单个域名 2023-11-27 10:31:41 +08:00
刘祥超
8453d754f1 添加域名时自动分割用逗号、顿号连接的域名列表 2023-11-27 10:20:54 +08:00
刘祥超
05f77f7a0d 修复go.mod 2023-11-24 10:21:07 +08:00
刘祥超
4334b6f148 修复若干测试用例 2023-11-24 09:13:18 +08:00
刘祥超
72955759d7 更新go.sum 2023-11-24 08:53:40 +08:00
刘祥超
18dc52996d 更新Dockerfile中版本 2023-11-23 17:40:28 +08:00
163 changed files with 25964 additions and 675 deletions

View File

@@ -11,6 +11,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/nodes"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/node/nodeutils"
_ "github.com/TeaOSLab/EdgeCommon/pkg/langs/messages"
"github.com/iwind/TeaGo/Tea"
_ "github.com/iwind/TeaGo/bootstrap"
@@ -40,7 +41,8 @@ func main() {
Option("demo", "switch to demo mode").
Option("dev", "switch to 'dev' mode").
Option("prod", "switch to 'prod' mode").
Option("upgrade [--url=URL]", "upgrade from official site or an url")
Option("upgrade [--url=URL]", "upgrade from official site or an url").
Option("install-local-node", "install a local node")
app.On("daemon", func() {
nodes.NewAdminNode().Daemon()
@@ -76,7 +78,7 @@ func main() {
fmt.Println("done")
})
app.On("recover", func() {
sock := gosock.NewTmpSock(teaconst.ProcessName)
var sock = gosock.NewTmpSock(teaconst.ProcessName)
if !sock.IsListening() {
fmt.Println("[ERROR]the service not started yet, you should start the service first")
return
@@ -89,7 +91,7 @@ func main() {
fmt.Println("enter recovery mode successfully")
})
app.On("demo", func() {
sock := gosock.NewTmpSock(teaconst.ProcessName)
var sock = gosock.NewTmpSock(teaconst.ProcessName)
if !sock.IsListening() {
fmt.Println("[ERROR]the service not started yet, you should start the service first")
return
@@ -178,6 +180,14 @@ func main() {
log.Println("restarting ...")
app.RunRestart()
})
app.On("install-local-node", func() {
err := nodeutils.InstallLocalNode()
if err != nil {
fmt.Println("[ERROR]" + err.Error())
return
}
fmt.Println("success")
})
app.Run(func() {
var adminNode = nodes.NewAdminNode()
adminNode.Run()

View File

@@ -1,7 +1,7 @@
FROM --platform=linux/amd64 alpine:latest
LABEL maintainer="goedge.cdn@gmail.com"
ENV TZ "Asia/Shanghai"
ENV VERSION 1.3.0
ENV VERSION 1.3.3
ENV ROOT_DIR /usr/local/goedge
ENV TAR_FILE edge-admin-linux-amd64-plus-v${VERSION}.zip

29
go.mod
View File

@@ -8,16 +8,16 @@ require (
github.com/TeaOSLab/EdgeCommon v0.0.0-00010101000000-000000000000
github.com/cespare/xxhash v1.1.0
github.com/go-sql-driver/mysql v1.5.0
github.com/iwind/TeaGo v0.0.0-20230623080147-cd1e53b4915f
github.com/iwind/TeaGo v0.0.0-20231214114820-1bfb0013bcf6
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4
github.com/miekg/dns v1.1.43
github.com/quic-go/quic-go v0.36.0
github.com/quic-go/quic-go v0.40.0
github.com/shirou/gopsutil/v3 v3.22.5
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tealeg/xlsx/v3 v3.2.3
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119
golang.org/x/crypto v0.10.0
golang.org/x/sys v0.9.0
golang.org/x/crypto v0.16.0
golang.org/x/sys v0.15.0
google.golang.org/grpc v1.45.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -26,29 +26,28 @@ require (
github.com/frankban/quicktest v1.11.3 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.0.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20231203200248-ad67f76aa53d // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/onsi/ginkgo/v2 v2.11.0 // indirect
github.com/onsi/ginkgo/v2 v2.13.2 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/rogpeppe/fastuuid v1.2.0 // indirect
github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa // indirect
github.com/tdewolff/minify/v2 v2.12.7 // indirect
github.com/tdewolff/parse/v2 v2.6.6 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/tools v0.10.0 // indirect
go.uber.org/mock v0.3.0 // indirect
golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.16.0 // indirect
google.golang.org/genproto v0.0.0-20220317150908-0efb43f6373e // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect

69
go.sum
View File

@@ -41,7 +41,7 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-redis/redis/v8 v8.0.0-beta.7/go.mod h1:FGJAWDWFht1sQ4qxyJHZZbVyvnVcKQN0E3u5/5lRz+g=
@@ -51,8 +51,6 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
@@ -79,17 +77,19 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
github.com/google/pprof v0.0.0-20231203200248-ad67f76aa53d h1:bM3Cipp7U7Sdn0DpYngVaLWFV9yonxQ1IB7ZPQ41OLw=
github.com/google/pprof v0.0.0-20231203200248-ad67f76aa53d/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iwind/TeaGo v0.0.0-20210411134150-ddf57e240c2f/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/iwind/TeaGo v0.0.0-20230623080147-cd1e53b4915f h1:xo6XmXLtveKcwcZAXV6VMxkWNzy/2dStfHEnyowsGAE=
github.com/iwind/TeaGo v0.0.0-20230623080147-cd1e53b4915f/go.mod h1:fi/Pq+/5m2HZoseM+39dMF57ANXRt6w4PkGu3NXPc5s=
github.com/iwind/TeaGo v0.0.0-20231214114820-1bfb0013bcf6 h1:l8MW552d+gLzRd+MAtM/2X+80n/uS5nJAlBTYWHroRI=
github.com/iwind/TeaGo v0.0.0-20231214114820-1bfb0013bcf6/go.mod h1:Ng3xWekHSVy0E/6/jYqJ7Htydm/H+mWIl0AS+Eg3H2M=
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4 h1:VWGsCqTzObdlbf7UUE3oceIpcEKi4C/YBUszQXk118A=
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4/go.mod h1:H5Q7SXwbx3a97ecJkaS2sD77gspzE7HFUafBO0peEyA=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -112,12 +112,12 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
@@ -130,12 +130,10 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:Om
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/quic-go v0.36.0 h1:JIrO7p7Ug6hssFcARjWDiqS2RAKJHCiwPxBAA989rbI=
github.com/quic-go/quic-go v0.36.0/go.mod h1:zPetvwDlILVxt15n3hr3Gf/I3mDf7LpLKPhR4Ez0AZQ=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw=
github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa h1:2cO3RojjYl3hVTbEvJVqrMaFmORhL6O06qdW42toftk=
@@ -167,21 +165,22 @@ github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYa
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119 h1:YyPWX3jLOtYKulBR6AScGIs74lLrJcgeKRwcbAuQOG4=
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119/go.mod h1:/nuTSlK+okRfR/vnIPqR89fFKonnWPiZymN5ydRJkX8=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/otel v0.7.0/go.mod h1:aZMyHG5TqDOXEgH2tyLiXSUKly1jT3yqE9PmrzIeCdo=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -190,9 +189,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -206,8 +204,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -215,7 +213,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -237,27 +235,24 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -174,6 +174,17 @@ func FindAdminLang(adminId int64) string {
return ""
}
// UpdateAdminLang 修改某个管理员选择的语言
func UpdateAdminLang(adminId int64, langCode string) {
locker.Lock()
defer locker.Unlock()
list, ok := sharedAdminModuleMapping[adminId]
if ok {
list.Lang = langCode
}
}
func FindAdminLangForAction(actionPtr actions.ActionWrapper) (langCode langs.LangCode) {
locker.Lock()
defer locker.Unlock()

View File

@@ -1,9 +1,9 @@
package teaconst
const (
Version = "1.3.1"
Version = "1.3.3"
APINodeVersion = "1.3.1"
APINodeVersion = "1.3.3"
ProductName = "Edge Admin"
ProcessName = "edge-admin"

View File

@@ -14,6 +14,7 @@ import (
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/files"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"io"
"os"
"path/filepath"
@@ -116,6 +117,24 @@ func generateComponentsJSFile() error {
buffer.Write([]byte{';', '\n', '\n'})
}
// WAF checkpoints
var wafCheckpointsMaps = []maps.Map{}
for _, checkpoint := range firewallconfigs.AllCheckpoints {
wafCheckpointsMaps = append(wafCheckpointsMaps, maps.Map{
"name": checkpoint.Name,
"prefix": checkpoint.Prefix,
"description": checkpoint.Description,
})
}
wafCheckpointsJSON, err := json.Marshal(wafCheckpointsMaps)
if err != nil {
logs.Println("ComponentsAction marshal waf rule checkpoints failed: " + err.Error())
} else {
buffer.WriteString("window.WAF_RULE_CHECKPOINTS = ")
buffer.Write(wafCheckpointsJSON)
buffer.Write([]byte{';', '\n', '\n'})
}
// WAF操作符
wafOperatorsJSON, err := json.Marshal(firewallconfigs.AllRuleOperators)
if err != nil {

View File

@@ -34,14 +34,9 @@ func TestRPCClient_NodeRPC(t *testing.T) {
func TestRPC_Dial_HTTP(t *testing.T) {
client, err := NewRPCClient(&configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
}{
Endpoints: []string{"http://127.0.0.1:8004"},
},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
RPCEndpoints: []string{"https://127.0.0.1:8003"},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
}, true)
if err != nil {
t.Fatal(err)
@@ -56,14 +51,9 @@ func TestRPC_Dial_HTTP(t *testing.T) {
func TestRPC_Dial_HTTP_2(t *testing.T) {
client, err := NewRPCClient(&configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
}{
Endpoints: []string{"https://127.0.0.1:8003"},
},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
RPCEndpoints: []string{"https://127.0.0.1:8003"},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
}, true)
if err != nil {
t.Fatal(err)
@@ -78,14 +68,9 @@ func TestRPC_Dial_HTTP_2(t *testing.T) {
func TestRPC_Dial_HTTPS(t *testing.T) {
client, err := NewRPCClient(&configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
}{
Endpoints: []string{"https://127.0.0.1:8004"},
},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
RPCEndpoints: []string{"https://127.0.0.1:8004"},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
}, true)
if err != nil {
t.Fatal(err)

162
internal/utils/exec/cmd.go Normal file
View File

@@ -0,0 +1,162 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package executils
import (
"bytes"
"context"
"os"
"os/exec"
"strings"
"time"
)
type Cmd struct {
name string
args []string
env []string
dir string
ctx context.Context
timeout time.Duration
cancelFunc func()
captureStdout bool
captureStderr bool
stdout *bytes.Buffer
stderr *bytes.Buffer
rawCmd *exec.Cmd
}
func NewCmd(name string, args ...string) *Cmd {
return &Cmd{
name: name,
args: args,
}
}
func NewTimeoutCmd(timeout time.Duration, name string, args ...string) *Cmd {
return (&Cmd{
name: name,
args: args,
}).WithTimeout(timeout)
}
func (this *Cmd) WithTimeout(timeout time.Duration) *Cmd {
this.timeout = timeout
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
this.ctx = ctx
this.cancelFunc = cancelFunc
return this
}
func (this *Cmd) WithStdout() *Cmd {
this.captureStdout = true
return this
}
func (this *Cmd) WithStderr() *Cmd {
this.captureStderr = true
return this
}
func (this *Cmd) WithEnv(env []string) *Cmd {
this.env = env
return this
}
func (this *Cmd) WithDir(dir string) *Cmd {
this.dir = dir
return this
}
func (this *Cmd) Start() error {
var cmd = this.compose()
return cmd.Start()
}
func (this *Cmd) Wait() error {
var cmd = this.compose()
return cmd.Wait()
}
func (this *Cmd) Run() error {
if this.cancelFunc != nil {
defer this.cancelFunc()
}
var cmd = this.compose()
return cmd.Run()
}
func (this *Cmd) RawStdout() string {
if this.stdout != nil {
return this.stdout.String()
}
return ""
}
func (this *Cmd) Stdout() string {
return strings.TrimSpace(this.RawStdout())
}
func (this *Cmd) RawStderr() string {
if this.stderr != nil {
return this.stderr.String()
}
return ""
}
func (this *Cmd) Stderr() string {
return strings.TrimSpace(this.RawStderr())
}
func (this *Cmd) String() string {
if this.rawCmd != nil {
return this.rawCmd.String()
}
var newCmd = exec.Command(this.name, this.args...)
return newCmd.String()
}
func (this *Cmd) Process() *os.Process {
if this.rawCmd != nil {
return this.rawCmd.Process
}
return nil
}
func (this *Cmd) compose() *exec.Cmd {
if this.rawCmd != nil {
return this.rawCmd
}
if this.ctx != nil {
this.rawCmd = exec.CommandContext(this.ctx, this.name, this.args...)
} else {
this.rawCmd = exec.Command(this.name, this.args...)
}
if this.env != nil {
this.rawCmd.Env = this.env
}
if len(this.dir) > 0 {
this.rawCmd.Dir = this.dir
}
if this.captureStdout {
this.stdout = &bytes.Buffer{}
this.rawCmd.Stdout = this.stdout
}
if this.captureStderr {
this.stderr = &bytes.Buffer{}
this.rawCmd.Stderr = this.stderr
}
return this.rawCmd
}

View File

@@ -0,0 +1,61 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package executils_test
import (
executils "github.com/TeaOSLab/EdgeAdmin/internal/utils/exec"
"testing"
"time"
)
func TestNewTimeoutCmd_Sleep(t *testing.T) {
var cmd = executils.NewTimeoutCmd(1*time.Second, "sleep", "3")
cmd.WithStdout()
cmd.WithStderr()
err := cmd.Run()
t.Log("error:", err)
t.Log("stdout:", cmd.Stdout())
t.Log("stderr:", cmd.Stderr())
}
func TestNewTimeoutCmd_Echo(t *testing.T) {
var cmd = executils.NewTimeoutCmd(10*time.Second, "echo", "-n", "hello")
cmd.WithStdout()
cmd.WithStderr()
err := cmd.Run()
t.Log("error:", err)
t.Log("stdout:", cmd.Stdout())
t.Log("stderr:", cmd.Stderr())
}
func TestNewTimeoutCmd_Echo2(t *testing.T) {
var cmd = executils.NewCmd("echo", "hello")
cmd.WithStdout()
cmd.WithStderr()
err := cmd.Run()
t.Log("error:", err)
t.Log("stdout:", cmd.Stdout())
t.Log("raw stdout:", cmd.RawStdout())
t.Log("stderr:", cmd.Stderr())
t.Log("raw stderr:", cmd.RawStderr())
}
func TestNewTimeoutCmd_Echo3(t *testing.T) {
var cmd = executils.NewCmd("echo", "-n", "hello")
err := cmd.Run()
t.Log("error:", err)
t.Log("stdout:", cmd.Stdout())
t.Log("stderr:", cmd.Stderr())
}
func TestCmd_Process(t *testing.T) {
var cmd = executils.NewCmd("echo", "-n", "hello")
err := cmd.Run()
t.Log("error:", err)
t.Log(cmd.Process())
}
func TestNewTimeoutCmd_String(t *testing.T) {
var cmd = executils.NewCmd("echo", "-n", "hello")
t.Log("stdout:", cmd.String())
}

View File

@@ -0,0 +1,58 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build linux
package executils
import (
"golang.org/x/sys/unix"
"io/fs"
"os"
"os/exec"
"syscall"
)
// LookPath customize our LookPath() function, to work in broken $PATH environment variable
func LookPath(file string) (string, error) {
result, err := exec.LookPath(file)
if err == nil && len(result) > 0 {
return result, nil
}
// add common dirs contains executable files these may be excluded in $PATH environment variable
var binPaths = []string{
"/usr/sbin",
"/usr/bin",
"/usr/local/sbin",
"/usr/local/bin",
}
for _, binPath := range binPaths {
var fullPath = binPath + string(os.PathSeparator) + file
stat, err := os.Stat(fullPath)
if err != nil {
continue
}
if stat.IsDir() {
return "", syscall.EISDIR
}
var mode = stat.Mode()
if mode.IsDir() {
return "", syscall.EISDIR
}
err = syscall.Faccessat(unix.AT_FDCWD, fullPath, unix.X_OK, unix.AT_EACCESS)
if err == nil || (err != syscall.ENOSYS && err != syscall.EPERM) {
return fullPath, err
}
if mode&0111 != 0 {
return fullPath, nil
}
return "", fs.ErrPermission
}
return "", &exec.Error{
Name: file,
Err: exec.ErrNotFound,
}
}

View File

@@ -0,0 +1,10 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build !linux
package executils
import "os/exec"
func LookPath(file string) (string, error) {
return exec.LookPath(file)
}

View File

@@ -3,6 +3,7 @@
package utils
import (
executils "github.com/TeaOSLab/EdgeAdmin/internal/utils/exec"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/types"
"os/exec"
@@ -14,7 +15,7 @@ func AddPortsToFirewall(ports []int) {
// Linux
if runtime.GOOS == "linux" {
// firewalld
firewallCmd, _ := exec.LookPath("firewall-cmd")
firewallCmd, _ := executils.LookPath("firewall-cmd")
if len(firewallCmd) > 0 {
err := exec.Command(firewallCmd, "--add-port="+types.String(port)+"/tcp").Run()
if err == nil {

View File

@@ -3,8 +3,10 @@ package utils
import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/logs"
"github.com/miekg/dns"
"sync"
)
var sharedDNSClient *dns.Client
@@ -33,17 +35,47 @@ func LookupCNAME(host string) (string, error) {
m.RecursionDesired = true
var lastErr error
for _, serverAddr := range sharedDNSConfig.Servers {
r, _, err := sharedDNSClient.Exchange(m, configutils.QuoteIP(serverAddr)+":"+sharedDNSConfig.Port)
if err != nil {
lastErr = err
continue
}
if len(r.Answer) == 0 {
continue
}
var success = false
var result = ""
return r.Answer[0].(*dns.CNAME).Target, nil
var serverAddrs = sharedDNSConfig.Servers
{
var publicDNSHosts = []string{"8.8.8.8" /** Google **/, "8.8.4.4" /** Google **/}
for _, publicDNSHost := range publicDNSHosts {
if !lists.ContainsString(serverAddrs, publicDNSHost) {
serverAddrs = append(serverAddrs, publicDNSHost)
}
}
}
var wg = &sync.WaitGroup{}
for _, serverAddr := range serverAddrs {
wg.Add(1)
go func(serverAddr string) {
defer wg.Done()
r, _, err := sharedDNSClient.Exchange(m, configutils.QuoteIP(serverAddr)+":"+sharedDNSConfig.Port)
if err != nil {
lastErr = err
return
}
success = true
if len(r.Answer) == 0 {
return
}
result = r.Answer[0].(*dns.CNAME).Target
}(serverAddr)
}
wg.Wait()
if success {
return result, nil
}
return "", lastErr
}

View File

@@ -8,5 +8,8 @@ import (
)
func TestLookupCNAME(t *testing.T) {
t.Log(utils.LookupCNAME("www.yun4s.cn"))
for _, domain := range []string{"www.yun4s.cn", "example.com"} {
result, err := utils.LookupCNAME(domain)
t.Log(domain, "=>", result, err)
}
}

View File

@@ -30,17 +30,17 @@ func FormatBytes(bytes int64) string {
if bytes < Pow1024(1) {
return FormatInt64(bytes) + "B"
} else if bytes < Pow1024(2) {
return TrimZeroSuffix(fmt.Sprintf("%.2fKB", float64(bytes)/float64(Pow1024(1))))
return TrimZeroSuffix(fmt.Sprintf("%.2fKiB", float64(bytes)/float64(Pow1024(1))))
} else if bytes < Pow1024(3) {
return TrimZeroSuffix(fmt.Sprintf("%.2fMB", float64(bytes)/float64(Pow1024(2))))
return TrimZeroSuffix(fmt.Sprintf("%.2fMiB", float64(bytes)/float64(Pow1024(2))))
} else if bytes < Pow1024(4) {
return TrimZeroSuffix(fmt.Sprintf("%.2fGB", float64(bytes)/float64(Pow1024(3))))
return TrimZeroSuffix(fmt.Sprintf("%.2fGiB", float64(bytes)/float64(Pow1024(3))))
} else if bytes < Pow1024(5) {
return TrimZeroSuffix(fmt.Sprintf("%.2fTB", float64(bytes)/float64(Pow1024(4))))
return TrimZeroSuffix(fmt.Sprintf("%.2fTiB", float64(bytes)/float64(Pow1024(4))))
} else if bytes < Pow1024(6) {
return TrimZeroSuffix(fmt.Sprintf("%.2fPB", float64(bytes)/float64(Pow1024(5))))
return TrimZeroSuffix(fmt.Sprintf("%.2fPiB", float64(bytes)/float64(Pow1024(5))))
} else {
return TrimZeroSuffix(fmt.Sprintf("%.2fEB", float64(bytes)/float64(Pow1024(6))))
return TrimZeroSuffix(fmt.Sprintf("%.2fEiB", float64(bytes)/float64(Pow1024(6))))
}
}

View File

@@ -5,6 +5,7 @@ package utils
import (
"errors"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
executils "github.com/TeaOSLab/EdgeAdmin/internal/utils/exec"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/files"
"os"
@@ -21,7 +22,7 @@ func (this *ServiceManager) Install(exePath string, args []string) error {
return errors.New("only root users can install the service")
}
systemd, err := exec.LookPath("systemctl")
systemd, err := executils.LookPath("systemctl")
if err != nil {
return this.installInitService(exePath, args)
}
@@ -36,7 +37,7 @@ func (this *ServiceManager) Start() error {
}
if files.NewFile(systemdServiceFile).Exists() {
systemd, err := exec.LookPath("systemctl")
systemd, err := executils.LookPath("systemctl")
if err != nil {
return err
}
@@ -53,7 +54,7 @@ func (this *ServiceManager) Uninstall() error {
}
if files.NewFile(systemdServiceFile).Exists() {
systemd, err := exec.LookPath("systemctl")
systemd, err := executils.LookPath("systemctl")
if err != nil {
return err
}
@@ -93,7 +94,7 @@ func (this *ServiceManager) installInitService(exePath string, args []string) er
return err
}
chkCmd, err := exec.LookPath("chkconfig")
chkCmd, err := executils.LookPath("chkconfig")
if err != nil {
return err
}

View File

@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
executils "github.com/TeaOSLab/EdgeAdmin/internal/utils/exec"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
@@ -87,10 +88,10 @@ func (this *UpgradeManager) Start() error {
}()
// 检查unzip
unzipExe, _ := exec.LookPath("unzip")
unzipExe, _ := executils.LookPath("unzip")
// 检查cp
cpExe, _ := exec.LookPath("cp")
cpExe, _ := executils.LookPath("cp")
if len(cpExe) == 0 {
return errors.New("can not find 'cp' command")
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/index/loginutils"
"github.com/TeaOSLab/EdgeCommon/pkg/langs"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
@@ -106,7 +107,7 @@ func (this *ParentAction) CreateLog(level string, messageCode langs.MessageCode,
}
}
}
err := dao.SharedLogDAO.CreateAdminLog(this.AdminContext(), level, this.Request.URL.Path, desc, this.RequestRemoteIP(), messageCode, args)
err := dao.SharedLogDAO.CreateAdminLog(this.AdminContext(), level, this.Request.URL.Path, desc, loginutils.RemoteIP(&this.ActionObject), messageCode, args)
if err != nil {
utils.PrintError(err)
}

View File

@@ -49,6 +49,7 @@ func init() {
Post("/start", new(node.StartAction)).
Post("/stop", new(node.StopAction)).
Post("/up", new(node.UpAction)).
Post("/updateIsOn", new(node.UpdateIsOnAction)).
Get("/detail", new(node.DetailAction)).
GetPost("/updateDNSPopup", new(node.UpdateDNSPopupAction)).
Post("/syncDomain", new(node.SyncDomainAction)).

View File

@@ -4,12 +4,23 @@ package nodeutils
import (
"errors"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
executils "github.com/TeaOSLab/EdgeAdmin/internal/utils/exec"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"gopkg.in/yaml.v3"
"os"
"runtime"
"strconv"
"time"
)
// InitNodeInfo 初始化节点信息
@@ -104,3 +115,124 @@ func InitNodeInfo(parentAction *actionutils.ParentAction, nodeId int64) (*pb.Nod
return nodeResp.Node, nil
}
// InstallLocalNode 安装本地节点
func InstallLocalNode() error {
var targetDir = Tea.Root
var nodeDir = targetDir + "/edge-node"
var apiAddr = "http://127.0.0.1:8001" // 先固定
// 查找节点安装文件
var zipFile = Tea.Root + "/edge-api/deploy/edge-node-linux-" + runtime.GOARCH + "-v" + teaconst.Version /** 默认和管理系统一致 **/ + ".zip"
{
stat, err := os.Stat(zipFile)
if err != nil {
if os.IsNotExist(err) {
return errors.New("installer file not found in '" + zipFile + "'")
}
return fmt.Errorf("open installer file failed: %w", err)
}
if stat.IsDir() {
return errors.New("invalid installer file '" + zipFile + "'")
}
}
// 解压节点
var unzip = utils.NewUnzip(zipFile, targetDir)
err := unzip.Run()
if err != nil {
return fmt.Errorf("unzip installer file failed: %w", err)
}
// 创建节点
rpcClient, err := rpc.SharedRPC()
if err != nil {
return fmt.Errorf("create rpc client failed: %w", err)
}
var ctx = rpcClient.Context(0)
nodeClustersResp, err := rpcClient.NodeClusterRPC().ListEnabledNodeClusters(ctx, &pb.ListEnabledNodeClustersRequest{
IdDesc: true,
Offset: 0,
Size: 1,
})
if err != nil {
return err
}
if len(nodeClustersResp.NodeClusters) == 0 {
return errors.New("no clusters yet, please create a cluster at least")
}
var clusterId = nodeClustersResp.NodeClusters[0].Id
// 检查节点是否已生成
countNodesResp, err := rpcClient.NodeRPC().CountAllEnabledNodesMatch(ctx, &pb.CountAllEnabledNodesMatchRequest{
NodeClusterId: clusterId,
})
if err != nil {
return err
}
if countNodesResp.Count > 0 {
// 这里先不判断是否有本地节点,只要有节点,就不允许再次执行
return errors.New("there are already nodes in the cluster")
}
createNodeResp, err := rpcClient.NodeRPC().CreateNode(ctx, &pb.CreateNodeRequest{
Name: "本地节点",
NodeClusterId: clusterId,
NodeLogin: nil,
NodeGroupId: 0,
DnsRoutes: nil,
NodeRegionId: 0,
})
if err != nil {
return err
}
var nodeId = createNodeResp.NodeId
nodeResp, err := rpcClient.NodeRPC().FindEnabledNode(ctx, &pb.FindEnabledNodeRequest{NodeId: nodeId})
if err != nil {
return err
}
if nodeResp.Node == nil {
return errors.New("could not find local node with created id '" + types.String(nodeId) + "'")
}
var node = nodeResp.Node
// 生成节点配置
var apiConfig = &configs.APIConfig{
RPCEndpoints: []string{apiAddr},
RPCDisableUpdate: true,
NodeId: node.UniqueId,
Secret: node.Secret,
}
apiConfigYAML, err := yaml.Marshal(apiConfig)
if err != nil {
return fmt.Errorf("encode config failed: %w", err)
}
err = os.WriteFile(nodeDir+"/configs/api_node.yaml", apiConfigYAML, 0666)
if err != nil {
return fmt.Errorf("write config file failed: %w", err)
}
// 测试节点
{
var cmd = executils.NewTimeoutCmd(5*time.Second, nodeDir+"/bin/edge-node", "test")
cmd.WithStdout()
cmd.WithStderr()
err = cmd.Run()
if err != nil {
return fmt.Errorf("node test failed: %w", err)
}
}
// 启动节点
{
var cmd = executils.NewTimeoutCmd(5*time.Second, nodeDir+"/bin/edge-node", "start")
cmd.WithStdout()
cmd.WithStderr()
err = cmd.Run()
if err != nil {
return fmt.Errorf("node start failed: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,16 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package nodeutils_test
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/node/nodeutils"
_ "github.com/iwind/TeaGo/bootstrap"
"testing"
)
func TestInstallLocalNode(t *testing.T) {
err := nodeutils.InstallLocalNode()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,35 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type UpdateIsOnAction struct {
actionutils.ParentAction
}
func (this *UpdateIsOnAction) RunPost(params struct {
NodeId int64
IsOn bool
}) {
if params.IsOn {
defer this.CreateLogInfo(codes.Node_LogUpdateNodeOn, params.NodeId)
} else {
defer this.CreateLogInfo(codes.Node_LogUpdateNodeOff, params.NodeId)
}
_, err := this.RPC().NodeRPC().UpdateNodeIsOn(this.AdminContext(), &pb.UpdateNodeIsOnRequest{
NodeId: params.NodeId,
IsOn: params.IsOn,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -45,6 +45,7 @@ func (this *IndexAction) RunGet(params struct {
func (this *IndexAction) RunPost(params struct {
ClusterId int64
IsOn bool
Quality int
RequireCache bool
MinLengthJSON []byte
MaxLengthJSON []byte
@@ -59,6 +60,13 @@ func (this *IndexAction) RunPost(params struct {
RequireCache: params.RequireCache,
}
if params.Quality < 0 {
params.Quality = 0
} else if params.Quality > 100 {
params.Quality = 100
}
config.Quality = params.Quality
if len(params.MinLengthJSON) > 0 {
var minLength = &shared.SizeCapacity{}
err := json.Unmarshal(params.MinLengthJSON, minLength)
@@ -79,6 +87,11 @@ func (this *IndexAction) RunPost(params struct {
config.MaxLength = maxLength
}
err := config.Init()
if err != nil {
this.Fail("配置校验失败:" + err.Error())
}
configJSON, err := json.Marshal(config)
if err != nil {
this.ErrorPage(err)
@@ -87,7 +100,7 @@ func (this *IndexAction) RunPost(params struct {
_, err = this.RPC().NodeClusterRPC().UpdateNodeClusterWebPPolicy(this.AdminContext(), &pb.UpdateNodeClusterWebPPolicyRequest{
NodeClusterId: params.ClusterId,
WebpPolicyJSON: configJSON,
WebPPolicyJSON: configJSON,
})
if err != nil {

View File

@@ -180,7 +180,7 @@ func (this *ClusterHelper) createSettingMenu(cluster *pb.NodeCluster, info *pb.F
"name": this.Lang(actionPtr, codes.NodeClusterMenu_SettingWebP),
"url": "/clusters/cluster/settings/webp?clusterId=" + clusterId,
"isActive": selectedItem == "webp",
"isOn": info != nil && info.WebpIsOn,
"isOn": info != nil && info.WebPIsOn,
})
items = this.filterMenuItems1(items, info, clusterId, selectedItem, actionPtr)

View File

@@ -1,7 +1,9 @@
package clusters
import (
"crypto/rand"
"encoding/json"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dns/domains/domainutils"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
@@ -9,6 +11,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/rands"
)
type CreateAction struct {
@@ -44,6 +47,18 @@ func (this *CreateAction) RunGet(params struct{}) {
}
this.Data["totalNodes"] = totalNodesResp.Count
// 随机子域名
var defaultDNSName = "g" + rands.HexString(6) + ".cdn"
{
var b = make([]byte, 3)
_, err = rand.Read(b)
if err == nil {
defaultDNSName = fmt.Sprintf("g%x.cdn", b)
}
}
this.Data["defaultDNSName"] = defaultDNSName
this.Data["dnsName"] = defaultDNSName
this.Show()
}

View File

@@ -175,7 +175,7 @@ func (this *IndexAction) RunPost(params struct {
})
if err != nil {
err = dao.SharedLogDAO.CreateAdminLog(rpcClient.Context(0), oplogs.LevelError, this.Request.URL.Path, langs.DefaultMessage(codes.AdminLogin_LogSystemError, err.Error()), this.RequestRemoteIP(), codes.AdminLogin_LogSystemError, []any{err.Error()})
err = dao.SharedLogDAO.CreateAdminLog(rpcClient.Context(0), oplogs.LevelError, this.Request.URL.Path, langs.DefaultMessage(codes.AdminLogin_LogSystemError, err.Error()), loginutils.RemoteIP(&this.ActionObject), codes.AdminLogin_LogSystemError, []any{err.Error()})
if err != nil {
utils.PrintError(err)
}
@@ -185,7 +185,7 @@ func (this *IndexAction) RunPost(params struct {
}
if !resp.IsOk {
err = dao.SharedLogDAO.CreateAdminLog(rpcClient.Context(0), oplogs.LevelWarn, this.Request.URL.Path, langs.DefaultMessage(codes.AdminLogin_LogFailed, params.Username), this.RequestRemoteIP(), codes.AdminLogin_LogFailed, []any{params.Username})
err = dao.SharedLogDAO.CreateAdminLog(rpcClient.Context(0), oplogs.LevelWarn, this.Request.URL.Path, langs.DefaultMessage(codes.AdminLogin_LogFailed, params.Username), loginutils.RemoteIP(&this.ActionObject), codes.AdminLogin_LogFailed, []any{params.Username})
if err != nil {
utils.PrintError(err)
}
@@ -225,7 +225,7 @@ func (this *IndexAction) RunPost(params struct {
params.Auth.StoreAdmin(adminId, params.Remember)
// 记录日志
err = dao.SharedLogDAO.CreateAdminLog(rpcClient.Context(adminId), oplogs.LevelInfo, this.Request.URL.Path, langs.DefaultMessage(codes.AdminLogin_LogSuccess, params.Username), this.RequestRemoteIP(), codes.AdminLogin_LogSuccess, []any{params.Username})
err = dao.SharedLogDAO.CreateAdminLog(rpcClient.Context(adminId), oplogs.LevelInfo, this.Request.URL.Path, langs.DefaultMessage(codes.AdminLogin_LogSuccess, params.Username), loginutils.RemoteIP(&this.ActionObject), codes.AdminLogin_LogSuccess, []any{params.Username})
if err != nil {
utils.PrintError(err)
}
@@ -235,7 +235,7 @@ func (this *IndexAction) RunPost(params struct {
// 检查登录区域
func (this *IndexAction) checkRegion() bool {
var ip = this.RequestRemoteIP()
var ip = loginutils.RemoteIP(&this.ActionObject)
var result = iplibrary.LookupIP(ip)
if result != nil && result.IsOk() && result.CountryId() > 0 && lists.ContainsInt64([]int64{9, 10}, result.CountryId()) {
return false

View File

@@ -3,12 +3,15 @@
package loginutils
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeCommon/pkg/iplibrary"
"github.com/iwind/TeaGo/actions"
stringutil "github.com/iwind/TeaGo/utils/string"
"net"
"net/http"
"regexp"
"strings"
)
// CalculateClientFingerprint 计算客户端指纹
@@ -17,8 +20,26 @@ func CalculateClientFingerprint(action *actions.ActionObject) string {
}
// RemoteIP 获取客户端IP
// TODO 将来增加是否使用代理设置即从X-Real-IP中获取IP
func RemoteIP(action *actions.ActionObject) string {
securityConfig, _ := configloaders.LoadSecurityConfig()
if securityConfig != nil {
if len(securityConfig.ClientIPHeaderNames) > 0 {
var headerNames = regexp.MustCompile(`[,;\s]`).Split(securityConfig.ClientIPHeaderNames, -1)
for _, headerName := range headerNames {
headerName = http.CanonicalHeaderKey(strings.TrimSpace(headerName))
if len(headerName) == 0 {
continue
}
var ipValue = action.Request.Header.Get(headerName)
if net.ParseIP(ipValue) != nil {
return ipValue
}
}
}
}
ip, _, _ := net.SplitHostPort(action.Request.RemoteAddr)
return ip
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/setup"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/index/loginutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
@@ -146,7 +147,7 @@ func (this *OtpAction) RunPost(params struct {
this.ErrorPage(err)
return
}
err = dao.SharedLogDAO.CreateAdminLog(rpcClient.Context(adminId), oplogs.LevelInfo, this.Request.URL.Path, this.Lang(codes.AdminLogin_LogOtpVerifiedSuccess), this.RequestRemoteIP(), codes.AdminLogin_LogOtpVerifiedSuccess, nil)
err = dao.SharedLogDAO.CreateAdminLog(rpcClient.Context(adminId), oplogs.LevelInfo, this.Request.URL.Path, this.Lang(codes.AdminLogin_LogOtpVerifiedSuccess), loginutils.RemoteIP(&this.ActionObject), codes.AdminLogin_LogOtpVerifiedSuccess, nil)
if err != nil {
utils.PrintError(err)
}

View File

@@ -36,31 +36,43 @@ func (this *AddServerNamePopupAction) RunPost(params struct {
// 去除空格
serverName = regexp.MustCompile(`\s+`).ReplaceAllString(serverName, "")
// 处理URL
if regexp.MustCompile(`^(?i)(http|https|ftp)://`).MatchString(serverName) {
u, err := url.Parse(serverName)
if err == nil && len(u.Host) > 0 {
serverName = u.Host
// 是否包含了多个域名
var splitReg = regexp.MustCompile(`([,、|,;|])`)
if splitReg.MatchString(serverName) {
params.ServerNames = strings.Join(splitReg.Split(serverName, -1), "\n")
params.Mode = "multiple"
} else {
// 处理URL
if regexp.MustCompile(`^(?i)(http|https|ftp)://`).MatchString(serverName) {
u, err := url.Parse(serverName)
if err == nil && len(u.Host) > 0 {
serverName = u.Host
}
}
}
// 去除端口
if regexp.MustCompile(`:\d+$`).MatchString(serverName) {
host, _, err := net.SplitHostPort(serverName)
if err == nil && len(host) > 0 {
serverName = host
// 去除端口
if regexp.MustCompile(`:\d+$`).MatchString(serverName) {
host, _, err := net.SplitHostPort(serverName)
if err == nil && len(host) > 0 {
serverName = host
}
}
}
params.Must.
Field("serverName", serverName).
Require("请输入域名")
params.Must.
Field("serverName", serverName).
Require("请输入域名")
this.Data["serverName"] = maps.Map{
"name": serverName,
"type": "full",
this.Data["serverName"] = maps.Map{
"name": serverName,
"type": "full",
}
this.Success()
return
}
} else if params.Mode == "multiple" {
}
if params.Mode == "multiple" {
if len(params.ServerNames) == 0 {
this.FailField("serverNames", "请输入至少域名")
}
@@ -99,8 +111,6 @@ func (this *AddServerNamePopupAction) RunPost(params struct {
"type": "full",
"subNames": serverNames,
}
} else {
this.Fail("错误的mode参数")
}
this.Success()

View File

@@ -54,7 +54,7 @@ func (this *CreateSetPopupAction) RunGet(params struct {
}
// 所有可选的动作
actionMaps := []maps.Map{}
var actionMaps = []maps.Map{}
for _, action := range firewallconfigs.AllActions {
actionMaps = append(actionMaps, maps.Map{
"name": action.Name,
@@ -64,6 +64,9 @@ func (this *CreateSetPopupAction) RunGet(params struct {
}
this.Data["actions"] = actionMaps
// 是否为全局
this.Data["isGlobalPolicy"] = firewallPolicy.ServerId == 0
this.Show()
}

View File

@@ -31,8 +31,8 @@ func (this *IndexAction) RunGet(params struct {
this.ErrorPage(err)
return
}
count := countResp.Count
page := this.NewPage(count)
var count = countResp.Count
var page = this.NewPage(count)
listResp, err := this.RPC().HTTPFirewallPolicyRPC().ListEnabledHTTPFirewallPolicies(this.AdminContext(), &pb.ListEnabledHTTPFirewallPoliciesRequest{
NodeClusterId: params.ClusterId,
@@ -44,10 +44,10 @@ func (this *IndexAction) RunGet(params struct {
this.ErrorPage(err)
return
}
policyMaps := []maps.Map{}
var policyMaps = []maps.Map{}
for _, policy := range listResp.HttpFirewallPolicies {
countInbound := 0
countOutbound := 0
var countInbound = 0
var countOutbound = 0
if len(policy.InboundJSON) > 0 {
inboundConfig := &firewallconfigs.HTTPFirewallInboundConfig{}
err = json.Unmarshal(policy.InboundJSON, inboundConfig)
@@ -72,7 +72,7 @@ func (this *IndexAction) RunGet(params struct {
this.ErrorPage(err)
return
}
countClusters := countClustersResp.Count
var countClusters = countClustersResp.Count
// mode
if len(policy.Mode) == 0 {

View File

@@ -95,6 +95,7 @@ func (this *PolicyAction) RunGet(params struct {
"modeInfo": firewallconfigs.FindFirewallMode(firewallPolicy.Mode),
"groups": internalGroups,
"blockOptions": firewallPolicy.BlockOptions,
"pageOptions": firewallPolicy.PageOptions,
"captchaOptions": firewallPolicy.CaptchaOptions,
"useLocalFirewall": firewallPolicy.UseLocalFirewall,
"synFlood": firewallPolicy.SYNFlood,

View File

@@ -34,6 +34,7 @@ func (this *UpdateAction) RunGet(params struct {
return
}
// block options
if firewallPolicy.BlockOptions == nil {
firewallPolicy.BlockOptions = &firewallconfigs.HTTPFirewallBlockAction{
StatusCode: http.StatusForbidden,
@@ -43,6 +44,11 @@ func (this *UpdateAction) RunGet(params struct {
}
}
// page options
if firewallPolicy.PageOptions == nil {
firewallPolicy.PageOptions = firewallconfigs.DefaultHTTPFirewallPageAction()
}
// mode
if len(firewallPolicy.Mode) == 0 {
firewallPolicy.Mode = firewallconfigs.FirewallModeDefend
@@ -71,6 +77,7 @@ func (this *UpdateAction) RunGet(params struct {
"isOn": firewallPolicy.IsOn,
"mode": firewallPolicy.Mode,
"blockOptions": firewallPolicy.BlockOptions,
"pageOptions": firewallPolicy.PageOptions,
"captchaOptions": firewallPolicy.CaptchaOptions,
"useLocalFirewall": firewallPolicy.UseLocalFirewall,
"synFloodConfig": firewallPolicy.SYNFlood,
@@ -107,6 +114,7 @@ func (this *UpdateAction) RunPost(params struct {
Name string
GroupCodes []string
BlockOptionsJSON []byte
PageOptionsJSON []byte
CaptchaOptionsJSON []byte
Description string
IsOn bool
@@ -132,6 +140,19 @@ func (this *UpdateAction) RunPost(params struct {
err := json.Unmarshal(params.BlockOptionsJSON, blockOptions)
if err != nil {
this.Fail("拦截动作参数校验失败:" + err.Error())
return
}
// 校验显示页面选项JSON
var pageOptions = &firewallconfigs.HTTPFirewallPageAction{}
err = json.Unmarshal(params.PageOptionsJSON, pageOptions)
if err != nil {
this.Fail("校验显示页面动作配置失败:" + err.Error())
return
}
if pageOptions.Status < 100 && pageOptions.Status > 999 {
this.Fail("显示页面动作的状态码配置错误:" + types.String(pageOptions.Status))
return
}
// 校验验证码选项JSON
@@ -139,6 +160,24 @@ func (this *UpdateAction) RunPost(params struct {
err = json.Unmarshal(params.CaptchaOptionsJSON, captchaOptions)
if err != nil {
this.Fail("验证码动作参数校验失败:" + err.Error())
return
}
// 检查极验配置
if captchaOptions.CaptchaType == firewallconfigs.CaptchaTypeGeeTest || captchaOptions.GeeTestConfig.IsOn {
if captchaOptions.CaptchaType == firewallconfigs.CaptchaTypeGeeTest && !captchaOptions.GeeTestConfig.IsOn {
this.Fail("人机识别动作配置的默认验证方式为极验-行为验,所以需要选择允许用户使用极验")
return
}
if len(captchaOptions.GeeTestConfig.CaptchaId) == 0 {
this.FailField("geetestCaptchaId", "请输入极验-验证ID")
return
}
if len(captchaOptions.GeeTestConfig.CaptchaKey) == 0 {
this.FailField("geetestCaptchaKey", "请输入极验-验证Key")
return
}
}
// 最大内容尺寸
@@ -153,6 +192,7 @@ func (this *UpdateAction) RunPost(params struct {
Description: params.Description,
FirewallGroupCodes: params.GroupCodes,
BlockOptionsJSON: params.BlockOptionsJSON,
PageOptionsJSON: params.PageOptionsJSON,
CaptchaOptionsJSON: params.CaptchaOptionsJSON,
Mode: params.Mode,
UseLocalFirewall: params.UseLocalFirewall,

View File

@@ -0,0 +1,28 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package servers
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
// DeleteServersAction 删除一组网站
type DeleteServersAction struct {
actionutils.ParentAction
}
func (this *DeleteServersAction) RunPost(params struct {
ServerIds []int64
}) {
defer this.CreateLogInfo(codes.Server_LogDeleteServers)
_, err := this.RPC().ServerRPC().DeleteServers(this.AdminContext(), &pb.DeleteServersRequest{ServerIds: params.ServerIds})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -53,12 +53,16 @@ func (this *IndexAction) RunPost(params struct {
defer this.CreateLogInfo(codes.ServerCache_LogUpdateCacheSettings, params.WebId)
// 校验配置
cacheConfig := &serverconfigs.HTTPCacheConfig{}
var cacheConfig = &serverconfigs.HTTPCacheConfig{}
err := json.Unmarshal(params.CacheJSON, cacheConfig)
if err != nil {
this.ErrorPage(err)
return
}
// 分组不支持主域名
cacheConfig.Key = nil
err = cacheConfig.Init()
if err != nil {
this.Fail("检查配置失败:" + err.Error())

View File

@@ -19,6 +19,7 @@ func init() {
GetPost("/create", new(CreateAction)).
GetPost("/update", new(UpdateAction)).
Post("/nearby", new(NearbyAction)).
Post("/deleteServers", new(DeleteServersAction)).
//
GetPost("/addPortPopup", new(AddPortPopupAction)).

View File

@@ -0,0 +1,82 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package iplists
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/types"
"strings"
)
type DeleteCountAction struct {
actionutils.ParentAction
}
func (this *DeleteCountAction) RunPost(params struct {
Ip string
Keyword string
GlobalOnly bool
Unread bool
EventLevel string
ListType string
Count int64
}) {
var count = params.Count
if count <= 0 || count >= 100_000 {
this.Fail("'count' 参数错误")
return
}
itemIdsResp, err := this.RPC().IPItemRPC().ListAllIPItemIds(this.AdminContext(), &pb.ListAllIPItemIdsRequest{
Keyword: params.Keyword,
GlobalOnly: params.GlobalOnly,
Unread: params.Unread,
EventLevel: params.EventLevel,
ListType: params.ListType,
Ip: params.Ip,
Offset: 0,
Size: count,
})
if err != nil {
this.ErrorPage(err)
return
}
var itemIds = itemIdsResp.IpItemIds
if len(itemIds) == 0 {
this.Success()
}
// 记录日志
defer func() {
var itemIdStrings = []string{}
for _, itemId := range itemIds {
itemIdStrings = append(itemIdStrings, types.String(itemId))
}
var itemIdsDescription = ""
if len(itemIdStrings) > 10 {
itemIdsDescription = strings.Join(itemIdStrings[:10], ", ") + " ..."
} else {
itemIdsDescription = strings.Join(itemIdStrings, ", ")
}
this.CreateLogInfo(codes.IPList_LogDeleteIPBatch, itemIdsDescription)
}()
_, err = this.RPC().IPItemRPC().DeleteIPItems(this.AdminContext(), &pb.DeleteIPItemsRequest{IpItemIds: itemIds})
if err != nil {
this.ErrorPage(err)
return
}
// 通知左侧菜单Badge更新
helpers.NotifyIPItemsCountChanges()
this.Success()
}

View File

@@ -63,6 +63,7 @@ func (this *IndexAction) RunGet(params struct {
}
var count = countResp.Count
var page = this.NewPage(count)
this.Data["totalItems"] = count
this.Data["page"] = page.AsHTML()
itemsResp, err := this.RPC().IPItemRPC().ListAllEnabledIPItems(this.AdminContext(), &pb.ListAllEnabledIPItemsRequest{

View File

@@ -22,6 +22,7 @@ func init() {
Get("/exportData", new(ExportDataAction)).
Post("/delete", new(DeleteAction)).
Post("/deleteItems", new(DeleteItemsAction)).
Post("/deleteCount", new(DeleteCountAction)).
GetPost("/test", new(TestAction)).
GetPost("/update", new(UpdateAction)).
Get("/items", new(ItemsAction)).

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dns/domains/domainutils"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
@@ -12,6 +13,7 @@ import (
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"strings"
)
type IndexAction struct {
@@ -92,6 +94,48 @@ func (this *IndexAction) RunPost(params struct {
return
}
// 检查Key
if cacheConfig.Key != nil && cacheConfig.Key.IsOn {
if cacheConfig.Key.Scheme != "http" && cacheConfig.Key.Scheme != "https" {
this.Fail("缓存主域名协议只能是http或者https")
return
}
if len(cacheConfig.Key.Host) == 0 {
this.Fail("请输入缓存主域名")
return
}
cacheConfig.Key.Host = strings.ToLower(strings.TrimSuffix(cacheConfig.Key.Host, "/"))
if !domainutils.ValidateDomainFormat(cacheConfig.Key.Host) {
this.Fail("请输入正确的缓存主域名")
return
}
// 检查域名所属
serverIdResp, err := this.RPC().HTTPWebRPC().FindServerIdWithHTTPWebId(this.AdminContext(), &pb.FindServerIdWithHTTPWebIdRequest{HttpWebId: params.WebId})
if err != nil {
this.ErrorPage(err)
return
}
var serverId = serverIdResp.ServerId
if serverId <= 0 {
this.Fail("找不到要操作的网站")
return
}
existServerNameResp, err := this.RPC().ServerRPC().CheckServerNameInServer(this.AdminContext(), &pb.CheckServerNameInServerRequest{
ServerId: serverId,
ServerName: cacheConfig.Key.Host,
})
if err != nil {
this.ErrorPage(err)
return
}
if !existServerNameResp.Exists {
this.Fail("域名 '" + cacheConfig.Key.Host + "' 在当前网站中并未绑定,不能作为缓存主域名")
return
}
}
err = cacheConfig.Init()
if err != nil {
this.Fail("检查配置失败:" + err.Error())

View File

@@ -1,6 +1,7 @@
package settings
import (
"encoding/json"
"errors"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
@@ -32,7 +33,7 @@ func (this *IndexAction) RunGet(params struct {
this.ErrorPage(err)
return
}
clusterMaps := []maps.Map{}
var clusterMaps = []maps.Map{}
for _, cluster := range resp.NodeClusters {
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
@@ -50,7 +51,7 @@ func (this *IndexAction) RunGet(params struct {
this.ErrorPage(err)
return
}
server := serverResp.Server
var server = serverResp.Server
if server == nil {
this.NotFound("server", params.ServerId)
return
@@ -71,7 +72,7 @@ func (this *IndexAction) RunGet(params struct {
this.initUserPlan(server)
// 集群
clusterId := int64(0)
var clusterId = int64(0)
this.Data["clusterName"] = ""
if server.NodeCluster != nil {
clusterId = server.NodeCluster.Id
@@ -79,7 +80,7 @@ func (this *IndexAction) RunGet(params struct {
}
// 分组
groupMaps := []maps.Map{}
var groupMaps = []maps.Map{}
if len(server.ServerGroups) > 0 {
for _, group := range server.ServerGroups {
groupMaps = append(groupMaps, maps.Map{
@@ -89,23 +90,36 @@ func (this *IndexAction) RunGet(params struct {
}
}
this.Data["server"] = maps.Map{
"id": server.Id,
"clusterId": clusterId,
"type": server.Type,
"name": server.Name,
"description": server.Description,
"isOn": server.IsOn,
"groups": groupMaps,
// 域名和限流状态
var trafficLimitStatus *serverconfigs.TrafficLimitStatus
if len(server.Config) > 0 {
var serverConfig = &serverconfigs.ServerConfig{}
err = json.Unmarshal(server.Config, serverConfig)
if err == nil {
if serverConfig.TrafficLimitStatus != nil && serverConfig.TrafficLimitStatus.IsValid() {
trafficLimitStatus = serverConfig.TrafficLimitStatus
}
}
}
serverType := serverconfigs.FindServerType(server.Type)
this.Data["server"] = maps.Map{
"id": server.Id,
"clusterId": clusterId,
"type": server.Type,
"name": server.Name,
"description": server.Description,
"isOn": server.IsOn,
"groups": groupMaps,
"trafficLimitStatus": trafficLimitStatus,
}
var serverType = serverconfigs.FindServerType(server.Type)
if serverType == nil {
this.ErrorPage(errors.New("invalid server type '" + server.Type + "'"))
return
}
typeName := serverType.GetString("name")
var typeName = serverType.GetString("name")
this.Data["typeName"] = typeName
// 记录最近使用

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dns/domains/domainutils"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
@@ -11,6 +12,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"strings"
)
type IndexAction struct {
@@ -71,12 +73,55 @@ func (this *IndexAction) RunPost(params struct {
defer this.CreateLogInfo(codes.ServerCache_LogUpdateCacheSettings, params.WebId)
// 校验配置
cacheConfig := &serverconfigs.HTTPCacheConfig{}
var cacheConfig = &serverconfigs.HTTPCacheConfig{}
err := json.Unmarshal(params.CacheJSON, cacheConfig)
if err != nil {
this.ErrorPage(err)
return
}
// 检查Key
if cacheConfig.Key != nil && cacheConfig.Key.IsOn {
if cacheConfig.Key.Scheme != "http" && cacheConfig.Key.Scheme != "https" {
this.Fail("缓存主域名协议只能是http或者https")
return
}
if len(cacheConfig.Key.Host) == 0 {
this.Fail("请输入缓存主域名")
return
}
cacheConfig.Key.Host = strings.ToLower(strings.TrimSuffix(cacheConfig.Key.Host, "/"))
if !domainutils.ValidateDomainFormat(cacheConfig.Key.Host) {
this.Fail("请输入正确的缓存主域名")
return
}
// 检查域名所属
serverIdResp, err := this.RPC().HTTPWebRPC().FindServerIdWithHTTPWebId(this.AdminContext(), &pb.FindServerIdWithHTTPWebIdRequest{HttpWebId: params.WebId})
if err != nil {
this.ErrorPage(err)
return
}
var serverId = serverIdResp.ServerId
if serverId <= 0 {
this.Fail("找不到要操作的网站")
return
}
existServerNameResp, err := this.RPC().ServerRPC().CheckServerNameInServer(this.AdminContext(), &pb.CheckServerNameInServerRequest{
ServerId: serverId,
ServerName: cacheConfig.Key.Host,
})
if err != nil {
this.ErrorPage(err)
return
}
if !existServerNameResp.Exists {
this.Fail("域名 '" + cacheConfig.Key.Host + "' 在当前网站中并未绑定,不能作为缓存主域名")
return
}
}
err = cacheConfig.Init()
if err != nil {
this.Fail("检查配置失败:" + err.Error())

View File

@@ -130,8 +130,6 @@ func (this *LocationHelper) createMenus(serverIdString string, locationIdString
"isOn": locationConfig != nil && locationConfig.Web != nil && locationConfig.Web.Compression != nil && locationConfig.Web.Compression.IsPrior,
})
menuItems = this.filterMenuItems3(locationConfig, menuItems, serverIdString, locationIdString, secondMenuItem, actionPtr)
menuItems = append(menuItems, maps.Map{
"name": this.Lang(actionPtr, codes.Server_MenuSettingPages),
"url": "/servers/server/settings/locations/pages?serverId=" + serverIdString + "&locationId=" + locationIdString,
@@ -156,6 +154,9 @@ func (this *LocationHelper) createMenus(serverIdString string, locationIdString
"isActive": secondMenuItem == "webp",
"isOn": locationConfig != nil && locationConfig.Web != nil && locationConfig.Web.WebP != nil && locationConfig.Web.WebP.IsPrior,
})
menuItems = this.filterMenuItems3(locationConfig, menuItems, serverIdString, locationIdString, secondMenuItem, actionPtr)
menuItems = append(menuItems, maps.Map{
"name": this.Lang(actionPtr, codes.Server_MenuSettingStat),
"url": "/servers/server/settings/locations/stat?serverId=" + serverIdString + "&locationId=" + locationIdString,

View File

@@ -1,6 +1,7 @@
package waf
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
@@ -37,12 +38,23 @@ func (this *IndexAction) RunGet(params struct {
return
}
if firewallPolicy != nil {
// captcha action
var captchaOptions = firewallconfigs.DefaultHTTPFirewallCaptchaAction()
if len(firewallPolicy.CaptchaOptionsJSON) > 0 {
err = json.Unmarshal(firewallPolicy.CaptchaOptionsJSON, captchaOptions)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Data["firewallPolicy"] = maps.Map{
"id": firewallPolicy.Id,
"name": firewallPolicy.Name,
"isOn": firewallPolicy.IsOn,
"mode": firewallPolicy.Mode,
"modeInfo": firewallconfigs.FindFirewallMode(firewallPolicy.Mode),
"id": firewallPolicy.Id,
"name": firewallPolicy.Name,
"isOn": firewallPolicy.IsOn,
"mode": firewallPolicy.Mode,
"modeInfo": firewallconfigs.FindFirewallMode(firewallPolicy.Mode),
"captchaOptions": captchaOptions,
}
} else {
this.Data["firewallPolicy"] = nil

View File

@@ -90,7 +90,7 @@ func (this *IndexAction) RunPost(params struct {
// 记录日志
defer this.CreateLogInfo(codes.Server_ServerNamesLogUpdateServerNames, params.ServerId)
serverNames := []*serverconfigs.ServerNameConfig{}
var serverNames = []*serverconfigs.ServerNameConfig{}
err := json.Unmarshal([]byte(params.ServerNames), &serverNames)
if err != nil {
this.Fail("域名解析失败:" + err.Error())
@@ -105,10 +105,13 @@ func (this *IndexAction) RunPost(params struct {
this.NotFound("server", params.ServerId)
return
}
clusterId := serverResp.Server.NodeCluster.Id
var clusterId = serverResp.Server.NodeCluster.Id
// 检查套餐
this.checkPlan(params.ServerId, serverNames)
// 检查域名是否已经存在
allServerNames := serverconfigs.PlainServerNames(serverNames)
var allServerNames = serverconfigs.PlainServerNames(serverNames)
if len(allServerNames) > 0 {
dupResp, err := this.RPC().ServerRPC().CheckServerNameDuplicationInNodeCluster(this.AdminContext(), &pb.CheckServerNameDuplicationInNodeClusterRequest{
ServerNames: allServerNames,

View File

@@ -0,0 +1,10 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build !plus
package serverNames
import "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
func (this *IndexAction) checkPlan(serverId int64, serverNames []*serverconfigs.ServerNameConfig) {
// stub
}

View File

@@ -1,6 +1,7 @@
package waf
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
@@ -50,12 +51,23 @@ func (this *IndexAction) RunGet(params struct {
return
}
if firewallPolicy != nil {
// captcha action
var captchaOptions = firewallconfigs.DefaultHTTPFirewallCaptchaAction()
if len(firewallPolicy.CaptchaOptionsJSON) > 0 {
err = json.Unmarshal(firewallPolicy.CaptchaOptionsJSON, captchaOptions)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Data["firewallPolicy"] = maps.Map{
"id": firewallPolicy.Id,
"name": firewallPolicy.Name,
"isOn": firewallPolicy.IsOn,
"mode": firewallPolicy.Mode,
"modeInfo": firewallconfigs.FindFirewallMode(firewallPolicy.Mode),
"id": firewallPolicy.Id,
"name": firewallPolicy.Name,
"isOn": firewallPolicy.IsOn,
"mode": firewallPolicy.Mode,
"modeInfo": firewallconfigs.FindFirewallMode(firewallPolicy.Mode),
"captchaAction": captchaOptions,
}
} else {
this.Data["firewallPolicy"] = nil

View File

@@ -344,8 +344,6 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
"configCode": serverconfigs.ConfigCodeCompression,
})
menuItems = this.filterMenuItems3(serverConfig, menuItems, serverIdString, secondMenuItem, actionPtr)
menuItems = append(menuItems, maps.Map{
"name": this.Lang(actionPtr, codes.Server_MenuSettingPages),
"url": "/servers/server/settings/pages?serverId=" + serverIdString,
@@ -374,6 +372,8 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
"configCode": serverconfigs.ConfigCodeWebp,
})
menuItems = this.filterMenuItems3(serverConfig, menuItems, serverIdString, secondMenuItem, actionPtr)
menuItems = append(menuItems, maps.Map{
"name": this.Lang(actionPtr, codes.Server_MenuSettingStat),
"url": "/servers/server/settings/stat?serverId=" + serverIdString,

View File

@@ -132,25 +132,25 @@ func (this *StatusAction) RunPost(params struct {
if err != nil {
m["type"] = "dnsResolveErr"
m["message"] = "域名解析错误"
m["todo"] = "错误信息:解析域名'" + serverName.Name + "' CNAME记录时出错" + err.Error() + ",请修复此问题。如果已经修改,请等待一个小时后再试。"
m["todo"] = "错误信息:解析域名'" + serverName.Name + "' CNAME记录时出错" + err.Error() + ",请修复此问题。如果已经修改,请等待一个小时后再试。如果长时间无法生效请咨询你的域名DNS服务商。"
return
}
if len(result) == 0 {
m["type"] = "dnsResolveErr"
m["message"] = "域名解析错误"
m["todo"] = "错误信息:找不到域名'" + serverName.Name + "'的CNAME记录请修复此问题。如果已经修改请等待一个小时后再试。"
m["todo"] = "错误信息:找不到域名'" + serverName.Name + "'的CNAME记录请修复此问题。如果已经修改请等待一个小时后再试。如果长时间无法生效请咨询你的域名DNS服务商。"
return
}
if result == serverName.Name+"." {
m["type"] = "dnsResolveErr"
m["message"] = "域名解析错误"
m["todo"] = "错误信息:找不到域名'" + serverName.Name + "'的CNAME记录请设置为'" + cname + "'。如果已经设置,请等待一个小时后再试。"
m["todo"] = "错误信息:找不到域名'" + serverName.Name + "'的CNAME记录请设置为'" + cname + "'。如果已经设置,请等待一个小时后再试。如果长时间无法生效请咨询你的域名DNS服务商。"
return
}
if result != cname {
m["type"] = "dnsResolveErr"
m["message"] = "域名解析错误"
m["todo"] = "错误信息:解析域名'" + serverName.Name + "' CNAME记录时出错当前的CNAME值为" + result + ",请修改为" + cname + "。如果已经修改,请等待一个小时后再试。"
m["todo"] = "错误信息:解析域名'" + serverName.Name + "' CNAME记录时出错当前的CNAME值为" + result + ",请修改为" + cname + "。如果已经修改,请等待一个小时后再试。如果长时间无法生效请咨询你的域名DNS服务商。"
return
}
} else {
@@ -160,25 +160,25 @@ func (this *StatusAction) RunPost(params struct {
if err != nil {
m["type"] = "dnsResolveErr"
m["message"] = "域名解析错误"
m["todo"] = "错误信息:解析域名'" + subName + "' CNAME记录时出错" + err.Error() + ",请修复此问题。如果已经修改,请等待一个小时后再试。"
m["todo"] = "错误信息:解析域名'" + subName + "' CNAME记录时出错" + err.Error() + ",请修复此问题。如果已经修改,请等待一个小时后再试。如果长时间无法生效请咨询你的域名DNS服务商。"
return
}
if len(result) == 0 {
m["type"] = "dnsResolveErr"
m["message"] = "域名解析错误"
m["todo"] = "错误信息:找不到域名'" + subName + "'的CNAME记录请修复此问题。如果已经修改请等待一个小时后再试。"
m["todo"] = "错误信息:找不到域名'" + subName + "'的CNAME记录请修复此问题。如果已经修改请等待一个小时后再试。如果长时间无法生效请咨询你的域名DNS服务商。"
return
}
if result == cname+"." {
m["type"] = "dnsResolveErr"
m["message"] = "域名解析错误"
m["todo"] = "错误信息:找不到域名'" + serverName.Name + "'的CNAME记录请设置为'" + cname + "'。如果已经设置,请等待一个小时后再试。"
m["todo"] = "错误信息:找不到域名'" + serverName.Name + "'的CNAME记录请设置为'" + cname + "'。如果已经设置,请等待一个小时后再试。如果长时间无法生效请咨询你的域名DNS服务商。"
return
}
if result != cname {
m["type"] = "dnsResolveErr"
m["message"] = "域名解析错误"
m["todo"] = "错误信息:解析域名'" + subName + "' CNAME记录时出错当前的CNAME值为" + result + ",请修改为" + cname + "。如果已经修改,请等待一个小时后再试。"
m["todo"] = "错误信息:解析域名'" + subName + "' CNAME记录时出错当前的CNAME值为" + result + ",请修改为" + cname + "。如果已经修改,请等待一个小时后再试。如果长时间无法生效请咨询你的域名DNS服务商。"
return
}
}

View File

@@ -0,0 +1,17 @@
package lang
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeServer)).
Prefix("/settings/lang").
Post("/switch", new(SwitchAction)).
EndAll()
})
}

View File

@@ -0,0 +1,36 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package lang
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type SwitchAction struct {
actionutils.ParentAction
}
func (this *SwitchAction) Init() {
this.Nav("", "", "")
}
func (this *SwitchAction) RunPost(params struct{}) {
var langCode = this.LangCode()
if len(langCode) == 0 || langCode == "zh-cn" {
langCode = "en-us"
} else {
langCode = "zh-cn"
}
configloaders.UpdateAdminLang(this.AdminId(), langCode)
_, err := this.RPC().AdminRPC().UpdateAdminLang(this.AdminContext(), &pb.UpdateAdminLangRequest{LangCode: langCode})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -77,6 +77,8 @@ func (this *IndexAction) RunPost(params struct {
AllowIPs []string
AllowRememberLogin bool
ClientIPHeaderNames string
DenySearchEngines bool
DenySpiders bool
@@ -137,6 +139,9 @@ func (this *IndexAction) RunPost(params struct {
// 允许本地
config.AllowLocal = params.AllowLocal
// 客户端IP获取方式
config.ClientIPHeaderNames = params.ClientIPHeaderNames
// 禁止搜索引擎和爬虫
config.DenySearchEngines = params.DenySearchEngines
config.DenySpiders = params.DenySpiders

View File

@@ -48,7 +48,7 @@ func (this *UpdateHostsAction) RunPost(params struct {
if err != nil {
this.FailField("host", "测试API节点时出错请检查配置错误信息"+err.Error())
}
_, err = client.APINodeRPC().FindCurrentAPINodeVersion(client.APIContext(0), &pb.FindCurrentAPINodeVersionRequest{})
_, err = client.APINodeRPC().FindCurrentAPINodeVersion(client.Context(0), &pb.FindCurrentAPINodeVersionRequest{})
if err != nil {
this.FailField("host", "无法连接此API节点错误信息"+err.Error())
}

View File

@@ -7,6 +7,7 @@ import (
"crypto/rand"
"errors"
"fmt"
executils "github.com/TeaOSLab/EdgeAdmin/internal/utils/exec"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
stringutil "github.com/iwind/TeaGo/utils/string"
timeutil "github.com/iwind/TeaGo/utils/time"
@@ -14,7 +15,6 @@ import (
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
@@ -57,7 +57,7 @@ func (this *MySQLInstaller) InstallFromFile(xzFilePath string, targetDir string)
// check 'tar' command
this.log("checking 'tar' command ...")
var tarExe, _ = exec.LookPath("tar")
var tarExe, _ = executils.LookPath("tar")
if len(tarExe) == 0 {
this.log("installing 'tar' command ...")
err = this.installTarCommand()
@@ -70,7 +70,7 @@ func (this *MySQLInstaller) InstallFromFile(xzFilePath string, targetDir string)
this.log("checking system commands ...")
var cmdList = []string{"tar" /** again **/, "chown", "sh"}
for _, cmd := range cmdList {
cmdPath, err := exec.LookPath(cmd)
cmdPath, err := executils.LookPath(cmd)
if err != nil || len(cmdPath) == 0 {
return errors.New("could not find '" + cmd + "' command in this system")
}
@@ -87,7 +87,7 @@ func (this *MySQLInstaller) InstallFromFile(xzFilePath string, targetDir string)
}
// ubuntu apt
aptExe, err := exec.LookPath("apt")
aptExe, err := executils.LookPath("apt")
if err == nil && len(aptExe) > 0 {
for _, lib := range []string{"libaio1", "libncurses5"} {
this.log("checking " + lib + " ...")
@@ -100,7 +100,7 @@ func (this *MySQLInstaller) InstallFromFile(xzFilePath string, targetDir string)
time.Sleep(1 * time.Second)
}
} else { // yum
yumExe, err := exec.LookPath("yum")
yumExe, err := executils.LookPath("yum")
if err == nil && len(yumExe) > 0 {
for _, lib := range []string{"libaio", "ncurses-libs", "ncurses-compat-libs", "numactl-libs"} {
var cmd = utils.NewCmd("yum", "-y", "install", lib)
@@ -562,7 +562,7 @@ func (this *MySQLInstaller) log(message string) {
// copy file
func (this *MySQLInstaller) installService(baseDir string) error {
_, err := exec.LookPath("systemctl")
_, err := executils.LookPath("systemctl")
if err != nil {
return err
}
@@ -618,14 +618,14 @@ WantedBy=multi-user.target`
// install 'tar' command automatically
func (this *MySQLInstaller) installTarCommand() error {
// yum
yumExe, err := exec.LookPath("yum")
yumExe, err := executils.LookPath("yum")
if err == nil && len(yumExe) > 0 {
var cmd = utils.NewTimeoutCmd(10*time.Second, yumExe, "-y", "install", "tar")
return cmd.Run()
}
// apt
aptExe, err := exec.LookPath("apt")
aptExe, err := executils.LookPath("apt")
if err == nil && len(aptExe) > 0 {
var cmd = utils.NewTimeoutCmd(10*time.Second, aptExe, "-y", "install", "tar")
return cmd.Run()
@@ -636,7 +636,7 @@ func (this *MySQLInstaller) installTarCommand() error {
func (this *MySQLInstaller) lookupGroupAdd() (string, error) {
for _, cmd := range []string{"groupadd", "addgroup"} {
path, err := exec.LookPath(cmd)
path, err := executils.LookPath(cmd)
if err == nil && len(path) > 0 {
return path, nil
}
@@ -646,7 +646,7 @@ func (this *MySQLInstaller) lookupGroupAdd() (string, error) {
func (this *MySQLInstaller) lookupUserAdd() (string, error) {
for _, cmd := range []string{"useradd", "adduser"} {
path, err := exec.LookPath(cmd)
path, err := executils.LookPath(cmd)
if err == nil && len(path) > 0 {
return path, nil
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/files"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"net/http"
)
@@ -124,6 +125,24 @@ func (this *ComponentsAction) RunGet(params struct{}) {
buffer.Write([]byte{';', '\n', '\n'})
}
// WAF checkpoints
var wafCheckpointsMaps = []maps.Map{}
for _, checkpoint := range firewallconfigs.AllCheckpoints {
wafCheckpointsMaps = append(wafCheckpointsMaps, maps.Map{
"name": checkpoint.Name,
"prefix": checkpoint.Prefix,
"description": checkpoint.Description,
})
}
wafCheckpointsJSON, err := json.Marshal(wafCheckpointsMaps)
if err != nil {
logs.Println("ComponentsAction marshal waf rule checkpoints failed: " + err.Error())
} else {
buffer.WriteString("window.WAF_RULE_CHECKPOINTS = ")
buffer.Write(wafCheckpointsJSON)
buffer.Write([]byte{';', '\n', '\n'})
}
// WAF操作符
wafOperatorsJSON, err := json.Marshal(firewallconfigs.AllRuleOperators)
if err != nil {

View File

@@ -81,11 +81,6 @@ func FindAllMenuMaps(langCode string, nodeLogsType string, countUnreadNodeLogs i
"url": "/servers/metrics",
"code": "metric",
},
{
"name": langs.Message(langCode, codes.AdminMenu_ServerGlobalSettings),
"url": "/servers/components",
"code": "global",
},
},
},
{

View File

@@ -17,7 +17,6 @@ import (
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"net"
"net/http"
"net/url"
"reflect"
@@ -145,12 +144,7 @@ func (this *userMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam
action.AddHeader("Content-Security-Policy", "default-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'")
// 检查IP
if !checkIP(securityConfig, action.RequestRemoteIP()) {
action.ResponseWriter.WriteHeader(http.StatusForbidden)
return false
}
remoteAddr, _, _ := net.SplitHostPort(action.Request.RemoteAddr)
if len(remoteAddr) > 0 && remoteAddr != action.RequestRemoteIP() && !checkIP(securityConfig, remoteAddr) {
if !checkIP(securityConfig, loginutils.RemoteIP(action)) {
action.ResponseWriter.WriteHeader(http.StatusForbidden)
return false
}

View File

@@ -6,7 +6,6 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/index/loginutils"
"github.com/iwind/TeaGo/actions"
"net"
"net/http"
)
@@ -33,12 +32,7 @@ func (this *UserShouldAuth) BeforeAction(actionPtr actions.ActionWrapper, paramN
action.AddHeader("Content-Security-Policy", "default-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'")
// 检查IP
if !checkIP(securityConfig, action.RequestRemoteIP()) {
action.ResponseWriter.WriteHeader(http.StatusForbidden)
return false
}
remoteAddr, _, _ := net.SplitHostPort(action.Request.RemoteAddr)
if len(remoteAddr) > 0 && remoteAddr != action.RequestRemoteIP() && !checkIP(securityConfig, remoteAddr) {
if !checkIP(securityConfig, loginutils.RemoteIP(action)) {
action.ResponseWriter.WriteHeader(http.StatusForbidden)
return false
}

View File

@@ -117,6 +117,7 @@ import (
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/backup"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/database"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/lang"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/login"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/profile"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/security"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,79 @@
Vue.component("bandwidth-size-capacity-box", {
props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"],
data: function () {
let v = this.vValue
if (v == null) {
v = {
count: this.vCount,
unit: this.vUnit
}
}
if (v.unit == null || v.unit.length == 0) {
v.unit = "mb"
}
if (typeof (v["count"]) != "number") {
v["count"] = -1
}
let vSize = this.size
if (vSize == null) {
vSize = 6
}
let vMaxlength = this.maxlength
if (vMaxlength == null) {
vMaxlength = 10
}
let supportedUnits = this.vSupportedUnits
if (supportedUnits == null) {
supportedUnits = []
}
return {
capacity: v,
countString: (v.count >= 0) ? v.count.toString() : "",
vSize: vSize,
vMaxlength: vMaxlength,
supportedUnits: supportedUnits
}
},
watch: {
"countString": function (newValue) {
let value = newValue.trim()
if (value.length == 0) {
this.capacity.count = -1
this.change()
return
}
let count = parseInt(value)
if (!isNaN(count)) {
this.capacity.count = count
}
this.change()
}
},
methods: {
change: function () {
this.$emit("change", this.capacity)
}
},
template: `<div class="ui fields inline">
<input type="hidden" :name="vName" :value="JSON.stringify(capacity)"/>
<div class="ui field">
<input type="text" v-model="countString" :maxlength="vMaxlength" :size="vSize"/>
</div>
<div class="ui field">
<select class="ui dropdown" v-model="capacity.unit" @change="change">
<option value="b" v-if="supportedUnits.length == 0 || supportedUnits.$contains('b')">Bps</option>
<option value="kb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('kb')">Kbps</option>
<option value="mb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('mb')">Mbps</option>
<option value="gb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('gb')">Gbps</option>
<option value="tb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('tb')">Tbps</option>
<option value="pb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('pb')">Pbps</option>
<option value="eb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('eb')">Ebps</option>
</select>
</div>
</div>`
})

View File

@@ -0,0 +1,15 @@
Vue.component("bandwidth-size-capacity-view", {
props: ["v-value"],
data: function () {
let capacity = this.vValue
if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") {
capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps"
}
return {
capacity: capacity
}
},
template: `<span>
<span v-if="capacity != null && capacity.count > 0">{{capacity.count}}{{capacity.unit}}</span>
</span>`
})

View File

@@ -63,12 +63,12 @@ Vue.component("size-capacity-box", {
<div class="ui field">
<select class="ui dropdown" v-model="capacity.unit" @change="change">
<option value="byte" v-if="supportedUnits.length == 0 || supportedUnits.$contains('byte')">字节</option>
<option value="kb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('kb')">KB</option>
<option value="mb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('mb')">MB</option>
<option value="gb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('gb')">GB</option>
<option value="tb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('tb')">TB</option>
<option value="pb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('pb')">PB</option>
<option value="eb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('eb')">EB</option>
<option value="kb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('kb')">KiB</option>
<option value="mb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('mb')">MiB</option>
<option value="gb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('gb')">GiB</option>
<option value="tb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('tb')">TiB</option>
<option value="pb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('pb')">PiB</option>
<option value="eb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('eb')">EiB</option>
</select>
</div>
</div>`

View File

@@ -1,7 +1,12 @@
Vue.component("size-capacity-view", {
props:["v-default-text", "v-value"],
methods: {
composeCapacity: function (capacity) {
return teaweb.convertSizeCapacityToString(capacity)
}
},
template: `<div>
<span v-if="vValue != null && vValue.count > 0">{{vValue.count}}{{vValue.unit.toUpperCase()}}</span>
<span v-if="vValue != null && vValue.count > 0">{{composeCapacity(vValue)}}</span>
<span v-else>{{vDefaultText}}</span>
</div>`
})

View File

@@ -1,11 +1,18 @@
Vue.component("ip-list-table", {
props: ["v-items", "v-keyword", "v-show-search-button"],
props: ["v-items", "v-keyword", "v-show-search-button", "v-total"/** total items >= items length **/],
data: function () {
let maxDeletes = 10000
if (this.vTotal != null && this.vTotal > 0 && this.vTotal < maxDeletes) {
maxDeletes = this.vTotal
}
return {
items: this.vItems,
keyword: (this.vKeyword != null) ? this.vKeyword : "",
selectedAll: false,
hasSelectedItems: false
hasSelectedItems: false,
MaxDeletes: maxDeletes
}
},
methods: {
@@ -71,6 +78,21 @@ Vue.component("ip-list-table", {
teaweb.successToast("批量删除成功", 1200, teaweb.reload)
})
},
deleteCount: function () {
let that = this
teaweb.confirm("确定要批量删除当前列表中的" + this.MaxDeletes + "个IP吗", function () {
let query = window.location.search
if (query.startsWith("?")) {
query = query.substring(1)
}
Tea.action("/servers/iplists/deleteCount?" + query)
.post()
.params({count: that.MaxDeletes})
.success(function () {
teaweb.successToast("批量删除成功", 1200, teaweb.reload)
})
})
},
formatSeconds: function (seconds) {
if (seconds < 60) {
return seconds + "秒"
@@ -82,11 +104,29 @@ Vue.component("ip-list-table", {
return Math.ceil(seconds / 3600) + "小时"
}
return Math.ceil(seconds / 86400) + "天"
},
cancelChecked: function () {
this.hasSelectedItems = false
this.selectedAll = false
let boxes = this.$refs.itemCheckBox
if (boxes == null) {
return
}
boxes.forEach(function (box) {
box.checked = false
})
}
},
template: `<div>
<div v-show="hasSelectedItems">
<a href="" @click.prevent="deleteAll">[批量删除]</a>
<div class="ui divider"></div>
<button class="ui button basic" type="button" @click.prevent="deleteAll">批量删除所选</button>
&nbsp; &nbsp;
<button class="ui button basic" type="button" @click.prevent="deleteCount" v-if="vTotal != null && vTotal >= MaxDeletes">批量删除{{MaxDeletes}}个</button>
&nbsp; &nbsp;
<button class="ui button basic" type="button" @click.prevent="cancelChecked">取消选中</button>
</div>
<table class="ui table selectable celled" v-if="items.length > 0">
<thead>

View File

@@ -19,14 +19,14 @@ Vue.component("plan-price-view", {
<span v-if="plan.priceType == 'traffic'">
按流量计费
<div>
<span class="grey small">基础价格:¥{{plan.trafficPrice.base}}元/GB</span>
<span class="grey small">基础价格:¥{{plan.trafficPrice.base}}元/GiB</span>
</div>
</span>
<div v-if="plan.priceType == 'bandwidth' && plan.bandwidthPrice != null && plan.bandwidthPrice.percentile > 0">
按{{plan.bandwidthPrice.percentile}}th带宽计费
<div>
<div v-for="range in plan.bandwidthPrice.ranges">
<span class="small grey">{{range.minMB}} - <span v-if="range.maxMB > 0">{{range.maxMB}}MB</span><span v-else>&infin;</span> <span v-if="range.totalPrice > 0">{{range.totalPrice}}元</span><span v-else="">{{range.pricePerMB}}元/MB</span></span>
<span class="small grey">{{range.minMB}} - <span v-if="range.maxMB > 0">{{range.maxMB}}MiB</span><span v-else>&infin;</span> <span v-if="range.totalPrice > 0">{{range.totalPrice}}元</span><span v-else="">{{range.pricePerMB}}元/MiB</span></span>
</div>
</div>
</div>

View File

@@ -105,7 +105,17 @@ Vue.component("domains-box", {
if (this.isEditing && this.editingIndex >= 0) {
this.domains[this.editingIndex] = this.addingDomain
} else {
this.domains.push(this.addingDomain)
// 分割逗号(,)、顿号(、)
if (this.addingDomain.match("[,、,;]")) {
let domainList = this.addingDomain.split(new RegExp("[,、,;]"))
domainList.forEach(function (v) {
if (v.length > 0) {
that.domains.push(v)
}
})
} else {
this.domains.push(this.addingDomain)
}
}
this.cancel()
this.change()

View File

@@ -85,7 +85,7 @@ Vue.component("http-access-log-config-box", {
<label :for="'access-log-field-' + index">{{field.name}}</label>
</div>
<p class="comment">在基础信息之外要存储的信息。
<span class="red" v-if="hasRequestBodyField">记录"请求Body"将会显著消耗更多的系统资源建议仅在调试时启用最大记录尺寸为2MB。</span>
<span class="red" v-if="hasRequestBodyField">记录"请求Body"将会显著消耗更多的系统资源建议仅在调试时启用最大记录尺寸为2MiB。</span>
</p>
</td>
</tr>

View File

@@ -19,11 +19,21 @@ Vue.component("http-cache-config-box", {
cacheConfig.cacheRefs = []
}
var maxBytes = null
let maxBytes = null
if (this.vCachePolicy != null && this.vCachePolicy.maxBytes != null) {
maxBytes = this.vCachePolicy.maxBytes
}
// key
if (cacheConfig.key == null) {
// use Vue.set to activate vue events
Vue.set(cacheConfig, "key", {
isOn: false,
scheme: "https",
host: ""
})
}
return {
cacheConfig: cacheConfig,
moreOptionsVisible: false,
@@ -31,7 +41,9 @@ Vue.component("http-cache-config-box", {
maxBytes: maxBytes,
searchBoxVisible: false,
searchKeyword: ""
searchKeyword: "",
keyOptionsVisible: false
}
},
watch: {
@@ -106,6 +118,44 @@ Vue.component("http-cache-config-box", {
</td>
</tr>
</tbody>
<tbody v-show="isOn() && !vIsGroup">
<tr>
<td>缓存主域名</td>
<td>
<div v-show="!cacheConfig.key.isOn">默认 &nbsp; <a href="" @click.prevent="keyOptionsVisible = !keyOptionsVisible"><span class="small">[修改]</span></a></div>
<div v-show="cacheConfig.key.isOn">使用主域名:{{cacheConfig.key.scheme}}://{{cacheConfig.key.host}} &nbsp; <a href="" @click.prevent="keyOptionsVisible = !keyOptionsVisible"><span class="small">[修改]</span></a></div>
<div v-show="keyOptionsVisible" style="margin-top: 1em">
<div class="ui divider"></div>
<table class="ui table definition">
<tr>
<td class="title">启用主域名</td>
<td><checkbox v-model="cacheConfig.key.isOn"></checkbox>
<p class="comment">启用主域名后,所有缓存键值中的协议和域名部分都会修改为主域名,用来实现缓存不区分域名。</p>
</td>
</tr>
<tr v-show="cacheConfig.key.isOn">
<td>主域名 *</td>
<td>
<div class="ui fields inline">
<div class="ui field">
<select class="ui dropdown" v-model="cacheConfig.key.scheme">
<option value="https">https://</option>
<option value="http">http://</option>
</select>
</div>
<div class="ui field">
<input type="text" v-model="cacheConfig.key.host" placeholder="example.com" @keyup.enter="keyOptionsVisible = false" @keypress.enter.prevent="1"/>
</div>
</div>
<p class="comment">此域名<strong>必须</strong>是当前网站已绑定域名,在刷新缓存时也需要使用此域名。</p>
</td>
</tr>
</table>
<button class="ui button tiny" type="button" @click.prevent="keyOptionsVisible = false">完成</button>
</div>
</td>
</tr>
</tbody>
<tbody v-show="isOn()">
<tr>
<td colspan="2">
@@ -164,6 +214,11 @@ Vue.component("http-cache-config-box", {
<http-cache-stale-config :v-cache-stale-config="cacheConfig.stale" @change="changeStale"></http-cache-stale-config>
</div>
<div v-show="isOn()">
<submit-btn></submit-btn>
<div class="ui divider"></div>
</div>
<div v-show="isOn()" style="margin-top: 1em">
<h4 style="position: relative">缓存条件 &nbsp; <a href="" style="font-size: 0.8em" @click.prevent="$refs.cacheRefsConfigBoxRef.addRef(false)">[添加]</a> &nbsp; <a href="" style="font-size: 0.8em" @click.prevent="showSearchBox" v-show="!searchBoxVisible">[搜索]</a>
<div class="ui input small right labeled" style="position: absolute; top: -0.4em; margin-left: 0.5em; zoom: 0.9" v-show="searchBoxVisible">

View File

@@ -159,6 +159,9 @@ Vue.component("http-cache-ref-box", {
}
dialog.style.width = width + "px"
if (this.ref.conds != null) {
this.ref.conds.isOn = true
}
break
}
},

View File

@@ -79,7 +79,17 @@ Vue.component("http-cache-refs-config-box", {
that.refs.push(newRef)
}
that.change()
// move to bottom
var afterChangeCallback = function () {
setTimeout(function () {
let rightBox = document.querySelector(".right-box")
if (rightBox != null) {
rightBox.scrollTo(0, isReverse ? 0 : 100000)
}
}, 100)
}
that.change(afterChangeCallback)
}
})
},
@@ -140,7 +150,7 @@ Vue.component("http-cache-refs-config-box", {
}
return unit
},
change: function () {
change: function (callback) {
this.$forceUpdate()
// 自动保存
@@ -159,7 +169,11 @@ Vue.component("http-cache-refs-config-box", {
})
.success(function (resp) {
if (resp.data.isUpdated) {
teaweb.successToast("保存成功")
teaweb.successToast("保存成功", null, function () {
if (typeof callback == "function") {
callback()
}
})
}
})
.post()

View File

@@ -7,7 +7,8 @@ Vue.component("http-charsets-box", {
isPrior: false,
isOn: false,
charset: "",
isUpper: false
isUpper: false,
force: false
}
}
return {
@@ -51,13 +52,20 @@ Vue.component("http-charsets-box", {
<more-options-tbody @change="changeAdvancedVisible" v-if="((!vIsLocation && !vIsGroup) || charsetConfig.isPrior) && charsetConfig.isOn"></more-options-tbody>
<tbody v-show="((!vIsLocation && !vIsGroup) || charsetConfig.isPrior) && charsetConfig.isOn && advancedVisible">
<tr>
<td>字符编码是否大写</td>
<td>强制替换</td>
<td>
<checkbox v-model="charsetConfig.force"></checkbox>
<p class="comment">选中后,表示强制覆盖已经设置的字符集;不选中,表示如果源站已经设置了字符集,则保留不修改。</p>
</td>
</tr>
<tr>
<td>字符编码大写</td>
<td>
<div class="ui checkbox">
<input type="checkbox" v-model="charsetConfig.isUpper"/>
<label></label>
</div>
<p class="comment">选中后将指定的字符编码转换为大写,比如默认为<span class="ui label tiny">utf-8</span>,选中后将改为<span class="ui label tiny">UTF-8</span>。</p>
<p class="comment">选中后将指定的字符编码转换为大写,比如默认为<code-label>utf-8</code-label>,选中后将改为<code-label>UTF-8</code-label>。</p>
</td>
</tr>
</tbody>

View File

@@ -896,7 +896,7 @@ Vue.component("http-cond-params", {
<td>不区分大小写</td>
<td>
<div class="ui checkbox">
<input type="checkbox" v-model="cond.isCaseInsensitive"/>
<input type="checkbox" name="condIsCaseInsensitive" v-model="cond.isCaseInsensitive"/>
<label></label>
</div>
<p class="comment">选中后表示对比时忽略参数值的大小写。</p>

View File

@@ -55,10 +55,12 @@ Vue.component("http-firewall-actions-box", {
var defaultPageBody = `<!DOCTYPE html>
<html lang="en">
<title>403 Forbidden</title>
<head>
\t<title>403 Forbidden</title>
\t<style>
\t\taddress { line-height: 1.8; }
\t</style>
</head>
<body>
<h1>403 Forbidden</h1>
<address>Connection: \${remoteAddr} (Client) -&gt; \${serverAddr} (Server)</address>
@@ -83,6 +85,8 @@ Vue.component("http-firewall-actions-box", {
ipListLevels: [],
// 动作参数
allowScope: "global",
blockTimeout: "",
blockTimeoutMax: "",
blockScope: "global",
@@ -103,6 +107,7 @@ Vue.component("http-firewall-actions-box", {
tagTags: [],
pageUseDefault: true,
pageStatus: 403,
pageBody: defaultPageBody,
defaultPageBody: defaultPageBody,
@@ -137,6 +142,9 @@ Vue.component("http-firewall-actions-box", {
})
this.actionOptions = {}
},
allowScope: function (v) {
this.actionOptions["scope"] = v
},
blockTimeout: function (v) {
v = parseInt(v)
if (isNaN(v)) {
@@ -271,11 +279,13 @@ Vue.component("http-firewall-actions-box", {
methods: {
add: function () {
this.action = null
this.actionCode = "block"
this.actionCode = "page"
this.isAdding = true
this.actionOptions = {}
// 动作参数
this.allowScope = "global"
this.blockTimeout = ""
this.blockTimeoutMax = ""
this.blockScope = "global"
@@ -300,6 +310,7 @@ Vue.component("http-firewall-actions-box", {
this.tagTags = []
this.pageUseDefault = true
this.pageStatus = 403
this.pageBody = this.defaultPageBody
@@ -359,6 +370,11 @@ Vue.component("http-firewall-actions-box", {
}
break
case "allow":
if (config.options != null && config.options.scope != null && config.options.scope.length > 0) {
this.allowScope = config.options.scope
} else {
this.allowScope = "global"
}
break
case "log":
break
@@ -427,8 +443,14 @@ Vue.component("http-firewall-actions-box", {
}
break
case "page":
this.pageUseDefault = true
this.pageStatus = 403
this.pageBody = this.defaultPageBody
if (typeof config.options.useDefault === "boolean") {
this.pageUseDefault = config.options.useDefault
} else {
this.pageUseDefault = false
}
if (config.options.status != null) {
this.pageStatus = config.options.status
}
@@ -531,6 +553,7 @@ Vue.component("http-firewall-actions-box", {
}
this.actionOptions = {
useDefault: this.pageUseDefault,
status: pageStatus,
body: this.pageBody
}
@@ -663,6 +686,13 @@ Vue.component("http-firewall-actions-box", {
<div v-for="(config, index) in configs" :data-index="index" :key="config.id" class="ui label small basic" :class="{blue: index == editingIndex}" style="margin-bottom: 0.4em">
{{config.name}} <span class="small">({{config.code.toUpperCase()}})</span>
<!-- allow -->
<span class="small" v-if="config.code == 'allow' && config.options != null && config.options.scope != null && config.options.scope.length > 0">
<span v-if="config.options.scope == 'group'">[分组]</span>
<span v-if="config.options.scope == 'server'">[网站]</span>
<span v-if="config.options.scope == 'global'">[网站和策略]</span>
</span>
<!-- block -->
<span v-if="config.code == 'block' && config.options.timeout > 0">:封禁时长{{config.options.timeout}}<span v-if="config.options.timeoutMax > config.options.timeout">-{{config.options.timeoutMax}}</span>秒</span>
@@ -689,7 +719,7 @@ Vue.component("http-firewall-actions-box", {
<span v-if="config.code == 'tag'">{{config.options.tags.join(", ")}}</span>
<!-- page -->
<span v-if="config.code == 'page'">[{{config.options.status}}]</span>
<span v-if="config.code == 'page'">[{{config.options.status}}]<span v-if="config.options.useDefault">&nbsp; [默认页面]</span></span>
<!-- redirect -->
<span v-if="config.code == 'redirect'">{{config.options.url}}</span>
@@ -701,7 +731,7 @@ Vue.component("http-firewall-actions-box", {
<span v-if="config.code == 'go_set'">{{config.options.groupName}} / {{config.options.setName}}</span>
<!-- 范围 -->
<span v-if="config.options.scope != null && config.options.scope.length > 0" class="small grey">
<span v-if="config.code != 'allow' && config.options.scope != null && config.options.scope.length > 0" class="small grey">
&nbsp;
<span v-if="config.options.scope == 'global'">[所有网站]</span>
<span v-if="config.options.scope == 'service'">[当前网站]</span>
@@ -724,6 +754,21 @@ Vue.component("http-firewall-actions-box", {
</td>
</tr>
<!-- allow -->
<tr v-if="actionCode == 'allow'">
<td>有效范围</td>
<td>
<select class="ui dropdown auto-width" v-model="allowScope">
<option value="group">分组</option>
<option value="server">网站</option>
<option value="global">网站和策略</option>
</select>
<p class="comment" v-if="allowScope == 'group'">跳过当前分组其他规则集,继续执行其他分组的规则集。</p>
<p class="comment" v-if="allowScope == 'server'">跳过当前网站所有的规则集。</p>
<p class="comment" v-if="allowScope =='global'">跳过当前网站和网站对应WAF策略所有的规则集。</p>
</td>
</tr>
<!-- block -->
<tr v-if="actionCode == 'block'">
<td>封禁范围</td>
@@ -891,11 +936,17 @@ Vue.component("http-firewall-actions-box", {
<!-- page -->
<tr v-if="actionCode == 'page'">
<td>状态码 *</td>
<td>使用默认提示</td>
<td>
<checkbox v-model="pageUseDefault"></checkbox>
</td>
</tr>
<tr v-if="actionCode == 'page' && !pageUseDefault">
<td class="color-border">状态码 *</td>
<td><input type="text" style="width: 4em" maxlength="3" v-model="pageStatus"/></td>
</tr>
<tr v-if="actionCode == 'page'">
<td>网页内容</td>
<tr v-if="actionCode == 'page' && !pageUseDefault">
<td class="color-border">网页内容</td>
<td>
<textarea v-model="pageBody"></textarea>
</td>

View File

@@ -3,7 +3,16 @@ Vue.component("http-firewall-actions-view", {
props: ["v-actions"],
template: `<div>
<div v-for="action in vActions" style="margin-bottom: 0.3em">
<span :class="{red: action.category == 'block', orange: action.category == 'verify', green: action.category == 'allow'}">{{action.name}} ({{action.code.toUpperCase()}})</span>
<span :class="{red: action.category == 'block', orange: action.category == 'verify', green: action.category == 'allow'}">{{action.name}} ({{action.code.toUpperCase()}})
<div v-if="action.options != null">
<span class="grey small" v-if="action.code.toLowerCase() == 'page'">[{{action.options.status}}]</span>
<span class="grey small" v-if="action.code.toLowerCase() == 'allow' && action.options != null && action.options.scope != null && action.options.scope.length > 0">
<span v-if="action.options.scope == 'group'">[分组]</span>
<span v-if="action.options.scope == 'server'">[网站]</span>
<span v-if="action.options.scope == 'global'">[网站和策略]</span>
</span>
</div>
</span>
</div>
</div>`
})

View File

@@ -57,6 +57,11 @@ Vue.component("http-firewall-captcha-options-viewer", {
summaryList.push("定制UI")
}
}
if (this.options.geeTestConfig != null && this.options.geeTestConfig.isOn) {
summaryList.push("已配置极验")
}
if (summaryList.length == 0) {
this.summary = "默认配置"
} else {

View File

@@ -22,7 +22,12 @@ Vue.component("http-firewall-captcha-options", {
uiFooter: "",
uiBody: "",
cookieId: "",
lang: ""
lang: "",
geeTestConfig: {
isOn: false,
captchaId: "",
captchaKey: ""
}
}
}
if (options.countLetters <= 0) {
@@ -33,6 +38,7 @@ Vue.component("http-firewall-captcha-options", {
options.captchaType = "default"
}
return {
options: options,
isEditing: false,
@@ -92,6 +98,9 @@ Vue.component("http-firewall-captcha-options", {
} else {
this.uiBodyWarning = ""
}
},
"options.geeTestConfig.isOn": function (v) {
this.updateSummary()
}
},
methods: {
@@ -127,6 +136,10 @@ Vue.component("http-firewall-captcha-options", {
}
}
if (this.options.geeTestConfig != null && this.options.geeTestConfig.isOn) {
summaryList.push("已配置极验")
}
if (summaryList.length == 0) {
this.summary = "默认配置"
} else {
@@ -202,7 +215,6 @@ Vue.component("http-firewall-captcha-options", {
<td class="color-border">定制UI</td>
<td><checkbox v-model="options.uiIsOn"></checkbox></td>
</tr>
</tbody>
<tbody v-show="options.uiIsOn && options.captchaType == 'default'">
<tr>
@@ -254,6 +266,31 @@ Vue.component("http-firewall-captcha-options", {
</tr>
</tbody>
</table>
<table class="ui table definition selectable">
<tr>
<td class="title">允许用户使用极验</td>
<td><checkbox v-model="options.geeTestConfig.isOn"></checkbox>
<p class="comment">选中后表示允许用户在WAF设置中选择极验。</p>
</td>
</tr>
<tbody v-show="options.geeTestConfig.isOn">
<tr>
<td class="color-border">极验-验证ID *</td>
<td>
<input type="text" maxlength="100" name="geetestCaptchaId" v-model="options.geeTestConfig.captchaId" spellcheck="false"/>
<p class="comment">在极验控制台--业务管理中获取。</p>
</td>
</tr>
<tr>
<td class="color-border">极验-验证Key *</td>
<td>
<input type="text" maxlength="100" name="geetestCaptchaKey" v-model="options.geeTestConfig.captchaKey" spellcheck="false"/>
<p class="comment">在极验控制台--业务管理中获取。</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
`

View File

@@ -16,11 +16,25 @@ Vue.component("http-firewall-config-box", {
firewall.defaultCaptchaType = "none"
}
let allCaptchaTypes = window.WAF_CAPTCHA_TYPES.$copy()
// geetest
let geeTestIsOn = false
if (this.vFirewallPolicy != null && this.vFirewallPolicy.captchaAction != null && this.vFirewallPolicy.captchaAction.geeTestConfig != null) {
geeTestIsOn = this.vFirewallPolicy.captchaAction.geeTestConfig.isOn
}
// 如果没有启用geetest则还原
if (!geeTestIsOn && firewall.defaultCaptchaType == "geetest") {
firewall.defaultCaptchaType = "none"
}
return {
firewall: firewall,
moreOptionsVisible: false,
execGlobalRules: !firewall.ignoreGlobalRules,
captchaTypes: window.WAF_CAPTCHA_TYPES
captchaTypes: allCaptchaTypes,
geeTestIsOn: geeTestIsOn
}
},
watch: {
@@ -66,7 +80,7 @@ Vue.component("http-firewall-config-box", {
<td>
<select class="ui dropdown auto-width" v-model="firewall.defaultCaptchaType">
<option value="none">默认</option>
<option v-for="captchaType in captchaTypes" :value="captchaType.code">{{captchaType.name}}</option>
<option v-for="captchaType in captchaTypes" v-if="captchaType.code != 'geetest' || geeTestIsOn" :value="captchaType.code">{{captchaType.name}}</option>
</select>
<p class="comment" v-if="firewall.defaultCaptchaType == 'none'">使用系统默认的设置。</p>
<p class="comment" v-for="captchaType in captchaTypes" v-if="captchaType.code == firewall.defaultCaptchaType">{{captchaType.description}}</p>

View File

@@ -0,0 +1,15 @@
Vue.component("http-firewall-page-options-viewer", {
props: ["v-page-options"],
data: function () {
return {
options: this.vPageOptions
}
},
template: `<div>
<span v-if="options == null">默认设置</span>
<div v-else>
状态码:{{options.status}} / 提示内容:<span v-if="options.body != null && options.body.length > 0">[{{options.body.length}}字符]</span>
</div>
</div>
`
})

View File

@@ -0,0 +1,67 @@
Vue.component("http-firewall-page-options", {
props: ["v-page-options"],
data: function () {
var defaultPageBody = `<!DOCTYPE html>
<html lang="en">
<head>
<title>403 Forbidden</title>
<style>
address { line-height: 1.8; }
</style>
</head>
<body>
<h1>403 Forbidden By WAF</h1>
<address>Connection: \${remoteAddr} (Client) -&gt; \${serverAddr} (Server)</address>
<address>Request ID: \${requestId}</address>
</body>
</html>`
return {
pageOptions: this.vPageOptions,
status: this.vPageOptions.status,
body: this.vPageOptions.body,
defaultPageBody: defaultPageBody,
isEditing: false
}
},
watch: {
status: function (v) {
if (typeof v === "string" && v.length != 3) {
return
}
let statusCode = parseInt(v)
if (isNaN(statusCode)) {
this.pageOptions.status = 403
} else {
this.pageOptions.status = statusCode
}
},
body: function (v) {
this.pageOptions.body = v
}
},
methods: {
edit: function () {
this.isEditing = !this.isEditing
}
},
template: `<div>
<input type="hidden" name="pageOptionsJSON" :value="JSON.stringify(pageOptions)"/>
<a href="" @click.prevent="edit">状态码:{{status}} / 提示内容:<span v-if="pageOptions.body != null && pageOptions.body.length > 0">[{{pageOptions.body.length}}字符]</span><span v-else class="disabled">[无]</span>
<i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i></a>
<table class="ui table" v-show="isEditing">
<tr>
<td class="title">状态码 *</td>
<td><input type="text" style="width: 4em" maxlength="3" v-model="status"/></td>
</tr>
<tr>
<td>网页内容</td>
<td>
<textarea v-model="body"></textarea>
<p class="comment"><a href="" @click.prevent="body = defaultPageBody">[使用模板]</a> </p>
</td>
</tr>
</table>
</div>
`
})

View File

@@ -22,6 +22,7 @@ Vue.component("http-firewall-policy-selector", {
select: function () {
let that = this
teaweb.popup("/servers/components/waf/selectPopup", {
height: "26em",
callback: function (resp) {
that.firewallPolicy = resp.data.firewallPolicy
}

View File

@@ -10,8 +10,32 @@ Vue.component("http-firewall-rule-label", {
showErr: function (err) {
teaweb.popupTip("规则校验错误,请修正:<span class=\"red\">" + teaweb.encodeHTML(err) + "</span>")
},
calculateParamName: function (param) {
let paramName = ""
if (param != null) {
window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) {
if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) {
paramName = checkpoint.name
}
})
}
return paramName
},
calculateParamDescription: function (param) {
let paramName = ""
let paramDescription = ""
if (param != null) {
window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) {
if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) {
paramName = checkpoint.name
paramDescription = checkpoint.description
}
})
}
return paramName + ": " + paramDescription
},
operatorName: function (operatorCode) {
var operatorName = operatorCode
let operatorName = operatorCode
if (typeof (window.WAF_RULE_OPERATORS) != null) {
window.WAF_RULE_OPERATORS.forEach(function (v) {
if (v.code == operatorCode) {
@@ -22,13 +46,39 @@ Vue.component("http-firewall-rule-label", {
return operatorName
},
operatorDescription: function (operatorCode) {
let operatorName = operatorCode
let operatorDescription = ""
if (typeof (window.WAF_RULE_OPERATORS) != null) {
window.WAF_RULE_OPERATORS.forEach(function (v) {
if (v.code == operatorCode) {
operatorName = v.name
operatorDescription = v.description
}
})
}
return operatorName + ": " + operatorDescription
},
operatorDataType: function (operatorCode) {
let operatorDataType = "none"
if (typeof (window.WAF_RULE_OPERATORS) != null) {
window.WAF_RULE_OPERATORS.forEach(function (v) {
if (v.code == operatorCode) {
operatorDataType = v.dataType
}
})
}
return operatorDataType
},
isEmptyString: function (v) {
return typeof v == "string" && v.length == 0
}
},
template: `<div>
<div class="ui label tiny basic" style="line-height: 1.5">
{{rule.name}}[{{rule.param}}]
<div class="ui label small basic" style="line-height: 1.5">
{{rule.name}} <span :title="calculateParamDescription(rule.param)" class="hover">{{calculateParamName(rule.param)}}<span class="small grey"> {{rule.param}}</span></span>
<!-- cc2 -->
<span v-if="rule.param == '\${cc2}'">
@@ -43,9 +93,9 @@ Vue.component("http-firewall-rule-label", {
<span v-else>
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span>
<span :class="{dash:!rule.isComposed && rule.isCaseInsensitive}" :title="(!rule.isComposed && rule.isCaseInsensitive) ? '大小写不敏感':''">{{operatorName(rule.operator)}}</span>
<span v-if="!isEmptyString(rule.value)">{{rule.value}}</span>
<span v-else class="disabled" style="font-weight: normal" title="空字符串">[空]</span>
<span class="hover" :class="{dash:!rule.isComposed && rule.isCaseInsensitive}" :title="operatorDescription(rule.operator) + ((!rule.isComposed && rule.isCaseInsensitive) ? '\\n[大小写不敏感] ':'')">&lt;{{operatorName(rule.operator)}}&gt;</span>
<span class="hover" v-if="!isEmptyString(rule.value)">{{rule.value}}</span>
<span v-else-if="operatorDataType(rule.operator) != 'none'" class="disabled" style="font-weight: normal" title="空字符串">[空]</span>
</span>
<!-- description -->

View File

@@ -37,7 +37,7 @@ Vue.component("http-firewall-rules-box", {
})
},
operatorName: function (operatorCode) {
var operatorName = operatorCode
let operatorName = operatorCode
if (typeof (window.WAF_RULE_OPERATORS) != null) {
window.WAF_RULE_OPERATORS.forEach(function (v) {
if (v.code == operatorCode) {
@@ -48,6 +48,56 @@ Vue.component("http-firewall-rules-box", {
return operatorName
},
operatorDescription: function (operatorCode) {
let operatorName = operatorCode
let operatorDescription = ""
if (typeof (window.WAF_RULE_OPERATORS) != null) {
window.WAF_RULE_OPERATORS.forEach(function (v) {
if (v.code == operatorCode) {
operatorName = v.name
operatorDescription = v.description
}
})
}
return operatorName + ": " + operatorDescription
},
operatorDataType: function (operatorCode) {
let operatorDataType = "none"
if (typeof (window.WAF_RULE_OPERATORS) != null) {
window.WAF_RULE_OPERATORS.forEach(function (v) {
if (v.code == operatorCode) {
operatorDataType = v.dataType
}
})
}
return operatorDataType
},
calculateParamName: function (param) {
let paramName = ""
if (param != null) {
window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) {
if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) {
paramName = checkpoint.name
}
})
}
return paramName
},
calculateParamDescription: function (param) {
let paramName = ""
let paramDescription = ""
if (param != null) {
window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) {
if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) {
paramName = checkpoint.name
paramDescription = checkpoint.description
}
})
}
return paramName + ": " + paramDescription
},
isEmptyString: function (v) {
return typeof v == "string" && v.length == 0
}
@@ -56,7 +106,7 @@ Vue.component("http-firewall-rules-box", {
<input type="hidden" name="rulesJSON" :value="JSON.stringify(rules)"/>
<div v-if="rules.length > 0">
<div v-for="(rule, index) in rules" class="ui label small basic" style="margin-bottom: 0.5em; line-height: 1.5">
{{rule.name}}[{{rule.param}}]
{{rule.name}} <span :title="calculateParamDescription(rule.param)" class="hover">{{calculateParamName(rule.param)}}<span class="small grey"> {{rule.param}}</span></span>
<!-- cc2 -->
<span v-if="rule.param == '\${cc2}'">
@@ -70,9 +120,9 @@ Vue.component("http-firewall-rules-box", {
</span>
<span v-else>
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span> <span :class="{dash:(!rule.isComposed && rule.isCaseInsensitive)}" :title="(!rule.isComposed && rule.isCaseInsensitive) ? '大小写不敏感':''">{{operatorName(rule.operator)}}</span>
<span v-if="!isEmptyString(rule.value)">{{rule.value}}</span>
<span v-else class="disabled" style="font-weight: normal" title="空字符串">[空]</span>
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span> <span class="hover" :title="operatorDescription(rule.operator) + ((!rule.isComposed && rule.isCaseInsensitive) ? '\\n[大小写不敏感] ':'')">&lt;{{operatorName(rule.operator)}}&gt;</span>
<span v-if="!isEmptyString(rule.value)" class="hover">{{rule.value}}</span>
<span v-else-if="operatorDataType(rule.operator) != 'none'" class="disabled" style="font-weight: normal" title="空字符串">[空]</span>
</span>
<!-- description -->

View File

@@ -6,7 +6,6 @@ Vue.component("http-webp-config-box", {
config = {
isPrior: false,
isOn: false,
quality: 50,
minLength: {count: 0, "unit": "kb"},
maxLength: {count: 0, "unit": "kb"},
mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico"],
@@ -24,21 +23,7 @@ Vue.component("http-webp-config-box", {
return {
config: config,
moreOptionsVisible: false,
quality: config.quality
}
},
watch: {
quality: function (v) {
let quality = parseInt(v)
if (isNaN(quality)) {
quality = 90
} else if (quality < 1) {
quality = 1
} else if (quality > 100) {
quality = 100
}
this.config.quality = quality
moreOptionsVisible: false
}
},
methods: {
@@ -95,16 +80,6 @@ Vue.component("http-webp-config-box", {
<p class="comment">响应的Content-Type里包含这些MimeType的内容将会被转成WebP。</p>
</td>
</tr>
<tr>
<td>图片质量</td>
<td>
<div class="ui input right labeled">
<input type="text" v-model="quality" style="width: 5em" maxlength="4"/>
<span class="ui label">%</span>
</div>
<p class="comment">取值在0到100之间数值越大生成的图像越清晰同时文件尺寸也会越大。</p>
</td>
</tr>
<tr>
<td>内容最小长度</td>
<td>

View File

@@ -1,12 +1,34 @@
Vue.component("script-config-box", {
props: ["id", "v-script-config", "comment"],
props: ["id", "v-script-config", "comment", "v-auditing-status"],
mounted: function () {
let that = this
setTimeout(function () {
that.$forceUpdate()
}, 100)
},
data: function () {
let config = this.vScriptConfig
if (config == null) {
config = {
isPrior: false,
isOn: false,
code: ""
code: "",
auditingCode: ""
}
}
let auditingStatus = null
if (config.auditingCodeMD5 != null && config.auditingCodeMD5.length > 0 && config.auditingCode != null && config.auditingCode.length > 0) {
config.code = config.auditingCode
if (this.vAuditingStatus != null) {
for (let i = 0; i < this.vAuditingStatus.length; i++) {
let status = this.vAuditingStatus[i]
if (status.md5 == config.auditingCodeMD5) {
auditingStatus = status
break
}
}
}
}
@@ -15,7 +37,8 @@ Vue.component("script-config-box", {
}
return {
config: config
config: config,
auditingStatus: auditingStatus
}
},
watch: {
@@ -30,6 +53,12 @@ Vue.component("script-config-box", {
changeCode: function (code) {
this.config.code = code
this.change()
},
isPlus: function () {
if (Tea == null || Tea.Vue == null) {
return false
}
return Tea.Vue.teaIsPlus
}
},
template: `<div>
@@ -43,7 +72,14 @@ Vue.component("script-config-box", {
<tbody>
<tr :style="{opacity: !config.isOn ? 0.5 : 1}">
<td>脚本代码</td>
<td><source-code-box :id="id" type="text/javascript" :read-only="false" @change="changeCode">{{config.code}}</source-code-box>
<td>
<p class="comment" v-if="auditingStatus != null">
<span class="green" v-if="auditingStatus.isPassed">管理员审核结果:审核通过。</span>
<span class="red" v-else-if="auditingStatus.isRejected">管理员审核结果:驳回 &nbsp; &nbsp; 驳回理由:{{auditingStatus.rejectedReason}}</span>
<span class="red" v-else>当前脚本将在审核后生效,请耐心等待审核结果。 <a href="/servers/user-scripts" target="_blank" v-if="isPlus()">去审核 &raquo;</a></span>
</p>
<p class="comment" v-if="auditingStatus == null"><span class="green">管理员审核结果:审核通过。</span></p>
<source-code-box :id="id" type="text/javascript" :read-only="false" @change="changeCode">{{config.code}}</source-code-box>
<p class="comment">{{comment}}</p>
</td>
</tr>

View File

@@ -1,5 +1,5 @@
Vue.component("script-group-config-box", {
props: ["v-group", "v-is-location"],
props: ["v-group", "v-auditing-status", "v-is-location"],
data: function () {
let group = this.vGroup
if (group == null) {
@@ -37,7 +37,7 @@ Vue.component("script-group-config-box", {
<prior-checkbox :v-config="group" v-if="vIsLocation"></prior-checkbox>
</table>
<div :style="{opacity: (!vIsLocation || group.isPrior) ? 1 : 0.5}">
<script-config-box :v-script-config="script" comment="在接收到客户端请求之后立即调用。预置req、resp变量。" @change="changeScript" :v-is-location="vIsLocation"></script-config-box>
<script-config-box :v-script-config="script" :v-auditing-status="vAuditingStatus" comment="在接收到客户端请求之后立即调用。预置req、resp变量。" @change="changeScript" :v-is-location="vIsLocation"></script-config-box>
</div>
</div>`
})

View File

@@ -0,0 +1,32 @@
Vue.component("server-traffic-limit-status-viewer", {
props: ["value"],
data: function () {
let targetTypeName = "流量"
if (this.value != null) {
targetTypeName = this.targetTypeToName(this.value.targetType)
}
return {
status: this.value,
targetTypeName: targetTypeName
}
},
methods: {
targetTypeToName: function (targetType) {
switch (targetType) {
case "traffic":
return "流量"
case "request":
return "请求数"
case "websocketConnections":
return "Websocket连接数"
}
return "流量"
}
},
template: `<span v-if="status != null">
<span v-if="status.dateType == 'day'" class="small red">已达到<span v-if="status.planId > 0">套餐</span>当日{{targetTypeName}}限制</span>
<span v-if="status.dateType == 'month'" class="small red">已达到<span v-if="status.planId > 0">套餐</span>当月{{targetTypeName}}限制</span>
<span v-if="status.dateType == 'total'" class="small red">已达到<span v-if="status.planId > 0">套餐</span>总体{{targetTypeName}}限制</span>
</span>`
})

View File

@@ -0,0 +1,2 @@
// generated by 'langs generate'
window.LANG_MESSAGES_BASE = {"admin_dashboard@ui_dns":"DNS","admin_dashboard@ui_events":"事件","admin_dashboard@ui_overview":"概况","admin_dashboard@ui_user":"用户","admin_dashboard@ui_waf":"WAF"};

View File

@@ -0,0 +1,14 @@
.main-menu .menu .item,
.left-box .menu .item {
font-size: 0.95em;
}
.main-menu .menu .sub-items .item,
.left-box .menu .sub-items .item {
line-height: 1.4;
}
.left-box .menu .item {
line-height: 1.4;
padding-top: 0.2em !important;
padding-bottom: 0.2em !important;
}
/*# sourceMappingURL=en-us.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["en-us.less"],"names":[],"mappings":"AAAA,UACC,MACC;AAFU,SACX,MACC;EACC,iBAAA;;AAHH,UACC,MAKC,WACC;AAPS,SACX,MAKC,WACC;EACC,gBAAA;;AAMJ,SACC,MACC;EACC,gBAAA;EACA,kBAAA;EACA,qBAAA","file":"en-us.css"}

View File

@@ -0,0 +1,2 @@
// generated by 'langs generate'
window.LANG_MESSAGES = {"admin_dashboard@ui_dns":"DNS","admin_dashboard@ui_events":"Events","admin_dashboard@ui_overview":"Overview","admin_dashboard@ui_user":"Users","admin_dashboard@ui_waf":"WAF"};

View File

@@ -0,0 +1,23 @@
.main-menu, .left-box {
.menu {
.item {
font-size: 0.95em;
}
.sub-items {
.item {
line-height: 1.4;
}
}
}
}
.left-box {
.menu {
.item {
line-height: 1.4;
padding-top: 0.2em !important;
padding-bottom: 0.2em !important;
}
}
}

View File

View File

@@ -0,0 +1,952 @@
/*!
* Quill Editor v1.3.7
* https://quilljs.com/
* Copyright (c) 2014, Jason Chen
* Copyright (c) 2013, salesforce.com
*/
.ql-container {
box-sizing: border-box;
font-family: Helvetica, Arial, sans-serif;
font-size: 13px;
height: 100%;
margin: 0px;
position: relative;
}
.ql-container.ql-disabled .ql-tooltip {
visibility: hidden;
}
.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
pointer-events: none;
}
.ql-clipboard {
left: -100000px;
height: 1px;
overflow-y: hidden;
position: absolute;
top: 50%;
}
.ql-clipboard p {
margin: 0;
padding: 0;
}
.ql-editor {
box-sizing: border-box;
line-height: 1.42;
height: 100%;
outline: none;
overflow-y: auto;
padding: 12px 15px;
tab-size: 4;
-moz-tab-size: 4;
text-align: left;
white-space: pre-wrap;
word-wrap: break-word;
}
.ql-editor > * {
cursor: text;
}
.ql-editor p,
.ql-editor ol,
.ql-editor ul,
.ql-editor pre,
.ql-editor blockquote,
.ql-editor h1,
.ql-editor h2,
.ql-editor h3,
.ql-editor h4,
.ql-editor h5,
.ql-editor h6 {
margin: 0;
padding: 0;
counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol,
.ql-editor ul {
padding-left: 1.5em;
}
.ql-editor ol > li,
.ql-editor ul > li {
list-style-type: none;
}
.ql-editor ul > li::before {
content: '\2022';
}
.ql-editor ul[data-checked=true],
.ql-editor ul[data-checked=false] {
pointer-events: none;
}
.ql-editor ul[data-checked=true] > li *,
.ql-editor ul[data-checked=false] > li * {
pointer-events: all;
}
.ql-editor ul[data-checked=true] > li::before,
.ql-editor ul[data-checked=false] > li::before {
color: #777;
cursor: pointer;
pointer-events: all;
}
.ql-editor ul[data-checked=true] > li::before {
content: '\2611';
}
.ql-editor ul[data-checked=false] > li::before {
content: '\2610';
}
.ql-editor li::before {
display: inline-block;
white-space: nowrap;
width: 1.2em;
}
.ql-editor li:not(.ql-direction-rtl)::before {
margin-left: -1.5em;
margin-right: 0.3em;
text-align: right;
}
.ql-editor li.ql-direction-rtl::before {
margin-left: 0.3em;
margin-right: -1.5em;
}
.ql-editor ol li:not(.ql-direction-rtl),
.ql-editor ul li:not(.ql-direction-rtl) {
padding-left: 1.5em;
}
.ql-editor ol li.ql-direction-rtl,
.ql-editor ul li.ql-direction-rtl {
padding-right: 1.5em;
}
.ql-editor ol li {
counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
counter-increment: list-0;
}
.ql-editor ol li:before {
content: counter(list-0, decimal) '. ';
}
.ql-editor ol li.ql-indent-1 {
counter-increment: list-1;
}
.ql-editor ol li.ql-indent-1:before {
content: counter(list-1, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-1 {
counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-2 {
counter-increment: list-2;
}
.ql-editor ol li.ql-indent-2:before {
content: counter(list-2, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-2 {
counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-3 {
counter-increment: list-3;
}
.ql-editor ol li.ql-indent-3:before {
content: counter(list-3, decimal) '. ';
}
.ql-editor ol li.ql-indent-3 {
counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-4 {
counter-increment: list-4;
}
.ql-editor ol li.ql-indent-4:before {
content: counter(list-4, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-4 {
counter-reset: list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-5 {
counter-increment: list-5;
}
.ql-editor ol li.ql-indent-5:before {
content: counter(list-5, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-5 {
counter-reset: list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-6 {
counter-increment: list-6;
}
.ql-editor ol li.ql-indent-6:before {
content: counter(list-6, decimal) '. ';
}
.ql-editor ol li.ql-indent-6 {
counter-reset: list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-7 {
counter-increment: list-7;
}
.ql-editor ol li.ql-indent-7:before {
content: counter(list-7, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-7 {
counter-reset: list-8 list-9;
}
.ql-editor ol li.ql-indent-8 {
counter-increment: list-8;
}
.ql-editor ol li.ql-indent-8:before {
content: counter(list-8, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-8 {
counter-reset: list-9;
}
.ql-editor ol li.ql-indent-9 {
counter-increment: list-9;
}
.ql-editor ol li.ql-indent-9:before {
content: counter(list-9, decimal) '. ';
}
.ql-editor .ql-indent-1:not(.ql-direction-rtl) {
padding-left: 3em;
}
.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
padding-left: 4.5em;
}
.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
padding-right: 3em;
}
.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
padding-right: 4.5em;
}
.ql-editor .ql-indent-2:not(.ql-direction-rtl) {
padding-left: 6em;
}
.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
padding-left: 7.5em;
}
.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
padding-right: 6em;
}
.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
padding-right: 7.5em;
}
.ql-editor .ql-indent-3:not(.ql-direction-rtl) {
padding-left: 9em;
}
.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
padding-left: 10.5em;
}
.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
padding-right: 9em;
}
.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
padding-right: 10.5em;
}
.ql-editor .ql-indent-4:not(.ql-direction-rtl) {
padding-left: 12em;
}
.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
padding-left: 13.5em;
}
.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
padding-right: 12em;
}
.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
padding-right: 13.5em;
}
.ql-editor .ql-indent-5:not(.ql-direction-rtl) {
padding-left: 15em;
}
.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
padding-left: 16.5em;
}
.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
padding-right: 15em;
}
.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
padding-right: 16.5em;
}
.ql-editor .ql-indent-6:not(.ql-direction-rtl) {
padding-left: 18em;
}
.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
padding-left: 19.5em;
}
.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
padding-right: 18em;
}
.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
padding-right: 19.5em;
}
.ql-editor .ql-indent-7:not(.ql-direction-rtl) {
padding-left: 21em;
}
.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
padding-left: 22.5em;
}
.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
padding-right: 21em;
}
.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
padding-right: 22.5em;
}
.ql-editor .ql-indent-8:not(.ql-direction-rtl) {
padding-left: 24em;
}
.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
padding-left: 25.5em;
}
.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
padding-right: 24em;
}
.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
padding-right: 25.5em;
}
.ql-editor .ql-indent-9:not(.ql-direction-rtl) {
padding-left: 27em;
}
.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
padding-left: 28.5em;
}
.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
padding-right: 27em;
}
.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
padding-right: 28.5em;
}
.ql-editor .ql-video {
display: block;
max-width: 100%;
}
.ql-editor .ql-video.ql-align-center {
margin: 0 auto;
}
.ql-editor .ql-video.ql-align-right {
margin: 0 0 0 auto;
}
.ql-editor .ql-bg-black {
background-color: #000;
}
.ql-editor .ql-bg-red {
background-color: #e60000;
}
.ql-editor .ql-bg-orange {
background-color: #f90;
}
.ql-editor .ql-bg-yellow {
background-color: #ff0;
}
.ql-editor .ql-bg-green {
background-color: #008a00;
}
.ql-editor .ql-bg-blue {
background-color: #06c;
}
.ql-editor .ql-bg-purple {
background-color: #93f;
}
.ql-editor .ql-color-white {
color: #fff;
}
.ql-editor .ql-color-red {
color: #e60000;
}
.ql-editor .ql-color-orange {
color: #f90;
}
.ql-editor .ql-color-yellow {
color: #ff0;
}
.ql-editor .ql-color-green {
color: #008a00;
}
.ql-editor .ql-color-blue {
color: #06c;
}
.ql-editor .ql-color-purple {
color: #93f;
}
.ql-editor .ql-font-serif {
font-family: Georgia, Times New Roman, serif;
}
.ql-editor .ql-font-monospace {
font-family: Monaco, Courier New, monospace;
}
.ql-editor .ql-size-small {
font-size: 0.75em;
}
.ql-editor .ql-size-large {
font-size: 1.5em;
}
.ql-editor .ql-size-huge {
font-size: 2.5em;
}
.ql-editor .ql-direction-rtl {
direction: rtl;
text-align: inherit;
}
.ql-editor .ql-align-center {
text-align: center;
}
.ql-editor .ql-align-justify {
text-align: justify;
}
.ql-editor .ql-align-right {
text-align: right;
}
.ql-editor.ql-blank::before {
color: rgba(0,0,0,0.6);
content: attr(data-placeholder);
font-style: italic;
left: 15px;
pointer-events: none;
position: absolute;
right: 15px;
}
.ql-bubble.ql-toolbar:after,
.ql-bubble .ql-toolbar:after {
clear: both;
content: '';
display: table;
}
.ql-bubble.ql-toolbar button,
.ql-bubble .ql-toolbar button {
background: none;
border: none;
cursor: pointer;
display: inline-block;
float: left;
height: 24px;
padding: 3px 5px;
width: 28px;
}
.ql-bubble.ql-toolbar button svg,
.ql-bubble .ql-toolbar button svg {
float: left;
height: 100%;
}
.ql-bubble.ql-toolbar button:active:hover,
.ql-bubble .ql-toolbar button:active:hover {
outline: none;
}
.ql-bubble.ql-toolbar input.ql-image[type=file],
.ql-bubble .ql-toolbar input.ql-image[type=file] {
display: none;
}
.ql-bubble.ql-toolbar button:hover,
.ql-bubble .ql-toolbar button:hover,
.ql-bubble.ql-toolbar button:focus,
.ql-bubble .ql-toolbar button:focus,
.ql-bubble.ql-toolbar button.ql-active,
.ql-bubble .ql-toolbar button.ql-active,
.ql-bubble.ql-toolbar .ql-picker-label:hover,
.ql-bubble .ql-toolbar .ql-picker-label:hover,
.ql-bubble.ql-toolbar .ql-picker-label.ql-active,
.ql-bubble .ql-toolbar .ql-picker-label.ql-active,
.ql-bubble.ql-toolbar .ql-picker-item:hover,
.ql-bubble .ql-toolbar .ql-picker-item:hover,
.ql-bubble.ql-toolbar .ql-picker-item.ql-selected,
.ql-bubble .ql-toolbar .ql-picker-item.ql-selected {
color: #fff;
}
.ql-bubble.ql-toolbar button:hover .ql-fill,
.ql-bubble .ql-toolbar button:hover .ql-fill,
.ql-bubble.ql-toolbar button:focus .ql-fill,
.ql-bubble .ql-toolbar button:focus .ql-fill,
.ql-bubble.ql-toolbar button.ql-active .ql-fill,
.ql-bubble .ql-toolbar button.ql-active .ql-fill,
.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-fill,
.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-fill,
.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-fill,
.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-fill,
.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-fill,
.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-fill,
.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-fill,
.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-fill,
.ql-bubble.ql-toolbar button:hover .ql-stroke.ql-fill,
.ql-bubble .ql-toolbar button:hover .ql-stroke.ql-fill,
.ql-bubble.ql-toolbar button:focus .ql-stroke.ql-fill,
.ql-bubble .ql-toolbar button:focus .ql-stroke.ql-fill,
.ql-bubble.ql-toolbar button.ql-active .ql-stroke.ql-fill,
.ql-bubble .ql-toolbar button.ql-active .ql-stroke.ql-fill,
.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,
.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill {
fill: #fff;
}
.ql-bubble.ql-toolbar button:hover .ql-stroke,
.ql-bubble .ql-toolbar button:hover .ql-stroke,
.ql-bubble.ql-toolbar button:focus .ql-stroke,
.ql-bubble .ql-toolbar button:focus .ql-stroke,
.ql-bubble.ql-toolbar button.ql-active .ql-stroke,
.ql-bubble .ql-toolbar button.ql-active .ql-stroke,
.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke,
.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke,
.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke,
.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke,
.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke,
.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke,
.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
.ql-bubble.ql-toolbar button:hover .ql-stroke-miter,
.ql-bubble .ql-toolbar button:hover .ql-stroke-miter,
.ql-bubble.ql-toolbar button:focus .ql-stroke-miter,
.ql-bubble .ql-toolbar button:focus .ql-stroke-miter,
.ql-bubble.ql-toolbar button.ql-active .ql-stroke-miter,
.ql-bubble .ql-toolbar button.ql-active .ql-stroke-miter,
.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,
.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter {
stroke: #fff;
}
@media (pointer: coarse) {
.ql-bubble.ql-toolbar button:hover:not(.ql-active),
.ql-bubble .ql-toolbar button:hover:not(.ql-active) {
color: #ccc;
}
.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-fill,
.ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-fill,
.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,
.ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill {
fill: #ccc;
}
.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke,
.ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke,
.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,
.ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter {
stroke: #ccc;
}
}
.ql-bubble {
box-sizing: border-box;
}
.ql-bubble * {
box-sizing: border-box;
}
.ql-bubble .ql-hidden {
display: none;
}
.ql-bubble .ql-out-bottom,
.ql-bubble .ql-out-top {
visibility: hidden;
}
.ql-bubble .ql-tooltip {
position: absolute;
transform: translateY(10px);
}
.ql-bubble .ql-tooltip a {
cursor: pointer;
text-decoration: none;
}
.ql-bubble .ql-tooltip.ql-flip {
transform: translateY(-10px);
}
.ql-bubble .ql-formats {
display: inline-block;
vertical-align: middle;
}
.ql-bubble .ql-formats:after {
clear: both;
content: '';
display: table;
}
.ql-bubble .ql-stroke {
fill: none;
stroke: #ccc;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
}
.ql-bubble .ql-stroke-miter {
fill: none;
stroke: #ccc;
stroke-miterlimit: 10;
stroke-width: 2;
}
.ql-bubble .ql-fill,
.ql-bubble .ql-stroke.ql-fill {
fill: #ccc;
}
.ql-bubble .ql-empty {
fill: none;
}
.ql-bubble .ql-even {
fill-rule: evenodd;
}
.ql-bubble .ql-thin,
.ql-bubble .ql-stroke.ql-thin {
stroke-width: 1;
}
.ql-bubble .ql-transparent {
opacity: 0.4;
}
.ql-bubble .ql-direction svg:last-child {
display: none;
}
.ql-bubble .ql-direction.ql-active svg:last-child {
display: inline;
}
.ql-bubble .ql-direction.ql-active svg:first-child {
display: none;
}
.ql-bubble .ql-editor h1 {
font-size: 2em;
}
.ql-bubble .ql-editor h2 {
font-size: 1.5em;
}
.ql-bubble .ql-editor h3 {
font-size: 1.17em;
}
.ql-bubble .ql-editor h4 {
font-size: 1em;
}
.ql-bubble .ql-editor h5 {
font-size: 0.83em;
}
.ql-bubble .ql-editor h6 {
font-size: 0.67em;
}
.ql-bubble .ql-editor a {
text-decoration: underline;
}
.ql-bubble .ql-editor blockquote {
border-left: 4px solid #ccc;
margin-bottom: 5px;
margin-top: 5px;
padding-left: 16px;
}
.ql-bubble .ql-editor code,
.ql-bubble .ql-editor pre {
background-color: #f0f0f0;
border-radius: 3px;
}
.ql-bubble .ql-editor pre {
white-space: pre-wrap;
margin-bottom: 5px;
margin-top: 5px;
padding: 5px 10px;
}
.ql-bubble .ql-editor code {
font-size: 85%;
padding: 2px 4px;
}
.ql-bubble .ql-editor pre.ql-syntax {
background-color: #23241f;
color: #f8f8f2;
overflow: visible;
}
.ql-bubble .ql-editor img {
max-width: 100%;
}
.ql-bubble .ql-picker {
color: #ccc;
display: inline-block;
float: left;
font-size: 14px;
font-weight: 500;
height: 24px;
position: relative;
vertical-align: middle;
}
.ql-bubble .ql-picker-label {
cursor: pointer;
display: inline-block;
height: 100%;
padding-left: 8px;
padding-right: 2px;
position: relative;
width: 100%;
}
.ql-bubble .ql-picker-label::before {
display: inline-block;
line-height: 22px;
}
.ql-bubble .ql-picker-options {
background-color: #444;
display: none;
min-width: 100%;
padding: 4px 8px;
position: absolute;
white-space: nowrap;
}
.ql-bubble .ql-picker-options .ql-picker-item {
cursor: pointer;
display: block;
padding-bottom: 5px;
padding-top: 5px;
}
.ql-bubble .ql-picker.ql-expanded .ql-picker-label {
color: #777;
z-index: 2;
}
.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-fill {
fill: #777;
}
.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-stroke {
stroke: #777;
}
.ql-bubble .ql-picker.ql-expanded .ql-picker-options {
display: block;
margin-top: -1px;
top: 100%;
z-index: 1;
}
.ql-bubble .ql-color-picker,
.ql-bubble .ql-icon-picker {
width: 28px;
}
.ql-bubble .ql-color-picker .ql-picker-label,
.ql-bubble .ql-icon-picker .ql-picker-label {
padding: 2px 4px;
}
.ql-bubble .ql-color-picker .ql-picker-label svg,
.ql-bubble .ql-icon-picker .ql-picker-label svg {
right: 4px;
}
.ql-bubble .ql-icon-picker .ql-picker-options {
padding: 4px 0px;
}
.ql-bubble .ql-icon-picker .ql-picker-item {
height: 24px;
width: 24px;
padding: 2px 4px;
}
.ql-bubble .ql-color-picker .ql-picker-options {
padding: 3px 5px;
width: 152px;
}
.ql-bubble .ql-color-picker .ql-picker-item {
border: 1px solid transparent;
float: left;
height: 16px;
margin: 2px;
padding: 0px;
width: 16px;
}
.ql-bubble .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg {
position: absolute;
margin-top: -9px;
right: 0;
top: 50%;
width: 18px;
}
.ql-bubble .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before,
.ql-bubble .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before,
.ql-bubble .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before,
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before,
.ql-bubble .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before,
.ql-bubble .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before {
content: attr(data-label);
}
.ql-bubble .ql-picker.ql-header {
width: 98px;
}
.ql-bubble .ql-picker.ql-header .ql-picker-label::before,
.ql-bubble .ql-picker.ql-header .ql-picker-item::before {
content: 'Normal';
}
.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
content: 'Heading 1';
}
.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
content: 'Heading 2';
}
.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
content: 'Heading 3';
}
.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
content: 'Heading 4';
}
.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
content: 'Heading 5';
}
.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
content: 'Heading 6';
}
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
font-size: 2em;
}
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
font-size: 1.5em;
}
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
font-size: 1.17em;
}
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
font-size: 1em;
}
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
font-size: 0.83em;
}
.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
font-size: 0.67em;
}
.ql-bubble .ql-picker.ql-font {
width: 108px;
}
.ql-bubble .ql-picker.ql-font .ql-picker-label::before,
.ql-bubble .ql-picker.ql-font .ql-picker-item::before {
content: 'Sans Serif';
}
.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,
.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
content: 'Serif';
}
.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,
.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
content: 'Monospace';
}
.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
font-family: Georgia, Times New Roman, serif;
}
.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
font-family: Monaco, Courier New, monospace;
}
.ql-bubble .ql-picker.ql-size {
width: 98px;
}
.ql-bubble .ql-picker.ql-size .ql-picker-label::before,
.ql-bubble .ql-picker.ql-size .ql-picker-item::before {
content: 'Normal';
}
.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=small]::before,
.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
content: 'Small';
}
.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=large]::before,
.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
content: 'Large';
}
.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,
.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
content: 'Huge';
}
.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
font-size: 10px;
}
.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
font-size: 18px;
}
.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
font-size: 32px;
}
.ql-bubble .ql-color-picker.ql-background .ql-picker-item {
background-color: #fff;
}
.ql-bubble .ql-color-picker.ql-color .ql-picker-item {
background-color: #000;
}
.ql-bubble .ql-toolbar .ql-formats {
margin: 8px 12px 8px 0px;
}
.ql-bubble .ql-toolbar .ql-formats:first-child {
margin-left: 12px;
}
.ql-bubble .ql-color-picker svg {
margin: 1px;
}
.ql-bubble .ql-color-picker .ql-picker-item.ql-selected,
.ql-bubble .ql-color-picker .ql-picker-item:hover {
border-color: #fff;
}
.ql-bubble .ql-tooltip {
background-color: #444;
border-radius: 25px;
color: #fff;
}
.ql-bubble .ql-tooltip-arrow {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
content: " ";
display: block;
left: 50%;
margin-left: -6px;
position: absolute;
}
.ql-bubble .ql-tooltip:not(.ql-flip) .ql-tooltip-arrow {
border-bottom: 6px solid #444;
top: -6px;
}
.ql-bubble .ql-tooltip.ql-flip .ql-tooltip-arrow {
border-top: 6px solid #444;
bottom: -6px;
}
.ql-bubble .ql-tooltip.ql-editing .ql-tooltip-editor {
display: block;
}
.ql-bubble .ql-tooltip.ql-editing .ql-formats {
visibility: hidden;
}
.ql-bubble .ql-tooltip-editor {
display: none;
}
.ql-bubble .ql-tooltip-editor input[type=text] {
background: transparent;
border: none;
color: #fff;
font-size: 13px;
height: 100%;
outline: none;
padding: 10px 20px;
position: absolute;
width: 100%;
}
.ql-bubble .ql-tooltip-editor a {
top: 10px;
position: absolute;
right: 20px;
}
.ql-bubble .ql-tooltip-editor a:before {
color: #ccc;
content: "\D7";
font-size: 16px;
font-weight: bold;
}
.ql-container.ql-bubble:not(.ql-disabled) a {
position: relative;
white-space: nowrap;
}
.ql-container.ql-bubble:not(.ql-disabled) a::before {
background-color: #444;
border-radius: 15px;
top: -5px;
font-size: 12px;
color: #fff;
content: attr(href);
font-weight: normal;
overflow: hidden;
padding: 5px 15px;
text-decoration: none;
z-index: 1;
}
.ql-container.ql-bubble:not(.ql-disabled) a::after {
border-top: 6px solid #444;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
top: 0;
content: " ";
height: 0;
width: 0;
}
.ql-container.ql-bubble:not(.ql-disabled) a::before,
.ql-container.ql-bubble:not(.ql-disabled) a::after {
left: 0;
margin-left: 50%;
position: absolute;
transform: translate(-50%, -100%);
transition: visibility 0s ease 200ms;
visibility: hidden;
}
.ql-container.ql-bubble:not(.ql-disabled) a:hover::before,
.ql-container.ql-bubble:not(.ql-disabled) a:hover::after {
visibility: visible;
}

View File

@@ -0,0 +1,397 @@
/*!
* Quill Editor v1.3.7
* https://quilljs.com/
* Copyright (c) 2014, Jason Chen
* Copyright (c) 2013, salesforce.com
*/
.ql-container {
box-sizing: border-box;
font-family: Helvetica, Arial, sans-serif;
font-size: 13px;
height: 100%;
margin: 0px;
position: relative;
}
.ql-container.ql-disabled .ql-tooltip {
visibility: hidden;
}
.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
pointer-events: none;
}
.ql-clipboard {
left: -100000px;
height: 1px;
overflow-y: hidden;
position: absolute;
top: 50%;
}
.ql-clipboard p {
margin: 0;
padding: 0;
}
.ql-editor {
box-sizing: border-box;
line-height: 1.42;
height: 100%;
outline: none;
overflow-y: auto;
padding: 12px 15px;
tab-size: 4;
-moz-tab-size: 4;
text-align: left;
white-space: pre-wrap;
word-wrap: break-word;
}
.ql-editor > * {
cursor: text;
}
.ql-editor p,
.ql-editor ol,
.ql-editor ul,
.ql-editor pre,
.ql-editor blockquote,
.ql-editor h1,
.ql-editor h2,
.ql-editor h3,
.ql-editor h4,
.ql-editor h5,
.ql-editor h6 {
margin: 0;
padding: 0;
counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol,
.ql-editor ul {
padding-left: 1.5em;
}
.ql-editor ol > li,
.ql-editor ul > li {
list-style-type: none;
}
.ql-editor ul > li::before {
content: '\2022';
}
.ql-editor ul[data-checked=true],
.ql-editor ul[data-checked=false] {
pointer-events: none;
}
.ql-editor ul[data-checked=true] > li *,
.ql-editor ul[data-checked=false] > li * {
pointer-events: all;
}
.ql-editor ul[data-checked=true] > li::before,
.ql-editor ul[data-checked=false] > li::before {
color: #777;
cursor: pointer;
pointer-events: all;
}
.ql-editor ul[data-checked=true] > li::before {
content: '\2611';
}
.ql-editor ul[data-checked=false] > li::before {
content: '\2610';
}
.ql-editor li::before {
display: inline-block;
white-space: nowrap;
width: 1.2em;
}
.ql-editor li:not(.ql-direction-rtl)::before {
margin-left: -1.5em;
margin-right: 0.3em;
text-align: right;
}
.ql-editor li.ql-direction-rtl::before {
margin-left: 0.3em;
margin-right: -1.5em;
}
.ql-editor ol li:not(.ql-direction-rtl),
.ql-editor ul li:not(.ql-direction-rtl) {
padding-left: 1.5em;
}
.ql-editor ol li.ql-direction-rtl,
.ql-editor ul li.ql-direction-rtl {
padding-right: 1.5em;
}
.ql-editor ol li {
counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
counter-increment: list-0;
}
.ql-editor ol li:before {
content: counter(list-0, decimal) '. ';
}
.ql-editor ol li.ql-indent-1 {
counter-increment: list-1;
}
.ql-editor ol li.ql-indent-1:before {
content: counter(list-1, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-1 {
counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-2 {
counter-increment: list-2;
}
.ql-editor ol li.ql-indent-2:before {
content: counter(list-2, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-2 {
counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-3 {
counter-increment: list-3;
}
.ql-editor ol li.ql-indent-3:before {
content: counter(list-3, decimal) '. ';
}
.ql-editor ol li.ql-indent-3 {
counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-4 {
counter-increment: list-4;
}
.ql-editor ol li.ql-indent-4:before {
content: counter(list-4, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-4 {
counter-reset: list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-5 {
counter-increment: list-5;
}
.ql-editor ol li.ql-indent-5:before {
content: counter(list-5, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-5 {
counter-reset: list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-6 {
counter-increment: list-6;
}
.ql-editor ol li.ql-indent-6:before {
content: counter(list-6, decimal) '. ';
}
.ql-editor ol li.ql-indent-6 {
counter-reset: list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-7 {
counter-increment: list-7;
}
.ql-editor ol li.ql-indent-7:before {
content: counter(list-7, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-7 {
counter-reset: list-8 list-9;
}
.ql-editor ol li.ql-indent-8 {
counter-increment: list-8;
}
.ql-editor ol li.ql-indent-8:before {
content: counter(list-8, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-8 {
counter-reset: list-9;
}
.ql-editor ol li.ql-indent-9 {
counter-increment: list-9;
}
.ql-editor ol li.ql-indent-9:before {
content: counter(list-9, decimal) '. ';
}
.ql-editor .ql-indent-1:not(.ql-direction-rtl) {
padding-left: 3em;
}
.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
padding-left: 4.5em;
}
.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
padding-right: 3em;
}
.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
padding-right: 4.5em;
}
.ql-editor .ql-indent-2:not(.ql-direction-rtl) {
padding-left: 6em;
}
.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
padding-left: 7.5em;
}
.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
padding-right: 6em;
}
.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
padding-right: 7.5em;
}
.ql-editor .ql-indent-3:not(.ql-direction-rtl) {
padding-left: 9em;
}
.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
padding-left: 10.5em;
}
.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
padding-right: 9em;
}
.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
padding-right: 10.5em;
}
.ql-editor .ql-indent-4:not(.ql-direction-rtl) {
padding-left: 12em;
}
.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
padding-left: 13.5em;
}
.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
padding-right: 12em;
}
.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
padding-right: 13.5em;
}
.ql-editor .ql-indent-5:not(.ql-direction-rtl) {
padding-left: 15em;
}
.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
padding-left: 16.5em;
}
.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
padding-right: 15em;
}
.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
padding-right: 16.5em;
}
.ql-editor .ql-indent-6:not(.ql-direction-rtl) {
padding-left: 18em;
}
.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
padding-left: 19.5em;
}
.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
padding-right: 18em;
}
.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
padding-right: 19.5em;
}
.ql-editor .ql-indent-7:not(.ql-direction-rtl) {
padding-left: 21em;
}
.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
padding-left: 22.5em;
}
.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
padding-right: 21em;
}
.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
padding-right: 22.5em;
}
.ql-editor .ql-indent-8:not(.ql-direction-rtl) {
padding-left: 24em;
}
.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
padding-left: 25.5em;
}
.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
padding-right: 24em;
}
.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
padding-right: 25.5em;
}
.ql-editor .ql-indent-9:not(.ql-direction-rtl) {
padding-left: 27em;
}
.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
padding-left: 28.5em;
}
.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
padding-right: 27em;
}
.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
padding-right: 28.5em;
}
.ql-editor .ql-video {
display: block;
max-width: 100%;
}
.ql-editor .ql-video.ql-align-center {
margin: 0 auto;
}
.ql-editor .ql-video.ql-align-right {
margin: 0 0 0 auto;
}
.ql-editor .ql-bg-black {
background-color: #000;
}
.ql-editor .ql-bg-red {
background-color: #e60000;
}
.ql-editor .ql-bg-orange {
background-color: #f90;
}
.ql-editor .ql-bg-yellow {
background-color: #ff0;
}
.ql-editor .ql-bg-green {
background-color: #008a00;
}
.ql-editor .ql-bg-blue {
background-color: #06c;
}
.ql-editor .ql-bg-purple {
background-color: #93f;
}
.ql-editor .ql-color-white {
color: #fff;
}
.ql-editor .ql-color-red {
color: #e60000;
}
.ql-editor .ql-color-orange {
color: #f90;
}
.ql-editor .ql-color-yellow {
color: #ff0;
}
.ql-editor .ql-color-green {
color: #008a00;
}
.ql-editor .ql-color-blue {
color: #06c;
}
.ql-editor .ql-color-purple {
color: #93f;
}
.ql-editor .ql-font-serif {
font-family: Georgia, Times New Roman, serif;
}
.ql-editor .ql-font-monospace {
font-family: Monaco, Courier New, monospace;
}
.ql-editor .ql-size-small {
font-size: 0.75em;
}
.ql-editor .ql-size-large {
font-size: 1.5em;
}
.ql-editor .ql-size-huge {
font-size: 2.5em;
}
.ql-editor .ql-direction-rtl {
direction: rtl;
text-align: inherit;
}
.ql-editor .ql-align-center {
text-align: center;
}
.ql-editor .ql-align-justify {
text-align: justify;
}
.ql-editor .ql-align-right {
text-align: right;
}
.ql-editor.ql-blank::before {
color: rgba(0,0,0,0.6);
content: attr(data-placeholder);
font-style: italic;
left: 15px;
pointer-events: none;
position: absolute;
right: 15px;
}

File diff suppressed because it is too large Load Diff

11562
web/public/js/quill/quill.js Normal file

File diff suppressed because it is too large Load Diff

8
web/public/js/quill/quill.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More