Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e5802dcd1 | ||
|
|
2b6d1c70a8 | ||
|
|
faaef3b353 | ||
|
|
6ffa1d4a1d | ||
|
|
10f0e0dcca | ||
|
|
0afad2e192 | ||
|
|
6a1e7266b9 | ||
|
|
6ffc552bd8 | ||
|
|
8d8245971a | ||
|
|
a2f730d57e | ||
|
|
bf282c41fd | ||
|
|
3e3f7b2cc6 | ||
|
|
0580e063be | ||
|
|
2924aad25e | ||
|
|
39907299cd | ||
|
|
77705895d5 | ||
|
|
3fb85edf0b | ||
|
|
0e7a968cac | ||
|
|
195a9dc771 | ||
|
|
ca227a846a | ||
|
|
e1057fc76e | ||
|
|
b5168c3174 | ||
|
|
4ced00de50 | ||
|
|
750c929506 | ||
|
|
8b46a5a5af | ||
|
|
10d606a101 | ||
|
|
45d36dadcb | ||
|
|
61484d5cb8 | ||
|
|
b7775a99ef | ||
|
|
c45ba38ffa | ||
|
|
e97d9fb8a0 | ||
|
|
bb56ec9ec5 | ||
|
|
6d8f659558 | ||
|
|
7e9047c8fe | ||
|
|
b5af560b67 | ||
|
|
5622636870 | ||
|
|
372c276afc | ||
|
|
250b5fdd10 | ||
|
|
d1fb0bf8b7 | ||
|
|
490c273d36 | ||
|
|
0773dbaa28 | ||
|
|
4d04964740 | ||
|
|
537e6c9a5e | ||
|
|
082a6721f3 | ||
|
|
d515a20129 | ||
|
|
a66d3ef61a | ||
|
|
3b05b1a933 | ||
|
|
105479ce4b | ||
|
|
1a950e3aad | ||
|
|
79fa76e039 | ||
|
|
9db9a70a68 | ||
|
|
632862c742 | ||
|
|
76d0783f6f | ||
|
|
fc6a0e9813 | ||
|
|
038e289057 | ||
|
|
4b2a4af1e7 | ||
|
|
0fb2555bea | ||
|
|
bd8a809065 | ||
|
|
fc6fb7dce2 | ||
|
|
bacb94abe6 | ||
|
|
eb04421412 | ||
|
|
2fbd5a785e | ||
|
|
d8ae1525be | ||
|
|
daaa165d25 | ||
|
|
27ea02be5e | ||
|
|
8eac1ef307 | ||
|
|
1181974ea5 | ||
|
|
5f57fa0470 | ||
|
|
c0ad74ab7b | ||
|
|
c27905e1cc | ||
|
|
cadb65a85f | ||
|
|
b3c152685a | ||
|
|
16ce0726f8 | ||
|
|
5be306e087 | ||
|
|
0d21fc27ab | ||
|
|
7deaa678bc | ||
|
|
0d230a9237 | ||
|
|
8453d754f1 |
@@ -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()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM --platform=linux/amd64 alpine:latest
|
||||
LABEL maintainer="goedge.cdn@gmail.com"
|
||||
ENV TZ "Asia/Shanghai"
|
||||
ENV VERSION 1.3.1
|
||||
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
29
go.mod
@@ -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.37.4
|
||||
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.12.0
|
||||
golang.org/x/sys v0.11.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-20230808223545-4887780b67fb // 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.3.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-20230809150735-7b3493d9a819 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
golang.org/x/tools v0.12.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
|
||||
|
||||
85
go.sum
85
go.sum
@@ -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,19 +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-20230808223545-4887780b67fb h1:oqpb3Cwpc7EOml5PVGMYbSGmwNui2R7i8IW83gs4W0c=
|
||||
github.com/google/pprof v0.0.0-20230808223545-4887780b67fb/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=
|
||||
@@ -114,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=
|
||||
@@ -132,16 +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.3.2 h1:rRgN3WfnKbyik4dBV8A6girlJVxGand/d+jVKbQq5GI=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.2/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
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/quic-go v0.37.3 h1:pkHH3xaMNUNAh6OtgEV/0K6Fz+YIJXhPzgd/ShiRDm4=
|
||||
github.com/quic-go/quic-go v0.37.3/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU=
|
||||
github.com/quic-go/quic-go v0.37.4 h1:ke8B73yMCWGq9MfrCCAw0Uzdm7GaViC3i39dsIdDlH4=
|
||||
github.com/quic-go/quic-go v0.37.4/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU=
|
||||
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=
|
||||
@@ -173,25 +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.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
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-20230807204917-050eac23e9de h1:l5Za6utMv/HsBWWqzt4S8X17j+kt1uVETUX5UFhn2rE=
|
||||
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 h1:EDuYyU/MkFXllv9QF9819VlI9a4tzGuCbhG0ExK9o1U=
|
||||
golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
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=
|
||||
@@ -200,11 +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.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.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=
|
||||
@@ -218,10 +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.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
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=
|
||||
@@ -229,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=
|
||||
@@ -251,29 +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.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.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.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
|
||||
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.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.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/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
||||
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
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=
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
162
internal/utils/exec/cmd.go
Normal file
162
internal/utils/exec/cmd.go
Normal 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
|
||||
}
|
||||
61
internal/utils/exec/cmd_test.go
Normal file
61
internal/utils/exec/cmd_test.go
Normal 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())
|
||||
}
|
||||
58
internal/utils/exec/look_linux.go
Normal file
58
internal/utils/exec/look_linux.go
Normal 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,
|
||||
}
|
||||
}
|
||||
10
internal/utils/exec/look_others.go
Normal file
10
internal/utils/exec/look_others.go
Normal 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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)).
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
28
internal/web/actions/default/servers/deleteServers.go
Normal file
28
internal/web/actions/default/servers/deleteServers.go
Normal 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()
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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)).
|
||||
|
||||
82
internal/web/actions/default/servers/iplists/deleteCount.go
Normal file
82
internal/web/actions/default/servers/iplists/deleteCount.go
Normal 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()
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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)).
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
// 记录最近使用
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
17
internal/web/actions/default/settings/lang/init.go
Normal file
17
internal/web/actions/default/settings/lang/init.go
Normal 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()
|
||||
})
|
||||
}
|
||||
36
internal/web/actions/default/settings/lang/switch.go
Normal file
36
internal/web/actions/default/settings/lang/switch.go
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>`
|
||||
})
|
||||
@@ -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>`
|
||||
})
|
||||
@@ -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>`
|
||||
|
||||
@@ -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>`
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
<button class="ui button basic" type="button" @click.prevent="deleteCount" v-if="vTotal != null && vTotal >= MaxDeletes">批量删除{{MaxDeletes}}个</button>
|
||||
|
||||
|
||||
<button class="ui button basic" type="button" @click.prevent="cancelChecked">取消选中</button>
|
||||
</div>
|
||||
<table class="ui table selectable celled" v-if="items.length > 0">
|
||||
<thead>
|
||||
|
||||
@@ -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>∞</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>∞</span>: <span v-if="range.totalPrice > 0">{{range.totalPrice}}元</span><span v-else="">{{range.pricePerMB}}元/MiB</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">默认 <a href="" @click.prevent="keyOptionsVisible = !keyOptionsVisible"><span class="small">[修改]</span></a></div>
|
||||
<div v-show="cacheConfig.key.isOn">使用主域名:{{cacheConfig.key.scheme}}://{{cacheConfig.key.host}} <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">缓存条件 <a href="" style="font-size: 0.8em" @click.prevent="$refs.cacheRefsConfigBoxRef.addRef(false)">[添加]</a> <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">
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) -> \${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"> [默认页面]</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">
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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>`
|
||||
})
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
})
|
||||
@@ -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) -> \${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>
|
||||
`
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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[大小写不敏感] ':'')"><{{operatorName(rule.operator)}}></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 -->
|
||||
|
||||
@@ -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[大小写不敏感] ':'')"><{{operatorName(rule.operator)}}></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 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">管理员审核结果:驳回 驳回理由:{{auditingStatus.rejectedReason}}</span>
|
||||
<span class="red" v-else>当前脚本将在审核后生效,请耐心等待审核结果。 <a href="/servers/user-scripts" target="_blank" v-if="isPlus()">去审核 »</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>
|
||||
|
||||
@@ -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>`
|
||||
})
|
||||
@@ -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>`
|
||||
})
|
||||
2
web/public/js/langs/base.js
Normal file
2
web/public/js/langs/base.js
Normal 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"};
|
||||
14
web/public/js/langs/en-us.css
Normal file
14
web/public/js/langs/en-us.css
Normal 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 */
|
||||
1
web/public/js/langs/en-us.css.map
Normal file
1
web/public/js/langs/en-us.css.map
Normal 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"}
|
||||
2
web/public/js/langs/en-us.js
Normal file
2
web/public/js/langs/en-us.js
Normal 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"};
|
||||
23
web/public/js/langs/en-us.less
Normal file
23
web/public/js/langs/en-us.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
0
web/public/js/langs/zh-cn.css
Normal file
0
web/public/js/langs/zh-cn.css
Normal file
952
web/public/js/quill/quill.bubble.css
Normal file
952
web/public/js/quill/quill.bubble.css
Normal 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;
|
||||
}
|
||||
397
web/public/js/quill/quill.core.css
Normal file
397
web/public/js/quill/quill.core.css
Normal 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;
|
||||
}
|
||||
8594
web/public/js/quill/quill.core.js
Normal file
8594
web/public/js/quill/quill.core.js
Normal file
File diff suppressed because it is too large
Load Diff
11562
web/public/js/quill/quill.js
Normal file
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
8
web/public/js/quill/quill.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/public/js/quill/quill.min.js.map
Normal file
1
web/public/js/quill/quill.min.js.map
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
Reference in New Issue
Block a user