├── .dockerignore ├── .gitignore ├── Makefile ├── README.md ├── apiserver ├── Makefile ├── README.md ├── conf │ ├── config.default.yaml │ └── config.docker.yaml ├── go.mod ├── go.sum ├── handler │ ├── blacklist │ │ └── ip.go │ ├── certificate │ │ └── cert.go │ ├── helpers.go │ ├── node │ │ └── node.go │ ├── ratelimit │ │ └── limit.go │ ├── sample_log │ │ └── attack_log.go │ ├── site │ │ ├── region_blacklist.go │ │ └── site.go │ ├── user │ │ └── user.go │ └── whitelist │ │ ├── ip.go │ │ └── url.go ├── internal │ ├── cache │ │ ├── cache.go │ │ └── user_cache.go │ ├── config │ │ └── config.go │ ├── ecode │ │ ├── README.md │ │ ├── code.go │ │ └── errcode.go │ ├── event │ │ ├── bus.go │ │ └── handler.go │ ├── model │ │ ├── blacklist_ip.go │ │ ├── cert.go │ │ ├── guard.go │ │ ├── migration.go │ │ ├── model.go │ │ ├── node.go │ │ ├── ratelimit.go │ │ ├── role.go │ │ ├── sample_log.go │ │ ├── site.go │ │ ├── site_config.go │ │ ├── site_origin.go │ │ ├── site_region_blacklist.go │ │ ├── user.go │ │ ├── user_role_ref.go │ │ ├── whitelist_ip.go │ │ └── whitelist_url.go │ ├── repository │ │ ├── blacklist_ip_repo.go │ │ ├── cert_repo.go │ │ ├── ratelimit_repo.go │ │ ├── repository.go │ │ ├── site_config_repo.go │ │ ├── site_origin_repo.go │ │ ├── site_repo.go │ │ ├── user_repo.go │ │ ├── whitelist_ip_repo.go │ │ └── whitelist_url_repo.go │ └── service │ │ ├── blacklist_ip_service.go │ │ ├── cert_service.go │ │ ├── node_service.go │ │ ├── ratelimit_service.go │ │ ├── sample_log_service.go │ │ ├── service.go │ │ ├── site_service.go │ │ ├── user_service.go │ │ └── whitelist_service.go ├── main.go ├── pkg │ ├── auth │ │ └── auth.go │ ├── graceful │ │ └── graceful.go │ ├── logx │ │ ├── logger.go │ │ └── zap.go │ ├── orm │ │ └── orm.go │ ├── redis │ │ ├── driver.go │ │ └── redis.go │ ├── token │ │ └── jwt.go │ └── utils │ │ ├── helpers.go │ │ ├── net.go │ │ └── pem.go └── router │ ├── api.go │ └── middleware │ ├── auth.go │ ├── header.go │ ├── logging.go │ └── requestid.go ├── docker-compose.yml ├── docker ├── apiserver │ └── Dockerfile ├── guard │ └── Dockerfile ├── prometheus │ ├── docker-compose.yml │ ├── prometheus.yml │ └── waf-1690340785108.json └── ui │ ├── Dockerfile │ └── nginx-default.conf ├── guard ├── certs │ ├── server.crt │ └── server.key ├── ipdb │ └── ipipfree.ipdb ├── lib │ ├── apis │ │ ├── configs.lua │ │ ├── init.lua │ │ └── metrics.lua │ ├── balancer.lua │ ├── binaryheap.lua │ ├── certificates.lua │ ├── configs.lua │ ├── constants.lua │ ├── core.lua │ ├── core │ │ ├── blacklist │ │ │ ├── ip.lua │ │ │ └── region.lua │ │ ├── ratelimit.lua │ │ └── whitelist │ │ │ ├── ip.lua │ │ │ └── url.lua │ ├── ctx.lua │ ├── events.lua │ ├── geo │ │ ├── init.lua │ │ └── parser │ │ │ ├── ip2region │ │ │ ├── libxdb.dylib │ │ │ ├── libxdb.so │ │ │ ├── xdb.lua │ │ │ └── xdbffi.lua │ │ │ └── ipip │ │ │ ├── city.lua │ │ │ └── reader.lua │ ├── lock.lua │ ├── log.lua │ ├── metrics.lua │ ├── muxwaf.lua │ ├── origins.lua │ ├── page │ │ ├── 403.lua │ │ ├── 410.lua │ │ └── 500.lua │ ├── resty │ │ ├── balancer │ │ │ ├── chash.lua │ │ │ ├── roundrobin.lua │ │ │ └── utils.lua │ │ ├── counter.lua │ │ ├── ctxdump.lua │ │ ├── dns │ │ │ └── resolver.lua │ │ ├── http │ │ │ ├── http_connect.lua │ │ │ ├── http_headers.lua │ │ │ └── init.lua │ │ ├── ipmatcher.lua │ │ ├── jsonschema │ │ │ ├── init.lua │ │ │ ├── store.lua │ │ │ └── url.lua │ │ ├── librestyradixtree.dylib │ │ ├── librestyradixtree.so │ │ ├── lrucache │ │ │ ├── init.lua │ │ │ └── pureffi.lua │ │ ├── prometheus.lua │ │ ├── prometheus │ │ │ ├── prometheus.lua │ │ │ ├── prometheus_keys.lua │ │ │ ├── prometheus_resty_counter.lua │ │ │ └── prometheus_test.lua │ │ ├── radixtree.lua │ │ ├── ratelimit.lua │ │ ├── redis.lua │ │ ├── tablepool.lua │ │ └── timer.lua │ ├── router.lua │ ├── sample_log.lua │ ├── schemas.lua │ ├── sites.lua │ ├── ssl.lua │ ├── status.lua │ ├── tasks.lua │ ├── time.lua │ ├── tree.lua │ ├── utils.lua │ ├── utils │ │ ├── json.lua │ │ ├── net.lua │ │ ├── net │ │ │ ├── dns.lua │ │ │ └── resolv_conf.lua │ │ ├── set.lua │ │ ├── stringx.lua │ │ ├── table │ │ │ └── deepcopy.lua │ │ └── tablex.lua │ └── vars.lua ├── logs │ └── .gitkeep ├── mime.types ├── nginx.conf └── nginx.docker.conf ├── screenshot ├── grafana.png └── screen01.png ├── scripts └── init-user-db.sh └── ui ├── .browserslistrc ├── .editorconfig ├── .env ├── .env.development ├── .env.docker ├── .env.preview ├── .eslintrc.js ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── README.zh-CN.md ├── babel.config.js ├── config ├── plugin.config.js └── themePluginConfig.js ├── docs ├── add-page-loading-animate.md └── webpack-bundle-analyzer.md ├── jest.config.js ├── jsconfig.json ├── package.json ├── postcss.config.js ├── public ├── avatar2.jpg ├── index.html └── logo.png ├── src ├── App.vue ├── api │ ├── blacklist │ │ └── ip.js │ ├── certificate.js │ ├── login.js │ ├── node.js │ ├── rateLimit.js │ ├── sampleLog.js │ ├── site │ │ ├── cert.js │ │ ├── https.js │ │ ├── index.js │ │ ├── origin.js │ │ └── regionBlacklist.js │ ├── user.js │ └── whitelist │ │ ├── ip.js │ │ └── url.js ├── assets │ ├── background.svg │ ├── icons │ │ └── bx-analyse.svg │ ├── logo.png │ └── logo.svg ├── components │ ├── ArticleListContent │ │ ├── ArticleListContent.vue │ │ └── index.js │ ├── AvatarList │ │ ├── Item.jsx │ │ ├── List.jsx │ │ ├── index.js │ │ ├── index.less │ │ └── index.md │ ├── Charts │ │ ├── Bar.vue │ │ ├── ChartCard.vue │ │ ├── Liquid.vue │ │ ├── MiniArea.vue │ │ ├── MiniBar.vue │ │ ├── MiniProgress.vue │ │ ├── MiniSmoothArea.vue │ │ ├── Radar.vue │ │ ├── RankList.vue │ │ ├── TagCloud.vue │ │ ├── TransferBar.vue │ │ ├── Trend.vue │ │ ├── chart.less │ │ └── smooth.area.less │ ├── Dialog.js │ ├── Ellipsis │ │ ├── Ellipsis.vue │ │ ├── index.js │ │ └── index.md │ ├── FooterToolbar │ │ ├── FooterToolBar.vue │ │ ├── index.js │ │ ├── index.less │ │ └── index.md │ ├── GlobalFooter │ │ ├── GlobalFooter.vue │ │ └── index.js │ ├── GlobalHeader │ │ ├── AvatarDropdown.vue │ │ └── RightContent.vue │ ├── IconSelector │ │ ├── IconSelector.vue │ │ ├── README.md │ │ ├── icons.js │ │ └── index.js │ ├── MultiTab │ │ ├── MultiTab.vue │ │ ├── events.js │ │ ├── index.js │ │ └── index.less │ ├── NProgress │ │ └── nprogress.less │ ├── NoticeIcon │ │ ├── NoticeIcon.vue │ │ └── index.js │ ├── NumberInfo │ │ ├── NumberInfo.vue │ │ ├── index.js │ │ ├── index.less │ │ └── index.md │ ├── PageLoading │ │ └── index.jsx │ ├── Search │ │ ├── GlobalSearch.jsx │ │ └── index.less │ ├── SelectLang │ │ ├── index.jsx │ │ └── index.less │ ├── SettingDrawer │ │ ├── SettingDrawer.vue │ │ ├── SettingItem.vue │ │ ├── index.js │ │ ├── settingConfig.js │ │ └── themeColor.js │ ├── StandardFormRow │ │ ├── StandardFormRow.vue │ │ └── index.js │ ├── Table │ │ ├── README.md │ │ └── index.js │ ├── TagSelect │ │ ├── TagSelectOption.jsx │ │ └── index.jsx │ ├── TextArea │ │ ├── index.jsx │ │ └── style.less │ ├── Tree │ │ └── Tree.jsx │ ├── Trend │ │ ├── Trend.vue │ │ ├── index.js │ │ ├── index.less │ │ └── index.md │ ├── _util │ │ └── util.js │ ├── index.js │ ├── index.less │ └── tools │ │ └── TwoStepCaptcha.vue ├── config │ ├── defaultSettings.js │ └── router.config.js ├── core │ ├── bootstrap.js │ ├── directives │ │ └── action.js │ ├── icons.js │ ├── lazy_use.js │ ├── permission │ │ └── permission.js │ └── use.js ├── global.less ├── layouts │ ├── BasicLayout.less │ ├── BasicLayout.vue │ ├── BlankLayout.vue │ ├── PageView.vue │ ├── RouteView.vue │ ├── UserLayout.vue │ └── index.js ├── locales │ ├── index.js │ └── lang │ │ ├── en-US.js │ │ ├── en-US │ │ ├── account.js │ │ ├── account │ │ │ └── settings.js │ │ ├── dashboard.js │ │ ├── dashboard │ │ │ └── analysis.js │ │ ├── form.js │ │ ├── form │ │ │ └── basicForm.js │ │ ├── global.js │ │ ├── menu.js │ │ ├── result.js │ │ ├── result │ │ │ ├── fail.js │ │ │ └── success.js │ │ ├── setting.js │ │ └── user.js │ │ ├── zh-CN.js │ │ └── zh-CN │ │ ├── account.js │ │ ├── account │ │ └── settings.js │ │ ├── dashboard.js │ │ ├── dashboard │ │ └── analysis.js │ │ ├── form.js │ │ ├── form │ │ └── basicForm.js │ │ ├── global.js │ │ ├── menu.js │ │ ├── result.js │ │ ├── result │ │ ├── fail.js │ │ └── success.js │ │ ├── setting.js │ │ └── user.js ├── main.js ├── mock │ ├── index.js │ ├── services │ │ ├── article.js │ │ ├── auth.js │ │ ├── manage.js │ │ ├── other.js │ │ ├── tagCloud.js │ │ └── user.js │ └── util.js ├── permission.js ├── reset.less ├── router │ ├── README.md │ └── index.js ├── store │ ├── app-mixin.js │ ├── device-mixin.js │ ├── getters.js │ ├── i18n-mixin.js │ ├── index.js │ ├── modules │ │ ├── app.js │ │ ├── permission.js │ │ ├── site.js │ │ └── user.js │ └── mutation-types.js ├── utils │ ├── axios.js │ ├── domUtil.js │ ├── filter.js │ ├── request.js │ ├── routeConvert.js │ ├── screenLog.js │ ├── util.js │ └── utils.less └── views │ ├── 404.vue │ ├── Login.vue │ ├── account │ └── settings │ │ ├── BasicSetting.vue │ │ ├── Index.vue │ │ └── Security.vue │ ├── dashboard │ ├── Analysis.vue │ ├── index.vue │ └── index1.vue │ ├── exception │ ├── 403.vue │ ├── 404.vue │ └── 500.vue │ ├── protect │ ├── blacklist │ │ └── ip.vue │ └── cc │ │ └── index.vue │ ├── sampleLog │ ├── block.vue │ ├── export.vue │ └── index.vue │ ├── site │ ├── certificate │ │ └── index.vue │ ├── index.vue │ └── settings │ │ ├── basic.vue │ │ ├── https.vue │ │ ├── index.vue │ │ ├── origin.vue │ │ └── regionBlacklist.vue │ ├── system │ └── node │ │ └── index.vue │ ├── templates.vue │ ├── user │ ├── index.vue │ └── profile │ │ ├── components │ │ ├── index.js │ │ ├── loginLog.vue │ │ └── profile.vue │ │ └── index.vue │ └── whitelist │ ├── ip.vue │ └── url.vue ├── tests └── unit │ └── .eslintrc.js ├── vue.config.js ├── webstorm.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | /guard/config.json 2 | /guard/*.conf 3 | !/guard/nginx.docker.conf 4 | 5 | .gitkeep 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | 4 | #ide 5 | .idea/ 6 | .vscode/ 7 | 8 | *.log 9 | *.swp 10 | *.pid 11 | *.xdb 12 | 13 | bin/ 14 | dist/ 15 | 16 | /guard/logs/ 17 | /guard/config.json 18 | 19 | /guard/nginx.dev.conf 20 | /apiserver/conf/config.dev.yaml 21 | 22 | test/ 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LDFLAGS := -s -w 2 | VERSION := 0.0.1 3 | GOOS ?= darwin 4 | GOARCH ?= amd64 5 | build-docker: build-ui build-docker-guard build-docker-apiserver build-docker-ui 6 | 7 | build-apiserver: 8 | cd apiserver && env CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} go build -trimpath -ldflags "$(LDFLAGS)" -o bin/muxwaf-apiserver . 9 | 10 | build-apiserver-linux-amd64: 11 | cd apiserver && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/muxwaf-apiserver . 12 | 13 | build-ui: 14 | cd ui && yarn -i && yarn run build 15 | 16 | build-docker-guard: 17 | docker buildx build -f docker/guard/Dockerfile -t xnile/muxwaf-guard:$(VERSION) ./ 18 | 19 | build-docker-apiserver: 20 | docker buildx build -f docker/apiserver/Dockerfile -t xnile/muxwaf-apiserver:$(VERSION) apiserver 21 | 22 | build-docker-ui: 23 | docker buildx build -f docker/ui/Dockerfile -t xnile/muxwaf-ui:$(VERSION) ./ 24 | 25 | push-docker: 26 | docker push xnile/muxwaf-guard:$(VERSION) 27 | docker push xnile/muxwaf-apiserver:$(VERSION) 28 | docker push xnile/muxwaf-ui:$(VERSION) 29 | 30 | run: 31 | docker-compose -f docker-compose.yml up 32 | stop: 33 | docker-compose -f docker-compose.yml stop 34 | rm: 35 | docker-compose -f docker-compose.yml rm -f 36 | 37 | restart-guard: 38 | rm -rf /opt/apps/muxwaf/guard/logs/*.log 39 | brew services restart openresty 40 | 41 | stop-guard: 42 | brew services stop openresty 43 | 44 | 45 | .PHONY: clean 46 | clean: 47 | rm -rf ./apiserver/bin 48 | rm -rf ./ui/dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MuxWaf 2 | 3 | MuxWaf是基于OpenResty实现的一款防CC的高性能WAF。 4 | 5 | ## MuxWAF能做什么? 6 | 7 | * 频率限制防CC 8 | * 白名单功能 9 | * IP及地域级IP黑名单功能 10 | * 支持API管理 11 | * 自带WEB管理后台 12 | 13 | ## 组件 14 | * guard: waf引擎,基于openresty开发。 15 | * apiserver: 管理后台api,基于golang开发。 16 | * ui: 管理后台前端页面,基于antdv开发。 17 | 18 | ## Overview 19 | 20 | ![](https://raw.githubusercontent.com/xnile/muxwaf/master/screenshot/screen01.png) 21 | 22 | ![](https://raw.githubusercontent.com/xnile/muxwaf/master/screenshot/grafana.png) 23 | 24 | ## 安装 25 | 26 | ### 快速体验 27 | 28 | 需要docker和docker-compose环境。 29 | 30 | * git clone https://github.com/xnile/muxwaf ./ 31 | * cd muxwaf 32 | * make run 33 | * waf 入口:http://localhost:8080/ 34 | * 管理后台地址:http://localhost:8000/ ,默认用户名和密码:admin/admin@123 35 | * 登录管理后台切到`系统管理->节点管理->添加节点`,输入 `guard/8083`添加waf节点 36 | * 添加网站,后台位置:`网站管理->网站管理->新增网站`。 37 | * 通过绑Host的方式将上边网站域名指向本机,然后访问http://域名:8080 就可以开始体验了。 -------------------------------------------------------------------------------- /apiserver/Makefile: -------------------------------------------------------------------------------- 1 | LDFLAGS := -s -w 2 | 3 | .PHONY: build 4 | build: 5 | env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/muxwaf-apiserver . 6 | 7 | 8 | docker: 9 | #docker buildx build -t xnile/muxwaf-apiserver:v0.0.1 ./ --push 10 | docker buildx build -t xnile/muxwaf-apiserver:v0.0.1 ./ 11 | 12 | .PHONY: clean 13 | clean: 14 | rm -rf ./bin -------------------------------------------------------------------------------- /apiserver/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnile/muxwaf/6d296fbfc7c3afc0dfaa65ea8640587b9a7718bd/apiserver/README.md -------------------------------------------------------------------------------- /apiserver/conf/config.default.yaml: -------------------------------------------------------------------------------- 1 | host: 0.0.0.0 2 | port: 8001 3 | postgresql: 4 | host: 127.0.0.1 5 | port: 5432 6 | db: muxwaf 7 | username: muxwaf 8 | password: muxwaf 9 | debug: false 10 | log: 11 | writers: file,stdout 12 | logger_level: error 13 | logger_file: /tmp/muxwaf-apiserver.log 14 | log_rolling_policy: daily 15 | log_rotate_date: 1 16 | log_backup_count: 7 -------------------------------------------------------------------------------- /apiserver/conf/config.docker.yaml: -------------------------------------------------------------------------------- 1 | host: 0.0.0.0 2 | port: 8001 3 | postgresql: 4 | host: db 5 | port: 5432 6 | db: muxwaf 7 | username: muxwaf 8 | password: muxwaf@password 9 | debug: true 10 | log: 11 | writers: file,stdout 12 | logger_level: info 13 | logger_file: /tmp/muxwaf-apiserver.log 14 | log_rolling_policy: daily 15 | log_rotate_date: 1 16 | log_backup_count: 7 -------------------------------------------------------------------------------- /apiserver/handler/helpers.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/xnile/muxwaf/internal/ecode" 6 | "net/http" 7 | ) 8 | 9 | // ResponseBuilder response wrapper 10 | type Response struct { 11 | Code int `json:"code"` 12 | Msg string `json:"msg"` 13 | Data interface{} `json:"data"` 14 | } 15 | 16 | // ResponseBuilder json response wrapper 17 | func ResponseBuilder(c *gin.Context, err error, data interface{}) { 18 | code, msg := ecode.DecodeErr(err) 19 | 20 | // always return http.StatusOK 21 | c.JSON(http.StatusOK, Response{ 22 | Code: code, 23 | Msg: msg, 24 | Data: data, 25 | }) 26 | } 27 | 28 | // RouteNotFound can't find route 29 | func RouteNotFound(c *gin.Context) { 30 | c.String(http.StatusNotFound, "the route not found") 31 | } 32 | -------------------------------------------------------------------------------- /apiserver/handler/sample_log/attack_log.go: -------------------------------------------------------------------------------- 1 | package sample_log 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/xnile/muxwaf/handler" 6 | "github.com/xnile/muxwaf/internal/ecode" 7 | "github.com/xnile/muxwaf/internal/model" 8 | "github.com/xnile/muxwaf/internal/service" 9 | "github.com/xnile/muxwaf/pkg/logx" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | func AddLog(c *gin.Context) { 15 | var payload = make([]*model.SampleLogModel, 0) 16 | if err := c.ShouldBindJSON(&payload); err != nil { 17 | logx.Warnf("request parameter error: %v", err) 18 | handler.ResponseBuilder(c, ecode.ErrParam, nil) 19 | return 20 | } 21 | 22 | //svc := service.SVC.AttackLog 23 | //err := svc.Add(c, &payload) 24 | //if err != nil { 25 | // logx.Warnf("add blacklist ip err, %v", err) 26 | //} 27 | //handler.ResponseBuilder(c, err, nil) 28 | service.SVC.AttackLog.Add(c, payload) 29 | } 30 | 31 | func GetLogList(c *gin.Context) { 32 | pageNum, _ := strconv.ParseInt(c.Query("page_num"), 10, 64) 33 | pageSize, _ := strconv.ParseInt(c.Query("page_size"), 10, 64) 34 | starTime, _ := strconv.ParseInt(c.Query("start_time"), 10, 64) 35 | endTime, _ := strconv.ParseInt(c.Query("end_time"), 10, 64) 36 | siteID, _ := strconv.ParseInt(c.Query("site_id"), 10, 64) 37 | content := strings.TrimSpace(c.Query("content")) 38 | 39 | var action int8 40 | switch c.Query("action") { 41 | case "1": 42 | action = 1 43 | case "2": 44 | action = 2 45 | default: 46 | action = -1 47 | } 48 | 49 | svc := service.SVC.AttackLog 50 | data, err := svc.List(pageNum, pageSize, starTime, endTime, siteID, action, content) 51 | handler.ResponseBuilder(c, err, data) 52 | } 53 | -------------------------------------------------------------------------------- /apiserver/handler/site/region_blacklist.go: -------------------------------------------------------------------------------- 1 | package site 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/xnile/muxwaf/handler" 6 | "github.com/xnile/muxwaf/internal/ecode" 7 | "github.com/xnile/muxwaf/internal/model" 8 | "github.com/xnile/muxwaf/internal/service" 9 | "github.com/xnile/muxwaf/pkg/logx" 10 | "strconv" 11 | ) 12 | 13 | func GetRegionBlacklist(c *gin.Context) { 14 | id, _ := strconv.ParseInt(c.Param("id"), 10, 64) 15 | data, err := service.SVC.Site.GetRegionBlacklist(id) 16 | handler.ResponseBuilder(c, err, data) 17 | } 18 | 19 | func UpdateRegionBlacklist(c *gin.Context) { 20 | id, _ := strconv.ParseInt(c.Param("id"), 10, 64) 21 | 22 | payload := new(model.SiteRegionBlacklistModel) 23 | if err := c.ShouldBindJSON(payload); err != nil { 24 | logx.Warnf("request parameter error: %v", err) 25 | handler.ResponseBuilder(c, ecode.ErrParam, nil) 26 | return 27 | } 28 | 29 | err := service.SVC.Site.UpdateRegionBlacklist(id, payload) 30 | handler.ResponseBuilder(c, err, nil) 31 | } 32 | -------------------------------------------------------------------------------- /apiserver/internal/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "github.com/xnile/muxwaf/pkg/redis" 4 | 5 | type Cache struct { 6 | User UserCache 7 | } 8 | 9 | func New(redis *redis.Redis) *Cache { 10 | return &Cache{ 11 | User: NewUserCache(redis), 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apiserver/internal/cache/user_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "github.com/xnile/muxwaf/pkg/redis" 6 | "time" 7 | ) 8 | 9 | type UserCache interface { 10 | AddUserToken(id int64, token string) error 11 | GetUserToken(id int64) (string, error) 12 | } 13 | 14 | type userCache struct { 15 | redis *redis.Redis 16 | } 17 | 18 | func NewUserCache(redis *redis.Redis) UserCache { 19 | return &userCache{ 20 | redis: redis, 21 | } 22 | } 23 | 24 | func (c *userCache) AddUserToken(id int64, token string) error { 25 | key := fmt.Sprintf("muxwaf:user:token:%s:string", id) 26 | ttl := time.Second * 3600 27 | err := c.redis.Set(key, token, ttl) 28 | return err 29 | } 30 | 31 | func (c *userCache) GetUserToken(id int64) (string, error) { 32 | key := fmt.Sprintf("muxwaf:user:token:%s:string", id) 33 | 34 | return c.redis.GetString(key) 35 | } 36 | -------------------------------------------------------------------------------- /apiserver/internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fsnotify/fsnotify" 6 | "github.com/spf13/viper" 7 | "github.com/xnile/muxwaf/pkg/logx" 8 | ) 9 | 10 | type Config struct { 11 | Name string 12 | } 13 | 14 | func (c *Config) initConfig() error { 15 | if c.Name != "" { 16 | viper.SetConfigFile(c.Name) 17 | } else { 18 | viper.AddConfigPath("./conf") 19 | viper.SetConfigFile("config") 20 | } 21 | 22 | viper.SetConfigType("yaml") 23 | viper.AutomaticEnv() 24 | viper.SetEnvPrefix("MUXWAF") 25 | 26 | if err := viper.ReadInConfig(); err != nil { 27 | return err 28 | } 29 | //fmt.Println("vvvvv:", viper.GetString("postgresql.host")) 30 | 31 | return nil 32 | } 33 | 34 | func (c *Config) watchConfig() { 35 | viper.WatchConfig() 36 | viper.OnConfigChange(func(e fsnotify.Event) { 37 | //log.Printf("Config file changed: %s", e.Name) 38 | }) 39 | } 40 | 41 | func (c *Config) initLog() { 42 | config := logx.Config{ 43 | Writers: viper.GetString("log.writers"), 44 | LoggerLevel: viper.GetString("log.logger_level"), 45 | LoggerFile: viper.GetString("log.logger_file"), 46 | LogBackupCount: viper.GetInt("log.log_backup_count"), 47 | } 48 | err := logx.NewLogger(&config, logx.InstanceZapLogger) 49 | if err != nil { 50 | fmt.Printf("InitWithConfig err: %v", err) 51 | } 52 | } 53 | 54 | func Init(cfg string) error { 55 | c := Config{cfg} 56 | 57 | if err := c.initConfig(); err != nil { 58 | return err 59 | } 60 | 61 | c.initLog() 62 | c.watchConfig() 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /apiserver/internal/ecode/README.md: -------------------------------------------------------------------------------- 1 | ## 错误码设计 2 | 3 | > 参考 新浪开放平台 [Error code](http://open.weibo.com/wiki/Error_code) 的设计 4 | 5 | #### 错误返回值格式 6 | 7 | ```json 8 | { 9 | "code": 10002, 10 | "message": "Error occurred while binding the request body to the struct." 11 | } 12 | ``` 13 | 14 | #### 错误代码说明 15 | 16 | | 1 | 00 | 02 | 17 | | :------ | :------ | :------ | 18 | | 服务级错误(1为系统级错误) | 服务模块代码 | 具体错误代码 | 19 | 20 | - 服务级别错误:1 为系统级错误;2 为普通错误,通常是由用户非法操作引起的 21 | - 服务模块为两位数:一个大型系统的服务模块通常不超过两位数,如果超过,说明这个系统该拆分了 22 | - 错误码为两位数:防止一个模块定制过多的错误码,后期不好维护 23 | - `code = 0` 说明是正确返回,`code > 0` 说明是错误返回 24 | - 错误通常包括系统级错误码和服务级错误码 25 | - 建议代码中按服务模块将错误分类 26 | - 错误码均为 >= 0 的数 27 | - 在本项目中 HTTP Code 固定为 http.StatusOK,错误码通过 code 来表示。 -------------------------------------------------------------------------------- /apiserver/internal/ecode/code.go: -------------------------------------------------------------------------------- 1 | package ecode 2 | 3 | //nolint: golint 4 | var ( 5 | // Common errors 6 | Success = &ErrCode{Code: 0, Msg: "Success"} 7 | InternalServerError = &ErrCode{Code: 10001, Msg: "系统错误"} 8 | ErrParam = &ErrCode{Code: 10003, Msg: "参数有误"} 9 | ErrIDNotFound = &ErrCode{Code: 10004, Msg: "ID不存在"} 10 | ErrCertInUse = &ErrCode{Code: 10005, Msg: "删除失败,有站点正在使用此证书"} 11 | ErrAtLeastOneOrigin = &ErrCode{Code: 10006, Msg: "删除源站失败,至少需要一个源站"} 12 | ErrRecordAlreadyExists = &ErrCode{Code: 10007, Msg: "记录已存在"} 13 | 14 | ErrIPInvalid = &ErrCode{Code: 10009, Msg: "无效的IP地址或CIDR"} 15 | ErrIPv6NotSupportedYet = &ErrCode{Code: 10010, Msg: "暂不支持IPv6地址或CIDR"} 16 | ErrIPAlreadyExisted = &ErrCode{Code: 10011, Msg: "IP地址或CIDR已经存在"} 17 | 18 | ErrSiteNotFound = &ErrCode{Code: 10013, Msg: "站点不存在"} 19 | ErrUpdate = &ErrCode{Code: 10014, Msg: "更新失败"} 20 | 21 | ErrCertNotFound = &ErrCode{Code: 20001, Msg: "该证书未找到"} 22 | ErrCertInvalid = &ErrCode{Code: 20002, Msg: "证书格式不正确"} 23 | ErrCertPriKeyInvalid = &ErrCode{Code: 20003, Msg: "证书私钥格式不正确"} 24 | ErrIPorCIDREmpty = &ErrCode{Code: 20004, Msg: "请输入IP或CIDR"} 25 | ErrUsernameOrPwdIncorrect = &ErrCode{Code: 20005, Msg: "用户名或密码不正确"} 26 | ) 27 | -------------------------------------------------------------------------------- /apiserver/internal/ecode/errcode.go: -------------------------------------------------------------------------------- 1 | package ecode 2 | 3 | import "fmt" 4 | 5 | // ErrCode 返回错误码和消息的结构体 6 | type ErrCode struct { 7 | Code int 8 | Msg string 9 | } 10 | 11 | func (code ErrCode) Error() string { 12 | return code.Msg 13 | } 14 | 15 | // Err represents an error 16 | type Err struct { 17 | Code int 18 | Msg string 19 | Err error 20 | } 21 | 22 | func (err *Err) Error() string { 23 | return fmt.Sprintf("Err - code: %d, message: %s, error: %s", err.Code, err.Msg, err.Err) 24 | } 25 | 26 | // DecodeErr 对错误进行解码,返回错误code和错误提示 27 | func DecodeErr(err error) (int, string) { 28 | if err == nil { 29 | return Success.Code, Success.Msg 30 | } 31 | 32 | switch typed := err.(type) { 33 | case *Err: 34 | return typed.Code, typed.Msg 35 | case *ErrCode: 36 | return typed.Code, typed.Msg 37 | default: 38 | } 39 | 40 | //return InternalServerError.Code, InternalServerError.Msg 41 | //return InternalServerError.Code, err.Error() 42 | return -1, err.Error() 43 | 44 | } 45 | -------------------------------------------------------------------------------- /apiserver/internal/event/bus.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "github.com/rs/xid" 5 | "time" 6 | ) 7 | 8 | type EventBus struct { 9 | bus chan Event 10 | handlers []Handler 11 | } 12 | 13 | type Event struct { 14 | UUID string `json:"uuid"` 15 | Date time.Time `json:"date"` 16 | Target Target `json:"target"` 17 | OpType OpType `json:"action"` 18 | Payload any `json:"payload"` 19 | WorkNodes []int64 `json:"work_node"` 20 | } 21 | 22 | func NewEventBus(size int) *EventBus { 23 | return &EventBus{ 24 | bus: make(chan Event, size), 25 | handlers: nil, 26 | } 27 | } 28 | 29 | func (e *EventBus) RegisterHandler(handler Handler) { 30 | e.handlers = append(e.handlers, handler) 31 | } 32 | 33 | func (e *EventBus) PushEvent(target Target, opType OpType, payload any, workNode ...int64) { 34 | event := Event{ 35 | UUID: xid.New().String(), 36 | Target: target, 37 | OpType: opType, 38 | Payload: payload, 39 | Date: time.Now(), 40 | WorkNodes: workNode, 41 | } 42 | e.bus <- event 43 | } 44 | 45 | func (e *EventBus) Close() { 46 | close(e.bus) 47 | } 48 | 49 | func (e *EventBus) StartWorkers(count int) { 50 | for i := 0; i < count; i++ { 51 | go e.worker() 52 | } 53 | } 54 | 55 | func (e *EventBus) worker() { 56 | for event := range e.bus { 57 | for _, h := range e.handlers { 58 | h.Next(event) 59 | } 60 | } 61 | } 62 | 63 | type Handler interface { 64 | Next(event Event) 65 | } 66 | -------------------------------------------------------------------------------- /apiserver/internal/model/blacklist_ip.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type BlacklistIPModel struct { 4 | Model 5 | IP string `json:"ip" gorm:"uniqueIndex;type varchar(39);not null" binding:"required,ip|cidr"` 6 | Status int8 `json:"status" gorm:"type smallint;not null;default:1"` 7 | Remark string `json:"remark" gorm:"type text"` 8 | } 9 | 10 | func (BlacklistIPModel) TableName() string { 11 | return "blacklist_ip" 12 | } 13 | 14 | type BlacklistBatchAddReq struct { 15 | IPList []string `json:"ip_list" binding:"required"` 16 | Remark string `json:"remark"` 17 | } 18 | -------------------------------------------------------------------------------- /apiserver/internal/model/cert.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/lib/pq" 4 | 5 | type CertModel struct { 6 | Model 7 | Name string `json:"name" gorm:"uniqueIndex;type:varchar(128);not null" binding:"required"` 8 | Cert string `json:"cert" gorm:"type:text;not null" binding:"required"` 9 | Key string `json:"key" gorm:"type:text;not null" binding:"required"` 10 | CN string `json:"cn" gorm:"type:varchar(255);not null;default:''"` 11 | Sans pq.StringArray `json:"sans" gorm:"type:varchar(255)[];not null;default:'{}'"` 12 | BeginTime int64 `json:"begin_time" gorm:"type:bigint;not null;default:0"` 13 | EndTime int64 `json:"end_time" gorm:"type:bigint;not null;default:0"` 14 | } 15 | 16 | func (CertModel) TableName() string { 17 | return "cert" 18 | } 19 | 20 | // CertResp 返回证书列表 21 | type CertResp struct { 22 | ID int64 `json:"id"` 23 | Name string `json:"name"` 24 | CN string `json:"cn"` 25 | Sans []string `json:"sans"` 26 | BeginTime int64 `json:"begin_time"` 27 | EndTime int64 `json:"end_time"` 28 | Sites []CertBindSite `json:"sites"` 29 | } 30 | 31 | type CertBindSite struct { 32 | ID int64 `json:"id"` 33 | Domain string `json:"domain"` 34 | } 35 | 36 | // CertCandidateResp 返回所有证书 37 | type CertCandidateResp struct { 38 | ID int64 `json:"id"` 39 | Name string `json:"name"` 40 | } 41 | -------------------------------------------------------------------------------- /apiserver/internal/model/migration.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/gorm" 4 | 5 | func AutoMigrate(db *gorm.DB) (err error) { 6 | if err = db.AutoMigrate(new(UserModel)); err != nil { 7 | return err 8 | } 9 | if err = db.AutoMigrate(new(SiteModel)); err != nil { 10 | return err 11 | } 12 | if err = db.AutoMigrate(new(BlacklistIPModel)); err != nil { 13 | return err 14 | } 15 | if err = db.AutoMigrate(new(WhitelistIPModel)); err != nil { 16 | return err 17 | } 18 | if err = db.AutoMigrate(new(WhitelistURLModel)); err != nil { 19 | return err 20 | } 21 | if err = db.AutoMigrate(new(RateLimitModel)); err != nil { 22 | return err 23 | } 24 | if err = db.AutoMigrate(new(SiteConfigModel)); err != nil { 25 | return err 26 | } 27 | if err = db.AutoMigrate(new(SiteOriginModel)); err != nil { 28 | return err 29 | } 30 | if err = db.AutoMigrate(new(SiteRegionBlacklistModel)); err != nil { 31 | return err 32 | } 33 | if err = db.AutoMigrate(new(CertModel)); err != nil { 34 | return err 35 | } 36 | if err = db.AutoMigrate(new(SampleLogModel)); err != nil { 37 | return err 38 | } 39 | if err = db.AutoMigrate(new(NodeModel)); err != nil { 40 | return err 41 | } 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /apiserver/internal/model/node.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type NodeModel struct { 4 | Model 5 | Name string `json:"name" gorm:"not null;default:''"` 6 | Addr string `json:"addr" gorm:"type:varchar(255);not null" binding:"required,ipv4|hostname_rfc1123"` 7 | Port int16 `json:"port" gorm:"type:smallint;not null" binding:"gte=1,lte=65535"` 8 | IsSampleLogUpload int8 `json:"is_sample_log_upload" gorm:"not null;default:0"` 9 | SampleLogUploadAPI string `json:"sample_log_upload_api" gorm:"not null;default:''"` 10 | SampleLogUploadAPIToken string `json:"sample_log_upload_api_token" gorm:"not null;default:''"` 11 | Status int8 `json:"status" gorm:"not null;default:1"` 12 | LastSyncStatus int8 `json:"last_sync_status" gorm:"not null;default:0"` 13 | LastSyncAt int64 `json:"last_sync_at" gorm:"not null;default:0"` 14 | Remark string `json:"remark" gorm:"type:text;not null;default:''"` 15 | } 16 | 17 | func (NodeModel) TableName() string { 18 | return "node" 19 | } 20 | -------------------------------------------------------------------------------- /apiserver/internal/model/ratelimit.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type RateLimitModel struct { 4 | Model 5 | SiteID int64 `json:"site_id" gorm:"index;not null" binding:"required,numeric"` 6 | SiteUUID string `json:"-" gorm:"index;type:char(20);default:''"` 7 | Host string `json:"host" gorm:"index;type:varchar(255);not null"` 8 | Path string `json:"path" gorm:"type:text;not null" binding:"required,uri"` 9 | Limit int64 `json:"limit" gorm:"type:bigint;not null" binding:"required,numeric"` 10 | Window int64 `json:"window" gorm:"type:bigint;not null" binding:"required,numeric"` 11 | MatchMode int8 `json:"match_mode" gorm:"type:smallint;not null;default:1;comment:1 prefix match,2 exact match" binding:"required,oneof=1 2"` 12 | Status int8 `json:"status" gorm:"type:smallint;not null;default:1"` 13 | Remark string `json:"remark" gorm:"type:text"` 14 | //Domain string `json:"domain" gorm:"-"` 15 | } 16 | 17 | func (RateLimitModel) TableName() string { 18 | return "rate_limit" 19 | } 20 | 21 | type RateLimitReq struct { 22 | SiteID int64 `json:"site_id" binding:"required,numeric"` 23 | Path string `json:"path" binding:"required,uri"` 24 | Limit int64 `json:"limit" binding:"required,numeric"` 25 | Window int64 `json:"window" binding:"required,numeric"` 26 | MatchMode int8 `json:"match_mode" binding:"required,oneof=1 2"` 27 | Remark string `json:"remark"` 28 | } 29 | -------------------------------------------------------------------------------- /apiserver/internal/model/role.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // RoleModel 用户角色 4 | type RoleModel struct { 5 | Model 6 | Name string `json:"name" gorm:"uniqueIndex;type:varchar" binding:"required"` 7 | Remark string `json:"remark" gorm:"type:text"` 8 | } 9 | 10 | func (RoleModel) TableName() string { 11 | return "role" 12 | } 13 | -------------------------------------------------------------------------------- /apiserver/internal/model/sample_log.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/datatypes" 4 | 5 | //type AttackLogModel struct { 6 | // Model 7 | // SiteID string `json:"site_id" gorm:"index;not null;default:0"` 8 | // Host string `json:"host"` 9 | // RemoteAddr string `json:"remote_addr"` 10 | // RealClientIp string `json:"real_client_ip"` 11 | // RequestID string `json:"request_id" gorm:"index;not null;default:''"` 12 | // RequestPath string `json:"request_path"` 13 | // RequestMethod string `json:"request_method"` 14 | // RequestTime int64 `json:"request_time"` 15 | // ProcessTime int64 `json:"process_time"` 16 | // RuleType string `json:"rule_type"` 17 | // Action int8 `json:"action" gorm:"index:not null;default:0"` 18 | // WorkerID int64 `json:"worker_id"` 19 | //} 20 | 21 | type SampleLogModel struct { 22 | Model 23 | Content datatypes.JSON `json:"content" gorm:"type:jsonb"` 24 | } 25 | 26 | func (SampleLogModel) TableName() string { 27 | return "sample_log" 28 | } 29 | -------------------------------------------------------------------------------- /apiserver/internal/model/site.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SiteModel struct { 4 | Model 5 | Domain string `json:"domain" gorm:"uniqueIndex;type:varchar(255);not null" binding:"required,fqdn"` 6 | Status int8 `json:"status" gorm:"type:smallint;not null;default:0"` 7 | Remark string `json:"remark" gorm:"type:text"` 8 | DeletedAt int64 `json:"-" gorm:"not null;default:0"` 9 | } 10 | 11 | func (SiteModel) TableName() string { 12 | return "site" 13 | } 14 | 15 | type SiteReq struct { 16 | Domain string `json:"domain" binding:"required,fqdn"` 17 | OriginProtocol OriginProtocol `json:"origin_protocol" binding:"required,oneof=http https"` 18 | Origins []*SiteOriginModel `json:"origins" binding:"gt=0,dive"` 19 | } 20 | 21 | type SiteRsp struct { 22 | Model 23 | Domain string `json:"domain"` 24 | Status int8 `json:"status"` 25 | Remark string `json:"remark"` 26 | Config *SiteConfigRsp `json:"config"` 27 | Origins []*SiteOriginRsp `json:"origins"` 28 | } 29 | 30 | // SiteGuardRsp guard sync entity 31 | //type SiteGuardRsp struct { 32 | // ID string `json:"id"` 33 | // Host string `json:"host"` 34 | // Config *SiteConfigGuard `json:"config"` 35 | // Origins []*SiteOriginGuard `json:"origins"` 36 | //} 37 | -------------------------------------------------------------------------------- /apiserver/internal/model/site_region_blacklist.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/lib/pq" 4 | 5 | type SiteRegionBlacklistModel struct { 6 | Model 7 | SiteID int64 `json:"site_id" gorm:"not null"` 8 | SiteUUID string `json:"-" gorm:"index;type:char(20);default:''"` 9 | Countries pq.StringArray `json:"countries" gorm:"type:varchar(20)[];not null;default:'{}'"` 10 | Regions pq.StringArray `json:"regions" gorm:"type:varchar(20)[];not null;default:'{}'"` 11 | MatchMode int8 `json:"match_mode" gorm:"not null;default:0;comment:0 blacklist mode,1 whitelist mode"` 12 | Status int8 `json:"status" gorm:"not null;default:1"` 13 | } 14 | 15 | func (SiteRegionBlacklistModel) TableName() string { 16 | return "site_region_blacklist" 17 | } 18 | 19 | type SiteRegionBlacklistRsp struct { 20 | Countries pq.StringArray `json:"countries"` 21 | Regions pq.StringArray `json:"regions"` 22 | MatchMode int8 `json:"match_mode"` 23 | } 24 | -------------------------------------------------------------------------------- /apiserver/internal/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type UserModel struct { 4 | Model 5 | Username string `json:"username" gorm:"uniqueIndex;type:varchar(64);not null" binding:"required"` 6 | Password string `json:"password" gorm:"index;type:varchar" binding:"required,gte=8"` 7 | Name string `json:"name" gorm:"uniqueIndex;type:varchar(64);not null"` 8 | Email string `json:"email" gorm:"type:varchar;not null" binding:"omitempty,email"` // omitempty 如果为空则跳过校验 9 | Phone string `json:"phone" gorm:"uniqueIndex;type:varchar(20)"` 10 | Avatar string `json:"avatar" gorm:"type:varchar"` 11 | UpdatedAt int64 `json:"updated_at"` 12 | BlockedAt int64 `json:"blocked_at"` 13 | LastSignInIP string `json:"last_sign_in_ip" gorm:"type:varchar"` 14 | LastSignInAt int64 `json:"last_sign_in_at"` 15 | Role []string `json:"role" gorm:"-"` 16 | } 17 | 18 | func (UserModel) TableName() string { 19 | return "user" 20 | } 21 | 22 | type UserLoginReq struct { 23 | Username string `json:"username"` 24 | Password string `json:"password"` 25 | } 26 | 27 | type UserUpdateReq struct { 28 | Password string `json:"password"` 29 | Name string `json:"name"` 30 | Email string `json:"email" binding:"omitempty,email"` 31 | Phone string `json:"phone"` 32 | } 33 | 34 | type UserPasswordResetReq struct { 35 | OldPassword string `json:"old_password" binding:"required"` 36 | NewPassword string `json:"new_password" binding:"required"` 37 | } 38 | -------------------------------------------------------------------------------- /apiserver/internal/model/user_role_ref.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // UserRoleModel 用户角色表 4 | type UserRoleModel struct { 5 | Model 6 | UID int64 `json:"uid" gorm:"index;not null" binding:"required"` 7 | RoleID int64 `json:"role_id" gorm:"index;not null" binding:"required"` 8 | } 9 | 10 | func (UserRoleModel) TableName() string { 11 | return "user_role" 12 | } 13 | -------------------------------------------------------------------------------- /apiserver/internal/model/whitelist_ip.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type WhitelistIPModel struct { 4 | Model 5 | IP string `json:"ip" gorm:"uniqueIndex;type:varchar(39)" binding:"required,ip4_addr|cidrv4"` 6 | Remark string `json:"remark" gorm:"type:text"` 7 | Status int8 `json:"status" gorm:"type:smallint;not null;default 1"` 8 | } 9 | 10 | func (WhitelistIPModel) TableName() string { 11 | return "whitelist_ip" 12 | } 13 | 14 | type WhitelistIPBatchAddReq struct { 15 | IPList []string `json:"ip_list" binding:"required"` 16 | Remark string `json:"remark"` 17 | } 18 | -------------------------------------------------------------------------------- /apiserver/internal/model/whitelist_url.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type WhitelistURLModel struct { 4 | Model 5 | SiteID int64 `json:"site_id" gorm:"index;type:bigint;not null" binding:"required"` 6 | SiteUUID string `json:"-" gorm:"index;type:char(20);default:''"` 7 | Host string `json:"host" gorm:"index;type:varchar(255);not null"` 8 | Path string `json:"path" gorm:"type:text;not null" binding:"required,uri"` 9 | MatchMode int8 `json:"match_mode" gorm:"type:smallint;not null;default:1;comment:url match mode,1 as prefix,2 as exact" binding:"required"` 10 | //Method int8 `json:"method" gorm:"type:smallint;not null;default:0;comment:not used yet"` 11 | Status int8 `json:"status" gorm:"type:smallint;not null;default:1"` 12 | Remark string `json:"remark" gorm:"type:text"` 13 | //Domain string `json:"domain" gorm:"-"` 14 | } 15 | 16 | func (WhitelistURLModel) TableName() string { 17 | return "whitelist_url" 18 | } 19 | -------------------------------------------------------------------------------- /apiserver/internal/repository/cert_repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/xnile/muxwaf/internal/model" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type ICertRepo interface { 9 | Insert(model *model.CertModel) error 10 | List(pageNum, pageSize int64) (model []*model.CertModel, count int64, err error) 11 | UpdateStatus(id int64) error 12 | Delete(id int64) error 13 | Get(id int64) (*model.CertModel, error) 14 | All() ([]*model.CertModel, error) 15 | } 16 | 17 | type certRepo struct { 18 | db *gorm.DB 19 | } 20 | 21 | func NewCertRepo(db *gorm.DB) ICertRepo { 22 | return &certRepo{ 23 | db: db, 24 | } 25 | } 26 | 27 | func (repo *certRepo) Insert(model *model.CertModel) error { 28 | return repo.db.Create(model).Error 29 | } 30 | 31 | func (repo *certRepo) List(pageNum, pageSize int64) (m []*model.CertModel, count int64, err error) { 32 | gDB := repo.db.Model(&model.CertModel{}) 33 | if err = gDB.Count(&count).Error; err != nil { 34 | return nil, 0, err 35 | } 36 | 37 | err = gDB.Offset(int((pageNum - 1) * pageSize)). 38 | Limit(int(pageSize)).Order("created_at DESC"). 39 | Find(&m).Error 40 | return 41 | } 42 | 43 | func (repo *certRepo) UpdateStatus(id int64) error { 44 | return repo.db.Model(&model.CertModel{}). 45 | Where("id = ?", id). 46 | UpdateColumn("status", gorm.Expr("ABS(status - ?)", 1)). 47 | Error 48 | } 49 | 50 | func (repo *certRepo) Delete(id int64) error { 51 | return repo.db.Where("id = ?", id). 52 | Delete(&model.CertModel{}).Error 53 | } 54 | 55 | func (repo *certRepo) Get(id int64) (*model.CertModel, error) { 56 | entity := new(model.CertModel) 57 | if err := repo.db.Where("id = ?", id).First(&entity).Error; err != nil { 58 | return nil, err 59 | } 60 | return entity, nil 61 | } 62 | 63 | func (repo *certRepo) All() (m []*model.CertModel, err error) { 64 | err = repo.db.Find(&m).Error 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /apiserver/internal/repository/ratelimit_repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/xnile/muxwaf/internal/model" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type IRateLimitRepo interface { 9 | Insert(model *model.RateLimitModel) error 10 | List(pageNum, pageSize int64) (model []*model.RateLimitModel, count int64, err error) 11 | UpdateStatus(id int64) error 12 | Delete(id int64) error 13 | Update(id int64, field map[string]any) error 14 | } 15 | 16 | type rateLimitRepo struct { 17 | db *gorm.DB 18 | } 19 | 20 | func NewRateLimitRepo(db *gorm.DB) IRateLimitRepo { 21 | return &rateLimitRepo{ 22 | db: db, 23 | } 24 | } 25 | 26 | func (repo *rateLimitRepo) Insert(model *model.RateLimitModel) error { 27 | return repo.db.Create(model).Error 28 | } 29 | 30 | func (repo *rateLimitRepo) List(pageNum, pageSize int64) (m []*model.RateLimitModel, count int64, err error) { 31 | gDB := repo.db.Model(&model.RateLimitModel{}) 32 | if err = gDB.Count(&count).Error; err != nil { 33 | return nil, 0, err 34 | } 35 | 36 | err = gDB.Offset(int((pageNum - 1) * pageSize)). 37 | Limit(int(pageSize)).Order("created_at DESC"). 38 | Find(&m).Error 39 | return 40 | } 41 | 42 | func (repo *rateLimitRepo) UpdateStatus(id int64) error { 43 | return repo.db.Model(&model.RateLimitModel{}). 44 | Where("id = ?", id). 45 | UpdateColumn("status", gorm.Expr("ABS(status - ?)", 1)). 46 | Error 47 | } 48 | 49 | func (repo *rateLimitRepo) Delete(id int64) error { 50 | return repo.db.Where("id = ?", id). 51 | Delete(&model.RateLimitModel{}).Error 52 | } 53 | 54 | func (repo *rateLimitRepo) Update(id int64, field map[string]any) error { 55 | return repo.db.Model(&model.RateLimitModel{}).Where("id = ?", id).Updates(field).Error 56 | } 57 | -------------------------------------------------------------------------------- /apiserver/internal/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type Repository struct { 8 | DB *gorm.DB 9 | User UserRepo 10 | BlacklistIP IBlacklistIPRepo 11 | Cert ICertRepo 12 | Site ISiteRepo 13 | SiteConfigRepo ISiteConfigRepo 14 | SiteOriginRepo ISiteOriginRepo 15 | WhitelistIPRepo IWhitelistIPRepo 16 | WhitelistURLRepo IWhitelistURLRepo 17 | RateLimitRepo IRateLimitRepo 18 | } 19 | 20 | func New(db *gorm.DB) *Repository { 21 | return &Repository{ 22 | DB: db, 23 | User: NewUserRepo(db), 24 | BlacklistIP: NewBlacklistIPRepo(db), 25 | Cert: NewCertRepo(db), 26 | Site: NewSiteRepo(db), 27 | SiteConfigRepo: NewSiteConfigRepo(db), 28 | SiteOriginRepo: NewSiteOriginRepo(db), 29 | WhitelistIPRepo: NewWhitelistIPRepo(db), 30 | WhitelistURLRepo: NewWhitelistURLRepo(db), 31 | RateLimitRepo: NewRateLimitRepo(db), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apiserver/internal/repository/site_config_repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/xnile/muxwaf/internal/model" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type ISiteConfigRepo interface { 9 | Insert(configModel *model.SiteConfigModel) error 10 | GetBySiteID(siteID int64) (*model.SiteConfigModel, error) 11 | Update(id int64, field map[string]interface{}) error 12 | Delete(id int64) error 13 | } 14 | 15 | type siteConfigRepo struct { 16 | db *gorm.DB 17 | } 18 | 19 | func NewSiteConfigRepo(db *gorm.DB) ISiteConfigRepo { 20 | return &siteConfigRepo{ 21 | db: db, 22 | } 23 | } 24 | 25 | func (repo *siteConfigRepo) Insert(configModel *model.SiteConfigModel) error { 26 | return repo.db.Create(configModel).Error 27 | 28 | } 29 | 30 | func (repo *siteConfigRepo) GetBySiteID(siteID int64) (*model.SiteConfigModel, error) { 31 | entity := new(model.SiteConfigModel) 32 | err := repo.db.Where("site_id = ?", siteID). 33 | First(entity).Error 34 | return entity, err 35 | } 36 | 37 | func (repo *siteConfigRepo) Update(id int64, field map[string]interface{}) error { 38 | return repo.db.Model(&model.SiteConfigModel{}).Where("id = ?", id).Updates(field).Error 39 | } 40 | 41 | func (repo *siteConfigRepo) Delete(id int64) error { 42 | return repo.db.Where("id = ?", id).Delete(&model.SiteConfigModel{}).Error 43 | } 44 | -------------------------------------------------------------------------------- /apiserver/internal/repository/site_origin_repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/xnile/muxwaf/internal/model" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type ISiteOriginRepo interface { 9 | Insert(model *model.SiteOriginModel) error 10 | GetBySiteID(siteID int64) ([]*model.SiteOriginModel, error) 11 | Update(id int64, field map[string]interface{}) error 12 | Delete(id int64) error 13 | } 14 | 15 | type siteOriginRepo struct { 16 | db *gorm.DB 17 | } 18 | 19 | func NewSiteOriginRepo(db *gorm.DB) ISiteOriginRepo { 20 | return &siteOriginRepo{ 21 | db: db, 22 | } 23 | } 24 | 25 | func (repo *siteOriginRepo) Insert(originModel *model.SiteOriginModel) error { 26 | return repo.db.Create(originModel).Error 27 | } 28 | 29 | func (repo *siteOriginRepo) GetBySiteID(siteID int64) ([]*model.SiteOriginModel, error) { 30 | entities := make([]*model.SiteOriginModel, 0) 31 | err := repo.db.Where("site_id = ?", siteID).Find(&entities).Error 32 | return entities, err 33 | } 34 | 35 | func (repo *siteOriginRepo) Update(id int64, field map[string]interface{}) error { 36 | return repo.db.Model(&model.SiteOriginModel{}).Where("id = ?", id).Updates(field).Error 37 | } 38 | 39 | func (repo *siteOriginRepo) Delete(id int64) error { 40 | return repo.db.Where("id = ?", id).Delete(&model.SiteOriginModel{}).Error 41 | } 42 | -------------------------------------------------------------------------------- /apiserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/labstack/gommon/color" 5 | "github.com/spf13/pflag" 6 | "github.com/spf13/viper" 7 | "github.com/xnile/muxwaf/internal/config" 8 | "github.com/xnile/muxwaf/internal/service" 9 | "github.com/xnile/muxwaf/pkg/graceful" 10 | "github.com/xnile/muxwaf/router" 11 | "log" 12 | "net/http" 13 | "time" 14 | ) 15 | 16 | const ( 17 | banner = ` 18 | _____ __ __ _____ 19 | / \ __ _____ ___/ \ / \_____ _/ ____\ 20 | / \ / \| | \ \/ /\ \/\/ /\__ \\ __\ 21 | / Y \ | /> < \ / / __ \| | 22 | \____|__ /____//__/\_ \ \__/\ / (____ /__| 23 | \/ \/ \/ \/ 24 | ` 25 | ) 26 | 27 | var cfg = pflag.StringP("config", "c", "", "muxwaf config file path") 28 | 29 | func main() { 30 | pflag.Parse() 31 | // init config 32 | if err := config.Init(*cfg); err != nil { 33 | panic(err) 34 | } 35 | 36 | host := viper.GetString("host") 37 | port := viper.GetString("port") 38 | 39 | service.Setup() 40 | app := router.Init() 41 | srv := &http.Server{ 42 | Addr: host + ":" + port, 43 | Handler: app, 44 | ReadTimeout: 60 * time.Second, 45 | WriteTimeout: 60 * time.Second, 46 | MaxHeaderBytes: 1 << 20, 47 | } 48 | go func() { 49 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 50 | log.Fatal("failed to listen: ", err.Error()) 51 | } 52 | }() 53 | color.Println(color.Green(banner)) 54 | graceful.Stop(srv) 55 | } 56 | -------------------------------------------------------------------------------- /apiserver/pkg/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/base32" 5 | "github.com/gorilla/securecookie" 6 | "golang.org/x/crypto/bcrypt" 7 | ) 8 | 9 | // HashCost sets the cost of bcrypt hashes 10 | // - if this changes hashed passwords would need to be recalculated. 11 | const HashCost = 10 12 | 13 | // CheckPassword compares a password hashed with bcrypt. 14 | func CheckPassword(pass, hash string) error { 15 | return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)) 16 | } 17 | 18 | // HashPassword hashes a password with a random salt using bcrypt. 19 | func HashPassword(pass string) string { 20 | hash, _ := bcrypt.GenerateFromPassword([]byte(pass), HashCost) 21 | return string(hash) 22 | } 23 | 24 | func GenHash() string { 25 | return base32.StdEncoding.EncodeToString( 26 | securecookie.GenerateRandomKey(32), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /apiserver/pkg/graceful/graceful.go: -------------------------------------------------------------------------------- 1 | package graceful 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | "context" 11 | ) 12 | 13 | // gracefulStop 优雅退出 14 | // 等待中断信号以超时 5 秒正常关闭服务器 15 | // 官方说明:https://github.com/gin-gonic/gin#graceful-restart-or-stop 16 | func Stop(srv *http.Server) { 17 | quit := make(chan os.Signal) 18 | // kill 命令发送信号 syscall.SIGTERM 19 | // kill -2 命令发送信号 syscall.SIGINT 20 | // kill -9 命令发送信号 syscall.SIGKILL 21 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 22 | <-quit 23 | log.Println("Shutdown Server ...") 24 | 25 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 26 | defer cancel() 27 | if err := srv.Shutdown(ctx); err != nil { 28 | log.Fatal("Server Shutdown:", err) 29 | } 30 | // 5 秒后捕获 ctx.Done() 信号 31 | select { 32 | case <-ctx.Done(): 33 | log.Println("timeout of 5 seconds.") 34 | default: 35 | } 36 | log.Println("Server exiting") 37 | } -------------------------------------------------------------------------------- /apiserver/pkg/orm/orm.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "github.com/xnile/muxwaf/pkg/logx" 5 | "gorm.io/driver/mysql" 6 | "gorm.io/driver/postgres" 7 | "gorm.io/gorm" 8 | "gorm.io/gorm/logger" 9 | "time" 10 | ) 11 | 12 | type Config struct { 13 | DSN string 14 | Active int 15 | Idle int 16 | IdleTimeout time.Duration 17 | Logger logger.Interface 18 | } 19 | 20 | func NewMySQL(c *Config) *gorm.DB { 21 | db, err := gorm.Open(mysql.Open(c.DSN), &gorm.Config{Logger: c.Logger}) 22 | if err != nil { 23 | logx.Errorf("db dns(%s) error: %v", c.DSN, err) 24 | panic(err) 25 | } 26 | sqlDB, _ := db.DB() 27 | sqlDB.SetMaxIdleConns(c.Idle) 28 | sqlDB.SetMaxOpenConns(c.Active) 29 | sqlDB.SetConnMaxLifetime(time.Duration(c.IdleTimeout) / time.Second) 30 | return db 31 | } 32 | 33 | func NewPgSQL(c *Config) *gorm.DB { 34 | db, err := gorm.Open(postgres.Open(c.DSN), &gorm.Config{Logger: c.Logger}) 35 | if err != nil { 36 | logx.Errorf("db dns(%s) error: %v", c.DSN, err) 37 | panic(err) 38 | } 39 | sqlDB, _ := db.DB() 40 | sqlDB.SetMaxIdleConns(c.Idle) 41 | sqlDB.SetMaxOpenConns(c.Active) 42 | sqlDB.SetConnMaxLifetime(time.Duration(c.IdleTimeout) / time.Second) 43 | return db 44 | } 45 | -------------------------------------------------------------------------------- /apiserver/pkg/redis/driver.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "github.com/gomodule/redigo/redis" 4 | 5 | func (r *Redis) GetString(key string) (string, error) { 6 | c := r.pool.Get() 7 | defer c.Close() 8 | 9 | return redis.String(c.Do("GET", key)) 10 | } 11 | -------------------------------------------------------------------------------- /apiserver/pkg/utils/helpers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "github.com/google/uuid" 7 | tnet "github.com/toolkits/net" 8 | "io" 9 | "math" 10 | "sync" 11 | ) 12 | 13 | var ( 14 | once sync.Once 15 | clientIP = "127.0.0.1" 16 | ) 17 | 18 | // GenUUID 生成随机字符串,eg: 76d27e8c-a80e-48c8-ad20-e5562e0f67e4 19 | func GenUUID() string { 20 | u, _ := uuid.NewRandom() 21 | return u.String() 22 | } 23 | 24 | // GetLocalIP 获取本地内网IP 25 | func GetLocalIP() string { 26 | once.Do(func() { 27 | ips, _ := tnet.IntranetIP() 28 | if len(ips) > 0 { 29 | clientIP = ips[0] 30 | } else { 31 | clientIP = "127.0.0.1" 32 | } 33 | }) 34 | return clientIP 35 | } 36 | 37 | func CheckPageSizeNum(pageNum, pageSize int64) (int64, int64) { 38 | if pageSize < 1 { 39 | pageSize = 10 40 | } 41 | if pageNum < 1 { 42 | pageNum = 1 43 | } 44 | return pageNum, pageSize 45 | } 46 | 47 | func CalPage(count, pageSize int64) int64 { 48 | return int64(math.Ceil(float64(count) / float64(pageSize))) 49 | } 50 | 51 | // MD5 hashes using md5 algorithm 52 | func MD5(text string) string { 53 | algorithm := md5.New() 54 | algorithm.Write([]byte(text)) 55 | return hex.EncodeToString(algorithm.Sum(nil)) 56 | } 57 | 58 | func Close(c io.Closer) { 59 | if c == nil { 60 | return 61 | } 62 | if err := c.Close(); err != nil { 63 | //log.WithError(err).Error("关闭资源文件失败。") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /apiserver/pkg/utils/net.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net" 5 | "net/netip" 6 | "regexp" 7 | ) 8 | 9 | const ( 10 | domainRegexString = `^(?i)[a-z0-9-]+(\.[a-z0-9-]+)+\.?$` 11 | fqdnRegexStringRFC1123 = `^([a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})(\.[a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})*?(\.[a-zA-Z]{1}[a-zA-Z0-9]{0,62})\.?$` 12 | ) 13 | 14 | type IPNet struct { 15 | IP *netip.Addr 16 | Net *netip.Prefix 17 | V4 bool 18 | V6 bool 19 | } 20 | 21 | func ParseIPorCIDR(s string) IPNet { 22 | var ipNet IPNet 23 | if addr, err := netip.ParseAddr(s); err == nil { 24 | ipNet.IP = &addr 25 | if addr.Is4() { 26 | ipNet.V4 = true 27 | } 28 | if addr.Is6() { 29 | ipNet.V6 = true 30 | } 31 | } 32 | if prefix, err := netip.ParsePrefix(s); err == nil { 33 | ipNet.Net = &prefix 34 | if prefix.Addr().Is4() { 35 | ipNet.V4 = true 36 | } 37 | if prefix.Addr().Is6() { 38 | ipNet.V6 = true 39 | } 40 | } 41 | return ipNet 42 | } 43 | 44 | func IsIPv4(s string) bool { 45 | ip := net.ParseIP(s) 46 | 47 | return ip != nil && ip.To4() != nil 48 | } 49 | 50 | func IsValidDomain(domain string) bool { 51 | domainRegexp := regexp.MustCompile(domainRegexString) 52 | return domainRegexp.MatchString(domain) 53 | } 54 | 55 | func IsFQDN(s string) bool { 56 | 57 | if s == "" { 58 | return false 59 | } 60 | fqdnRegexRFC1123 := regexp.MustCompile(fqdnRegexStringRFC1123) 61 | 62 | return fqdnRegexRFC1123.MatchString(s) 63 | } 64 | 65 | func GetOutboundIP() net.IP { 66 | conn, err := net.Dial("udp", "8.8.8.8:80") 67 | if err != nil { 68 | return net.IP{} 69 | } 70 | defer conn.Close() 71 | localAddr := conn.LocalAddr().(*net.UDPAddr) 72 | return localAddr.IP 73 | } 74 | -------------------------------------------------------------------------------- /apiserver/pkg/utils/pem.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "regexp" 4 | 5 | var pemCertificatePattern = regexp.MustCompile(`^(-{5}BEGIN CERTIFICATE-{5}\x{000D}?\x{000A}([A-Za-z0-9/+]{64}\x{000D}?\x{000A})*[A-Za-z0-9/+]{1,64}={0,2}\x{000D}?\x{000A}-{5}END CERTIFICATE-{5}\x{000D}?\x{000A})*-{5}BEGIN CERTIFICATE-{5}\x{000D}?\x{000A}([A-Za-z0-9/+]{64}\x{000D}?\x{000A})*[A-Za-z0-9/+]{1,64}={0,2}\x{000D}?\x{000A}-{5}END CERTIFICATE-{5}(\x{000D}?\x{000A})?$`) 6 | var pemPrivateKeyPattern = regexp.MustCompile(`^-{5}BEGIN (RSA|EC) PRIVATE KEY-{5}\x{000D}?\x{000A}([A-Za-z0-9/+]{64}\x{000D}?\x{000A})*[A-Za-z0-9/+]{1,64}={0,2}\x{000D}?\x{000A}-{5}END (RSA|EC) PRIVATE KEY-{5}(\x{000D}?\x{000A})?$`) 7 | 8 | func IsValidPEMCertificate(cert string) bool { 9 | return pemCertificatePattern.MatchString(cert) 10 | } 11 | 12 | func IsValidPEMPrivateKey(key string) bool { 13 | return pemPrivateKeyPattern.MatchString(key) 14 | } 15 | -------------------------------------------------------------------------------- /apiserver/router/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | jwt "github.com/dgrijalva/jwt-go" 6 | "github.com/gin-gonic/gin" 7 | "github.com/xnile/muxwaf/pkg/token" 8 | ) 9 | 10 | // ParseToken 处理Token 11 | func ParseToken() gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | if c.GetBool("bypass") { 14 | return 15 | } 16 | 17 | var bearer = c.GetHeader("Authorization") 18 | var tokenStr string 19 | fmt.Sscanf(bearer, "Bearer %s", &tokenStr) 20 | claims, err := token.Decode(tokenStr) 21 | if err == nil { 22 | if uid, ok := claims["uid"].(float64); ok { 23 | c.Set("uid", int64(uid)) 24 | } else { 25 | // TODO 日志 26 | c.Set("uid", 0) 27 | } 28 | } else { 29 | if ve, ok := err.(*jwt.ValidationError); ok { 30 | if ve.Errors&jwt.ValidationErrorExpired != 0 { 31 | c.JSON(402, gin.H{"success": 402, "message": "登陆超时,请重新登陆", "data": nil}) 32 | c.Abort() 33 | return 34 | } else { 35 | // TODO 36 | c.Set("uid", 0) 37 | } 38 | } else { 39 | //TODO 40 | c.Set("uid", 0) 41 | } 42 | } 43 | c.Next() 44 | } 45 | } 46 | 47 | // ParseToken 处理Token 48 | //func ParseToken() gin.HandlerFunc { 49 | // return func(c *gin.Context) { 50 | // var bearer = c.GetHeader("Authorization") 51 | // var jwt string 52 | // fmt.Sscanf(bearer, "Bearer %s", &jwt) 53 | // if id, ok := token.Decode(jwt); ok { 54 | // c.Set("uid", id) 55 | // } else { 56 | // c.Set("uid", "") 57 | // } 58 | // c.Next() 59 | // } 60 | //} 61 | 62 | // AuthRequired 需要登陆 63 | func AuthRequired() gin.HandlerFunc { 64 | return func(c *gin.Context) { 65 | if c.GetBool("bypass") { 66 | return 67 | } 68 | if id := c.GetInt64("uid"); id == 0 { 69 | c.JSON(403, gin.H{"success": 403, "message": "请先登录", "data": nil}) 70 | c.Abort() 71 | return 72 | } 73 | c.Next() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /apiserver/router/middleware/header.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | //func Cors(c *gin.Context) { 9 | // if c.Request.Method != "OPTIONS" { 10 | // c.Header("Access-Control-Allow-Origin", "*") 11 | // c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") 12 | // c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept") 13 | // c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS") 14 | // c.Header("Content-Type", "application/json") 15 | // c.Next() 16 | // } else { 17 | // c.Header("Access-Control-Allow-Origin", "*") 18 | // c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") 19 | // c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept") 20 | // c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS") 21 | // c.Header("Content-Type", "application/json") 22 | // c.AbortWithStatus(200) 23 | // } 24 | //} 25 | 26 | func Cors(c *gin.Context) { 27 | orgin := c.Request.Header.Get("Origin") 28 | if len(orgin) == 0 { 29 | return 30 | } 31 | 32 | host := c.Request.Host 33 | if orgin == "http://"+host || orgin == "https://"+host { 34 | return 35 | } 36 | 37 | if c.Request.Method == "OPTIONS" { 38 | generateHeaders(c) 39 | c.AbortWithStatus(http.StatusNoContent) 40 | return 41 | } 42 | 43 | generateHeaders(c) 44 | 45 | } 46 | 47 | func generateHeaders(c *gin.Context) { 48 | c.Header("Access-Control-Allow-Origin", "*") 49 | c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") 50 | c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept, access-token") 51 | c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS") 52 | c.Header("Content-Type", "application/json") 53 | } 54 | -------------------------------------------------------------------------------- /apiserver/router/middleware/requestid.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/xnile/muxwaf/pkg/utils" 6 | ) 7 | 8 | // RequestID 透传Request-ID,如果没有则生成一个 9 | func RequestID() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | // Check for incoming header, use it if exists 12 | requestID := c.Request.Header.Get("X-Request-ID") 13 | 14 | // Create request id with UUID4 15 | if requestID == "" { 16 | requestID = utils.GenUUID() 17 | } 18 | 19 | // Expose it for use in the application 20 | c.Set("X-Request-ID", requestID) 21 | 22 | // Set X-Request-ID header 23 | c.Writer.Header().Set("X-Request-ID", requestID) 24 | c.Next() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | 5 | guard: 6 | image: xnile/muxwaf-guard:0.0.1 7 | restart: always 8 | ports: 9 | - 8083:8083 10 | - 8080:80 11 | - 8443:443 12 | 13 | db: 14 | image: postgres:13 15 | restart: always 16 | environment: 17 | POSTGRES_PASSWORD: example 18 | volumes: 19 | - ./scripts/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh 20 | 21 | apiserver: 22 | image: xnile/muxwaf-apiserver:0.0.1 23 | restart: always 24 | #volumes: 25 | # - ./config.yaml:/etc/muxwaf/config.yaml 26 | depends_on: 27 | - db 28 | 29 | ui: 30 | image: xnile/muxwaf-ui:0.0.1 31 | restart: always 32 | ports: 33 | - 8000:80 34 | 35 | # adminer: 36 | # image: adminer 37 | # restart: always 38 | # ports: 39 | # - 8088:8080 40 | -------------------------------------------------------------------------------- /docker/apiserver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18 AS build 2 | 3 | ENV GOPROXY=https://goproxy.cn,direct 4 | COPY . /build 5 | WORKDIR /build 6 | 7 | RUN make build 8 | 9 | FROM alpine:3 10 | 11 | COPY --from=build /build/bin/muxwaf-apiserver /usr/bin/muxwaf-apiserver 12 | COPY conf/config.docker.yaml /etc/muxwaf/config.yaml 13 | 14 | ENTRYPOINT ["/usr/bin/muxwaf-apiserver", "-c", "/etc/muxwaf/config.yaml"] 15 | -------------------------------------------------------------------------------- /docker/guard/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openresty/openresty:1.21.4.1-0-alpine-apk 2 | COPY guard /opt/apps/muxwaf/guard 3 | 4 | RUN chown -R nobody. /opt/apps/muxwaf \ 5 | && ln -sf /dev/stdout /opt/apps/muxwaf/guard/logs/access.log \ 6 | && ln -sf /dev/stdout /opt/apps/muxwaf/guard/logs/sampled.log \ 7 | && ln -sf /dev/stderr /opt/apps/muxwaf/guard/logs/error.log 8 | 9 | #CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"] 10 | ENTRYPOINT ["/usr/local/openresty/bin/openresty", "-p", "/opt/apps/muxwaf/guard", "-c", "nginx.docker.conf", "-g", "daemon off;"] -------------------------------------------------------------------------------- /docker/prometheus/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | 5 | prom: 6 | image: bitnami/prometheus:2.43.0 7 | restart: always 8 | volumes: 9 | - ./prometheus.yml:/opt/bitnami/prometheus/conf/prometheus.yml 10 | ports: 11 | - 9090:9090 12 | grafana: 13 | image: grafana/grafana:9.4.7 14 | environment: 15 | - GF_SECURITY_ALLOW_EMBEDDING=true 16 | restart: always 17 | ports: 18 | - 3000:3000 19 | 20 | -------------------------------------------------------------------------------- /docker/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Alertmanager configuration 8 | alerting: 9 | alertmanagers: 10 | - static_configs: 11 | - targets: 12 | # - alertmanager:9093 13 | 14 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 15 | rule_files: 16 | # - "first_rules.yml" 17 | # - "second_rules.yml" 18 | 19 | # A scrape configuration containing exactly one endpoint to scrape: 20 | # Here it's Prometheus itself. 21 | scrape_configs: 22 | # The job name is added as a label `job=` to any timeseries scraped from this config. 23 | - job_name: "prometheus" 24 | 25 | # metrics_path defaults to '/metrics' 26 | # scheme defaults to 'http'. 27 | 28 | static_configs: 29 | - targets: ["localhost:9090"] 30 | 31 | - job_name: "muxwaf" 32 | static_configs: 33 | - targets: 34 | - "host.docker.internal:8083" 35 | metrics_path: "/api/sys/metrics" 36 | # params: 37 | # format: ['prom'] 38 | # relabel_configs: 39 | # - source_labels: [__address__] 40 | # regex: '([^:]+)(:[0-9]+)?' 41 | # replacement: '$1' 42 | # target_label: node 43 | -------------------------------------------------------------------------------- /docker/ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.23.3 2 | COPY docker/ui/nginx-default.conf /etc/nginx/conf.d/default.conf 3 | COPY ui/dist /usr/share/nginx/html -------------------------------------------------------------------------------- /docker/ui/nginx-default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name localhost; 5 | 6 | location /api { 7 | proxy_pass http://apiserver:8001; 8 | proxy_set_header Host $http_host; 9 | proxy_set_header X-Forward-For $remote_addr; 10 | proxy_set_header X-real-ip $remote_addr; 11 | } 12 | 13 | location / { 14 | root /usr/share/nginx/html; 15 | index index.html index.htm; 16 | try_files $uri $uri/ /index.html; 17 | } 18 | } -------------------------------------------------------------------------------- /guard/certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICsDCCAZgCCQCvffWx5X3mFTANBgkqhkiG9w0BAQUFADAZMRcwFQYDVQQDDA53 3 | d3cubXV4d2FmLmNvbTAeFw0yMjEyMTQwNzIyMTBaFw0yMzAxMTMwNzIyMTBaMBsx 4 | GTAXBgNVBAMMEHd3dy5ub3RleGlzdC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB 5 | DwAwggEKAoIBAQCwNLaAD62jXz93FxUob4aJ4rVHn+JdBasQj0JgUdO7I4DN0xZn 6 | Bvl6A6elM8QYmH/CutkI5w5GtwgiN1a8uNwWY7yjZN1hwAW9lG93WWCHsuY6xnay 7 | 4WDuIWzX9E/D3VFiGGVKTGcDojqouF7AkX7uDVB/eN+GfGuAy39xlUuPMIR7WEAK 8 | aVH9lEcwNcy9mtslkBklbncpGWKGt+gFZSbCuWD6yj6bG4kyk/FzxLgy4LimSxzI 9 | 3uWoWqbFD0C8Szf4HM3EchAjgGJAVQOdIhr/9KSOMBVKRdbuHaUUSQrofQsAm+BU 10 | fkR1knJgnLRCrlbrIEfCH6pea+26csg21g3PAgMBAAEwDQYJKoZIhvcNAQEFBQAD 11 | ggEBAECq7xJvytfHVqIni9Tsk5sKyzODtdKdZPgxGAj/ypF6kwf+gMYPfrwtqwzI 12 | q9E6xx4pXGuskOI5W6mgSOBY/A92ziTMJsRVcrLkxQnCe5YWTdX0i1Qrqh8xz1po 13 | BFbEauZ6+xpOEtouG0Cmjn7Ol9v/wP0XwzEHHbIGd3brrNKUpyZuwOGRpNFL2p+i 14 | 7DIFW+vazqAYyECHbYCd6r0dZtHveI1iwAX4eYYjkWJfYYJombNpfD3r7gWZkcej 15 | D7KXo/suLI3yAl0+qzN/tak3pMYCsf8tfC6y6C4Pk/pYiZ1+UZggEx//RE38zbz/ 16 | H1K5vNK8OgrVDSCj6yKnoruzDyo= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /guard/certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAsDS2gA+to18/dxcVKG+GieK1R5/iXQWrEI9CYFHTuyOAzdMW 3 | Zwb5egOnpTPEGJh/wrrZCOcORrcIIjdWvLjcFmO8o2TdYcAFvZRvd1lgh7LmOsZ2 4 | suFg7iFs1/RPw91RYhhlSkxnA6I6qLhewJF+7g1Qf3jfhnxrgMt/cZVLjzCEe1hA 5 | CmlR/ZRHMDXMvZrbJZAZJW53KRlihrfoBWUmwrlg+so+mxuJMpPxc8S4MuC4pksc 6 | yN7lqFqmxQ9AvEs3+BzNxHIQI4BiQFUDnSIa//SkjjAVSkXW7h2lFEkK6H0LAJvg 7 | VH5EdZJyYJy0Qq5W6yBHwh+qXmvtunLINtYNzwIDAQABAoIBAQCrBE5Ilm7pjE01 8 | kYKofy2t8AoPnR7/N7wi01gR0flUvQ2LNp+cmQlTIe75CPf/Ayuk3OI+oLqbaZOM 9 | WfdmLWj2lhCE6SVwdQVdTY6t4GIu8jQ/wVV9If+Iv4o7QtST/d65/YF5jhPBVBfo 10 | nyDfd5chBm0lezQ++8z+8QMdQ1BVhJ27a2wIzPyc6JJ0miAJpVvNQyHZ4g/Pgje5 11 | elxC0V6w3ENbIMxZB1ZzQYbA6h7EwDo/N6fXCEKsXTUqMpZq6G0o0VM5T1dIZTmV 12 | 4YiOQ79feQLs87AIiY9FailkDft+YWZXdbY8dDrNfSwULuO2GMzR9z+8ciYkHKfk 13 | bBnNqhZBAoGBAN2Yty8o54zjCaEMjhzL7Sb2yMrhAkeYi0nPGRgq6U2+hNooeIjY 14 | M+R6ZlKQ8Vymh07BKYkhWTwZ607zAfe/eDNSoyRLpwF3zPWVfKMGICCgyj0rNAvD 15 | YD9113CSXPlB3ZmV1ZOg3wJ+SMvwZqndo+nWifpAzwDw2sHUdc60UlEhAoGBAMuP 16 | 9f5ezeI4CLGKZD4ZDws05XhzQzgu7W3UQX/+jgRrAz4/D/PKewrqqAEOGpehM4qF 17 | iFLPN6wvoA5aDEjVKU7I3+rxfwIIvwcejy6dpbRxAnoiSOa5bcO+7SWipD3fevrx 18 | MBIBKhXLvYodb1dETDC05S4oaePxOGcBSbBWYlDvAoGAK5IZgqZSUPtbK1Vio/rc 19 | 2GpxtKJlZlpyGMMltgDJ+sVZLeQDPzTzjxsZ/2YMbyE3PZ7xYcsy1oRqJg3V9zao 20 | lb7L4RkW70z0+dy4sv1DV+cFnN1f0GhMBm5PPA0rRr9SqR4y7mIVN1JJUgAkz4WK 21 | 8zGB07jmetLg0HfL6ia+gMECgYB2w3sl+XHtNfv8eV2azUv1odrvt2/Ua6yik1/T 22 | qVR6R2URAAHynoPDDr0OQIhLRwWnH9k1VyiIERE0zly0cYk0KgZaMKOFME0jfjTT 23 | Ya634+cqEq6tzim4NsBtYMKqRqSdm/ILzxoatU/UMPAw3ewFzsSXdYSyNJ5PmnS/ 24 | qOabiQKBgQC4fjv6XqfYZ1w1VhY6O/oy9hnBva2wHVPOeU07/APJmiAIIktuLoWJ 25 | HI3vtDka9mK1SFEYyZoq0SwYNYkjvytepHAzQi9JP3HU2KhLJYPoMAuoa3MYdXeh 26 | 7F+Q0aXQwOFoWAdUHntYqluXe4ZlTqvg+ntauMV23y3S3R8E83EDEg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /guard/ipdb/ipipfree.ipdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnile/muxwaf/6d296fbfc7c3afc0dfaa65ea8640587b9a7718bd/guard/ipdb/ipipfree.ipdb -------------------------------------------------------------------------------- /guard/lib/apis/configs.lua: -------------------------------------------------------------------------------- 1 | local events = require("events") 2 | local schemas = require("schemas") 3 | local configs = require("configs") 4 | local json = require("utils.json") 5 | local metrics = require("metrics") 6 | local setmetatable = setmetatable 7 | 8 | 9 | return setmetatable({}, { 10 | __index = function(_, configType) 11 | return setmetatable({}, { 12 | __index = function(_, operation) 13 | return function(c) 14 | local event = { 15 | configType = configType, 16 | operation = operation, 17 | data = {}, 18 | } 19 | 20 | if operation == "reset" then 21 | events:send(c.encode(event)) 22 | return c.say_ok() 23 | elseif operation == "show" then 24 | c.say_json(configs.get_raw()) 25 | else 26 | local data, err = c.get_and_decode_body_data() 27 | if not data then 28 | return c.say_err(500, "invalid json data: " .. err) 29 | end 30 | 31 | local validator = schemas.validator[configType][operation] 32 | local ok, err = validator(data) 33 | if not ok then 34 | return c.say_err(500, err) 35 | end 36 | 37 | event.data = data 38 | events:send(c.encode(event)) 39 | metrics:incr_config_updates() 40 | return c.say_ok() 41 | end 42 | end 43 | end 44 | }) 45 | end 46 | }) 47 | -------------------------------------------------------------------------------- /guard/lib/apis/init.lua: -------------------------------------------------------------------------------- 1 | local router = require("router") 2 | local metrics = require("apis.metrics") 3 | local configs = require("apis.configs") 4 | 5 | 6 | local r = router.new("/api") 7 | r:POST("/sites", configs.sites.add) 8 | r:PUT("/sites", configs.sites.update) 9 | r:DELETE("/sites", configs.sites.del) 10 | 11 | r:POST("/certificates", configs.certificates.add) 12 | r:PUT("/certificates", configs.certificates.update) 13 | r:DELETE("/certificates", configs.certificates.del) 14 | 15 | r:POST("/blacklist/ip", configs.blacklist_ip.add) 16 | r:DELETE("/blacklist/ip", configs.blacklist_ip.del) 17 | 18 | r:PUT("/blacklist/region", configs.blacklist_region.add) 19 | r:DELETE("/blacklist/region", configs.blacklist_region.del) 20 | 21 | r:POST("/whitelist/ip", configs.whitelist_ip.add) 22 | r:DELETE("/whitelist/ip", configs.whitelist_ip.del) 23 | 24 | r:POST("/whitelist/url", configs.whitelist_url.add) 25 | r:PUT("/whitelist/url", configs.whitelist_url.update) 26 | r:DELETE("/whitelist/url", configs.whitelist_url.del) 27 | 28 | r:POST("/rate-limit", configs.rate_limit.add) 29 | r:PUT("/rate-limit", configs.rate_limit.update) 30 | r:DELETE("/rate-limit", configs.rate_limit.del) 31 | 32 | r:PUT("/sys/configs/sample_log_upload",configs.sample_log.update) 33 | r:GET("/sys/configs", configs.this.show) 34 | r:POST("/sys/configs", configs.this.full_sync) 35 | r:GET("/sys/metrics", metrics.get) 36 | r:DELETE("/sys/configs/rules", configs.rules.reset) 37 | r:DELETE("/sys/configs", configs.this.reset) 38 | 39 | 40 | return r -------------------------------------------------------------------------------- /guard/lib/apis/metrics.lua: -------------------------------------------------------------------------------- 1 | local metrics = require("metrics") 2 | 3 | return { 4 | get = function(c) 5 | return metrics.collect(c) 6 | end 7 | } -------------------------------------------------------------------------------- /guard/lib/events.lua: -------------------------------------------------------------------------------- 1 | local require = require 2 | local configs = require("configs") 3 | local cjson = require("cjson.safe") 4 | local ngx = ngx 5 | local ngx_worker_count = ngx.worker.count 6 | local assert = assert 7 | 8 | local _dict_name = require("constants").DICTS.EVENTS 9 | local shm = ngx.shared[_dict_name] 10 | 11 | local _M = { 12 | _VERSION = 0.1 13 | } 14 | 15 | 16 | local function handle(event) 17 | local r, err = cjson.decode(event) 18 | if not r then 19 | log.error("could not parse event: ", err) 20 | return 21 | end 22 | 23 | local configType, operation, data = r.configType, r.operation, r.data 24 | configs:sync(configType, operation, data) 25 | end 26 | 27 | 28 | function _M.send(_, event) 29 | for i = 0, ngx_worker_count() -1 do 30 | local key = "pid:" .. i 31 | shm:rpush(key, event) 32 | end 33 | end 34 | 35 | 36 | function _M.pop(self, worker_pid) 37 | local key = "pid:" .. worker_pid 38 | 39 | local len, err = shm:llen(key) 40 | if len == 0 then 41 | return 42 | end 43 | 44 | for i = 1, len do 45 | local event = assert(shm:lpop(key)) 46 | handle(event) 47 | end 48 | end 49 | 50 | 51 | return _M -------------------------------------------------------------------------------- /guard/lib/geo/parser/ip2region/libxdb.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnile/muxwaf/6d296fbfc7c3afc0dfaa65ea8640587b9a7718bd/guard/lib/geo/parser/ip2region/libxdb.dylib -------------------------------------------------------------------------------- /guard/lib/geo/parser/ip2region/libxdb.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnile/muxwaf/6d296fbfc7c3afc0dfaa65ea8640587b9a7718bd/guard/lib/geo/parser/ip2region/libxdb.so -------------------------------------------------------------------------------- /guard/lib/geo/parser/ipip/city.lua: -------------------------------------------------------------------------------- 1 | 2 | local _M = { 3 | _VERSION = '0.1' 4 | } 5 | 6 | local mt = { 7 | __index = _M 8 | } 9 | 10 | local reader = require("geo.parser.ipip.reader") 11 | 12 | function _M.new(self, name) 13 | return setmetatable({ 14 | db = reader:new(name), 15 | }, mt) 16 | end 17 | 18 | function _M.find(self, ips, language) 19 | return self.db:find(ips, language) 20 | end 21 | 22 | function _M.build(self) 23 | return self.db.meta["build"] 24 | end 25 | 26 | function _M.fields(self) 27 | return table.concat(self.db.meta.fields, ",") 28 | end 29 | 30 | function _M.languages(self) 31 | local lang = {} 32 | for k, v in pairs(self.db.meta.languages) do 33 | table.insert(lang, k) 34 | end 35 | return table.concat(lang, ",") 36 | end 37 | 38 | return _M 39 | -------------------------------------------------------------------------------- /guard/lib/lock.lua: -------------------------------------------------------------------------------- 1 | local lrucache = require("resty.lrucache") 2 | 3 | local _M = { 4 | _VERSION = 0.1 5 | } 6 | 7 | 8 | function _M.new(size) 9 | local c, err = lrucache.new(100) 10 | if not c then 11 | return nil, "failed to create the cache: " .. (err or "unknown") 12 | end 13 | 14 | local self = { 15 | cache = c 16 | } 17 | 18 | return setmetatable(self, { __index = _M}), nil 19 | end 20 | 21 | 22 | function _M.lock(self, key, ttl, flags) 23 | if not self.cache:get(key) then 24 | self.cache:set(key, true, ttl) 25 | return true 26 | end 27 | return false 28 | end 29 | 30 | 31 | function _M.unlock(self, key) 32 | self.cache:delete(key) 33 | end 34 | 35 | 36 | return _M -------------------------------------------------------------------------------- /guard/lib/log.lua: -------------------------------------------------------------------------------- 1 | local ngx_errlog = require("ngx.errlog") 2 | local ngx = ngx 3 | local ngx_log = ngx.log 4 | local string_format = string.format 5 | local ngx_get_phase = ngx.get_phase 6 | 7 | 8 | local _M = { 9 | _VERSION = 0.1 10 | } 11 | 12 | local ngx_log_levels = { 13 | stderr = ngx.STDERR, 14 | emerg = ngx.EMERG, 15 | alert = ngx.ALERT, 16 | crit = ngx.CRIT, 17 | error = ngx.ERR, 18 | warn = ngx.WARN, 19 | notice = ngx.NOTICE, 20 | info = ngx.INFO, 21 | debug = ngx.DEBUG, 22 | } 23 | 24 | 25 | local function get_cnt_log_level() 26 | local cnt 27 | -- https://github.com/openresty/lua-nginx-module/issues/467#issuecomment-82647228 28 | if ngx_get_phase() ~= "init" then 29 | cnt = ngx.config.subsystem == "http" and ngx_errlog.get_sys_filter_level() 30 | end 31 | return cnt 32 | end 33 | 34 | 35 | setmetatable(_M, { 36 | __index = function(self, level) 37 | local log_level = ngx_log_levels[level] 38 | local cnt_level = get_cnt_log_level() 39 | local cmd 40 | if not log_level then 41 | ngx_log(ngx_log_levels.error, string_format("command '%s' is not supported", level)) 42 | cmd = function() end 43 | elseif cnt_level and (log_level > cnt_level) then 44 | cmd = function() end 45 | else 46 | cmd = function(...) 47 | if log_level == ngx_log_levels.error then 48 | require("metrics").incr_errors() 49 | end 50 | return ngx_log(log_level, ...) 51 | end 52 | end 53 | 54 | if ngx_get_phase() ~= "init" then 55 | self[level] = cmd 56 | end 57 | 58 | return cmd 59 | end 60 | }) 61 | 62 | return _M -------------------------------------------------------------------------------- /guard/lib/origins.lua: -------------------------------------------------------------------------------- 1 | 2 | local _M = { 3 | _VERSION = 0.1 4 | } 5 | 6 | 7 | function _M.add_origins(_, items) 8 | for _, item in ipairs(items) do 9 | local host, origins = item.host, item.origins 10 | 11 | if origins[host] then 12 | log.warn("failed to add origin of the site '", host, "', origin already exists") 13 | goto continue 14 | end 15 | 16 | site_origins[host] = table_clone(origins) 17 | ::continue:: 18 | end 19 | end 20 | 21 | 22 | function _M.del_origins(_, items) 23 | for _, host in ipairs(items) do 24 | if not origins[host] then 25 | log.warn("Failed to remove the origin server of the site '", host, "', the site does not exist") 26 | goto continue 27 | end 28 | 29 | site_origins[host] = nil 30 | balancers[host] = nil 31 | upstream_servers[host] = nil 32 | ::continue:: 33 | end 34 | end 35 | 36 | 37 | function _M.update_origins(_, items) 38 | for _, item in ipairs(items) do 39 | local host, origins = item.host, item.origins 40 | 41 | if origins[host] then 42 | log.warn("failed to update origin of the site '", host, "', the site does not exist") 43 | goto continue 44 | end 45 | 46 | site_origins[host] = table_clone(origins) 47 | ::continue:: 48 | end 49 | end 50 | 51 | 52 | function _M.full_sync_origins(_, items) 53 | local new_origins = {} 54 | for _, item in ipairs(items) do 55 | local host, origins = item.host, item.origins 56 | new_origins[host] = table_clone(origins) 57 | end 58 | site_origins = new_origins 59 | end 60 | -------------------------------------------------------------------------------- /guard/lib/page/403.lua: -------------------------------------------------------------------------------- 1 | -- local page_403 = [[ 2 | -- 3 | -- 4 | -- 5 | -- 6 | -- 403 Forbidden 7 | -- 14 | -- 15 | -- 16 | --

403 Forbidden

17 | --

当前访问可能对网站造成威胁,已被阻断,如有疑问请联系网站管理员并提供请求ID。

18 | --

您的请求ID为:{{REQUEST_ID}}

19 | -- 20 | -- ]] 21 | -- return page_403 22 | 23 | 24 | local page_403 = { 25 | [1] = [[ 26 | 27 | 28 | 29 | 30 | 403 Forbidden 31 | 38 | 39 | 40 |

403 Forbidden

41 |

当前访问可能对网站造成威胁,已被阻断,如有疑问请联系网站管理员并提供请求ID。

42 |

您的请求ID为:]], 43 | [2] = "{{REQUEST_ID}}", 44 | [3] = [[

45 | 46 | ]], 47 | } 48 | 49 | return page_403 -------------------------------------------------------------------------------- /guard/lib/page/410.lua: -------------------------------------------------------------------------------- 1 | local page_410 = [[ 2 | 3 | 4 | 5 | 6 | 410 Gone 7 | 12 | 13 | 14 |

410 Gone

15 |

您访问的网站不存在

16 |
17 |

MuxWAF

18 | 19 | ]] 20 | 21 | 22 | return page_410 -------------------------------------------------------------------------------- /guard/lib/page/500.lua: -------------------------------------------------------------------------------- 1 | local page_500 = [[ 2 | 3 | 4 | 5 | 6 | 500 Internal Server Error 7 | 14 | 15 | 16 |

An error occurred.

17 |

You can report issue to MuxWAF

18 |

Faithfully yours, MuxWAF.

19 | 20 | ]] 21 | 22 | 23 | return page_500 -------------------------------------------------------------------------------- /guard/lib/resty/balancer/utils.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | _M.name = "balancer-utils" 4 | _M.version = "0.03" 5 | 6 | local new_tab 7 | do 8 | local ok 9 | ok, new_tab = pcall(require, "table.new") 10 | if not ok or type(new_tab) ~= "function" then 11 | new_tab = function (narr, nrec) return {} end 12 | end 13 | end 14 | _M.new_tab = new_tab 15 | 16 | 17 | local nkeys, tab_nkeys 18 | do 19 | local ok 20 | ok, nkeys = pcall(require, "table.nkeys") 21 | if not ok or type(nkeys) ~= "function" then 22 | nkeys = function(tab) 23 | local count = 0 24 | for _, _ in pairs(tab) do 25 | count = count + 1 26 | end 27 | return count 28 | end 29 | 30 | else 31 | tab_nkeys = nkeys 32 | end 33 | end 34 | _M.nkeys = nkeys 35 | 36 | 37 | function _M.copy(nodes) 38 | local newnodes = new_tab(0, tab_nkeys and tab_nkeys(nodes) or 4) 39 | for id, weight in pairs(nodes) do 40 | newnodes[id] = tonumber(weight) 41 | end 42 | 43 | return newnodes 44 | end 45 | 46 | 47 | return _M 48 | -------------------------------------------------------------------------------- /guard/lib/resty/ctxdump.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) UPYUN, Inc. 2 | -- Copyright (C) Alex Zhang 3 | 4 | local tonumber = tonumber 5 | local _M = { _VERSION = "0.03" } 6 | local memo = {} 7 | local FREE_LIST_REF = 0 8 | 9 | 10 | local function ref_in_table(tb, key) 11 | if key == nil then 12 | return -1 13 | end 14 | 15 | local ref = tb[FREE_LIST_REF] 16 | if ref and ref ~= FREE_LIST_REF then 17 | tb[FREE_LIST_REF] = tb[ref] 18 | else 19 | ref = #tb + 1 20 | end 21 | 22 | tb[ref] = key 23 | 24 | return ref 25 | end 26 | 27 | 28 | function _M.stash_ngx_ctx() 29 | local ctx_ref = ref_in_table(memo, ngx.ctx) 30 | return ctx_ref 31 | end 32 | 33 | 34 | function _M.apply_ngx_ctx(ref) 35 | ref = tonumber(ref) 36 | if not ref or ref <= FREE_LIST_REF then 37 | return nil, "bad ref value" 38 | end 39 | 40 | local old_ngx_ctx = memo[ref] 41 | 42 | -- dereference 43 | memo[ref] = memo[FREE_LIST_REF] 44 | memo[FREE_LIST_REF] = ref 45 | 46 | return old_ngx_ctx 47 | end 48 | 49 | 50 | return _M 51 | -------------------------------------------------------------------------------- /guard/lib/resty/http/http_headers.lua: -------------------------------------------------------------------------------- 1 | local rawget, rawset, setmetatable = 2 | rawget, rawset, setmetatable 3 | 4 | local str_lower = string.lower 5 | 6 | local _M = { 7 | _VERSION = '0.17.0-beta.1', 8 | } 9 | 10 | 11 | -- Returns an empty headers table with internalised case normalisation. 12 | function _M.new() 13 | local mt = { 14 | normalised = {}, 15 | } 16 | 17 | mt.__index = function(t, k) 18 | return rawget(t, mt.normalised[str_lower(k)]) 19 | end 20 | 21 | mt.__newindex = function(t, k, v) 22 | local k_normalised = str_lower(k) 23 | 24 | -- First time seeing this header field? 25 | if not mt.normalised[k_normalised] then 26 | -- Create a lowercased entry in the metatable proxy, with the value 27 | -- of the given field case 28 | mt.normalised[k_normalised] = k 29 | 30 | -- Set the header using the given field case 31 | rawset(t, k, v) 32 | else 33 | -- We're being updated just with a different field case. Use the 34 | -- normalised metatable proxy to give us the original key case, and 35 | -- perorm a rawset() to update the value. 36 | rawset(t, mt.normalised[k_normalised], v) 37 | end 38 | end 39 | 40 | return setmetatable({}, mt) 41 | end 42 | 43 | 44 | return _M 45 | -------------------------------------------------------------------------------- /guard/lib/resty/librestyradixtree.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnile/muxwaf/6d296fbfc7c3afc0dfaa65ea8640587b9a7718bd/guard/lib/resty/librestyradixtree.dylib -------------------------------------------------------------------------------- /guard/lib/resty/librestyradixtree.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnile/muxwaf/6d296fbfc7c3afc0dfaa65ea8640587b9a7718bd/guard/lib/resty/librestyradixtree.so -------------------------------------------------------------------------------- /guard/lib/resty/prometheus.lua: -------------------------------------------------------------------------------- 1 | return require("resty.prometheus.prometheus") -------------------------------------------------------------------------------- /guard/lib/resty/ratelimit.lua: -------------------------------------------------------------------------------- 1 | -- Fixed Window algorithm 2 | 3 | local ngx = ngx 4 | local ngx_shared = ngx.shared 5 | local setmetatable = setmetatable 6 | local error = error 7 | 8 | local _M = {} 9 | local _mt = { __index = _M } 10 | 11 | function _M.new(dict_name) 12 | local dict = ngx_shared[dict_name] 13 | if not dict then 14 | return error("lua_shared_dict '" .. dict_name .. "' not found", 2) 15 | end 16 | 17 | local self = { 18 | dict = dict 19 | } 20 | 21 | return setmetatable(self, _mt) 22 | end 23 | 24 | function _M.incomming(self, key, limit, window) 25 | local dict = self.dict 26 | local remaining, err = dict:incr(key, -1, limit, window) 27 | if not remaining then 28 | return false, err 29 | end 30 | 31 | if remaining < 0 then 32 | return false, "rejected" 33 | end 34 | 35 | return true, remaining 36 | end 37 | 38 | 39 | function _M.flush_all(self) 40 | local shm = self.dict 41 | shm:flush_all() 42 | shm:flush_expired() 43 | end 44 | 45 | return _M 46 | -------------------------------------------------------------------------------- /guard/lib/resty/tablepool.lua: -------------------------------------------------------------------------------- 1 | local newtab = require "table.new" 2 | local cleartab = require "table.clear" 3 | local setmetatable = setmetatable 4 | 5 | 6 | local _M = newtab(0, 2) 7 | local max_pool_size = 200 8 | local pools = newtab(0, 4) 9 | 10 | 11 | function _M.fetch(tag, narr, nrec) 12 | local pool = pools[tag] 13 | if not pool then 14 | pool = newtab(4, 1) 15 | pools[tag] = pool 16 | pool.c = 0 17 | pool[0] = 0 18 | 19 | else 20 | local len = pool[0] 21 | if len > 0 then 22 | local obj = pool[len] 23 | pool[len] = nil 24 | pool[0] = len - 1 25 | -- ngx.log(ngx.ERR, "HIT") 26 | return obj 27 | end 28 | end 29 | 30 | return newtab(narr, nrec) 31 | end 32 | 33 | 34 | function _M.release(tag, obj, noclear) 35 | if not obj then 36 | error("object empty", 2) 37 | end 38 | 39 | local pool = pools[tag] 40 | if not pool then 41 | pool = newtab(4, 1) 42 | pools[tag] = pool 43 | pool.c = 0 44 | pool[0] = 0 45 | end 46 | 47 | if not noclear then 48 | setmetatable(obj, nil) 49 | cleartab(obj) 50 | end 51 | 52 | do 53 | local cnt = pool.c + 1 54 | if cnt >= 20000 then 55 | pool = newtab(4, 1) 56 | pools[tag] = pool 57 | pool.c = 0 58 | pool[0] = 0 59 | return 60 | end 61 | pool.c = cnt 62 | end 63 | 64 | local len = pool[0] + 1 65 | if len > max_pool_size then 66 | -- discard it simply 67 | return 68 | end 69 | 70 | pool[len] = obj 71 | pool[0] = len 72 | end 73 | 74 | 75 | return _M 76 | 77 | -- vi: ft=lua ts=4 sw=4 et 78 | -------------------------------------------------------------------------------- /guard/lib/tasks.lua: -------------------------------------------------------------------------------- 1 | local require = require 2 | local log = require("log") 3 | local sample_log = require("sample_log") 4 | local events = require("events") 5 | local metrics = require("metrics") 6 | local constants = require("constants") 7 | local cjson = require("cjson.safe") 8 | local ngx = ngx 9 | local every = ngx.timer.every 10 | local ngx_worker_id = ngx.worker.id 11 | local io_open = io.open 12 | local tostring = tostring 13 | local assert = assert 14 | 15 | local CONFIG_SYNC_INTERVAL = constants.CONFIG_SYNC_INTERVAL 16 | local SAMPLE_LOG_SYNC_INTERVAL = constants.SAMPLE_LOG_SYNC_INTERVAL 17 | 18 | local _M = { 19 | _VERSION = 0.1 20 | } 21 | 22 | local function send_sample_logs() 23 | -- just need one worker 24 | if ngx_worker_id() == 0 then 25 | log.debug("start timer for send sample logs on worker ", tostring(ngx_worker_id())) 26 | local ok, err = every(SAMPLE_LOG_SYNC_INTERVAL, sample_log.iterator) 27 | assert(ok, "failed to setting up timer for save logs: " .. tostring(err)) 28 | end 29 | end 30 | 31 | local function sync_config() 32 | log.debug("start timer for sync configs on worker ", tostring(ngx_worker_id())) 33 | local ok, err = every(CONFIG_SYNC_INTERVAL, events.pop, ngx_worker_id()) 34 | assert(ok, "failed to setting up timer for config sync: " .. tostring(err)) 35 | end 36 | 37 | 38 | local function start() 39 | sync_config() 40 | send_sample_logs() 41 | end 42 | 43 | 44 | _M.init_worker = start 45 | 46 | 47 | return _M -------------------------------------------------------------------------------- /guard/lib/time.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | 3 | local _M = { 4 | _VERSION = 0.1 5 | } 6 | 7 | ffi.cdef[[ 8 | struct timeval { 9 | long int tv_sec; 10 | long int tv_usec; 11 | }; 12 | int gettimeofday(struct timeval *tv, void *tz); 13 | ]] 14 | 15 | local tv = ffi.new("struct timeval") 16 | 17 | -- in microseconds 18 | function _M.now() 19 | ffi.C.gettimeofday(tv, nil) 20 | return tonumber(tv.tv_sec) * 1000000 + tonumber(tv.tv_usec) 21 | end 22 | 23 | return _M 24 | -------------------------------------------------------------------------------- /guard/lib/utils.lua: -------------------------------------------------------------------------------- 1 | local table_new = table.new 2 | 3 | local tmp_table = {} 4 | 5 | local _M = { 6 | _VERSION = 0.1 7 | } 8 | 9 | function _M.pretty_bytes(c) 10 | if c > 1024 * 1024 then 11 | return string.format('%.2f', c / 1024 / 1024) .. 'M' 12 | elseif c > 1024 then 13 | return string.format('%.2f', c / 1024) .. 'K' 14 | else 15 | return string.format('%.2f', c) .. 'B' 16 | end 17 | end 18 | 19 | function _M.pretty_bandwidth(c) 20 | if c > 1024 * 1024 then 21 | return string.format('%.2f', c / 1024 / 1024) .. 'Mbps' 22 | elseif c > 1024 then 23 | return string.format('%.2f', c / 1024) .. 'Kbps' 24 | else 25 | return string.format('%.2f', c) .. 'bps' 26 | end 27 | end 28 | 29 | 30 | function _M.pretty_number(num) 31 | if not num then return 0 end 32 | if math.abs(num) < 1000 then return num end 33 | local neg = num < 0 and "-" or "" 34 | local left, mid, right = tostring(math.abs(num)):match("^([^%d]*%d)(%d*)(.-)$") 35 | return ("%s%s%s%s"):format(neg, left, mid:reverse():gsub("(%d%d%d)", "%1,"):reverse(), right) 36 | end 37 | 38 | 39 | -- @param old table, kv like table 40 | -- @param new table, array like table 41 | function _M.diff_cfg_ids(old, new) 42 | local ids = table_new(0, #new) 43 | for _, v in ipairs(new) do 44 | if v.id then 45 | ids[v.id] = tmp_table 46 | end 47 | end 48 | 49 | local diff_ids = {} 50 | for id, _ in pairs(old) do 51 | if not ids[id] then 52 | diff_ids[#diff_ids+1] = id 53 | end 54 | end 55 | 56 | return diff_ids 57 | end 58 | 59 | return _M -------------------------------------------------------------------------------- /guard/lib/utils/net.lua: -------------------------------------------------------------------------------- 1 | local dns = require("utils.net.dns") 2 | local ffi = require "ffi" 3 | local C = ffi.C 4 | local ffi_cdef = ffi.cdef 5 | local ffi_new = ffi.new 6 | 7 | 8 | local AF_INET = 2 9 | local AF_INET6 = 10 10 | 11 | if ffi.os == "OSX" then 12 | AF_INET6 = 30 13 | end 14 | 15 | 16 | local _M = { 17 | dns = dns, 18 | _VERSION = 0.1 19 | } 20 | 21 | 22 | ffi_cdef[[ 23 | int inet_pton(int af, const char * restrict src, void * restrict dst); 24 | ]] 25 | 26 | local inet = ffi_new("unsigned int [1]") 27 | 28 | 29 | function _M.is_valid_ip(ip) 30 | if type(ip) ~= "string" then 31 | return false 32 | end 33 | 34 | -- local inet = ffi_new("unsigned int [1]") 35 | if C.inet_pton(AF_INET, ip, inet) == 1 then 36 | return true 37 | end 38 | 39 | if C.inet_pton(AF_INET6, ip, inet) == 1 then 40 | return true 41 | end 42 | 43 | return false 44 | 45 | end 46 | 47 | 48 | function _M.is_valid_ipv4(ip) 49 | -- local inet = ffi_new("unsigned int [1]") 50 | if C.inet_pton(AF_INET, ip, inet) == 1 then 51 | return true 52 | end 53 | 54 | return false 55 | end 56 | 57 | 58 | function _M.is_valid_ipv6(ip) 59 | -- local inet = ffi_new("unsigned int [1]") 60 | if C.inet_pton(AF_INET6, ip, inet) == 1 then 61 | return true 62 | end 63 | 64 | return false 65 | end 66 | 67 | return _M -------------------------------------------------------------------------------- /guard/lib/utils/set.lua: -------------------------------------------------------------------------------- 1 | local table_new = table.new 2 | local table_insert = table.insert 3 | local table_remove = table.remove 4 | local setmetatable = setmetatable 5 | 6 | local _M = { 7 | _VERSION = 0.1 8 | } 9 | 10 | function _M.new(size) 11 | local size = size or 0 12 | local self = table_new(size, 0) 13 | local reverse = table_new(0, size) 14 | 15 | return setmetatable(self, { 16 | __index = { 17 | insert = function(self, elem) 18 | -- print(elem) 19 | if not reverse[elem] then 20 | table_insert(self, elem) 21 | reverse[elem] = #self 22 | end 23 | end, 24 | 25 | remove = function(self, elem) 26 | local idx = reverse[elem] 27 | if idx then 28 | reverse[elem] = nil 29 | local end_elem = table_remove(self) 30 | if end_elem ~= elem then 31 | reverse[end_elem] = idx 32 | self[idx] = end_elem 33 | end 34 | end 35 | end, 36 | 37 | contains = function(self, elem) 38 | return reverse[elem] ~= nil 39 | end 40 | } 41 | }) 42 | 43 | end 44 | 45 | 46 | return _M -------------------------------------------------------------------------------- /guard/lib/utils/stringx.lua: -------------------------------------------------------------------------------- 1 | local string = string 2 | local str_byte = string.byte 3 | local setmetatable = setmetatable 4 | 5 | 6 | local _M = { 7 | _VERSION = 0.1 8 | } 9 | 10 | setmetatable(_M, {__index = string}) 11 | 12 | 13 | function _M.isempty(s) 14 | return s == nil or s == '' 15 | end 16 | 17 | function _M.rfind_char(s, ch, idx) 18 | local b = str_byte(ch) 19 | for i = idx or #s, 1, -1 do 20 | if str_byte(s, i, i) == b then 21 | return i 22 | end 23 | end 24 | return nil 25 | end 26 | 27 | function _M.lfind_char(s, ch, idx) 28 | local b = str_byte(ch) 29 | for i = idx or 1, #s, 1 do 30 | if str_byte(s, i, i) == b then 31 | return i 32 | end 33 | end 34 | return nil 35 | end 36 | 37 | 38 | return _M 39 | 40 | -------------------------------------------------------------------------------- /guard/lib/utils/tablex.lua: -------------------------------------------------------------------------------- 1 | local table = table 2 | local table_new = table.new 3 | local table_insert = table.insert 4 | local ipairs = ipairs 5 | local pairs = pairs 6 | 7 | 8 | local _M = { 9 | version = 0.1, 10 | } 11 | 12 | setmetatable(_M, {__index = table}) 13 | 14 | function _M.array_merge(tabA, tabB) 15 | local lenA, lenB = #tabA, #tabB 16 | local tab = table.new(lenA + lenB, 0) 17 | 18 | for i = 1, lenA do 19 | tab[i] = tabA[i] 20 | end 21 | for i = 1, lenB do 22 | tab[lenA+i] = tabB[i] 23 | end 24 | 25 | return tab 26 | end 27 | 28 | 29 | function _M.array_contains(t, e) 30 | local r = false 31 | 32 | for _, v in ipairs(t) do 33 | if e == v then 34 | r = true 35 | end 36 | end 37 | 38 | return r 39 | end 40 | 41 | 42 | -- function _M.is_empty(t) 43 | -- return t == nil or next(t) == nil 44 | -- end 45 | 46 | 47 | -- from http://lua-users.org/wiki/CopyTable 48 | -- function _M.deepcopy(orig) 49 | -- local orig_type = type(orig) 50 | -- local copy 51 | -- if orig_type == 'table' then 52 | -- copy = {} 53 | -- for orig_key, orig_value in next, orig, nil do 54 | -- copy[_M.deepcopy(orig_key)] = _M.deepcopy(orig_value) 55 | -- end 56 | -- setmetatable(copy, _M.deepcopy(getmetatable(orig))) 57 | -- else -- number, string, boolean, etc 58 | -- copy = orig 59 | -- end 60 | -- return copy 61 | -- end 62 | 63 | 64 | return _M 65 | -------------------------------------------------------------------------------- /guard/lib/vars.lua: -------------------------------------------------------------------------------- 1 | local require = require 2 | local tablex = require("utils.tablex") 3 | local ngx_var = ngx.var 4 | local ipairs = ipairs 5 | local table_new = table.new 6 | local string_format = string.format 7 | 8 | local _M = {} 9 | 10 | -- local NGX_VARS = { 11 | -- "uri", 12 | -- "host", 13 | -- "https", 14 | -- "scheme", 15 | -- "remote_addr", 16 | -- "realip_remote_addr", 17 | -- "request_id", 18 | -- "request_uri", 19 | -- "request_time", 20 | -- "request_method", 21 | -- "ssl_server_name", 22 | -- "server_port", 23 | -- } 24 | 25 | -- local CUSTOME_VARS = { 26 | -- "upstream_x_real_ip", 27 | -- "upstream_scheme" 28 | -- } 29 | 30 | 31 | local function getter(self, key) 32 | -- if type(key) ~= "string" then 33 | -- return error("invalid argument, string expect", 2) 34 | -- end 35 | 36 | -- local vars = tablex.array_merge(NGX_VARS, CUSTOME_VARS) 37 | -- if not tablex.array_contains(vars, key) then 38 | -- return error(string_format("var '%s' is not allowed to access", key), 2) 39 | -- end 40 | 41 | -- lazy caching 42 | -- self[key] = ngx_var[key] 43 | 44 | return ngx_var[key] 45 | end 46 | 47 | 48 | local function setter(_, key, value) 49 | -- if type(key) ~= "string" then 50 | -- return error("invalid argument, string expect", 2) 51 | -- end 52 | 53 | -- if not tablex.array_contains(CUSTOME_VARS, key) then 54 | -- return error(string_format("update var '%s' is not supported", key), 2) 55 | -- end 56 | ngx_var[key] = value 57 | end 58 | 59 | -- return setmetatable({}, { 60 | -- __index = getter, 61 | -- __newindex = setter 62 | -- }) 63 | 64 | 65 | function _M.new() 66 | return setmetatable({}, { 67 | __index = getter, 68 | __newindex = setter 69 | }) 70 | end 71 | 72 | return _M -------------------------------------------------------------------------------- /guard/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnile/muxwaf/6d296fbfc7c3afc0dfaa65ea8640587b9a7718bd/guard/logs/.gitkeep -------------------------------------------------------------------------------- /screenshot/grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnile/muxwaf/6d296fbfc7c3afc0dfaa65ea8640587b9a7718bd/screenshot/grafana.png -------------------------------------------------------------------------------- /screenshot/screen01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnile/muxwaf/6d296fbfc7c3afc0dfaa65ea8640587b9a7718bd/screenshot/screen01.png -------------------------------------------------------------------------------- /scripts/init-user-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL 5 | CREATE USER muxwaf WITH PASSWORD 'muxwaf@password'; 6 | CREATE DATABASE muxwaf; 7 | GRANT ALL PRIVILEGES ON DATABASE muxwaf TO muxwaf; 8 | CREATE TYPE origin_protocol AS ENUM ('http', 'https', 'follow'); 9 | EOSQL -------------------------------------------------------------------------------- /ui/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 10 4 | -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line=lf 4 | insert_final_newline=false 5 | indent_style=space 6 | indent_size=2 7 | 8 | [{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}] 9 | indent_style=space 10 | indent_size=2 11 | 12 | [{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}] 13 | indent_style=space 14 | indent_size=2 15 | 16 | [{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}] 17 | indent_style=space 18 | indent_size=2 19 | 20 | [*.svg] 21 | indent_style=space 22 | indent_size=2 23 | 24 | [*.js.map] 25 | indent_style=space 26 | indent_size=2 27 | 28 | [*.less] 29 | indent_style=space 30 | indent_size=2 31 | 32 | [*.vue] 33 | indent_style=space 34 | indent_size=2 35 | 36 | [{.analysis_options,*.yml,*.yaml}] 37 | indent_style=space 38 | indent_size=2 39 | 40 | -------------------------------------------------------------------------------- /ui/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | VUE_APP_PREVIEW=false 3 | VUE_APP_API_BASE_URL=/ -------------------------------------------------------------------------------- /ui/.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | VUE_APP_PREVIEW=true 3 | VUE_APP_API_BASE_URL=http://localhost:8001 -------------------------------------------------------------------------------- /ui/.env.docker: -------------------------------------------------------------------------------- 1 | NODE_ENV=docker 2 | VUE_APP_PREVIEW=false 3 | VUE_APP_API_BASE_URL=/:8080/api -------------------------------------------------------------------------------- /ui/.env.preview: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | VUE_APP_PREVIEW=true 3 | VUE_APP_API_BASE_URL=/api -------------------------------------------------------------------------------- /ui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": false, 4 | "singleQuote": true, 5 | "prettier.spaceBeforeFunctionParen": true 6 | } 7 | -------------------------------------------------------------------------------- /ui/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10.15.0 4 | cache: yarn 5 | script: 6 | - yarn 7 | - yarn run lint --no-fix && yarn run build 8 | -------------------------------------------------------------------------------- /ui/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Anan Yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /ui/babel.config.js: -------------------------------------------------------------------------------- 1 | const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV) 2 | 3 | const plugins = [] 4 | if (IS_PROD) { 5 | plugins.push('transform-remove-console') 6 | } 7 | 8 | // lazy load ant-design-vue 9 | // if your use import on Demand, Use this code 10 | plugins.push(['import', { 11 | 'libraryName': 'ant-design-vue', 12 | 'libraryDirectory': 'es', 13 | 'style': true // `style: true` 会加载 less 文件 14 | }]) 15 | 16 | module.exports = { 17 | presets: [ 18 | '@vue/cli-plugin-babel/preset', 19 | [ 20 | '@babel/preset-env', 21 | { 22 | 'useBuiltIns': 'entry', 23 | 'corejs': 3 24 | } 25 | ] 26 | ], 27 | plugins 28 | } 29 | -------------------------------------------------------------------------------- /ui/docs/add-page-loading-animate.md: -------------------------------------------------------------------------------- 1 | 为首屏增加 加载动画 2 | ==== 3 | 4 | 5 | 6 | ## 需求 7 | 8 | > 为了缓解用户第一次访问时,加载 JS 过大所导致用户等待白屏时间过长导致的用户体验不好,进行的一个优化动效。 9 | 10 | 11 | 12 | ## 实现方案 13 | 14 | 1. 将 动画加载 dom 元素放在 #app 内,Vue 生命周期开始时,会自动清掉 #app 下的所有元素。 15 | 2. 将 动画加载 dom 元素放在 body 下,Vue 生命周期开始时 App.vue (created, mounted) 调用 `@/utils/utll` 下的 removeLoadingAnimate(#id, timeout) 则会移除加载动画 16 | 17 | 最后一步: 18 | ​ 将样式插入到 `public/index.html` 文件的 `` 最好写成内联 `` 19 | 20 | 21 | 22 | ---- 23 | 24 | 目前提供有两个样式,均在 `public/loading` 文件夹内。且 pro 已经默认使用了一套 loading 动画方案,可以直接参考 `public/index.html` 25 | 26 | 27 | ## 写在最后 28 | 29 | 目前 pro 有页面 overflow 显示出浏览器滚动条时,页面会抖动一下的问题。 30 | 31 | 欢迎各位提供能解决的方案和实现 demo。如果在条件允许的情况下,建议请直接使用 pro 进行改造,也欢迎直接 PR 到 pro 的仓库 32 | -------------------------------------------------------------------------------- /ui/docs/webpack-bundle-analyzer.md: -------------------------------------------------------------------------------- 1 | 先增加依赖 2 | 3 | ```bash 4 | // npm 5 | $ npm install --save-dev webpack-bundle-analyzer 6 | 7 | // or yarn 8 | $ yarn add webpack-bundle-analyzer -D 9 | ``` 10 | 11 | 配置文件 `vue.config.js` 增加 `configureWebpack.plugins` 参数 12 | 13 | ``` 14 | const path = require('path') 15 | const webpack = require('webpack') 16 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 17 | 18 | function resolve (dir) { 19 | return path.join(__dirname, dir) 20 | } 21 | 22 | // vue.config.js 23 | module.exports = { 24 | configureWebpack: { 25 | plugins: [ 26 | // Ignore all locale files of moment.js 27 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 28 | // 依赖大小分析工具 29 | new BundleAnalyzerPlugin(), 30 | ] 31 | }, 32 | 33 | 34 | ... 35 | } 36 | ``` 37 | 38 | 39 | 40 | 启动 `cli` 的 `build` 命令进行项目编译,编译完成时,会自动运行一个 http://localhost:8888 的地址,完整显示了支持库依赖 -------------------------------------------------------------------------------- /ui/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | 'json', 6 | 'vue' 7 | ], 8 | transform: { 9 | '^.+\\.vue$': 'vue-jest', 10 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 11 | '^.+\\.jsx?$': 'babel-jest' 12 | }, 13 | moduleNameMapper: { 14 | '^@/(.*)$': '/src/$1' 15 | }, 16 | snapshotSerializers: [ 17 | 'jest-serializer-vue' 18 | ], 19 | testMatch: [ 20 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 21 | ], 22 | testURL: 'http://localhost/' 23 | } 24 | -------------------------------------------------------------------------------- /ui/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["src/*"] 7 | } 8 | }, 9 | "exclude": ["node_modules", "dist"], 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /ui/public/avatar2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnile/muxwaf/6d296fbfc7c3afc0dfaa65ea8640587b9a7718bd/ui/public/avatar2.jpg -------------------------------------------------------------------------------- /ui/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnile/muxwaf/6d296fbfc7c3afc0dfaa65ea8640587b9a7718bd/ui/public/logo.png -------------------------------------------------------------------------------- /ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | -------------------------------------------------------------------------------- /ui/src/api/blacklist/ip.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 分页获取IP黑名单 4 | export function ListBlacklistIP(params) { 5 | return request({ 6 | url: '/api/blacklist/ip', 7 | method: 'get', 8 | params: params 9 | }) 10 | } 11 | 12 | // 添加IP黑名单 13 | export function InsertBlacklistIP(data) { 14 | return request({ 15 | url: '/api/blacklist/ip', 16 | method: 'post', 17 | data: data 18 | }) 19 | } 20 | 21 | // 编辑IP黑名单 22 | export function UpdateBlacklistIP(id, data) { 23 | return request({ 24 | url: `/api/blacklist/ip/${id}`, 25 | method: 'put', 26 | data: data 27 | }) 28 | } 29 | 30 | // 删除IP黑名单 31 | export function DeleteBlacklistIP(id) { 32 | return request({ 33 | url: `/api/blacklist/ip/${id}`, 34 | method: 'delete' 35 | }) 36 | } 37 | 38 | // 更新IP黑名单状态 39 | export function UpdateBlacklistIPStatus(id) { 40 | return request({ 41 | url: `/api/blacklist/ip/${id}/status`, 42 | method: 'put' 43 | }) 44 | } 45 | 46 | export function IsIncluded(ip) { 47 | return request({ 48 | url: '/api/blacklist/ip/isIncluded', 49 | method: 'get', 50 | params: { 51 | ip: ip 52 | } 53 | }) 54 | } 55 | 56 | export function BatchAdd(data) { 57 | return request({ 58 | url: '/api/blacklist/ip/batch', 59 | method: 'post', 60 | data: data 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /ui/src/api/certificate.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function GetAll(params) { 4 | return request({ 5 | url: '/api/certificates/all', 6 | method: 'get', 7 | params: params 8 | }) 9 | } 10 | 11 | export function AddCert(data) { 12 | return request({ 13 | url: '/api/certificates', 14 | method: 'post', 15 | data: data 16 | }) 17 | } 18 | 19 | export function UpdateCert(id, data) { 20 | return request({ 21 | url: `/api/certificates/${id}`, 22 | method: 'put', 23 | data: data 24 | }) 25 | } 26 | 27 | export function ListCert(params) { 28 | return request({ 29 | url: '/api/certificates', 30 | method: 'get', 31 | params: params 32 | }) 33 | } 34 | 35 | export function DelCert(id) { 36 | return request({ 37 | url: `/api/certificates/${id}`, 38 | method: 'delete' 39 | }) 40 | } 41 | 42 | export function GetDomainCert(domain) { 43 | return request({ 44 | url: `/api/certificates`, 45 | method: 'get', 46 | params: { 47 | domain: domain 48 | } 49 | }) 50 | } 51 | 52 | // export function GetCertName(id) { 53 | // return request({ 54 | // url: `/api/certificates/${id}/name`, 55 | // method: 'get' 56 | // }) 57 | // } 58 | -------------------------------------------------------------------------------- /ui/src/api/login.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | const userApi = { 4 | Login: '/api/auth/login', 5 | Logout: '/api/auth/logout', 6 | ForgePassword: '/auth/forge-password', 7 | Register: '/auth/register', 8 | twoStepCode: '/auth/2step-code', 9 | SendSms: '/account/sms', 10 | SendSmsErr: '/account/sms_err', 11 | // get my info 12 | UserInfo: '/api/users/info', 13 | UserMenu: '/user/nav' 14 | } 15 | 16 | /** 17 | * login func 18 | * parameter: { 19 | * username: '', 20 | * password: '', 21 | * remember_me: true, 22 | * captcha: '12345' 23 | * } 24 | * @param parameter 25 | * @returns {*} 26 | */ 27 | export function login(parameter) { 28 | return request({ 29 | url: userApi.Login, 30 | method: 'post', 31 | data: parameter 32 | }) 33 | } 34 | 35 | export function getSmsCaptcha(parameter) { 36 | return request({ 37 | url: userApi.SendSms, 38 | method: 'post', 39 | data: parameter 40 | }) 41 | } 42 | 43 | export function getInfo() { 44 | return request({ 45 | url: userApi.UserInfo, 46 | method: 'get', 47 | headers: { 48 | 'Content-Type': 'application/json;charset=UTF-8' 49 | } 50 | }) 51 | } 52 | 53 | export function getCurrentUserNav() { 54 | return request({ 55 | url: userApi.UserMenu, 56 | method: 'get' 57 | }) 58 | } 59 | 60 | export function logout() { 61 | return request({ 62 | url: userApi.Logout, 63 | method: 'post', 64 | headers: { 65 | 'Content-Type': 'application/json;charset=UTF-8' 66 | } 67 | }) 68 | } 69 | 70 | /** 71 | * get user 2step code open? 72 | * @param parameter {*} 73 | */ 74 | export function get2step(parameter) { 75 | return request({ 76 | url: userApi.twoStepCode, 77 | method: 'post', 78 | data: parameter 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /ui/src/api/node.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function ListNodes(params) { 4 | return request({ 5 | url: '/api/nodes', 6 | method: 'get', 7 | params: params 8 | }) 9 | } 10 | 11 | export function AddNode(data) { 12 | return request({ 13 | url: '/api/nodes', 14 | method: 'post', 15 | data: data 16 | }) 17 | } 18 | 19 | export function DelNode(id) { 20 | return request({ 21 | url: `/api/nodes/${id}`, 22 | method: 'delete' 23 | }) 24 | } 25 | 26 | export function SyncCfg(id) { 27 | return request({ 28 | url: `/api/nodes/${id}/sync`, 29 | method: 'put' 30 | }) 31 | } 32 | 33 | export function SwitchLogUpload(id) { 34 | return request({ 35 | url: `/api/nodes/${id}/sample_log_upload`, 36 | method: 'put' 37 | }) 38 | } 39 | 40 | export function SwitchStatus(id) { 41 | return request({ 42 | url: `/api/nodes/${id}/status`, 43 | method: 'put' 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /ui/src/api/rateLimit.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function Add(data) { 4 | return request({ 5 | url: '/api/rate-limit', 6 | method: 'post', 7 | data: data 8 | }) 9 | } 10 | 11 | export function GetList(params) { 12 | return request({ 13 | url: '/api/rate-limit', 14 | method: 'get', 15 | params: params 16 | }) 17 | } 18 | 19 | export function Update(id, data) { 20 | return request({ 21 | url: `/api/rate-limit/${id}`, 22 | method: 'put', 23 | data: data 24 | }) 25 | } 26 | 27 | export function UpdateStatus(id) { 28 | return request({ 29 | url: `/api/rate-limit/${id}/status`, 30 | method: 'put' 31 | }) 32 | } 33 | 34 | export function Delete(id) { 35 | return request({ 36 | url: `/api/rate-limit/${id}`, 37 | method: 'delete' 38 | }) 39 | } 40 | 41 | export function BatchAdd(data) { 42 | return request({ 43 | url: '/api/rate-limit/batch', 44 | method: 'post', 45 | data: data 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /ui/src/api/sampleLog.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function ListAttackLog(params) { 4 | return request({ 5 | url: '/api/sample-logs', 6 | method: 'get', 7 | params: params 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/api/site/cert.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function GetCandidateCertificates(siteID, params) { 4 | return request({ 5 | url: `/api/sites/${siteID}/certificates`, 6 | method: 'get', 7 | params: params 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/api/site/https.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function GetHttpsConfigs(siteID) { 4 | return request({ 5 | url: `/api/sites/${siteID}/configs/https`, 6 | method: 'get' 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /ui/src/api/site/index.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // const api = { 4 | // site: '/api/sites' 5 | // } 6 | 7 | export function AddSite(data) { 8 | return request({ 9 | url: '/api/sites', 10 | method: 'post', 11 | data: data 12 | }) 13 | } 14 | 15 | export function ListSite(params) { 16 | return request({ 17 | url: '/api/sites', 18 | method: 'get', 19 | params: params 20 | }) 21 | } 22 | 23 | export function GetALLSite() { 24 | return request({ 25 | url: '/api/sites/all', 26 | method: 'get' 27 | }) 28 | } 29 | 30 | export function UpdateSiteBasicConfigs(id, data) { 31 | return request({ 32 | url: `/api/sites/${id}/configs/basic`, 33 | method: 'put', 34 | data: data 35 | }) 36 | } 37 | 38 | export function UpdateSiteHttpsConfigs(id, data) { 39 | return request({ 40 | url: `/api/sites/${id}/configs/https`, 41 | method: 'put', 42 | data: data 43 | }) 44 | } 45 | 46 | // export function GetConfigs(id) { 47 | // return request({ 48 | // url: `/api/sites/${id}/configs`, 49 | // method: 'get' 50 | // }) 51 | // } 52 | 53 | export function GetBasicConfigs(id) { 54 | return request({ 55 | url: `/api/sites/${id}/configs/basic`, 56 | method: 'get' 57 | }) 58 | } 59 | 60 | export function DelSite(id) { 61 | return request({ 62 | url: `/api/sites/${id}`, 63 | method: 'delete' 64 | }) 65 | } 66 | 67 | export function UpdateStatus(id) { 68 | return request({ 69 | url: `/api/sites/${id}/status`, 70 | method: 'put' 71 | }) 72 | } 73 | 74 | // export function GetDomain(id) { 75 | // return request({ 76 | // url: `/api/sites/${id}/domain`, 77 | // method: 'get' 78 | // }) 79 | // } 80 | -------------------------------------------------------------------------------- /ui/src/api/site/origin.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { method } from 'lodash' 3 | 4 | export function GetOrigins(id) { 5 | return request({ 6 | url: `/api/sites/${id}/configs/origin`, 7 | method: 'get' 8 | }) 9 | } 10 | 11 | export function UpdateOriginCfg(siteID, data) { 12 | return request({ 13 | url: `/api/sites/${siteID}/configs/origin`, 14 | method: 'put', 15 | data: data 16 | }) 17 | } 18 | 19 | export function AddOrigins(id, data) { 20 | return request({ 21 | url: `/api/sites/${id}/origins`, 22 | method: 'post', 23 | data: data 24 | }) 25 | } 26 | 27 | export function DelOrigin(id) { 28 | return request({ 29 | url: `/api/sites/origins/${id}`, 30 | method: 'delete' 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/api/site/regionBlacklist.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function UpdateSiteRegionBlacklist(id, data) { 4 | return request({ 5 | url: `/api/sites/${id}/region-blacklist`, 6 | method: 'put', 7 | data: data 8 | }) 9 | } 10 | 11 | export function GetSiteRegionBlacklist(id) { 12 | return request({ 13 | url: `/api/sites/${id}/region-blacklist`, 14 | method: 'get' 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | const api = { 4 | user: '/api/users' 5 | } 6 | 7 | export function InsertUser(data) { 8 | return request({ 9 | url: '/api/users', 10 | method: 'post', 11 | data: data 12 | }) 13 | } 14 | 15 | export function ListUsers(params) { 16 | return request({ 17 | url: '/api/users', 18 | method: 'get', 19 | params: params 20 | }) 21 | } 22 | 23 | export function GetUser(uid) { 24 | return request({ 25 | url: `/api/users/${uid}`, 26 | method: 'get' 27 | }) 28 | } 29 | 30 | export function UpdateUser(uid, data) { 31 | return request({ 32 | url: `${api.user}/${uid}`, 33 | method: 'put', 34 | data: data 35 | }) 36 | } 37 | 38 | export function BlockUser(uid) { 39 | return request({ 40 | url: `/api/users/${uid}/block`, 41 | method: 'put' 42 | }) 43 | } 44 | 45 | export function DeleteUser(uid) { 46 | return request({ 47 | url: `/api/users/${uid}`, 48 | method: 'delete' 49 | }) 50 | } 51 | 52 | export function ResetPassword(data) { 53 | return request({ 54 | url: '/api/users/reset-password', 55 | method: 'put', 56 | data: data 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /ui/src/api/whitelist/ip.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 分页获取IP白名单 4 | export function GetIPList(params) { 5 | return request({ 6 | url: '/api/whitelist/ip', 7 | method: 'get', 8 | params: params 9 | }) 10 | } 11 | 12 | // 添加IP白名单 13 | export function AddIP(data) { 14 | return request({ 15 | url: '/api/whitelist/ip', 16 | method: 'post', 17 | data: data 18 | }) 19 | } 20 | 21 | // 编辑IP白名单 22 | export function UpdateIP(id, data) { 23 | return request({ 24 | url: `/api/whitelist/ip/${id}`, 25 | method: 'put', 26 | data: data 27 | }) 28 | } 29 | 30 | // 删除IP白名单 31 | export function DeleteIP(id) { 32 | return request({ 33 | url: `/api/whitelist/ip/${id}`, 34 | method: 'delete' 35 | }) 36 | } 37 | 38 | // 更新IP白名单状态 39 | export function UpdateIPStatus(id) { 40 | return request({ 41 | url: `/api/whitelist/ip/${id}/status`, 42 | method: 'put' 43 | }) 44 | } 45 | 46 | // 检测IP是否已经包含在白名单库中 47 | export function IsIncluded(ip) { 48 | return request({ 49 | url: '/api/whitelist/ip/isIncluded', 50 | method: 'get', 51 | params: { 52 | ip: ip 53 | } 54 | }) 55 | } 56 | 57 | // 批量添加IP白名单 58 | export function BatchAddIP(data) { 59 | return request({ 60 | url: '/api/whitelist/ip/batch', 61 | method: 'post', 62 | data: data 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /ui/src/api/whitelist/url.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 分页获取URL白名单 4 | export function GetURLList(params) { 5 | return request({ 6 | url: '/api/whitelist/url', 7 | method: 'get', 8 | params: params 9 | }) 10 | } 11 | 12 | // 添加URL白名单 13 | export function AddURL(data) { 14 | return request({ 15 | url: '/api/whitelist/url', 16 | method: 'post', 17 | data: data 18 | }) 19 | } 20 | 21 | // 编辑URL白名单 22 | export function UpdateURL(id, data) { 23 | return request({ 24 | url: `/api/whitelist/url/${id}`, 25 | method: 'put', 26 | data: data 27 | }) 28 | } 29 | 30 | // 删除URL白名单 31 | export function DeleteURL(id) { 32 | return request({ 33 | url: `/api/whitelist/url/${id}`, 34 | method: 'delete' 35 | }) 36 | } 37 | 38 | // 更新URL白名单状态 39 | export function UpdateURLStatus(id) { 40 | return request({ 41 | url: `/api/whitelist/url/${id}/status`, 42 | method: 'put' 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /ui/src/assets/icons/bx-analyse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnile/muxwaf/6d296fbfc7c3afc0dfaa65ea8640587b9a7718bd/ui/src/assets/logo.png -------------------------------------------------------------------------------- /ui/src/components/ArticleListContent/index.js: -------------------------------------------------------------------------------- 1 | import ArticleListContent from './ArticleListContent' 2 | 3 | export default ArticleListContent 4 | -------------------------------------------------------------------------------- /ui/src/components/AvatarList/Item.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'ant-design-vue/es/_util/vue-types' 2 | import { Tooltip, Avatar } from 'ant-design-vue' 3 | import { getSlotOptions } from 'ant-design-vue/lib/_util/props-util' 4 | import { warning } from 'ant-design-vue/lib/vc-util/warning' 5 | 6 | export const AvatarListItemProps = { 7 | tips: PropTypes.string.def(null), 8 | src: PropTypes.string.def('') 9 | } 10 | 11 | const Item = { 12 | __ANT_AVATAR_CHILDREN: true, 13 | name: 'AvatarListItem', 14 | props: AvatarListItemProps, 15 | created () { 16 | warning(getSlotOptions(this.$parent).__ANT_AVATAR_LIST, 'AvatarListItem must be a subcomponent of AvatarList') 17 | }, 18 | render () { 19 | const AvatarDom = 20 | return this.tips && {AvatarDom} || 21 | } 22 | } 23 | 24 | export default Item 25 | -------------------------------------------------------------------------------- /ui/src/components/AvatarList/index.js: -------------------------------------------------------------------------------- 1 | import AvatarList from './List' 2 | import Item from './Item' 3 | 4 | export { 5 | AvatarList, 6 | Item as AvatarListItem 7 | } 8 | 9 | export default AvatarList 10 | -------------------------------------------------------------------------------- /ui/src/components/AvatarList/index.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @avatar-list-prefix-cls: ~"@{ant-pro-prefix}-avatar-list"; 4 | @avatar-list-item-prefix-cls: ~"@{ant-pro-prefix}-avatar-list-item"; 5 | 6 | .@{avatar-list-prefix-cls} { 7 | display: inline-block; 8 | 9 | ul { 10 | list-style: none; 11 | display: inline-block; 12 | padding: 0; 13 | margin: 0 0 0 8px; 14 | font-size: 0; 15 | } 16 | } 17 | 18 | .@{avatar-list-item-prefix-cls} { 19 | display: inline-block; 20 | font-size: @font-size-base; 21 | margin-left: -8px; 22 | width: @avatar-size-base; 23 | height: @avatar-size-base; 24 | 25 | :global { 26 | .ant-avatar { 27 | border: 1px solid #fff; 28 | cursor: pointer; 29 | } 30 | } 31 | 32 | &.large { 33 | width: @avatar-size-lg; 34 | height: @avatar-size-lg; 35 | } 36 | 37 | &.small { 38 | width: @avatar-size-sm; 39 | height: @avatar-size-sm; 40 | } 41 | 42 | &.mini { 43 | width: 20px; 44 | height: 20px; 45 | 46 | :global { 47 | .ant-avatar { 48 | width: 20px; 49 | height: 20px; 50 | line-height: 20px; 51 | 52 | .ant-avatar-string { 53 | font-size: 12px; 54 | line-height: 18px; 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /ui/src/components/Charts/Bar.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 63 | -------------------------------------------------------------------------------- /ui/src/components/Charts/Liquid.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 64 | 65 | 68 | -------------------------------------------------------------------------------- /ui/src/components/Charts/MiniArea.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 53 | 54 | 57 | -------------------------------------------------------------------------------- /ui/src/components/Charts/MiniBar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 54 | 55 | 58 | -------------------------------------------------------------------------------- /ui/src/components/Charts/MiniProgress.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 36 | 37 | 76 | -------------------------------------------------------------------------------- /ui/src/components/Charts/MiniSmoothArea.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /ui/src/components/Charts/Radar.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 65 | 66 | 69 | -------------------------------------------------------------------------------- /ui/src/components/Charts/RankList.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 80 | -------------------------------------------------------------------------------- /ui/src/components/Charts/TransferBar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 65 | -------------------------------------------------------------------------------- /ui/src/components/Charts/chart.less: -------------------------------------------------------------------------------- 1 | .antv-chart-mini { 2 | position: relative; 3 | width: 100%; 4 | 5 | .chart-wrapper { 6 | position: absolute; 7 | bottom: -28px; 8 | width: 100%; 9 | 10 | /* margin: 0 -5px; 11 | overflow: hidden;*/ 12 | } 13 | } -------------------------------------------------------------------------------- /ui/src/components/Charts/smooth.area.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @smoothArea-prefix-cls: ~"@{ant-pro-prefix}-smooth-area"; 4 | 5 | .@{smoothArea-prefix-cls} { 6 | position: relative; 7 | width: 100%; 8 | 9 | .chart-wrapper { 10 | position: absolute; 11 | bottom: -28px; 12 | width: 100%; 13 | } 14 | } -------------------------------------------------------------------------------- /ui/src/components/Ellipsis/Ellipsis.vue: -------------------------------------------------------------------------------- 1 | 65 | -------------------------------------------------------------------------------- /ui/src/components/Ellipsis/index.js: -------------------------------------------------------------------------------- 1 | import Ellipsis from './Ellipsis' 2 | 3 | export default Ellipsis 4 | -------------------------------------------------------------------------------- /ui/src/components/Ellipsis/index.md: -------------------------------------------------------------------------------- 1 | # Ellipsis 文本自动省略号 2 | 3 | 文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。 4 | 5 | 6 | 7 | 引用方式: 8 | 9 | ```javascript 10 | import Ellipsis from '@/components/Ellipsis' 11 | 12 | export default { 13 | components: { 14 | Ellipsis 15 | } 16 | } 17 | ``` 18 | 19 | 20 | 21 | ## 代码演示 [demo](https://pro.loacg.com/test/home) 22 | 23 | ```html 24 | 25 | There were injuries alleged in three cases in 2015, and a 26 | fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall. 27 | 28 | ``` 29 | 30 | 31 | 32 | ## API 33 | 34 | 35 | 参数 | 说明 | 类型 | 默认值 36 | ----|------|-----|------ 37 | tooltip | 移动到文本展示完整内容的提示 | boolean | - 38 | length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | - -------------------------------------------------------------------------------- /ui/src/components/FooterToolbar/FooterToolBar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 44 | 45 | 48 | -------------------------------------------------------------------------------- /ui/src/components/FooterToolbar/index.js: -------------------------------------------------------------------------------- 1 | import FooterToolBar from './FooterToolBar' 2 | import './index.less' 3 | 4 | export default FooterToolBar 5 | -------------------------------------------------------------------------------- /ui/src/components/FooterToolbar/index.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @footer-toolbar-prefix-cls: ~"@{ant-pro-prefix}-footer-toolbar"; 4 | 5 | .@{footer-toolbar-prefix-cls} { 6 | position: fixed; 7 | width: 100%; 8 | bottom: 0; 9 | right: 0; 10 | height: 56px; 11 | line-height: 56px; 12 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03); 13 | background: #fff; 14 | border-top: 1px solid #e8e8e8; 15 | padding: 0 24px; 16 | z-index: 9; 17 | 18 | &:after { 19 | content: ""; 20 | display: block; 21 | clear: both; 22 | } 23 | } -------------------------------------------------------------------------------- /ui/src/components/FooterToolbar/index.md: -------------------------------------------------------------------------------- 1 | # FooterToolbar 底部工具栏 2 | 3 | 固定在底部的工具栏。 4 | 5 | 6 | 7 | ## 何时使用 8 | 9 | 固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。 10 | 11 | 12 | 13 | 引用方式: 14 | 15 | ```javascript 16 | import FooterToolBar from '@/components/FooterToolbar' 17 | 18 | export default { 19 | components: { 20 | FooterToolBar 21 | } 22 | } 23 | ``` 24 | 25 | 26 | 27 | ## 代码演示 28 | 29 | ```html 30 | 31 | 提交 32 | 33 | ``` 34 | 或 35 | ```html 36 | 37 | 提交 38 | 39 | ``` 40 | 41 | 42 | ## API 43 | 44 | 参数 | 说明 | 类型 | 默认值 45 | ----|------|-----|------ 46 | children (slot) | 工具栏内容,向右对齐 | - | - 47 | extra | 额外信息,向左对齐 | String, Object | - 48 | 49 | -------------------------------------------------------------------------------- /ui/src/components/GlobalFooter/GlobalFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /ui/src/components/GlobalFooter/index.js: -------------------------------------------------------------------------------- 1 | import GlobalFooter from './GlobalFooter' 2 | export default GlobalFooter -------------------------------------------------------------------------------- /ui/src/components/GlobalHeader/RightContent.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 62 | -------------------------------------------------------------------------------- /ui/src/components/IconSelector/README.md: -------------------------------------------------------------------------------- 1 | IconSelector 2 | ==== 3 | 4 | > 图标选择组件,常用于为某一个数据设定一个图标时使用 5 | > eg: 设定菜单列表时,为每个菜单设定一个图标 6 | 7 | 该组件由 [@Saraka](https://github.com/saraka-tsukai) 封装 8 | 9 | 10 | 11 | ### 使用方式 12 | 13 | ```vue 14 | 19 | 20 | 39 | ``` 40 | 41 | 42 | 43 | ### 事件 44 | 45 | 46 | | 名称 | 说明 | 类型 | 默认值 | 47 | | ------ | -------------------------- | ------ | ------ | 48 | | change | 当改变了 `icon` 选中项触发 | String | - | 49 | -------------------------------------------------------------------------------- /ui/src/components/IconSelector/index.js: -------------------------------------------------------------------------------- 1 | import IconSelector from './IconSelector' 2 | export default IconSelector 3 | -------------------------------------------------------------------------------- /ui/src/components/MultiTab/events.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | export default new Vue() 3 | -------------------------------------------------------------------------------- /ui/src/components/MultiTab/index.js: -------------------------------------------------------------------------------- 1 | import events from './events' 2 | import MultiTab from './MultiTab' 3 | import './index.less' 4 | 5 | const api = { 6 | /** 7 | * open new tab on route fullPath 8 | * @param config 9 | */ 10 | open: function (config) { 11 | events.$emit('open', config) 12 | }, 13 | rename: function (key, name) { 14 | events.$emit('rename', { key: key, name: name }) 15 | }, 16 | /** 17 | * close current page 18 | */ 19 | closeCurrentPage: function () { 20 | this.close() 21 | }, 22 | /** 23 | * close route fullPath tab 24 | * @param config 25 | */ 26 | close: function (config) { 27 | events.$emit('close', config) 28 | } 29 | } 30 | 31 | MultiTab.install = function (Vue) { 32 | if (Vue.prototype.$multiTab) { 33 | return 34 | } 35 | api.instance = events 36 | Vue.prototype.$multiTab = api 37 | Vue.component('multi-tab', MultiTab) 38 | } 39 | 40 | export default MultiTab 41 | -------------------------------------------------------------------------------- /ui/src/components/MultiTab/index.less: -------------------------------------------------------------------------------- 1 | @import '../index'; 2 | 3 | @multi-tab-prefix-cls: ~"@{ant-pro-prefix}-multi-tab"; 4 | @multi-tab-wrapper-prefix-cls: ~"@{ant-pro-prefix}-multi-tab-wrapper"; 5 | 6 | /* 7 | .topmenu .@{multi-tab-prefix-cls} { 8 | max-width: 1200px; 9 | margin: -23px auto 24px auto; 10 | } 11 | */ 12 | .@{multi-tab-prefix-cls} { 13 | margin: -23px -24px 24px -24px; 14 | background: #fff; 15 | } 16 | 17 | .topmenu .@{multi-tab-wrapper-prefix-cls} { 18 | max-width: 1200px; 19 | margin: 0 auto; 20 | } 21 | 22 | .topmenu.content-width-Fluid .@{multi-tab-wrapper-prefix-cls} { 23 | max-width: 100%; 24 | margin: 0 auto; 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/components/NoticeIcon/index.js: -------------------------------------------------------------------------------- 1 | import NoticeIcon from './NoticeIcon' 2 | export default NoticeIcon 3 | -------------------------------------------------------------------------------- /ui/src/components/NumberInfo/NumberInfo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 51 | 52 | 55 | -------------------------------------------------------------------------------- /ui/src/components/NumberInfo/index.js: -------------------------------------------------------------------------------- 1 | import NumberInfo from './NumberInfo' 2 | 3 | export default NumberInfo 4 | -------------------------------------------------------------------------------- /ui/src/components/NumberInfo/index.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @numberInfo-prefix-cls: ~"@{ant-pro-prefix}-number-info"; 4 | 5 | .@{numberInfo-prefix-cls} { 6 | 7 | .ant-pro-number-info-subtitle { 8 | color: @text-color-secondary; 9 | font-size: @font-size-base; 10 | height: 22px; 11 | line-height: 22px; 12 | overflow: hidden; 13 | text-overflow: ellipsis; 14 | word-break: break-all; 15 | white-space: nowrap; 16 | } 17 | 18 | .number-info-value { 19 | margin-top: 4px; 20 | font-size: 0; 21 | overflow: hidden; 22 | text-overflow: ellipsis; 23 | word-break: break-all; 24 | white-space: nowrap; 25 | 26 | & > span { 27 | color: @heading-color; 28 | display: inline-block; 29 | line-height: 32px; 30 | height: 32px; 31 | font-size: 24px; 32 | margin-right: 32px; 33 | } 34 | 35 | .sub-total { 36 | color: @text-color-secondary; 37 | font-size: @font-size-lg; 38 | vertical-align: top; 39 | margin-right: 0; 40 | i { 41 | font-size: 12px; 42 | transform: scale(0.82); 43 | margin-left: 4px; 44 | } 45 | :global { 46 | .anticon-caret-up { 47 | color: @red-6; 48 | } 49 | .anticon-caret-down { 50 | color: @green-6; 51 | } 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /ui/src/components/NumberInfo/index.md: -------------------------------------------------------------------------------- 1 | # NumberInfo 数据文本 2 | 3 | 常用在数据卡片中,用于突出展示某个业务数据。 4 | 5 | 6 | 7 | 引用方式: 8 | 9 | ```javascript 10 | import NumberInfo from '@/components/NumberInfo' 11 | 12 | export default { 13 | components: { 14 | NumberInfo 15 | } 16 | } 17 | ``` 18 | 19 | 20 | 21 | ## 代码演示 [demo](https://pro.loacg.com/test/home) 22 | 23 | ```html 24 | 29 | ``` 30 | 31 | 32 | 33 | ## API 34 | 35 | 参数 | 说明 | 类型 | 默认值 36 | ----|------|-----|------ 37 | title | 标题 | ReactNode\|string | - 38 | subTitle | 子标题 | ReactNode\|string | - 39 | total | 总量 | ReactNode\|string | - 40 | subTotal | 子总量 | ReactNode\|string | - 41 | status | 增加状态 | 'up \| down' | - 42 | theme | 状态样式 | string | 'light' 43 | gap | 设置数字和描述之间的间距(像素)| number | 8 44 | -------------------------------------------------------------------------------- /ui/src/components/Search/GlobalSearch.jsx: -------------------------------------------------------------------------------- 1 | import { Select } from 'ant-design-vue' 2 | import './index.less' 3 | 4 | const GlobalSearch = { 5 | name: 'GlobalSearch', 6 | data () { 7 | return { 8 | visible: false 9 | } 10 | }, 11 | mounted () { 12 | const keyboardHandle = (e) => { 13 | e.preventDefault() 14 | e.stopPropagation() 15 | const { ctrlKey, shiftKey, altKey, keyCode } = e 16 | console.log('keyCode:', e.keyCode, e) 17 | // key is `K` and hold ctrl 18 | if (keyCode === 75 && ctrlKey && !shiftKey && !altKey) { 19 | this.visible = !this.visible 20 | } 21 | } 22 | document.addEventListener('keydown', keyboardHandle) 23 | }, 24 | render () { 25 | const { visible } = this 26 | const handleSearch = (e) => { 27 | this.$emit('search', e) 28 | } 29 | 30 | const handleChange = (e) => { 31 | this.$emit('change', e) 32 | } 33 | if (!visible) { 34 | return null 35 | } 36 | return ( 37 | 55 | ) 56 | } 57 | } 58 | 59 | GlobalSearch.install = function (Vue) { 60 | Vue.component(GlobalSearch.name, GlobalSearch) 61 | } 62 | 63 | export default GlobalSearch 64 | -------------------------------------------------------------------------------- /ui/src/components/Search/index.less: -------------------------------------------------------------------------------- 1 | @import "~ant-design-vue/es/style/themes/default"; 2 | 3 | .global-search-wrapper { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | z-index: @zindex-modal-mask; 10 | background: @modal-mask-bg; 11 | 12 | .global-search-box { 13 | position: absolute; 14 | top: 20%; 15 | left: 50%; 16 | width: 450px; 17 | transform: translate(-50%, -50%); 18 | 19 | .global-search-tips { 20 | color: @white; 21 | font-size: @font-size-lg; 22 | text-align: right; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /ui/src/components/SelectLang/index.jsx: -------------------------------------------------------------------------------- 1 | import './index.less' 2 | 3 | import { Icon, Menu, Dropdown } from 'ant-design-vue' 4 | import { i18nRender } from '@/locales' 5 | import i18nMixin from '@/store/i18n-mixin' 6 | 7 | const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'] 8 | const languageLabels = { 9 | 'zh-CN': '简体中文', 10 | 'zh-TW': '繁体中文', 11 | 'en-US': 'English', 12 | 'pt-BR': 'Português' 13 | } 14 | // eslint-disable-next-line 15 | const languageIcons = { 16 | 'zh-CN': '🇨🇳', 17 | 'zh-TW': '🇭🇰', 18 | 'en-US': '🇺🇸', 19 | 'pt-BR': '🇧🇷' 20 | } 21 | 22 | const SelectLang = { 23 | props: { 24 | prefixCls: { 25 | type: String, 26 | default: 'ant-pro-drop-down' 27 | } 28 | }, 29 | name: 'SelectLang', 30 | mixins: [i18nMixin], 31 | render () { 32 | const { prefixCls } = this 33 | const changeLang = ({ key }) => { 34 | this.setLang(key) 35 | } 36 | const langMenu = ( 37 | 38 | {locales.map(locale => ( 39 | 40 | 41 | {languageIcons[locale]} 42 | {' '} 43 | {languageLabels[locale]} 44 | 45 | ))} 46 | 47 | ) 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | } 57 | 58 | export default SelectLang 59 | -------------------------------------------------------------------------------- /ui/src/components/SelectLang/index.less: -------------------------------------------------------------------------------- 1 | @import "~ant-design-vue/es/style/themes/default"; 2 | 3 | @header-menu-prefix-cls: ~'@{ant-prefix}-pro-header-menu'; 4 | @header-drop-down-prefix-cls: ~'@{ant-prefix}-pro-drop-down'; 5 | 6 | .@{header-menu-prefix-cls} { 7 | 8 | .anticon { 9 | margin-right: 8px; 10 | } 11 | .ant-dropdown-menu-item { 12 | min-width: 160px; 13 | } 14 | } 15 | 16 | .@{header-drop-down-prefix-cls} { 17 | 18 | line-height: @layout-header-height; 19 | vertical-align: top; 20 | cursor: pointer; 21 | 22 | > i { 23 | font-size: 16px !important; 24 | transform: none !important; 25 | 26 | svg { 27 | position: relative; 28 | top: -1px; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/components/SettingDrawer/SettingItem.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 39 | -------------------------------------------------------------------------------- /ui/src/components/SettingDrawer/index.js: -------------------------------------------------------------------------------- 1 | import SettingDrawer from './SettingDrawer' 2 | export default SettingDrawer 3 | -------------------------------------------------------------------------------- /ui/src/components/SettingDrawer/settingConfig.js: -------------------------------------------------------------------------------- 1 | import message from 'ant-design-vue/es/message' 2 | // import defaultSettings from '../defaultSettings'; 3 | import themeColor from './themeColor.js' 4 | 5 | // let lessNodesAppended 6 | const colorList = [ 7 | { 8 | key: '薄暮', color: '#F5222D' 9 | }, 10 | { 11 | key: '火山', color: '#FA541C' 12 | }, 13 | { 14 | key: '日暮', color: '#FAAD14' 15 | }, 16 | { 17 | key: '明青', color: '#13C2C2' 18 | }, 19 | { 20 | key: '极光绿', color: '#52C41A' 21 | }, 22 | { 23 | key: '拂晓蓝(默认)', color: '#1890FF' 24 | }, 25 | { 26 | key: '极客蓝', color: '#2F54EB' 27 | }, 28 | { 29 | key: '酱紫', color: '#722ED1' 30 | } 31 | ] 32 | 33 | const updateTheme = newPrimaryColor => { 34 | const hideMessage = message.loading('正在切换主题!', 0) 35 | themeColor.changeColor(newPrimaryColor).finally(() => { 36 | setTimeout(() => { 37 | hideMessage() 38 | }, 10) 39 | }) 40 | } 41 | 42 | const updateColorWeak = colorWeak => { 43 | // document.body.className = colorWeak ? 'colorWeak' : ''; 44 | const app = document.body.querySelector('#app') 45 | colorWeak ? app.classList.add('colorWeak') : app.classList.remove('colorWeak') 46 | } 47 | 48 | export { updateTheme, colorList, updateColorWeak } 49 | -------------------------------------------------------------------------------- /ui/src/components/SettingDrawer/themeColor.js: -------------------------------------------------------------------------------- 1 | import client from 'webpack-theme-color-replacer/client' 2 | import generate from '@ant-design/colors/lib/generate' 3 | 4 | export default { 5 | getAntdSerials (color) { 6 | // 淡化(即less的tint) 7 | const lightens = new Array(9).fill().map((t, i) => { 8 | return client.varyColor.lighten(color, i / 10) 9 | }) 10 | // colorPalette变换得到颜色值 11 | const colorPalettes = generate(color) 12 | const rgb = client.varyColor.toNum3(color.replace('#', '')).join(',') 13 | return lightens.concat(colorPalettes).concat(rgb) 14 | }, 15 | changeColor (newColor) { 16 | var options = { 17 | newColors: this.getAntdSerials(newColor), // new colors array, one-to-one corresponde with `matchColors` 18 | changeUrl (cssUrl) { 19 | return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path 20 | } 21 | } 22 | return client.changer.changeColor(options, Promise) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/components/StandardFormRow/index.js: -------------------------------------------------------------------------------- 1 | import StandardFormRow from './StandardFormRow' 2 | 3 | export default StandardFormRow 4 | -------------------------------------------------------------------------------- /ui/src/components/TagSelect/TagSelectOption.jsx: -------------------------------------------------------------------------------- 1 | import { Tag } from 'ant-design-vue' 2 | const { CheckableTag } = Tag 3 | 4 | export default { 5 | name: 'TagSelectOption', 6 | props: { 7 | prefixCls: { 8 | type: String, 9 | default: 'ant-pro-tag-select-option' 10 | }, 11 | value: { 12 | type: [String, Number, Object], 13 | default: '' 14 | }, 15 | checked: { 16 | type: Boolean, 17 | default: false 18 | } 19 | }, 20 | data () { 21 | return { 22 | localChecked: this.checked || false 23 | } 24 | }, 25 | watch: { 26 | 'checked' (val) { 27 | this.localChecked = val 28 | }, 29 | '$parent.items': { 30 | handler: function (val) { 31 | this.value && val.hasOwnProperty(this.value) && (this.localChecked = val[this.value]) 32 | }, 33 | deep: true 34 | } 35 | }, 36 | render () { 37 | const { $slots, value } = this 38 | const onChange = (checked) => { 39 | this.$emit('change', { value, checked }) 40 | } 41 | return ( 42 | {$slots.default} 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ui/src/components/TextArea/style.less: -------------------------------------------------------------------------------- 1 | .ant-textarea-limit { 2 | position: relative; 3 | 4 | .limit { 5 | position: absolute; 6 | color: #909399; 7 | background: #fff; 8 | font-size: 12px; 9 | bottom: 5px; 10 | right: 10px; 11 | } 12 | } -------------------------------------------------------------------------------- /ui/src/components/Trend/Trend.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /ui/src/components/Trend/index.js: -------------------------------------------------------------------------------- 1 | import Trend from './Trend.vue' 2 | 3 | export default Trend 4 | -------------------------------------------------------------------------------- /ui/src/components/Trend/index.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @trend-prefix-cls: ~"@{ant-pro-prefix}-trend"; 4 | 5 | .@{trend-prefix-cls} { 6 | display: inline-block; 7 | font-size: @font-size-base; 8 | line-height: 22px; 9 | 10 | .up, 11 | .down { 12 | margin-left: 4px; 13 | position: relative; 14 | top: 1px; 15 | 16 | i { 17 | font-size: 12px; 18 | transform: scale(0.83); 19 | } 20 | } 21 | 22 | .item-text { 23 | display: inline-block; 24 | margin-left: 8px; 25 | color: rgba(0,0,0,.85); 26 | } 27 | 28 | .up { 29 | color: @red-6; 30 | } 31 | .down { 32 | color: @green-6; 33 | top: -1px; 34 | } 35 | 36 | &.reverse-color .up { 37 | color: @green-6; 38 | } 39 | &.reverse-color .down { 40 | color: @red-6; 41 | } 42 | } -------------------------------------------------------------------------------- /ui/src/components/Trend/index.md: -------------------------------------------------------------------------------- 1 | # Trend 趋势标记 2 | 3 | 趋势符号,标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外。 4 | 5 | 6 | 7 | 引用方式: 8 | 9 | ```javascript 10 | import Trend from '@/components/Trend' 11 | 12 | export default { 13 | components: { 14 | Trend 15 | } 16 | } 17 | ``` 18 | 19 | 20 | 21 | ## 代码演示 [demo](https://pro.loacg.com/test/home) 22 | 23 | ```html 24 | 5% 25 | ``` 26 | 或 27 | ```html 28 | 29 | 工资 30 | 5% 31 | 32 | ``` 33 | 或 34 | ```html 35 | 5% 36 | ``` 37 | 38 | 39 | ## API 40 | 41 | | 参数 | 说明 | 类型 | 默认值 | 42 | |----------|------------------------------------------|-------------|-------| 43 | | flag | 上升下降标识:`up|down` | string | - | 44 | | reverseColor | 颜色反转 | Boolean | false | 45 | 46 | -------------------------------------------------------------------------------- /ui/src/components/_util/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * components util 3 | */ 4 | 5 | /** 6 | * 清理空值,对象 7 | * @param children 8 | * @returns {*[]} 9 | */ 10 | export function filterEmpty (children = []) { 11 | return children.filter(c => c.tag || (c.text && c.text.trim() !== '')) 12 | } 13 | 14 | /** 15 | * 获取字符串长度,英文字符 长度1,中文字符长度2 16 | * @param {*} str 17 | */ 18 | export const getStrFullLength = (str = '') => 19 | str.split('').reduce((pre, cur) => { 20 | const charCode = cur.charCodeAt(0) 21 | if (charCode >= 0 && charCode <= 128) { 22 | return pre + 1 23 | } 24 | return pre + 2 25 | }, 0) 26 | 27 | /** 28 | * 截取字符串,根据 maxLength 截取后返回 29 | * @param {*} str 30 | * @param {*} maxLength 31 | */ 32 | export const cutStrByFullLength = (str = '', maxLength) => { 33 | let showLength = 0 34 | return str.split('').reduce((pre, cur) => { 35 | const charCode = cur.charCodeAt(0) 36 | if (charCode >= 0 && charCode <= 128) { 37 | showLength += 1 38 | } else { 39 | showLength += 2 40 | } 41 | if (showLength <= maxLength) { 42 | return pre + cur 43 | } 44 | return pre 45 | }, '') 46 | } 47 | -------------------------------------------------------------------------------- /ui/src/components/index.js: -------------------------------------------------------------------------------- 1 | // chart 2 | import Bar from '@/components/Charts/Bar' 3 | import ChartCard from '@/components/Charts/ChartCard' 4 | import Liquid from '@/components/Charts/Liquid' 5 | import MiniArea from '@/components/Charts/MiniArea' 6 | import MiniSmoothArea from '@/components/Charts/MiniSmoothArea' 7 | import MiniBar from '@/components/Charts/MiniBar' 8 | import MiniProgress from '@/components/Charts/MiniProgress' 9 | import Radar from '@/components/Charts/Radar' 10 | import RankList from '@/components/Charts/RankList' 11 | import TransferBar from '@/components/Charts/TransferBar' 12 | import TagCloud from '@/components/Charts/TagCloud' 13 | 14 | // pro components 15 | import AvatarList from '@/components/AvatarList' 16 | import Ellipsis from '@/components/Ellipsis' 17 | import FooterToolbar from '@/components/FooterToolbar' 18 | import NumberInfo from '@/components/NumberInfo' 19 | import Tree from '@/components/Tree/Tree' 20 | import Trend from '@/components/Trend' 21 | import STable from '@/components/Table' 22 | import MultiTab from '@/components/MultiTab' 23 | import IconSelector from '@/components/IconSelector' 24 | import TagSelect from '@/components/TagSelect' 25 | import StandardFormRow from '@/components/StandardFormRow' 26 | import ArticleListContent from '@/components/ArticleListContent' 27 | 28 | import Dialog from '@/components/Dialog' 29 | 30 | export { 31 | AvatarList, 32 | Bar, 33 | ChartCard, 34 | Liquid, 35 | MiniArea, 36 | MiniSmoothArea, 37 | MiniBar, 38 | MiniProgress, 39 | Radar, 40 | TagCloud, 41 | RankList, 42 | TransferBar, 43 | Trend, 44 | Ellipsis, 45 | FooterToolbar, 46 | NumberInfo, 47 | Tree, 48 | STable, 49 | MultiTab, 50 | IconSelector, 51 | TagSelect, 52 | StandardFormRow, 53 | ArticleListContent, 54 | 55 | Dialog 56 | } 57 | -------------------------------------------------------------------------------- /ui/src/components/index.less: -------------------------------------------------------------------------------- 1 | @import "~ant-design-vue/lib/style/index"; 2 | 3 | // The prefix to use on all css classes from ant-pro. 4 | @ant-pro-prefix : ant-pro; 5 | @ant-global-sider-zindex : 106; 6 | @ant-global-header-zindex : 105; -------------------------------------------------------------------------------- /ui/src/config/defaultSettings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 项目默认配置项 3 | * primaryColor - 默认主题色, 如果修改颜色不生效,请清理 localStorage 4 | * navTheme - sidebar theme ['dark', 'light'] 两种主题 5 | * colorWeak - 色盲模式 6 | * layout - 整体布局方式 ['sidemenu', 'topmenu'] 两种布局 7 | * fixedHeader - 固定 Header : boolean 8 | * fixSiderbar - 固定左侧菜单栏 : boolean 9 | * contentWidth - 内容区布局: 流式 | 固定 10 | * 11 | * storageOptions: {} - Vue-ls 插件配置项 (localStorage/sessionStorage) 12 | * 13 | */ 14 | 15 | export default { 16 | navTheme: 'dark', // theme for nav menu 17 | primaryColor: '#1890ff', // primary color of ant design 18 | layout: 'sidemenu', // nav menu position: `sidemenu` or `topmenu` 19 | contentWidth: 'Fluid', // layout of content: `Fluid` or `Fixed`, only works when layout is topmenu 20 | fixedHeader: false, // sticky header 21 | fixSiderbar: false, // sticky siderbar 22 | colorWeak: false, 23 | menu: { 24 | locale: true 25 | }, 26 | title: 'MuxWAF', 27 | pwa: false, 28 | iconfontUrl: '', 29 | production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true' 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/core/bootstrap.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | import storage from 'store' 3 | import { 4 | ACCESS_TOKEN, 5 | APP_LANGUAGE, 6 | TOGGLE_CONTENT_WIDTH, 7 | TOGGLE_FIXED_HEADER, 8 | TOGGLE_FIXED_SIDEBAR, TOGGLE_HIDE_HEADER, 9 | TOGGLE_LAYOUT, TOGGLE_NAV_THEME, TOGGLE_WEAK, 10 | TOGGLE_COLOR, TOGGLE_MULTI_TAB 11 | } from '@/store/mutation-types' 12 | // import { printANSI } from '@/utils/screenLog' 13 | import defaultSettings from '@/config/defaultSettings' 14 | 15 | export default function Initializer () { 16 | // printANSI() // 请自行移除该行. please remove this line 17 | 18 | store.commit(TOGGLE_LAYOUT, storage.get(TOGGLE_LAYOUT, defaultSettings.layout)) 19 | store.commit(TOGGLE_FIXED_HEADER, storage.get(TOGGLE_FIXED_HEADER, defaultSettings.fixedHeader)) 20 | store.commit(TOGGLE_FIXED_SIDEBAR, storage.get(TOGGLE_FIXED_SIDEBAR, defaultSettings.fixSiderbar)) 21 | store.commit(TOGGLE_CONTENT_WIDTH, storage.get(TOGGLE_CONTENT_WIDTH, defaultSettings.contentWidth)) 22 | store.commit(TOGGLE_HIDE_HEADER, storage.get(TOGGLE_HIDE_HEADER, defaultSettings.autoHideHeader)) 23 | store.commit(TOGGLE_NAV_THEME, storage.get(TOGGLE_NAV_THEME, defaultSettings.navTheme)) 24 | store.commit(TOGGLE_WEAK, storage.get(TOGGLE_WEAK, defaultSettings.colorWeak)) 25 | store.commit(TOGGLE_COLOR, storage.get(TOGGLE_COLOR, defaultSettings.primaryColor)) 26 | store.commit(TOGGLE_MULTI_TAB, storage.get(TOGGLE_MULTI_TAB, defaultSettings.multiTab)) 27 | store.commit('SET_TOKEN', storage.get(ACCESS_TOKEN)) 28 | 29 | // store.dispatch('setLang', storage.get(APP_LANGUAGE, 'en-US')) 30 | store.dispatch('setLang', storage.get(APP_LANGUAGE, 'zh-CN')) 31 | // last step 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/core/directives/action.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from '@/store' 3 | 4 | /** 5 | * Action 权限指令 6 | * 指令用法: 7 | * - 在需要控制 action 级别权限的组件上使用 v-action:[method] , 如下: 8 | * 添加用户 9 | * 删除用户 10 | * 修改 11 | * 12 | * - 当前用户没有权限时,组件上使用了该指令则会被隐藏 13 | * - 当后台权限跟 pro 提供的模式不同时,只需要针对这里的权限过滤进行修改即可 14 | * 15 | * @see https://github.com/vueComponent/ant-design-vue-pro/pull/53 16 | */ 17 | const action = Vue.directive('action', { 18 | inserted: function (el, binding, vnode) { 19 | const actionName = binding.arg 20 | const roles = store.getters.roles 21 | const elVal = vnode.context.$route.meta.permission 22 | const permissionId = elVal instanceof String && [elVal] || elVal 23 | roles.permissions.forEach(p => { 24 | if (!permissionId.includes(p.permissionId)) { 25 | return 26 | } 27 | if (p.actionList && !p.actionList.includes(actionName)) { 28 | el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none') 29 | } 30 | }) 31 | } 32 | }) 33 | 34 | export default action 35 | -------------------------------------------------------------------------------- /ui/src/core/icons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom icon list 3 | * All icons are loaded here for easy management 4 | * @see https://vue.ant.design/components/icon/#Custom-Font-Icon 5 | * 6 | * 自定义图标加载表 7 | * 所有图标均从这里加载,方便管理 8 | */ 9 | import bxAnaalyse from '@/assets/icons/bx-analyse.svg?inline' // path to your '*.svg?inline' file. 10 | 11 | export { bxAnaalyse } 12 | -------------------------------------------------------------------------------- /ui/src/core/permission/permission.js: -------------------------------------------------------------------------------- 1 | export const PERMISSION_ENUM = { 2 | 'add': { key: 'add', label: '新增' }, 3 | 'delete': { key: 'delete', label: '删除' }, 4 | 'edit': { key: 'edit', label: '修改' }, 5 | 'query': { key: 'query', label: '查询' }, 6 | 'get': { key: 'get', label: '详情' }, 7 | 'enable': { key: 'enable', label: '启用' }, 8 | 'disable': { key: 'disable', label: '禁用' }, 9 | 'import': { key: 'import', label: '导入' }, 10 | 'export': { key: 'export', label: '导出' } 11 | } 12 | 13 | /** 14 | * Button 15 | * @param Vue 16 | */ 17 | function plugin (Vue) { 18 | if (plugin.installed) { 19 | return 20 | } 21 | 22 | !Vue.prototype.$auth && Object.defineProperties(Vue.prototype, { 23 | $auth: { 24 | get () { 25 | const _this = this 26 | return (permissions) => { 27 | const [permission, action] = permissions.split('.') 28 | const permissionList = _this.$store.getters.roles.permissions 29 | return permissionList.find((val) => { 30 | return val.permissionId === permission 31 | }).actionList.findIndex((val) => { 32 | return val === action 33 | }) > -1 34 | } 35 | } 36 | } 37 | }) 38 | 39 | !Vue.prototype.$enum && Object.defineProperties(Vue.prototype, { 40 | $enum: { 41 | get () { 42 | // const _this = this; 43 | return (val) => { 44 | let result = PERMISSION_ENUM 45 | val && val.split('.').forEach(v => { 46 | result = result && result[v] || null 47 | }) 48 | return result 49 | } 50 | } 51 | } 52 | }) 53 | } 54 | 55 | export default plugin 56 | -------------------------------------------------------------------------------- /ui/src/core/use.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // base library 4 | import Antd from 'ant-design-vue' 5 | import Viser from 'viser-vue' 6 | import VueCropper from 'vue-cropper' 7 | import 'ant-design-vue/dist/antd.less' 8 | 9 | // ext library 10 | import VueClipboard from 'vue-clipboard2' 11 | import MultiTab from '@/components/MultiTab' 12 | import PageLoading from '@/components/PageLoading' 13 | import PermissionHelper from '@/core/permission/permission' 14 | // import '@/components/use' 15 | import './directives/action' 16 | 17 | VueClipboard.config.autoSetContainer = true 18 | 19 | Vue.use(Antd) 20 | Vue.use(Viser) 21 | Vue.use(MultiTab) 22 | Vue.use(PageLoading) 23 | Vue.use(VueClipboard) 24 | Vue.use(PermissionHelper) 25 | Vue.use(VueCropper) 26 | 27 | process.env.NODE_ENV !== 'production' && console.warn('[antd-pro] WARNING: Antd now use fulled imported.') 28 | -------------------------------------------------------------------------------- /ui/src/layouts/BasicLayout.less: -------------------------------------------------------------------------------- 1 | @import "~ant-design-vue/es/style/themes/default.less"; 2 | 3 | .ant-pro-global-header-index-right { 4 | margin-right: 8px; 5 | 6 | &.ant-pro-global-header-index-dark { 7 | .ant-pro-global-header-index-action { 8 | color: hsla(0, 0%, 100%, .85); 9 | 10 | &:hover { 11 | background: #1890ff; 12 | } 13 | } 14 | } 15 | 16 | .ant-pro-account-avatar { 17 | .antd-pro-global-header-index-avatar { 18 | margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0; 19 | margin-right: 8px; 20 | color: @primary-color; 21 | vertical-align: top; 22 | background: rgba(255, 255, 255, 0.85); 23 | } 24 | } 25 | 26 | .menu { 27 | .anticon { 28 | margin-right: 8px; 29 | } 30 | 31 | .ant-dropdown-menu-item { 32 | min-width: 100px; 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /ui/src/layouts/BlankLayout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /ui/src/layouts/PageView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /ui/src/layouts/RouteView.vue: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /ui/src/layouts/index.js: -------------------------------------------------------------------------------- 1 | import UserLayout from './UserLayout' 2 | import BlankLayout from './BlankLayout' 3 | import BasicLayout from './BasicLayout' 4 | import RouteView from './RouteView' 5 | import PageView from './PageView' 6 | 7 | export { UserLayout, BasicLayout, BlankLayout, RouteView, PageView } 8 | -------------------------------------------------------------------------------- /ui/src/locales/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import storage from 'store' 4 | import moment from 'moment' 5 | 6 | // default lang 7 | // import enUS from './lang/en-US' 8 | import zhCN from './lang/zh-CN' 9 | 10 | Vue.use(VueI18n) 11 | 12 | // export const defaultLang = 'en-US' 13 | export const defaultLang = 'zh-CN' 14 | 15 | // const messages = { 16 | // 'en-US': { 17 | // ...enUS 18 | // } 19 | // } 20 | 21 | const messages = { 22 | 'zh-CN': { 23 | ...zhCN 24 | } 25 | } 26 | 27 | const i18n = new VueI18n({ 28 | silentTranslationWarn: true, 29 | locale: defaultLang, 30 | fallbackLocale: defaultLang, 31 | messages 32 | }) 33 | 34 | const loadedLanguages = [defaultLang] 35 | 36 | function setI18nLanguage (lang) { 37 | i18n.locale = lang 38 | // request.headers['Accept-Language'] = lang 39 | document.querySelector('html').setAttribute('lang', lang) 40 | return lang 41 | } 42 | 43 | export function loadLanguageAsync (lang = defaultLang) { 44 | return new Promise(resolve => { 45 | // 缓存语言设置 46 | storage.set('lang', lang) 47 | if (i18n.locale !== lang) { 48 | if (!loadedLanguages.includes(lang)) { 49 | return import(/* webpackChunkName: "lang-[request]" */ `./lang/${lang}`).then(msg => { 50 | const locale = msg.default 51 | i18n.setLocaleMessage(lang, locale) 52 | loadedLanguages.push(lang) 53 | moment.updateLocale(locale.momentName, locale.momentLocale) 54 | return setI18nLanguage(lang) 55 | }) 56 | } 57 | return resolve(setI18nLanguage(lang)) 58 | } 59 | return resolve(lang) 60 | }) 61 | } 62 | 63 | export function i18nRender (key) { 64 | return i18n.t(`${key}`) 65 | } 66 | 67 | export default i18n 68 | -------------------------------------------------------------------------------- /ui/src/locales/lang/en-US.js: -------------------------------------------------------------------------------- 1 | import antdEnUS from 'ant-design-vue/es/locale-provider/en_US' 2 | import momentEU from 'moment/locale/eu' 3 | import global from './en-US/global' 4 | 5 | import menu from './en-US/menu' 6 | import setting from './en-US/setting' 7 | import user from './en-US/user' 8 | 9 | import dashboard from './en-US/dashboard' 10 | import form from './en-US/form' 11 | import result from './en-US/result' 12 | import account from './en-US/account' 13 | 14 | const components = { 15 | antLocale: antdEnUS, 16 | momentName: 'eu', 17 | momentLocale: momentEU 18 | } 19 | 20 | export default { 21 | message: '-', 22 | 23 | 'layouts.usermenu.dialog.title': 'Message', 24 | 'layouts.usermenu.dialog.content': 'Are you sure you would like to logout?', 25 | 'layouts.userLayout.title': 'Ant Design is the most influential web design specification in Xihu district', 26 | ...components, 27 | ...global, 28 | ...menu, 29 | ...setting, 30 | ...user, 31 | ...dashboard, 32 | ...form, 33 | ...result, 34 | ...account 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/locales/lang/en-US/account.js: -------------------------------------------------------------------------------- 1 | import settings from './account/settings' 2 | 3 | export default { 4 | ...settings 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/locales/lang/en-US/dashboard.js: -------------------------------------------------------------------------------- 1 | import analysis from './dashboard/analysis' 2 | 3 | export default { 4 | ...analysis 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/locales/lang/en-US/form.js: -------------------------------------------------------------------------------- 1 | import basicForm from './form/basicForm' 2 | 3 | export default { 4 | ...basicForm 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/locales/lang/en-US/global.js: -------------------------------------------------------------------------------- 1 | export default { 2 | submit: 'Submit', 3 | save: 'Save', 4 | 'submit.ok': 'Submit successfully', 5 | 'save.ok': 'Saved successfully' 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/locales/lang/en-US/menu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.home': 'Home', 4 | 'menu.dashboard': 'Dashboard', 5 | 'menu.dashboard.analysis': 'Analysis', 6 | 'menu.dashboard.monitor': 'Monitor', 7 | 'menu.dashboard.workplace': 'Workplace', 8 | 'menu.form': 'Form', 9 | 'menu.form.basic-form': 'Basic Form', 10 | 'menu.form.step-form': 'Step Form', 11 | 'menu.form.step-form.info': 'Step Form(write transfer information)', 12 | 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)', 13 | 'menu.form.step-form.result': 'Step Form(finished)', 14 | 'menu.form.advanced-form': 'Advanced Form', 15 | 'menu.list': 'List', 16 | 'menu.list.table-list': 'Search Table', 17 | 'menu.list.basic-list': 'Basic List', 18 | 'menu.list.card-list': 'Card List', 19 | 'menu.list.search-list': 'Search List', 20 | 'menu.list.search-list.articles': 'Search List(articles)', 21 | 'menu.list.search-list.projects': 'Search List(projects)', 22 | 'menu.list.search-list.applications': 'Search List(applications)', 23 | 'menu.profile': 'Profile', 24 | 'menu.profile.basic': 'Basic Profile', 25 | 'menu.profile.advanced': 'Advanced Profile', 26 | 'menu.result': 'Result', 27 | 'menu.result.success': 'Success', 28 | 'menu.result.fail': 'Fail', 29 | 'menu.exception': 'Exception', 30 | 'menu.exception.not-permission': '403', 31 | 'menu.exception.not-find': '404', 32 | 'menu.exception.server-error': '500', 33 | 'menu.exception.trigger': 'Trigger', 34 | 'menu.account': 'Account', 35 | 'menu.account.center': 'Account Center', 36 | 'menu.account.settings': 'Account Settings', 37 | 'menu.account.trigger': 'Trigger Error', 38 | 'menu.account.logout': 'Logout' 39 | } 40 | -------------------------------------------------------------------------------- /ui/src/locales/lang/en-US/result.js: -------------------------------------------------------------------------------- 1 | import success from './result/success' 2 | import fail from './result/fail' 3 | 4 | export default { 5 | ...success, 6 | ...fail 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/locales/lang/en-US/result/fail.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'result.fail.error.title': 'Submission Failed', 3 | 'result.fail.error.description': 4 | 'Please check and modify the following information before resubmitting.', 5 | 'result.fail.error.hint-title': 'The content you submitted has the following error:', 6 | 'result.fail.error.hint-text1': 'Your account has been frozen', 7 | 'result.fail.error.hint-btn1': 'Thaw immediately', 8 | 'result.fail.error.hint-text2': 'Your account is not yet eligible to apply', 9 | 'result.fail.error.hint-btn2': 'Upgrade immediately', 10 | 'result.fail.error.btn-text': 'Return to modify' 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/locales/lang/en-US/result/success.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'result.success.title': 'Submission Success', 3 | 'result.success.description': 4 | 'The submission results page is used to feed back the results of a series of operational tasks. If it is a simple operation, use the Message global prompt feedback. This text area can show a simple supplementary explanation. If there is a similar requirement for displaying “documents”, the following gray area can present more complicated content.', 5 | 'result.success.operate-title': 'Project Name', 6 | 'result.success.operate-id': 'Project ID', 7 | 'result.success.principal': 'Principal', 8 | 'result.success.operate-time': 'Effective time', 9 | 'result.success.step1-title': 'Create project', 10 | 'result.success.step1-operator': 'Qu Lili', 11 | 'result.success.step2-title': 'Departmental preliminary review', 12 | 'result.success.step2-operator': 'Zhou Maomao', 13 | 'result.success.step2-extra': 'Urge', 14 | 'result.success.step3-title': 'Financial review', 15 | 'result.success.step4-title': 'Finish', 16 | 'result.success.btn-return': 'Back List', 17 | 'result.success.btn-project': 'View Project', 18 | 'result.success.btn-print': 'Print' 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/locales/lang/en-US/setting.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Page style setting', 3 | 'app.setting.pagestyle.light': 'Light style', 4 | 'app.setting.pagestyle.dark': 'Dark style', 5 | 'app.setting.pagestyle.realdark': 'RealDark style', 6 | 'app.setting.themecolor': 'Theme Color', 7 | 'app.setting.navigationmode': 'Navigation Mode', 8 | 'app.setting.content-width': 'Content Width', 9 | 'app.setting.fixedheader': 'Fixed Header', 10 | 'app.setting.fixedsidebar': 'Fixed Sidebar', 11 | 'app.setting.sidemenu': 'Side Menu Layout', 12 | 'app.setting.topmenu': 'Top Menu Layout', 13 | 'app.setting.content-width.fixed': 'Fixed', 14 | 'app.setting.content-width.fluid': 'Fluid', 15 | 'app.setting.othersettings': 'Other Settings', 16 | 'app.setting.weakmode': 'Weak Mode', 17 | 'app.setting.copy': 'Copy Setting', 18 | 'app.setting.loading': 'Loading theme', 19 | 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/config/defaultSettings.js', 20 | 'app.setting.production.hint': 'Setting panel shows in development environment only, please manually modify', 21 | 'app.setting.themecolor.daybreak': 'Daybreak Blue', 22 | 'app.setting.themecolor.dust': 'Dust Red', 23 | 'app.setting.themecolor.volcano': 'Volcano', 24 | 'app.setting.themecolor.sunset': 'Sunset Orange', 25 | 'app.setting.themecolor.cyan': 'Cyan', 26 | 'app.setting.themecolor.green': 'Polar Green', 27 | 'app.setting.themecolor.geekblue': 'Geek Blue', 28 | 'app.setting.themecolor.purple': 'Golden Purple' 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/locales/lang/zh-CN.js: -------------------------------------------------------------------------------- 1 | import antd from 'ant-design-vue/es/locale-provider/zh_CN' 2 | import momentCN from 'moment/locale/zh-cn' 3 | import global from './zh-CN/global' 4 | 5 | import menu from './zh-CN/menu' 6 | import setting from './zh-CN/setting' 7 | import user from './zh-CN/user' 8 | import dashboard from './zh-CN/dashboard' 9 | import form from './zh-CN/form' 10 | import result from './zh-CN/result' 11 | import account from './zh-CN/account' 12 | 13 | const components = { 14 | antLocale: antd, 15 | momentName: 'zh-cn', 16 | momentLocale: momentCN 17 | } 18 | 19 | export default { 20 | message: '-', 21 | 22 | 'layouts.usermenu.dialog.title': '信息', 23 | 'layouts.usermenu.dialog.content': '您确定要注销吗?', 24 | 'layouts.userLayout.title': 'Ant Design 是西湖区最具影响力的 Web 设计规范', 25 | ...components, 26 | ...global, 27 | ...menu, 28 | ...setting, 29 | ...user, 30 | ...dashboard, 31 | ...form, 32 | ...result, 33 | ...account 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/locales/lang/zh-CN/account.js: -------------------------------------------------------------------------------- 1 | import settings from './account/settings' 2 | 3 | export default { 4 | ...settings 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/locales/lang/zh-CN/dashboard.js: -------------------------------------------------------------------------------- 1 | import analysis from './dashboard/analysis' 2 | 3 | export default { 4 | ...analysis 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/locales/lang/zh-CN/dashboard/analysis.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'dashboard.analysis.test': '工专路 {no} 号店', 3 | 'dashboard.analysis.introduce': '指标说明', 4 | 'dashboard.analysis.total-sales': '总销售额', 5 | 'dashboard.analysis.day-sales': '日均销售额¥', 6 | 'dashboard.analysis.visits': '访问量', 7 | 'dashboard.analysis.visits-trend': '访问量趋势', 8 | 'dashboard.analysis.visits-ranking': '门店访问量排名', 9 | 'dashboard.analysis.day-visits': '日访问量', 10 | 'dashboard.analysis.week': '周同比', 11 | 'dashboard.analysis.day': '日同比', 12 | 'dashboard.analysis.payments': '支付笔数', 13 | 'dashboard.analysis.conversion-rate': '转化率', 14 | 'dashboard.analysis.operational-effect': '运营活动效果', 15 | 'dashboard.analysis.sales-trend': '销售趋势', 16 | 'dashboard.analysis.sales-ranking': '门店销售额排名', 17 | 'dashboard.analysis.all-year': '全年', 18 | 'dashboard.analysis.all-month': '本月', 19 | 'dashboard.analysis.all-week': '本周', 20 | 'dashboard.analysis.all-day': '今日', 21 | 'dashboard.analysis.search-users': '搜索用户数', 22 | 'dashboard.analysis.per-capita-search': '人均搜索次数', 23 | 'dashboard.analysis.online-top-search': '线上热门搜索', 24 | 'dashboard.analysis.the-proportion-of-sales': '销售额类别占比', 25 | 'dashboard.analysis.dropdown-option-one': '操作一', 26 | 'dashboard.analysis.dropdown-option-two': '操作二', 27 | 'dashboard.analysis.channel.all': '全部渠道', 28 | 'dashboard.analysis.channel.online': '线上', 29 | 'dashboard.analysis.channel.stores': '门店', 30 | 'dashboard.analysis.sales': '销售额', 31 | 'dashboard.analysis.traffic': '客流量', 32 | 'dashboard.analysis.table.rank': '排名', 33 | 'dashboard.analysis.table.search-keyword': '搜索关键词', 34 | 'dashboard.analysis.table.users': '用户数', 35 | 'dashboard.analysis.table.weekly-range': '周涨幅' 36 | } 37 | -------------------------------------------------------------------------------- /ui/src/locales/lang/zh-CN/form.js: -------------------------------------------------------------------------------- 1 | import basicForm from './form/basicForm' 2 | 3 | export default { 4 | ...basicForm 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/locales/lang/zh-CN/global.js: -------------------------------------------------------------------------------- 1 | export default { 2 | submit: '提交', 3 | save: '保存', 4 | 'submit.ok': '提交成功', 5 | 'save.ok': '保存成功' 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/locales/lang/zh-CN/menu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '欢迎', 3 | 'menu.home': '主页', 4 | 'menu.dashboard': '仪表盘', 5 | 'menu.dashboard.analysis': '分析页', 6 | 'menu.dashboard.monitor': '监控页', 7 | 'menu.dashboard.workplace': '工作台', 8 | 'menu.form': '表单页', 9 | 'menu.form.basic-form': '基础表单', 10 | 'menu.form.step-form': '分步表单', 11 | 'menu.form.step-form.info': '分步表单(填写转账信息)', 12 | 'menu.form.step-form.confirm': '分步表单(确认转账信息)', 13 | 'menu.form.step-form.result': '分步表单(完成)', 14 | 'menu.form.advanced-form': '高级表单', 15 | 'menu.list': '列表页', 16 | 'menu.list.table-list': '查询表格', 17 | 'menu.list.basic-list': '标准列表', 18 | 'menu.list.card-list': '卡片列表', 19 | 'menu.list.search-list': '搜索列表', 20 | 'menu.list.search-list.articles': '搜索列表(文章)', 21 | 'menu.list.search-list.projects': '搜索列表(项目)', 22 | 'menu.list.search-list.applications': '搜索列表(应用)', 23 | 'menu.profile': '详情页', 24 | 'menu.profile.basic': '基础详情页', 25 | 'menu.profile.advanced': '高级详情页', 26 | 'menu.result': '结果页', 27 | 'menu.result.success': '成功页', 28 | 'menu.result.fail': '失败页', 29 | 'menu.exception': '异常页', 30 | 'menu.exception.not-permission': '403', 31 | 'menu.exception.not-find': '404', 32 | 'menu.exception.server-error': '500', 33 | 'menu.exception.trigger': '触发错误', 34 | 'menu.account': '个人页', 35 | 'menu.account.center': '个人中心', 36 | 'menu.account.settings': '个人设置', 37 | 'menu.account.trigger': '触发报错', 38 | 'menu.account.logout': '退出登录' 39 | } 40 | -------------------------------------------------------------------------------- /ui/src/locales/lang/zh-CN/result.js: -------------------------------------------------------------------------------- 1 | import success from './result/success' 2 | import fail from './result/fail' 3 | 4 | export default { 5 | ...success, 6 | ...fail 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/locales/lang/zh-CN/result/fail.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'result.fail.error.title': '提交失败', 3 | 'result.fail.error.description': '请核对并修改以下信息后,再重新提交。', 4 | 'result.fail.error.hint-title': '您提交的内容有如下错误:', 5 | 'result.fail.error.hint-text1': '您的账户已被冻结', 6 | 'result.fail.error.hint-btn1': '立即解冻', 7 | 'result.fail.error.hint-text2': '您的账户还不具备申请资格', 8 | 'result.fail.error.hint-btn2': '立即升级', 9 | 'result.fail.error.btn-text': '返回修改' 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/locales/lang/zh-CN/result/success.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'result.success.title': '提交成功', 3 | 'result.success.description': 4 | '提交结果页用于反馈一系列操作任务的处理结果, 如果仅是简单操作,使用 Message 全局提示反馈即可。 本文字区域可以展示简单的补充说明,如果有类似展示 “单据”的需求,下面这个灰色区域可以呈现比较复杂的内容。', 5 | 'result.success.operate-title': '项目名称', 6 | 'result.success.operate-id': '项目 ID', 7 | 'result.success.principal': '负责人', 8 | 'result.success.operate-time': '生效时间', 9 | 'result.success.step1-title': '创建项目', 10 | 'result.success.step1-operator': '曲丽丽', 11 | 'result.success.step2-title': '部门初审', 12 | 'result.success.step2-operator': '周毛毛', 13 | 'result.success.step2-extra': '催一下', 14 | 'result.success.step3-title': '财务复核', 15 | 'result.success.step4-title': '完成', 16 | 'result.success.btn-return': '返回列表', 17 | 'result.success.btn-project': '查看项目', 18 | 'result.success.btn-print': '打印' 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/locales/lang/zh-CN/setting.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整体风格设置', 3 | 'app.setting.pagestyle.light': '亮色菜单风格', 4 | 'app.setting.pagestyle.dark': '暗色菜单风格', 5 | 'app.setting.pagestyle.realdark': '暗黑模式', 6 | 'app.setting.themecolor': '主题色', 7 | 'app.setting.navigationmode': '导航模式', 8 | 'app.setting.content-width': '内容区域宽度', 9 | 'app.setting.fixedheader': '固定 Header', 10 | 'app.setting.fixedsidebar': '固定侧边栏', 11 | 'app.setting.sidemenu': '侧边菜单布局', 12 | 'app.setting.topmenu': '顶部菜单布局', 13 | 'app.setting.content-width.fixed': 'Fixed', 14 | 'app.setting.content-width.fluid': 'Fluid', 15 | 'app.setting.othersettings': '其他设置', 16 | 'app.setting.weakmode': '色弱模式', 17 | 'app.setting.copy': '拷贝设置', 18 | 'app.setting.loading': '加载主题中', 19 | 'app.setting.copyinfo': '拷贝设置成功 src/config/defaultSettings.js', 20 | 'app.setting.production.hint': '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', 21 | 'app.setting.themecolor.daybreak': '拂晓蓝', 22 | 'app.setting.themecolor.dust': '薄暮', 23 | 'app.setting.themecolor.volcano': '火山', 24 | 'app.setting.themecolor.sunset': '日暮', 25 | 'app.setting.themecolor.cyan': '明青', 26 | 'app.setting.themecolor.green': '极光绿', 27 | 'app.setting.themecolor.geekblue': '极客蓝', 28 | 'app.setting.themecolor.purple': '酱紫' 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/main.js: -------------------------------------------------------------------------------- 1 | // with polyfills 2 | import 'core-js/stable' 3 | import 'regenerator-runtime/runtime' 4 | 5 | import Vue from 'vue' 6 | import App from './App.vue' 7 | import router from './router' 8 | import store from './store/' 9 | import i18n from './locales' 10 | import { VueAxios } from './utils/request' 11 | import ProLayout, { PageHeaderWrapper } from '@ant-design-vue/pro-layout' 12 | import themePluginConfig from '../config/themePluginConfig' 13 | 14 | // mock 15 | // WARNING: `mockjs` NOT SUPPORT `IE` PLEASE DO NOT USE IN `production` ENV. 16 | // import './mock' 17 | 18 | import bootstrap from './core/bootstrap' 19 | import './core/lazy_use' // use lazy load components 20 | import './permission' // permission control 21 | import './utils/filter' // global filter 22 | import './reset.less' // global style 23 | import './global.less' // global style 24 | 25 | Vue.config.productionTip = false 26 | 27 | // mount axios to `Vue.$http` and `this.$http` 28 | Vue.use(VueAxios) 29 | // use pro-layout components 30 | Vue.component('pro-layout', ProLayout) 31 | Vue.component('page-container', PageHeaderWrapper) 32 | Vue.component('page-header-wrapper', PageHeaderWrapper) 33 | 34 | window.umi_plugin_ant_themeVar = themePluginConfig.theme 35 | 36 | new Vue({ 37 | router, 38 | store, 39 | i18n, 40 | // init localstorage, vuex 41 | created: bootstrap, 42 | render: h => h(App) 43 | }).$mount('#app') 44 | -------------------------------------------------------------------------------- /ui/src/mock/index.js: -------------------------------------------------------------------------------- 1 | import { isIE } from '@/utils/util' 2 | 3 | // 判断环境不是 prod 或者 preview 是 true 时,加载 mock 服务 4 | if (process.env.NODE_ENV !== 'production' || process.env.VUE_APP_PREVIEW === 'true') { 5 | if (isIE()) { 6 | console.error('[antd-pro] ERROR: `mockjs` NOT SUPPORT `IE` PLEASE DO NOT USE IN `production` ENV.') 7 | } 8 | // 使用同步加载依赖 9 | // 防止 vuex 中的 GetInfo 早于 mock 运行,导致无法 mock 请求返回结果 10 | console.log('[antd-pro] mock mounting') 11 | const Mock = require('mockjs2') 12 | require('./services/auth') 13 | require('./services/user') 14 | require('./services/manage') 15 | require('./services/other') 16 | require('./services/tagCloud') 17 | require('./services/article') 18 | 19 | Mock.setup({ 20 | timeout: 800 // setter delay time 21 | }) 22 | console.log('[antd-pro] mock mounted') 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/mock/services/auth.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs2' 2 | import { builder, getBody } from '../util' 3 | 4 | const username = ['admin', 'super'] 5 | // 强硬要求 ant.design 相同密码 6 | // '21232f297a57a5a743894a0e4a801fc3', 7 | const password = ['8914de686ab28dc22f30d3d8e107ff6c', '21232f297a57a5a743894a0e4a801fc3'] // admin, ant.design 8 | 9 | const login = (options) => { 10 | const body = getBody(options) 11 | console.log('mock: body', body) 12 | if (!username.includes(body.username) || !password.includes(body.password)) { 13 | return builder({ isLogin: true }, '账户或密码错误', 401) 14 | } 15 | 16 | return builder({ 17 | 'id': Mock.mock('@guid'), 18 | 'name': Mock.mock('@name'), 19 | 'username': 'admin', 20 | 'password': '', 21 | 'avatar': 'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png', 22 | 'status': 1, 23 | 'telephone': '', 24 | 'lastLoginIp': '27.154.74.117', 25 | 'lastLoginTime': 1534837621348, 26 | 'creatorId': 'admin', 27 | 'createTime': 1497160610259, 28 | 'deleted': 0, 29 | 'roleId': 'admin', 30 | 'lang': 'zh-CN', 31 | 'token': '4291d7da9005377ec9aec4a71ea837f' 32 | }, '', 200, { 'Custom-Header': Mock.mock('@guid') }) 33 | } 34 | 35 | const logout = () => { 36 | return builder({}, '[测试接口] 注销成功') 37 | } 38 | 39 | const smsCaptcha = () => { 40 | return builder({ captcha: Mock.mock('@integer(10000, 99999)') }) 41 | } 42 | 43 | const twofactor = () => { 44 | return builder({ stepCode: Mock.mock('@integer(0, 1)') }) 45 | } 46 | 47 | Mock.mock(/\/auth\/login/, 'post', login) 48 | Mock.mock(/\/auth\/logout/, 'post', logout) 49 | Mock.mock(/\/account\/sms/, 'post', smsCaptcha) 50 | Mock.mock(/\/auth\/2step-code/, 'post', twofactor) 51 | -------------------------------------------------------------------------------- /ui/src/mock/util.js: -------------------------------------------------------------------------------- 1 | const responseBody = { 2 | message: '', 3 | timestamp: 0, 4 | result: null, 5 | code: 0 6 | } 7 | 8 | export const builder = (data, message, code = 0, headers = {}) => { 9 | responseBody.result = data 10 | if (message !== undefined && message !== null) { 11 | responseBody.message = message 12 | } 13 | if (code !== undefined && code !== 0) { 14 | responseBody.code = code 15 | responseBody._status = code 16 | } 17 | if (headers !== null && typeof headers === 'object' && Object.keys(headers).length > 0) { 18 | responseBody._headers = headers 19 | } 20 | responseBody.timestamp = new Date().getTime() 21 | return responseBody 22 | } 23 | 24 | export const getQueryParameters = (options) => { 25 | const url = options.url 26 | const search = url.split('?')[1] 27 | if (!search) { 28 | return {} 29 | } 30 | return JSON.parse('{"' + decodeURIComponent(search) 31 | .replace(/"/g, '\\"') 32 | .replace(/&/g, '","') 33 | .replace(/=/g, '":"') + '"}') 34 | } 35 | 36 | export const getBody = (options) => { 37 | return options.body && JSON.parse(options.body) 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/reset.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/dist/antd.less'; //———>后缀是 less,别写成 css 。 2 | 3 | @font-family: -apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif; 4 | 5 | @tabs-horizontal-margin:0 16px 0 0; 6 | 7 | @primary-color: #2991fa; -------------------------------------------------------------------------------- /ui/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import { constantRouterMap } from '@/config/router.config' 4 | 5 | // hack router push callback 6 | const originalPush = Router.prototype.push 7 | Router.prototype.push = function push (location, onResolve, onReject) { 8 | if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject) 9 | return originalPush.call(this, location).catch(err => err) 10 | } 11 | 12 | Vue.use(Router) 13 | 14 | export default new Router({ 15 | mode: 'history', 16 | routes: constantRouterMap 17 | }) 18 | -------------------------------------------------------------------------------- /ui/src/store/app-mixin.js: -------------------------------------------------------------------------------- 1 | import { mapState } from 'vuex' 2 | 3 | const baseMixin = { 4 | computed: { 5 | ...mapState({ 6 | layout: state => state.app.layout, 7 | navTheme: state => state.app.theme, 8 | primaryColor: state => state.app.color, 9 | colorWeak: state => state.app.weak, 10 | fixedHeader: state => state.app.fixedHeader, 11 | fixedSidebar: state => state.app.fixedSidebar, 12 | contentWidth: state => state.app.contentWidth, 13 | autoHideHeader: state => state.app.autoHideHeader, 14 | 15 | isMobile: state => state.app.isMobile, 16 | sideCollapsed: state => state.app.sideCollapsed, 17 | multiTab: state => state.app.multiTab 18 | }), 19 | isTopMenu () { 20 | return this.layout === 'topmenu' 21 | } 22 | }, 23 | methods: { 24 | isSideMenu () { 25 | return !this.isTopMenu 26 | } 27 | } 28 | } 29 | 30 | export { 31 | baseMixin 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/store/device-mixin.js: -------------------------------------------------------------------------------- 1 | import { mapState } from 'vuex' 2 | 3 | const deviceMixin = { 4 | computed: { 5 | ...mapState({ 6 | isMobile: state => state.app.isMobile 7 | }) 8 | } 9 | } 10 | 11 | export { deviceMixin } 12 | -------------------------------------------------------------------------------- /ui/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | isMobile: state => state.app.isMobile, 3 | lang: state => state.app.lang, 4 | theme: state => state.app.theme, 5 | color: state => state.app.color, 6 | token: state => state.user.token, 7 | avatar: state => state.user.avatar, 8 | nickname: state => state.user.name, 9 | welcome: state => state.user.welcome, 10 | roles: state => state.user.roles, 11 | userInfo: state => state.user.info, 12 | addRouters: state => state.permission.addRouters, 13 | multiTab: state => state.app.multiTab 14 | } 15 | 16 | export default getters 17 | -------------------------------------------------------------------------------- /ui/src/store/i18n-mixin.js: -------------------------------------------------------------------------------- 1 | import { mapState } from 'vuex' 2 | 3 | const i18nMixin = { 4 | computed: { 5 | ...mapState({ 6 | currentLang: state => state.app.lang 7 | }) 8 | }, 9 | methods: { 10 | setLang (lang) { 11 | this.$store.dispatch('setLang', lang) 12 | } 13 | } 14 | } 15 | 16 | export default i18nMixin 17 | -------------------------------------------------------------------------------- /ui/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import app from './modules/app' 5 | import user from './modules/user' 6 | import site from './modules/site' 7 | 8 | // default router permission control 9 | import permission from './modules/permission' 10 | 11 | // dynamic router permission control (Experimental) 12 | // import permission from './modules/async-router' 13 | import getters from './getters' 14 | 15 | Vue.use(Vuex) 16 | 17 | export default new Vuex.Store({ 18 | modules: { 19 | app, 20 | user, 21 | permission, 22 | site 23 | }, 24 | state: {}, 25 | mutations: {}, 26 | actions: {}, 27 | getters 28 | }) 29 | -------------------------------------------------------------------------------- /ui/src/store/modules/site.js: -------------------------------------------------------------------------------- 1 | import storage from 'store' 2 | 3 | const site = { 4 | state: { 5 | id: 0, 6 | domain: '' 7 | }, 8 | 9 | mutations: { 10 | SET_ID: (state, id) => { 11 | state.id = id 12 | }, 13 | SET_DOMAIN: (state, domain) => { 14 | state.domain = domain 15 | } 16 | }, 17 | 18 | actions: {} 19 | } 20 | 21 | export default site 22 | -------------------------------------------------------------------------------- /ui/src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const ACCESS_TOKEN = 'Access-Token' 2 | 3 | export const SIDEBAR_TYPE = 'sidebar_type' 4 | export const TOGGLE_MOBILE_TYPE = 'is_mobile' 5 | export const TOGGLE_NAV_THEME = 'nav_theme' 6 | export const TOGGLE_LAYOUT = 'layout' 7 | export const TOGGLE_FIXED_HEADER = 'fixed_header' 8 | export const TOGGLE_FIXED_SIDEBAR = 'fixed_sidebar' 9 | export const TOGGLE_CONTENT_WIDTH = 'content_width' 10 | export const TOGGLE_HIDE_HEADER = 'auto_hide_header' 11 | export const TOGGLE_COLOR = 'color' 12 | export const TOGGLE_WEAK = 'weak' 13 | export const TOGGLE_MULTI_TAB = 'multi_tab' 14 | export const APP_LANGUAGE = 'app_language' 15 | 16 | export const CONTENT_WIDTH_TYPE = { 17 | Fluid: 'Fluid', 18 | Fixed: 'Fixed' 19 | } 20 | 21 | export const NAV_THEME = { 22 | LIGHT: 'light', 23 | DARK: 'dark' 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/utils/axios.js: -------------------------------------------------------------------------------- 1 | const VueAxios = { 2 | vm: {}, 3 | // eslint-disable-next-line no-unused-vars 4 | install (Vue, instance) { 5 | if (this.installed) { 6 | return 7 | } 8 | this.installed = true 9 | 10 | if (!instance) { 11 | // eslint-disable-next-line no-console 12 | console.error('You have to install axios') 13 | return 14 | } 15 | 16 | Vue.axios = instance 17 | 18 | Object.defineProperties(Vue.prototype, { 19 | axios: { 20 | get: function get () { 21 | return instance 22 | } 23 | }, 24 | $http: { 25 | get: function get () { 26 | return instance 27 | } 28 | } 29 | }) 30 | } 31 | } 32 | 33 | export { 34 | VueAxios 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/utils/domUtil.js: -------------------------------------------------------------------------------- 1 | import config from '@/config/defaultSettings' 2 | 3 | export const setDocumentTitle = function (title) { 4 | document.title = title 5 | const ua = navigator.userAgent 6 | // eslint-disable-next-line 7 | const regex = /\bMicroMessenger\/([\d\.]+)/ 8 | if (regex.test(ua) && /ip(hone|od|ad)/i.test(ua)) { 9 | const i = document.createElement('iframe') 10 | i.src = '/favicon.ico' 11 | i.style.display = 'none' 12 | i.onload = function () { 13 | setTimeout(function () { 14 | i.remove() 15 | }, 9) 16 | } 17 | document.body.appendChild(i) 18 | } 19 | } 20 | 21 | export const domTitle = config.title 22 | -------------------------------------------------------------------------------- /ui/src/utils/filter.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import moment from 'moment' 3 | import 'moment/locale/zh-cn' 4 | moment.locale('zh-cn') 5 | 6 | Vue.filter('NumberFormat', function (value) { 7 | if (!value) { 8 | return '0' 9 | } 10 | const intPartFormat = value.toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,') // 将整数部分逢三一断 11 | return intPartFormat 12 | }) 13 | 14 | Vue.filter('dayjs', function (dataStr, pattern = 'YYYY-MM-DD HH:mm:ss') { 15 | return moment(dataStr).format(pattern) 16 | }) 17 | 18 | Vue.filter('moment', function (dataStr, pattern = 'YYYY-MM-DD HH:mm:ss') { 19 | // return moment(dataStr).format(pattern) 20 | return moment(dataStr * 1000).format(pattern) 21 | }) 22 | -------------------------------------------------------------------------------- /ui/src/utils/routeConvert.js: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash.clonedeep' 2 | 3 | export function convertRoutes (nodes) { 4 | if (!nodes) return null 5 | 6 | nodes = cloneDeep(nodes) 7 | 8 | let queue = Array.isArray(nodes) ? nodes.concat() : [nodes] 9 | 10 | while (queue.length) { 11 | const levelSize = queue.length 12 | 13 | for (let i = 0; i < levelSize; i++) { 14 | const node = queue.shift() 15 | 16 | if (!node.children || !node.children.length) continue 17 | 18 | node.children.forEach(child => { 19 | // 转化相对路径 20 | if (child.path[0] !== '/' && !child.path.startsWith('http')) { 21 | child.path = node.path.replace(/(\w*)[/]*$/, `$1/${child.path}`) 22 | } 23 | }) 24 | 25 | queue = queue.concat(node.children) 26 | } 27 | } 28 | 29 | return nodes 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/utils/screenLog.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export const printANSI = () => { 3 | // console.clear() 4 | console.log('[antd pro] created()') 5 | // ASCII - ANSI Shadow 6 | let text = ` 7 | █████╗ ███╗ ██╗████████╗██████╗ ██████╗ ██████╗ ██████╗ 8 | ██╔══██╗████╗ ██║╚══██╔══╝██╔══██╗ ██╔══██╗██╔══██╗██╔═══██╗ 9 | ███████║██╔██╗ ██║ ██║ ██║ ██║ ██████╔╝██████╔╝██║ ██║ 10 | ██╔══██║██║╚██╗██║ ██║ ██║ ██║ ██╔═══╝ ██╔══██╗██║ ██║ 11 | ██║ ██║██║ ╚████║ ██║ ██████╔╝ ██║ ██║ ██║╚██████╔╝ 12 | ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ 13 | \t\t\t\t\tPublished ${APP_VERSION}-${GIT_HASH} @ antdv.com 14 | \t\t\t\t\tBuild date: ${BUILD_DATE}` 15 | console.log(`%c${text}`, 'color: #fc4d50') 16 | console.log('%c感谢使用 antd pro!', 'color: #000; font-size: 14px; font-family: Hiragino Sans GB,Microsoft YaHei,\\\\5FAE\\8F6F\\96C5\\9ED1,Droid Sans Fallback,Source Sans,Wenquanyi Micro Hei,WenQuanYi Micro Hei Mono,WenQuanYi Zen Hei,Apple LiGothic Medium,SimHei,ST Heiti,WenQuanYi Zen Hei Sharp,sans-serif;') 17 | console.log('%cThanks for using antd pro!', 'color: #fff; font-size: 14px; font-weight: 300; text-shadow:#000 1px 0 0,#000 0 1px 0,#000 -1px 0 0,#000 0 -1px 0;') 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } -------------------------------------------------------------------------------- /ui/src/views/404.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /ui/src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /ui/src/views/exception/403.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /ui/src/views/exception/404.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /ui/src/views/exception/500.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /ui/src/views/sampleLog/export.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /ui/src/views/user/profile/components/index.js: -------------------------------------------------------------------------------- 1 | import profile from './profile.vue' 2 | import loginLog from './loginLog.vue' 3 | 4 | export { profile, loginLog } -------------------------------------------------------------------------------- /ui/src/views/user/profile/components/profile.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 52 | -------------------------------------------------------------------------------- /ui/src/views/user/profile/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /ui/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /ui/webstorm.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const webpackConfig = require('@vue/cli-service/webpack.config.js') 3 | module.exports = webpackConfig 4 | --------------------------------------------------------------------------------