├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yaml │ ├── config.yml │ ├── feature-request.yaml │ └── other.yaml └── workflows │ └── slmcp-docker.yml ├── .gitignore ├── .gitmodules ├── LICENSE.md ├── README.md ├── README_CN.md ├── compose.yaml ├── images ├── auth-1.gif ├── auth-2.gif ├── banner.png ├── blocked-for-access-too-fast.png ├── blocked-for-attack-detected.png ├── blocked.png ├── captcha-1.gif ├── captcha-2.gif ├── dynamic-html-1.png ├── dynamic-html-2.png ├── dynamic-js-1.png ├── dynamic-js-2.png ├── how-it-works.png ├── logo.svg ├── screenshot-1.png ├── screenshot-2.png ├── screenshot-3.png ├── screenshot-4.png ├── skeleton.png └── wechat.png ├── management ├── .gitignore ├── .golangci.yml ├── Makefile ├── README.md ├── scripts │ └── genproto.sh ├── tcontrollerd │ ├── README.md │ ├── config.yml │ ├── controller │ │ ├── controller.go │ │ ├── template.go │ │ └── website.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── model │ │ └── website.go │ ├── pkg │ │ ├── config │ │ │ ├── config.go │ │ │ ├── global.go │ │ │ └── log.go │ │ ├── constants │ │ │ ├── constants.go │ │ │ └── forbidden_page.go │ │ ├── cron │ │ │ ├── cron.go │ │ │ └── forbidden_page.go │ │ ├── log │ │ │ └── log.go │ │ └── ngcmd │ │ │ └── ngcmd.go │ ├── proto │ │ └── website │ │ │ └── website.proto │ └── utils │ │ ├── file.go │ │ └── random.go └── webserver │ ├── .gitignore │ ├── README.md │ ├── api │ ├── auth.go │ ├── behaviour.go │ ├── cert.go │ ├── common.go │ ├── dashboard.go │ ├── detectlog.go │ ├── endpoints.go │ ├── policygroup.go │ ├── policyrule.go │ ├── response │ │ ├── error.go │ │ ├── jsonbody.go │ │ └── png.go │ └── website.go │ ├── cmd │ ├── fake_logs.go │ ├── gen_certs.go │ ├── push_fsl.go │ ├── reset_user.go │ └── show_fsl.go │ ├── config.yml │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── middleware │ └── auth.go │ ├── model │ ├── base.go │ ├── behaviour.go │ ├── db_patch_1_4_0.go │ ├── detectlog.go │ ├── init.go │ ├── option.go │ ├── policygroup.go │ ├── policyrule.go │ ├── statistics.go │ ├── user.go │ └── website.go │ ├── pkg │ ├── config │ │ ├── config.go │ │ ├── db.go │ │ ├── detector.go │ │ ├── global.go │ │ ├── grpc.go │ │ ├── log.go │ │ ├── server.go │ │ └── telemetry.go │ ├── constants │ │ ├── constants.go │ │ ├── detectlog.go │ │ ├── detector.go │ │ ├── option.go │ │ ├── session.go │ │ └── telemetry.go │ ├── cron │ │ ├── cron.go │ │ └── update_policy.go │ ├── database │ │ └── postgres.go │ ├── fvm │ │ ├── fsl │ │ │ ├── action.go │ │ │ ├── quote.go │ │ │ ├── selector.go │ │ │ ├── state.go │ │ │ ├── table.go │ │ │ └── target.go │ │ ├── fvm.go │ │ ├── generator.go │ │ └── helper.go │ ├── log │ │ └── log.go │ └── telemetry.go │ ├── proto │ └── website │ │ └── website.proto │ ├── rpc │ ├── main.go │ └── website.go │ ├── tools │ └── init_db.sh │ └── utils │ ├── cert.go │ ├── file.go │ ├── healthy.go │ ├── httpclient.go │ ├── random.go │ └── url.go ├── mcp_server ├── Dockerfile ├── README.md ├── config.yaml ├── docker-compose.yml ├── go.mod ├── go.sum ├── internal │ ├── api │ │ ├── analyze │ │ │ └── get_event_list.go │ │ ├── app │ │ │ └── create_application.go │ │ ├── client.go │ │ ├── response.go │ │ ├── rule │ │ │ └── create_rule.go │ │ ├── service.go │ │ └── types.go │ ├── config │ │ └── config.go │ └── tools │ │ ├── analyze │ │ └── get_atttack_events.go │ │ ├── app │ │ └── create_application.go │ │ ├── example.go │ │ ├── init.go │ │ ├── rule │ │ ├── create_blacklist_rule.go │ │ └── create_whitelist_rule.go │ │ └── tool.go ├── main.go └── pkg │ ├── config │ └── config.go │ ├── errors │ └── errors.go │ ├── logger │ ├── field.go │ └── logger.go │ └── mcp │ ├── mcp.go │ ├── schema.go │ └── schema_test.go ├── scripts └── manage.py ├── sdk ├── ingress-nginx │ ├── README.md │ ├── ingress-nginx-safeline-1.0.2-1.rockspec │ ├── ingress-nginx-safeline-1.0.3-1.rockspec │ ├── ingress-nginx-safeline-1.0.4-1.rockspec │ └── lib │ │ └── safeline │ │ └── main.lua ├── kong │ ├── Readme.md │ ├── kong-safeline-1.0.0-1.rockspec │ ├── kong-safeline-1.0.1-1.rockspec │ ├── kong-safeline-1.0.2-1.rockspec │ ├── kong-safeline-1.0.3-1.rockspec │ ├── kong-safeline-1.0.4-1.rockspec │ ├── kong-safeline-1.0.5-1.rockspec │ ├── kong-safeline-1.0.6-1.rockspec │ ├── kong-safeline-1.0.7-1.rockspec │ └── kong │ │ └── plugins │ │ └── safeline │ │ ├── handler.lua │ │ └── schema.lua └── lua-resty-t1k │ ├── .github │ └── workflows │ │ ├── release.yml │ │ └── test.yml │ ├── .gitignore │ ├── .luacheckrc │ ├── LICENSE │ ├── README.md │ ├── ci │ ├── .dockerignore │ ├── Dockerfile │ └── bytecode │ ├── lib │ └── resty │ │ ├── t1k.lua │ │ └── t1k │ │ ├── buffer.lua │ │ ├── constants.lua │ │ ├── file.lua │ │ ├── filter.lua │ │ ├── handler.lua │ │ ├── log.lua │ │ ├── request.lua │ │ ├── utils.lua │ │ └── uuid.lua │ ├── mainspec │ └── lua-resty-t1k-main-0-0.rockspec │ ├── rockspec │ ├── lua-resty-t1k-1.0.0-0.rockspec │ ├── lua-resty-t1k-1.0.1-0.rockspec │ ├── lua-resty-t1k-1.0.2-0.rockspec │ ├── lua-resty-t1k-1.0.3-0.rockspec │ ├── lua-resty-t1k-1.1.0-0.rockspec │ ├── lua-resty-t1k-1.1.1-0.rockspec │ ├── lua-resty-t1k-1.1.2-0.rockspec │ ├── lua-resty-t1k-1.1.3-0.rockspec │ ├── lua-resty-t1k-1.1.4-0.rockspec │ └── lua-resty-t1k-1.1.5-0.rockspec │ └── t │ ├── buffer.t │ ├── file.t │ ├── filter.t │ ├── handler.t │ ├── integration.t │ ├── log.t │ ├── option.t │ ├── request.t │ ├── utils.t │ └── uuid.t └── yanshi ├── .gitignore ├── Makefile ├── README.md ├── contrib ├── vim │ ├── compiler │ │ └── yanshi.vim │ ├── ftdetect │ │ └── yanshi.vim │ ├── ftplugin │ │ └── yanshi.vim │ ├── syntax │ │ └── yanshi.vim │ └── syntax_checkers │ │ └── yanshi │ │ └── yanshi.vim └── zsh │ └── _yanshi ├── src ├── common.cc ├── common.hh ├── compiler.cc ├── compiler.hh ├── fsa.cc ├── fsa.hh ├── fsa_anno.cc ├── fsa_anno.hh ├── lexer.l ├── lexer_helper.cc ├── lexer_helper.hh ├── loader.cc ├── loader.hh ├── location.cc ├── location.hh ├── main.cc ├── option.cc ├── option.hh ├── parser.y ├── repl.cc ├── repl.hh ├── syntax.cc └── syntax.hh └── unittest ├── determinize_test.cc ├── difference_test.cc ├── intersection_test.cc ├── minimize_test.cc ├── union_test.cc └── unittest_helper.hh /.github/ISSUE_TEMPLATE/bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug 2 | description: Report a bug 3 | # Create a report to help us improve 4 | title: "[Bug] " 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Please search the [open issues](https://github.com/chaitin/SafeLine/issues) and [discussion](https://github.com/chaitin/SafeLine/discussions) for duplicate issue first. 11 | The more information you share, the faster we can identify and fix the bug. 12 | # Please check for duplicate issue first. 13 | - type: textarea 14 | id: Description 15 | attributes: 16 | label: What happened? 17 | # Describe the bug 18 | validations: 19 | required: false 20 | - type: textarea 21 | id: Reproduce 22 | attributes: 23 | label: How we reproduce? 24 | description: | 25 | Reports cannot be reproduced will Most likely be closed. 26 | # To Reproduce 27 | value: | 28 | 1. ... 29 | 2. ... 30 | 3. ... 31 | - type: textarea 32 | id: Expected 33 | attributes: 34 | label: Expected behavior 35 | # placeholder: | 36 | # Descript what you expected to happen. 37 | # Expected behavior. Descript what you expected to happen. 38 | - type: textarea 39 | id: Errorlog 40 | attributes: 41 | label: Error log 42 | placeholder: | 43 | Paste the error logs if any. 44 | # Expected behavior. Descript what you expected to happen. 45 | 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discord 4 | url: https://discord.gg/wyshSVuvxC 5 | about: Ask questions and discuss with other SafeLine users in real time. 6 | - name: Home page 官网 7 | url: https://waf.chaitin.com/ 8 | about: Get feature descriptions, technical documentation and more information. 获取功能描述、技术文档和更多信息。 9 | - name: 绕过反馈 10 | url: https://stack.chaitin.com/security-challenge/safeline/index 11 | about: Waf 绕过可在 CT Stack 安全挑战赛提交细节 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: Suggestion 2 | # Feature request 3 | description: New feature or improvements. 4 | title: "[Suggestion] " 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Please search the [open issues](https://github.com/chaitin/SafeLine/issues) and [discussion](https://github.com/chaitin/SafeLine/discussions) for duplicate issue first. 11 | Please rise only one suggestion in an issue. 12 | - type: textarea 13 | id: solution 14 | attributes: 15 | label: What would you like to be added or improved? 16 | # Describe the solution you'd like 17 | # placeholder: | 18 | # 19 | - type: textarea 20 | id: problem 21 | attributes: 22 | label: Why is it needed? 23 | # Background and the specific problem that frustrates you 24 | placeholder: | 25 | Background and the problem that frustrates you 26 | 27 | validations: 28 | required: true 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.yaml: -------------------------------------------------------------------------------- 1 | name: Other 2 | description: Other issues such as Doc 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Please search the [open issues](https://github.com/chaitin/SafeLine/issues) and [discussion](https://github.com/chaitin/SafeLine/discussions) for duplicate issue first. 8 | Please rise only one suggestion in an issue. 9 | - type: textarea 10 | id: content 11 | attributes: 12 | label: Content -------------------------------------------------------------------------------- /.github/workflows/slmcp-docker.yml: -------------------------------------------------------------------------------- 1 | name: MCP Docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*" 9 | paths: 10 | - "mcp_server/**" 11 | - ".github/workflows/slmcp-docker.yml" 12 | 13 | jobs: 14 | build-and-push: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v3 22 | with: 23 | platforms: linux/amd64,linux/arm64 24 | 25 | - name: Login to DockerHub 26 | uses: docker/login-action@v3 27 | with: 28 | username: ${{ secrets.DOCKERIO_USERNAME }} 29 | password: ${{ secrets.DOCKERIO_PASSWORD }} 30 | 31 | - name: Build and push 32 | uses: docker/build-push-action@v5 33 | with: 34 | context: ./mcp_server 35 | push: true 36 | platforms: linux/amd64,linux/arm64 37 | tags: | 38 | chaitin/safeline-mcp:latest 39 | chaitin/safeline-mcp:${{ github.ref_name }} 40 | cache-from: type=registry,ref=chaitin/safeline-mcp:buildcache 41 | cache-to: type=registry,ref=chaitin/safeline-mcp:buildcache,mode=max 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.Zone.Identifier 2 | .DS_Store 3 | *.zip 4 | *.tar 5 | *.tar.gz 6 | build.sh 7 | compose.yml 8 | __pycache__ 9 | .cursor 10 | .vscode -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "blazehttp"] 2 | path = blazehttp 3 | url = https://github.com/chaitin/blazehttp 4 | 5 | 6 | [submodule "sdk/traefik-safeline"] 7 | path = sdk/traefik-safeline 8 | url = https://github.com/chaitin/traefik-safeline 9 | -------------------------------------------------------------------------------- /images/auth-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/auth-1.gif -------------------------------------------------------------------------------- /images/auth-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/auth-2.gif -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/banner.png -------------------------------------------------------------------------------- /images/blocked-for-access-too-fast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/blocked-for-access-too-fast.png -------------------------------------------------------------------------------- /images/blocked-for-attack-detected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/blocked-for-attack-detected.png -------------------------------------------------------------------------------- /images/blocked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/blocked.png -------------------------------------------------------------------------------- /images/captcha-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/captcha-1.gif -------------------------------------------------------------------------------- /images/captcha-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/captcha-2.gif -------------------------------------------------------------------------------- /images/dynamic-html-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/dynamic-html-1.png -------------------------------------------------------------------------------- /images/dynamic-html-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/dynamic-html-2.png -------------------------------------------------------------------------------- /images/dynamic-js-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/dynamic-js-1.png -------------------------------------------------------------------------------- /images/dynamic-js-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/dynamic-js-2.png -------------------------------------------------------------------------------- /images/how-it-works.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/how-it-works.png -------------------------------------------------------------------------------- /images/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/screenshot-1.png -------------------------------------------------------------------------------- /images/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/screenshot-2.png -------------------------------------------------------------------------------- /images/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/screenshot-3.png -------------------------------------------------------------------------------- /images/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/screenshot-4.png -------------------------------------------------------------------------------- /images/skeleton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/skeleton.png -------------------------------------------------------------------------------- /images/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/images/wechat.png -------------------------------------------------------------------------------- /management/.gitignore: -------------------------------------------------------------------------------- 1 | # Test binary, build with `go test -c` 2 | *.test 3 | 4 | # Output of the go coverage tool, specifically when used with LiteIDE 5 | *.out 6 | 7 | **/.idea 8 | *.iml 9 | 10 | #OSX system files. 11 | **/.DS_Store 12 | 13 | /build 14 | 15 | *.pb.go 16 | 17 | submodule/ -------------------------------------------------------------------------------- /management/.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | - deadcode 5 | - errcheck 6 | - gofmt 7 | - goimports 8 | - gosimple 9 | - govet 10 | - ineffassign 11 | - staticcheck 12 | - structcheck 13 | - typecheck 14 | - unused 15 | - varcheck 16 | -------------------------------------------------------------------------------- /management/Makefile: -------------------------------------------------------------------------------- 1 | GO = GO111MODULE=on go 2 | #GO = GO111MODULE=on GOOS=linux GOARCH=amd64 go 3 | GOBUILD = $(GO) build -mod=readonly 4 | GOTEST = $(GO) test -v -p 1 -coverprofile=coverage-management.out 5 | 6 | STAMP = $(shell date +%s) 7 | GITHASH = $(shell git rev-parse --short=8 HEAD) 8 | GITTAG = $(shell git describe --tags --abbrev=0) 9 | 10 | BUILDFLAGS := -ldflags "-X main.buildstamp=$(STAMP) -X main.githash=$(GITHASH) -X main.version=$(GITTAG)" 11 | pkgs = ./... 12 | 13 | all: build-all 14 | 15 | .PHONY: build-all 16 | build-all: proto build-webserver build-tcd 17 | 18 | .PHONY: build-webserver 19 | build-webserver: 20 | cd webserver && $(GOBUILD) $(BUILDFLAGS) -o ../build/webserver main.go 21 | 22 | .PHONY: build-tcd 23 | build-tcd: 24 | cd tcontrollerd && CGO_ENABLED=0 $(GOBUILD) $(BUILDFLAGS) -o ../build/tcontrollerd main.go 25 | 26 | .PHONY: test 27 | test: 28 | $(GOTEST) -failfast $(pkgs) 29 | 30 | .PHONY: proto 31 | proto: 32 | @./scripts/genproto.sh 33 | 34 | .PHONY: lint 35 | lint: 36 | # 'go list' needs to be executed before staticcheck to prepopulate the modules cache. 37 | # Otherwise staticcheck might fail randomly for some reason not yet explained. 38 | $(GO) list -e -compiled -test=true -export=false -deps=true -find=false -tags= -- ./... > /dev/null 39 | goimports -local chaitin.cn -w $$(find . -type f -name '*.go' -not -path "./vendor/*") 40 | golangci-lint version 41 | cd webserver && golangci-lint run -v --skip-dirs vendor --deadline 10m 42 | 43 | .PHONY: clean 44 | clean: 45 | rm -rf build 46 | 47 | -------------------------------------------------------------------------------- /management/README.md: -------------------------------------------------------------------------------- 1 | # Management Micro Service 2 | 3 | ## Requirements 4 | 5 | Go 1.18+ 6 | -------------------------------------------------------------------------------- /management/scripts/genproto.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Generate all protobuf bindings. 4 | # Run from repository root. 5 | 6 | set -u 7 | 8 | if ! [[ "$0" =~ scripts/genproto.sh ]]; then 9 | echo "must be run from repository root" 10 | exit 255 11 | fi 12 | 13 | DIRS="webserver/proto tcontrollerd/proto" 14 | 15 | echo "generating code" 16 | protoc --version 17 | for dir in ${DIRS}; do 18 | pushd "${dir}" || return 19 | find . -type d -print0 | while IFS= read -r -d '' sdir ; do 20 | pushd "${sdir}" || return 21 | # shellcheck disable=SC2010 22 | FS=$(ls | grep "\.proto\$") 23 | if [ -n "${FS}" ] ; then 24 | protoc --go_out=. --go_opt=paths=source_relative \ 25 | --go-grpc_out=. --go-grpc_opt=paths=source_relative \ 26 | "${FS}" 27 | 28 | goimports -local chaitin.cn -w ./*.pb.go 29 | fi 30 | popd || return 31 | done 32 | popd || return 33 | done 34 | -------------------------------------------------------------------------------- /management/tcontrollerd/README.md: -------------------------------------------------------------------------------- 1 | # TControllerD 2 | 3 | Tengine Controller Daemon (abbr. TCD) will be running in the Tengine container, 4 | designed to be in place with minion on the host machine. 5 | 6 | ## Requirements 7 | 8 | Go 1.18+ 9 | 10 | ## Development 11 | 12 | ### init protobuf 13 | 14 | ```shell 15 | # Refer: https://grpc.io/docs/languages/go/quickstart/ 16 | # 1. Install protoc 17 | # https://grpc.io/docs/protoc-installation/ 18 | 19 | # 2. Install Go plugins 20 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30.0 21 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 22 | 23 | # 3. Update your PATH so that the protoc compiler can find the plugins 24 | # export PATH="$PATH:$(go env GOPATH)/bin" 25 | 26 | # 4. Generate proto go code 27 | # cd /path/to/management 28 | ./scripts/genproto.sh 29 | ``` -------------------------------------------------------------------------------- /management/tcontrollerd/config.yml: -------------------------------------------------------------------------------- 1 | # develop use only. For production, refer to `package/build/tengine/tcontrollerd/config.yml` 2 | log: 3 | output: stdout # "stdout", "stderr" or file path 4 | level: debug # "debug", "info", "warn" or "error" 5 | mgt_addr: 169.254.0.4:9002 # gRPC addr of mgt-api webserver -------------------------------------------------------------------------------- /management/tcontrollerd/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "google.golang.org/grpc" 5 | "google.golang.org/grpc/credentials/insecure" 6 | 7 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/config" 8 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log" 9 | pb "chaitin.cn/patronus/safeline-2/management/tcontrollerd/proto/website" 10 | ) 11 | 12 | var ( 13 | logger = log.GetLogger("controller") 14 | ) 15 | 16 | func Handle() error { 17 | logger.Infof("Connect mgt-webserver at %s", config.GlobalConfig.MgtWebserver) 18 | gRPCConn, err := grpc.Dial(config.GlobalConfig.MgtWebserver, []grpc.DialOption{ 19 | grpc.WithTransportCredentials(insecure.NewCredentials()), 20 | }...) 21 | if err != nil { 22 | logger.Errorf("Fail to dial: %v", err) 23 | return err 24 | } 25 | 26 | wsClient := pb.NewWebsiteClient(gRPCConn) 27 | 28 | defer func(conn *grpc.ClientConn) { 29 | err := conn.Close() 30 | if err != nil { 31 | logger.Errorf("Fail to close: %v", err) 32 | return 33 | } 34 | }(gRPCConn) 35 | 36 | if err = websiteHandler(wsClient); err != nil { 37 | return err 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /management/tcontrollerd/controller/template.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | var nginxConfigTpl = ` 4 | upstream %s { 5 | %s 6 | keepalive 128; 7 | keepalive_timeout 75; 8 | } 9 | server { 10 | %s 11 | %s 12 | %s 13 | %s 14 | location = /forbidden_page { 15 | internal; 16 | root /etc/nginx/forbidden_pages; 17 | try_files /default_forbidden_page.html =403; 18 | } 19 | location ^~ / { 20 | proxy_pass %s://%s; 21 | include proxy_params; 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | proxy_set_header Accept-Encoding ""; 24 | t1k_append_header SL-CE-SUID %d; 25 | t1k_body_size 1024k; 26 | tx_body_size 4k; 27 | t1k_error_page 403 /forbidden_page; 28 | tx_error_page 403 /forbidden_page; 29 | } 30 | }` 31 | 32 | var upstreamAddrTpl = `server %s;` 33 | var serverListenTpl = `listen 0.0.0.0:%s%s%s;` 34 | var addrAnyPropertiesTpl = " default_server backlog=65536 reuseport" 35 | var serverNameTpl = `server_name %s;` 36 | var certTpl = `ssl_certificate /etc/nginx/certs/%s;` 37 | var certKeyTpl = `ssl_certificate_key /etc/nginx/certs/%s;` 38 | var backendTpl = `backend_%d` 39 | -------------------------------------------------------------------------------- /management/tcontrollerd/go.mod: -------------------------------------------------------------------------------- 1 | module chaitin.cn/patronus/safeline-2/management/tcontrollerd 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.3 6 | 7 | require ( 8 | chaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6 9 | chaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c 10 | chaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c 11 | github.com/robfig/cron/v3 v3.0.1 12 | github.com/sirupsen/logrus v1.9.3 13 | google.golang.org/grpc v1.65.0 14 | ) 15 | 16 | require ( 17 | golang.org/x/net v0.33.0 // indirect 18 | golang.org/x/sys v0.20.0 // indirect 19 | golang.org/x/text v0.15.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /management/tcontrollerd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "strconv" 10 | "syscall" 11 | "time" 12 | 13 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/controller" 14 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/cron" 15 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/ngcmd" 16 | 17 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/config" 18 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/constants" 19 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log" 20 | ) 21 | 22 | var ( 23 | logger = log.GetLogger("main") 24 | 25 | version = "undefined" 26 | githash = "undefined" 27 | buildstamp = "undefined" 28 | goVersion = "undefined" 29 | ) 30 | 31 | func init() { 32 | // do something that do not raise error 33 | } 34 | 35 | func handleLoop(ctx context.Context) { 36 | for { 37 | select { 38 | case <-ctx.Done(): 39 | os.Exit(0) 40 | default: 41 | if err := controller.Handle(); err != nil { 42 | logger.Error("Error occurred when handling controller: ", err) 43 | } 44 | time.Sleep(time.Second * 5) 45 | } 46 | } 47 | } 48 | 49 | func main() { 50 | log.SetLogFormatter() 51 | 52 | fs := flag.NewFlagSet("tcontrollerd", flag.ExitOnError) 53 | showVersion := fs.Bool("v", false, "show version") 54 | nginxTest := fs.Bool("t", false, "nginx -t") 55 | nginxReload := fs.Bool("r", false, "nginx -s reload") 56 | cfgFile := fs.String("c", constants.ConfigFilePath, "config file path") 57 | if err := fs.Parse(os.Args[1:]); err != nil { 58 | logger.Fatalln("Failed to parse args: ", err) 59 | } 60 | 61 | if *showVersion { 62 | i, _ := strconv.Atoi(buildstamp) 63 | t := time.Unix(int64(i), 0).Format("2006-01-02 15:04:05") 64 | fmt.Println("Version: ", version) 65 | fmt.Println("Githash: ", githash) 66 | fmt.Println("Build: ", t) 67 | fmt.Println("Go version: ", goVersion) 68 | return 69 | } 70 | 71 | // init configs 72 | if err := config.InitConfigs(*cfgFile); err != nil { 73 | logger.Fatalln("Failed to init configs: ", err) 74 | } 75 | 76 | if err := log.InitLogger(); err != nil { 77 | logger.Fatalln("Failed to init db: ", err) 78 | } 79 | 80 | if *nginxTest { 81 | if err := ngcmd.NginxConfTest(); err != nil { 82 | logger.Fatalln("Failed to test nginx conf: ", err) 83 | } 84 | return 85 | } 86 | 87 | if *nginxReload { 88 | if err := ngcmd.NginxConfReload(); err != nil { 89 | logger.Fatalln("Failed to reload nginx conf: ", err) 90 | } 91 | return 92 | } 93 | 94 | if err := cron.StartCron(); err != nil { 95 | logger.Fatalln("Failed to start cron: ", err) 96 | } 97 | 98 | ctx, cancel := context.WithCancel(context.Background()) 99 | sig := make(chan os.Signal, 1) 100 | signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) 101 | go func() { 102 | <-sig 103 | cancel() 104 | }() 105 | 106 | handleLoop(ctx) 107 | } 108 | -------------------------------------------------------------------------------- /management/tcontrollerd/model/website.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // WebsiteConfig is supposed to be same with webserver/model/website.go 4 | type WebsiteConfig struct { 5 | Id int `json:"id"` 6 | ServerNames []string `json:"server_names"` 7 | Ports []string `json:"ports"` 8 | Upstreams []string `json:"upstreams"` 9 | CertFilename string `json:"cert_filename"` 10 | KeyFilename string `json:"key_filename"` 11 | } 12 | -------------------------------------------------------------------------------- /management/tcontrollerd/pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "chaitin.cn/dev/go/settings" 7 | ) 8 | 9 | var ( 10 | GlobalConfig = DefaultGlobalConfig() 11 | ) 12 | 13 | func InitConfigs(configFilePath string) error { 14 | s, err := settings.New(configFilePath) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | if err = GlobalConfig.Log.Load(s); err != nil { 20 | return err 21 | } 22 | 23 | if err := s.Unmarshal("mgt_addr", &GlobalConfig.MgtWebserver); err != nil { 24 | return err 25 | } 26 | 27 | if v, ok := os.LookupEnv("MGT_ADDR"); ok { 28 | GlobalConfig.MgtWebserver = v 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /management/tcontrollerd/pkg/config/global.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Log LogConfig 5 | MgtWebserver string 6 | } 7 | 8 | func DefaultGlobalConfig() Config { 9 | return Config{ 10 | Log: DefaultLogConfig(), 11 | MgtWebserver: "169.254.0.4:9002", 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /management/tcontrollerd/pkg/config/log.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "chaitin.cn/dev/go/settings" 5 | ) 6 | 7 | type LogConfig struct { 8 | Output string `yaml:"output"` 9 | Level string `yaml:"level"` 10 | } 11 | 12 | func DefaultLogConfig() LogConfig { 13 | return LogConfig{ 14 | Output: "stdout", 15 | Level: "info", 16 | } 17 | } 18 | 19 | func (lc *LogConfig) Load(setting *settings.Setting) error { 20 | if err := setting.Unmarshal("log", lc); err != nil { 21 | return err 22 | } 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /management/tcontrollerd/pkg/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | ConfigFilePath = "config.yml" 5 | ) 6 | -------------------------------------------------------------------------------- /management/tcontrollerd/pkg/constants/forbidden_page.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | DefaultForbiddenPageMd5 = "d9921f84f36a6cc92a6fc13946a18e98" 5 | DefaultForbiddenPage = `` 6 | ) 7 | -------------------------------------------------------------------------------- /management/tcontrollerd/pkg/cron/cron.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/robfig/cron/v3" 5 | 6 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log" 7 | ) 8 | 9 | var logger = log.GetLogger("cron") 10 | 11 | func newCronWithSeconds() *cron.Cron { 12 | secondParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | 13 | cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor) 14 | return cron.New(cron.WithParser(secondParser), cron.WithChain()) 15 | } 16 | 17 | func StartCron() error { 18 | cronInstance := newCronWithSeconds() 19 | 20 | _, err := cronInstance.AddFunc(specCheckForbiddenPage, checkAndUpdateForbiddenPage) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | cronInstance.Start() 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /management/tcontrollerd/pkg/cron/forbidden_page.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "io/ioutil" 7 | 8 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/constants" 9 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/utils" 10 | ) 11 | 12 | const ( 13 | // SpecUpdatePolicy http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-06.html 14 | // Seconds Minutes Hours Day-of-Month Month Day-of-Week Year (optional field) 15 | // every 30 second (starting from 0s) 16 | specCheckForbiddenPage = "0/30 * * * * ?" 17 | 18 | forbiddenPagePath = "/etc/nginx/forbidden_pages/default_forbidden_page.html" 19 | ) 20 | 21 | func checkAndUpdateForbiddenPage() { 22 | existed, err := utils.FileExist(forbiddenPagePath) 23 | if err != nil { 24 | logger.Error(err) 25 | return 26 | } 27 | if !existed { 28 | err = utils.EnsureWriteFile(forbiddenPagePath, []byte(constants.DefaultForbiddenPage), 0644) 29 | if err != nil { 30 | logger.Error(err) 31 | } 32 | return 33 | } 34 | 35 | content, err := ioutil.ReadFile(forbiddenPagePath) 36 | if err != nil { 37 | logger.Error(err) 38 | return 39 | } 40 | 41 | hash := md5.New() 42 | hash.Write([]byte(content)) 43 | forbiddenMd5 := hex.EncodeToString(hash.Sum(nil)) 44 | if forbiddenMd5 == constants.DefaultForbiddenPageMd5 { 45 | return 46 | } 47 | 48 | err = ioutil.WriteFile(forbiddenPagePath, []byte(constants.DefaultForbiddenPage), 0644) 49 | if err != nil { 50 | logger.Error(err) 51 | return 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /management/tcontrollerd/pkg/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/sirupsen/logrus" 10 | 11 | "chaitin.cn/dev/go/log" 12 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/config" 13 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/utils" 14 | ) 15 | 16 | func GetLogger(name string) *log.Logger { 17 | return log.GetLogger(name) 18 | } 19 | 20 | func LoadLogLevel() { 21 | lv, _ := log.ParseLevel(config.GlobalConfig.Log.Level) 22 | log.SetLevel(log.AllLoggers, lv) 23 | } 24 | 25 | func SetLogFormatter() { 26 | // format 27 | formatter := new(log.TextFormatter) 28 | formatter.FullTimestamp = true 29 | formatter.TimestampFormat = "2006/01/02 15:04:05" 30 | log.SetFormatter(log.AllLoggers, formatter) 31 | } 32 | 33 | func InitLogger() error { 34 | // output 35 | switch config.GlobalConfig.Log.Output { 36 | case "stdout": 37 | log.SetOutput(log.AllLoggers, os.Stdout) 38 | case "stderr": 39 | log.SetOutput(log.AllLoggers, os.Stderr) 40 | default: 41 | exist, err := utils.FileExist(config.GlobalConfig.Log.Output) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | fileFlag := os.O_WRONLY | os.O_APPEND | os.O_SYNC 47 | if !exist { 48 | if err := utils.EnsureFileDir(config.GlobalConfig.Log.Output); err != nil { 49 | return err 50 | } 51 | fileFlag = fileFlag | os.O_CREATE 52 | } 53 | 54 | if fp, err := os.OpenFile(config.GlobalConfig.Log.Output, fileFlag, os.ModePerm); err != nil { 55 | return fmt.Errorf("failed to open log file: %s", err.Error()) 56 | } else { 57 | log.SetOutput(log.AllLoggers, log.NewLockOutput(fp)) 58 | } 59 | } 60 | 61 | // hook 62 | log.AddHook(log.AllLoggers, NewRuntimeHook()) 63 | log.AddHook(log.AllLoggers, log.NewErrorStackHook(true)) 64 | // level 65 | LoadLogLevel() 66 | 67 | return nil 68 | } 69 | 70 | type RuntimeHook struct{} 71 | 72 | func (h *RuntimeHook) Levels() []logrus.Level { 73 | return logrus.AllLevels 74 | } 75 | 76 | func (h *RuntimeHook) Fire(entry *logrus.Entry) error { 77 | file := "???" 78 | funcName := "???" 79 | line := 0 80 | 81 | pc := make([]uintptr, 64) 82 | // Skip runtime.Callers, self, and another call from logrus 83 | n := runtime.Callers(3, pc) 84 | if n != 0 { 85 | pc = pc[:n] // pass only valid pcs to runtime.CallersFrames 86 | frames := runtime.CallersFrames(pc) 87 | 88 | // Loop to get frames. 89 | // A fixed number of pcs can expand to an indefinite number of Frames. 90 | for { 91 | frame, more := frames.Next() 92 | if !strings.Contains(frame.File, "github.com/sirupsen/logrus") && !strings.Contains(frame.Function, "chaitin.cn/dev/go") { 93 | file = frame.File 94 | funcName = frame.Function 95 | line = frame.Line 96 | break 97 | } 98 | if !more { 99 | break 100 | } 101 | } 102 | } 103 | 104 | slices := strings.Split(file, "/") 105 | file = slices[len(slices)-1] 106 | 107 | funcName = strings.ReplaceAll(funcName, "chaitin.cn", "") 108 | 109 | entry.Data["file"] = file 110 | entry.Data["func"] = funcName 111 | entry.Data["line"] = line 112 | return nil 113 | } 114 | 115 | func NewRuntimeHook() *RuntimeHook { 116 | return &RuntimeHook{} 117 | } 118 | -------------------------------------------------------------------------------- /management/tcontrollerd/pkg/ngcmd/ngcmd.go: -------------------------------------------------------------------------------- 1 | package ngcmd 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | 8 | "chaitin.cn/dev/go/errors" 9 | "chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log" 10 | ) 11 | 12 | var logger = log.GetLogger("ngcmd") 13 | 14 | // NginxConfTest exec nginx -t and return stderr 15 | func NginxConfTest() error { 16 | out, err := exec.Command("nginx", "-t").CombinedOutput() 17 | logger.Debugf("nginx -t output: %v", string(out)) 18 | if err != nil { 19 | return errors.Wrap(err, string(out)) 20 | } 21 | 22 | //logger.Debugf("nginx -t output: %v", out) 23 | if strings.Contains(string(out), "syntax is ok") && strings.Contains(string(out), "test is successful") { 24 | return nil 25 | } else { 26 | return errors.New(fmt.Sprintf("nginx conf test error: %s", string(out))) 27 | } 28 | } 29 | 30 | // NginxConfReload exec nginx -t and return stderr 31 | func NginxConfReload() error { 32 | out, err := exec.Command("nginx", "-s", "reload").CombinedOutput() 33 | logger.Debugf("nginx -s reload output: %v", string(out)) 34 | if err != nil { 35 | return errors.Wrap(err, string(out)) 36 | } 37 | 38 | if len(out) == 0 { 39 | return nil 40 | } else { 41 | return errors.New(fmt.Sprintf("nginx conf reload error: %s", string(out))) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /management/tcontrollerd/proto/website/website.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package website; 3 | option go_package = "proto/website"; 4 | 5 | service Website { 6 | rpc Subscribe(stream Response) returns (stream Event) {} 7 | } 8 | 9 | // From client-side, may be "pong" 10 | message Response { 11 | string type = 1; 12 | bytes msg = 2; 13 | bool err = 3; 14 | } 15 | 16 | // From server-side, may be "ping" 17 | message Event { 18 | string type = 1; // ping/website 19 | bytes msg = 2; 20 | } 21 | -------------------------------------------------------------------------------- /management/tcontrollerd/utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func EnsureDir(dir string) error { 12 | if _, err := os.Stat(dir); os.IsNotExist(err) { 13 | return os.MkdirAll(dir, os.FileMode(0755)) 14 | } 15 | return nil 16 | } 17 | 18 | func EnsureFileDir(path string) error { 19 | return EnsureDir(filepath.Dir(path)) 20 | } 21 | 22 | func FileExist(path string) (bool, error) { 23 | stat, err := os.Stat(path) 24 | if err != nil { 25 | if os.IsNotExist(err) { 26 | return false, nil 27 | } else { 28 | return false, err 29 | } 30 | } else { 31 | if stat.IsDir() { 32 | return false, fmt.Errorf("%s is dir", path) 33 | } else { 34 | return true, nil 35 | } 36 | } 37 | } 38 | 39 | func FilesExist(paths ...string) (bool, error) { 40 | for _, path := range paths { 41 | exist, err := FileExist(path) 42 | if err != nil { 43 | return false, err 44 | } 45 | if !exist { 46 | return false, nil 47 | } 48 | } 49 | return true, nil 50 | } 51 | 52 | func RenameWriteFile(filename string, data []byte, perm os.FileMode) error { 53 | randFileName := filename + ".tmp." + RandStr(8) 54 | if err := ioutil.WriteFile(randFileName, data, perm); err != nil { 55 | return err 56 | } 57 | return os.Rename(randFileName, filename) 58 | } 59 | 60 | func EnsureRenameWriteFile(path string, data []byte, mode os.FileMode) error { 61 | err := EnsureFileDir(path) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return RenameWriteFile(path, data, mode) 67 | } 68 | 69 | func EnsureWriteFile(path string, data []byte, mode os.FileMode) error { 70 | err := EnsureFileDir(path) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | return ioutil.WriteFile(path, data, mode) 76 | } 77 | 78 | func CopyFile(srcPath, dstPath string) error { 79 | srcFile, err := os.Open(srcPath) 80 | if err != nil { 81 | return err 82 | } 83 | defer func(srcFile *os.File) { 84 | err := srcFile.Close() 85 | if err != nil { 86 | 87 | } 88 | }(srcFile) 89 | 90 | fileInfo, err := srcFile.Stat() 91 | if err != nil { 92 | return err 93 | } 94 | 95 | return CopyFileFromIO(srcFile, dstPath, fileInfo.Mode()) 96 | } 97 | 98 | func CopyFileFromIO(src io.Reader, dstPath string, perm os.FileMode) error { 99 | if err := EnsureFileDir(dstPath); err != nil { 100 | return err 101 | } 102 | 103 | dstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm) 104 | if err != nil { 105 | return err 106 | } 107 | defer func(dstFile *os.File) { 108 | err := dstFile.Close() 109 | if err != nil { 110 | 111 | } 112 | }(dstFile) 113 | 114 | _, err = io.Copy(dstFile, src) 115 | return err 116 | } 117 | 118 | func CopyFileIfNotExist(srcPath, dstPath string) error { 119 | if exist, err := FileExist(dstPath); err != nil { 120 | return err 121 | } else if !exist { 122 | return CopyFile(srcPath, dstPath) 123 | } else { 124 | return nil 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /management/tcontrollerd/utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 9 | 10 | func RandStr(n int) string { 11 | rand.Seed(time.Now().UnixNano()) 12 | 13 | b := make([]rune, n) 14 | for i := range b { 15 | b[i] = letters[rand.Intn(len(letters))] 16 | } 17 | return string(b) 18 | } 19 | -------------------------------------------------------------------------------- /management/webserver/.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | Taskfile.yml 3 | .air.toml 4 | -------------------------------------------------------------------------------- /management/webserver/README.md: -------------------------------------------------------------------------------- 1 | # Web Server 2 | 3 | web server for mgt-api 4 | 5 | ## Requirements 6 | 7 | Go 1.18+ 8 | 9 | ## Development 10 | 11 | ### Init protobuf 12 | 13 | ```shell 14 | # Refer: https://grpc.io/docs/languages/go/quickstart/ 15 | # 1. Install protoc 16 | # https://grpc.io/docs/protoc-installation/ 17 | 18 | # 2. Install Go plugins 19 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30.0 20 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 21 | 22 | # 3. Update your PATH so that the protoc compiler can find the plugins 23 | # export PATH="$PATH:$(go env GOPATH)/bin" 24 | 25 | # 4. Generate proto go code 26 | # cd /path/to/management 27 | ./scripts/genproto.sh 28 | ``` 29 | 30 | ### Init fvm libs 31 | 32 | ```shell 33 | # Due to the fvm c header files 34 | mkdir -p management/webserver/submodule/fvm/ 35 | mkdir -p management/webserver/submodule/libct/ 36 | 37 | cd management/webserver/submodule/fvm/ 38 | # Download https://chaitin.cn/patronus/fvm/-/tags 1.8.21 release:release, https://chaitin.cn/patronus/fvm/-/jobs/6716645 39 | unzip artifacts.zip 40 | rm artifacts.zip 41 | 42 | cd management/webserver/submodule/libct/ 43 | # Download https://chaitin.cn/patronus/libct/-/tags 1.1.1.0 release, https://chaitin.cn/patronus/libct/-/jobs/7229201 44 | # rename 45 | rm artifacts.zip 46 | 47 | cd management/webserver/submodule/ 48 | # Download https://chaitin.cn/patronus/fusion-2/-/tags 5.3.9-r1 build:release, https://chaitin.cn/patronus/fusion-2/-/jobs/7326007 49 | # rename 50 | unzip artifacts.zip 51 | mv artifacts/lib/libfusion.so libfvm.so 52 | rm artifacts.zip 53 | rm -r artifacts/ 54 | ``` 55 | 56 | ### Build 57 | 58 | ```shell 59 | cd management/ 60 | docker run -it --rm -w="/mnt" --mount type=bind,source="$(pwd)",target=/mnt chaitin.cn/ci/golang:1.18 bash 61 | cp webserver/submodule/libfvm.so /usr/lib/ 62 | make build-webserver 63 | ``` -------------------------------------------------------------------------------- /management/webserver/api/behaviour.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "chaitin.cn/patronus/safeline-2/management/webserver/api/response" 9 | "chaitin.cn/patronus/safeline-2/management/webserver/model" 10 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/database" 11 | ) 12 | 13 | type PostBehaviourRequest struct { 14 | model.Behaviour 15 | } 16 | 17 | func PostBehaviour(c *gin.Context) { 18 | var params PostBehaviourRequest 19 | if err := c.BindJSON(¶ms); err != nil { 20 | logger.Error(err) 21 | response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError) 22 | return 23 | } 24 | db := database.GetDB() 25 | db.Create(&model.Behaviour{SrcRouter: params.SrcRouter, DstRouter: params.DstRouter}) 26 | response.Success(c, nil) 27 | } 28 | -------------------------------------------------------------------------------- /management/webserver/api/common.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/rogpeppe/go-internal/semver" 12 | 13 | "chaitin.cn/patronus/safeline-2/management/webserver/api/response" 14 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/config" 15 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants" 16 | "chaitin.cn/patronus/safeline-2/management/webserver/utils" 17 | ) 18 | 19 | const VersionInfoEntrypoint = "/release/latest/version.json" 20 | 21 | type idsRequest struct { 22 | IDs []uint `json:"ids" form:"ids"` 23 | } 24 | 25 | type pageRequest struct { 26 | Page int `json:"page" form:"page,default=1" binding:"min=1"` 27 | PageSize int `json:"page_size" form:"page_size,default=10" binding:"min=1"` 28 | } 29 | 30 | type versionInfoResponse struct { 31 | LatestVersion string `json:"latest_version"` 32 | RecVersion string `json:"rec_version"` 33 | } 34 | 35 | func GetVersion(c *gin.Context) { 36 | response.Success(c, gin.H{"version": strings.TrimPrefix(constants.Version, "ce-")}) 37 | } 38 | 39 | func GetUpgradeTips(ctx *gin.Context) { 40 | client := utils.GetHTTPClient() 41 | logger.Debugf("GetUpgradeTips: %s", config.GlobalConfig.PlatformAddr+VersionInfoEntrypoint) 42 | versionInfoReq, err := http.NewRequest(http.MethodGet, config.GlobalConfig.PlatformAddr+VersionInfoEntrypoint, nil) 43 | if err != nil { 44 | logger.Warn(err) 45 | response.Success(ctx, gin.H{"upgrade_tips": constants.NotUpgrade}) 46 | return 47 | } 48 | 49 | versionInfoRsp, err := client.Do(versionInfoReq) 50 | if err != nil { 51 | logger.Warn(err) 52 | response.Success(ctx, gin.H{"upgrade_tips": constants.NotUpgrade}) 53 | return 54 | } 55 | body, err := ioutil.ReadAll(versionInfoRsp.Body) 56 | if err != nil { 57 | logger.Warn(err) 58 | response.Success(ctx, gin.H{"upgrade_tips": constants.NotUpgrade}) 59 | return 60 | } 61 | 62 | versionInfo := &versionInfoResponse{} 63 | err = json.Unmarshal(body, versionInfo) 64 | if err != nil { 65 | logger.Warnf("err: %v, body: %s", err, body) 66 | response.Success(ctx, gin.H{"upgrade_tips": constants.NotUpgrade}) 67 | return 68 | } 69 | 70 | currentVersion := fmt.Sprintf("v%s", constants.Version) 71 | latestVersionCmp := semver.Compare(currentVersion, versionInfo.LatestVersion) 72 | recVersionCmp := semver.Compare(currentVersion, versionInfo.RecVersion) 73 | if semver.Compare(versionInfo.LatestVersion, versionInfo.RecVersion) == -1 || latestVersionCmp == 1 { 74 | logger.Warnf("The version number is invalid, current version: %s, latest version: %s, rec version: %s", 75 | currentVersion, versionInfo.LatestVersion, versionInfo.RecVersion) 76 | response.Success(ctx, gin.H{"upgrade_tips": constants.NotUpgrade}) 77 | return 78 | } 79 | 80 | var upgradeTips int 81 | if recVersionCmp == -1 { 82 | upgradeTips = constants.MustUpgrade 83 | } else if recVersionCmp == 0 { 84 | if latestVersionCmp == 0 { 85 | upgradeTips = constants.NotUpgrade 86 | } else { 87 | upgradeTips = constants.RecommendedUpgrade 88 | } 89 | } else { 90 | if latestVersionCmp < 0 { 91 | upgradeTips = constants.RecommendedUpgrade 92 | } else { 93 | upgradeTips = constants.NotUpgrade 94 | } 95 | } 96 | 97 | response.Success(ctx, gin.H{"upgrade_tips": upgradeTips}) 98 | } 99 | -------------------------------------------------------------------------------- /management/webserver/api/endpoints.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | const ( 4 | Version = "/Version" 5 | UpgradeTips = "/UpgradeTips" 6 | Login = "/Login" 7 | Logout = "/Logout" 8 | OTPUrl = "/OTPUrl" 9 | User = "/User" 10 | DetectLogList = "/DetectLogList" 11 | DetectLogDetail = "/DetectLogDetail" 12 | Behaviour = "/Behaviour" 13 | FalsePositives = "/FalsePositives" 14 | Website = "/Website" 15 | UploadSSLCert = "/UploadSSLCert" 16 | SSLCert = "/SSLCert" 17 | PolicyRule = "/PolicyRule" 18 | SwitchPolicyRule = "/SwitchPolicyRule" 19 | DashboardCounts = "/dashboard/counts" 20 | DashboardSites = "/dashboard/sites" 21 | DashboardQps = "/dashboard/qps" 22 | DashboardRequests = "/dashboard/requests" 23 | DashboardIntercepts = "/dashboard/intercepts" 24 | PolicyGroupGlobal = "/PolicyGroupGlobal" 25 | SrcIPConfig = "/SrcIPConfig" 26 | ) 27 | -------------------------------------------------------------------------------- /management/webserver/api/response/error.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | const ( 4 | ErrLoginRequired = "login-required" 5 | ErrWrongPasscode = "wrong-passcode" 6 | ErrWrongTimeGap = "wrong-time-gap" 7 | ErrInternalError = "internal-error" 8 | ErrDataNotExist = "data-not-exist" 9 | ErrWrongFileType = "wrong-filetype" 10 | ErrReadOnly = "read-only" 11 | ) 12 | -------------------------------------------------------------------------------- /management/webserver/api/response/jsonbody.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type JSONBody struct { 10 | Err string 11 | Msg string 12 | Data interface{} 13 | } 14 | 15 | var ( 16 | ErrorLoginRequired = JSONBody{ErrLoginRequired, "Login required", nil} 17 | ErrorParamNotOK = JSONBody{ErrInternalError, "Error occurred when extracting params", nil} 18 | ErrorDataNotExist = JSONBody{ErrDataNotExist, "Data queried does not exist", nil} 19 | ErrorReadOnly = JSONBody{ErrReadOnly, "This environment is read only", nil} 20 | ) 21 | 22 | func Success(c *gin.Context, data interface{}) { 23 | c.JSON(http.StatusOK, gin.H{ 24 | "data": data, 25 | "msg": "", 26 | "err": nil, 27 | }) 28 | //c.Abort() 29 | } 30 | 31 | func SuccessWithList(c *gin.Context, data interface{}) { 32 | c.JSON(http.StatusOK, gin.H{ 33 | "data": data, 34 | "msg": "", 35 | "err": nil, 36 | }) 37 | //c.Abort() 38 | } 39 | 40 | func SuccessWithMsg(c *gin.Context, data interface{}, msg string) { 41 | c.JSON(http.StatusOK, gin.H{ 42 | "data": data, 43 | "msg": msg, 44 | "err": nil, 45 | }) 46 | //c.Abort() 47 | } 48 | 49 | func Error(c *gin.Context, rsp JSONBody, status int) { 50 | c.JSON(status, gin.H{ 51 | "data": rsp.Data, 52 | "msg": rsp.Msg, 53 | "err": rsp.Err, 54 | }) 55 | //c.Abort() 56 | } 57 | -------------------------------------------------------------------------------- /management/webserver/api/response/png.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | const StreamContentType = "application/octet-stream" 10 | 11 | func PNG(c *gin.Context, bytes []byte) { 12 | c.Data(http.StatusOK, StreamContentType, bytes) 13 | } 14 | -------------------------------------------------------------------------------- /management/webserver/cmd/fake_logs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "chaitin.cn/patronus/safeline-2/management/webserver/model" 4 | 5 | func FakeLogs() { 6 | model.InitDetectLogSamples() 7 | } 8 | -------------------------------------------------------------------------------- /management/webserver/cmd/gen_certs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/x509/pkix" 5 | "path/filepath" 6 | 7 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/config" 8 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants" 9 | "chaitin.cn/patronus/safeline-2/management/webserver/utils" 10 | ) 11 | 12 | func GenCerts() error { 13 | if err := genServerCert(); err != nil { 14 | return err 15 | } 16 | if err := genClientCACert(); err != nil { 17 | return err 18 | } 19 | return nil 20 | } 21 | 22 | func genServerCert() error { 23 | certPath := filepath.Join(config.GlobalConfig.MgtResDir, constants.CertsPath, "server.crt") 24 | keyPath := filepath.Join(config.GlobalConfig.MgtResDir, constants.CertsPath, "server.key") 25 | if err := utils.WriteCertIfNotExist( 26 | certPath, 27 | keyPath, 28 | func() ([]byte, []byte, error) { 29 | return utils.GenerateCert( 30 | []string{}, 31 | 3650, 32 | 4096, 33 | &pkix.Name{ 34 | Country: []string{"CN"}, 35 | Province: []string{"Beijing"}, 36 | Locality: []string{"Beijing"}, 37 | Organization: []string{"Beijing WAF Technology Co., Ltd."}, 38 | OrganizationalUnit: []string{"Service Infrastructure Department"}, 39 | CommonName: "WAF Management Server", 40 | }, 41 | false, 42 | ) 43 | }); err != nil { 44 | return err 45 | } 46 | return nil 47 | } 48 | 49 | func genClientCACert() error { 50 | certPath := filepath.Join(config.GlobalConfig.MgtResDir, constants.CertsPath, "client_ca.crt") 51 | keyPath := filepath.Join(config.GlobalConfig.MgtResDir, constants.CertsPath, "client_ca.key") 52 | if err := utils.WriteCertIfNotExist( 53 | certPath, 54 | keyPath, 55 | func() ([]byte, []byte, error) { 56 | return utils.GenerateCert( 57 | []string{}, 58 | 3650, 59 | 4096, 60 | &pkix.Name{ 61 | Country: []string{"CN"}, 62 | Province: []string{"Beijing"}, 63 | Locality: []string{"Beijing"}, 64 | Organization: []string{"Beijing WAF Technology Co., Ltd."}, 65 | OrganizationalUnit: []string{"Service Infrastructure Department"}, 66 | CommonName: "WAF Client Certificate Authority", 67 | }, 68 | true, 69 | ) 70 | }); err != nil { 71 | return err 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /management/webserver/cmd/push_fsl.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/database" 5 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm" 6 | ) 7 | 8 | func PushFSL() error { 9 | return fvm.PushFSL(database.GetDB().DB) 10 | } 11 | -------------------------------------------------------------------------------- /management/webserver/cmd/reset_user.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "chaitin.cn/patronus/safeline-2/management/webserver/model" 5 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/database" 6 | ) 7 | 8 | func ResetUser(username string) { 9 | db := database.GetDB() 10 | var user model.User 11 | db.Where(&model.User{Username: username}).First(&user) 12 | user.LastLoginTime = 0 13 | db.Save(&user) 14 | } 15 | -------------------------------------------------------------------------------- /management/webserver/cmd/show_fsl.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/database" 5 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm" 6 | ) 7 | 8 | func ShowFSL() (string, error) { 9 | return fvm.GenerateFullFSL(database.GetDB().DB) 10 | } 11 | -------------------------------------------------------------------------------- /management/webserver/config.yml: -------------------------------------------------------------------------------- 1 | # develop use only. For production, refer to `package/build/mgt-api/webserver/config.yml` 2 | log: 3 | output: stdout # "stdout", "stderr" or file path 4 | level: debug # "debug", "info", "warn" or "error" 5 | server: 6 | listen_addr: :9001 7 | dev_mode: true 8 | db: 9 | url: postgres://safeline-ce:safeline-ce@127.0.0.1/safeline-ce 10 | log_sql: false 11 | detector: 12 | addr: "" 13 | fsl_bytecode: fvm/bytecode 14 | grpc_server: 15 | listen_addr: :9002 16 | 17 | -------------------------------------------------------------------------------- /management/webserver/go.mod: -------------------------------------------------------------------------------- 1 | module chaitin.cn/patronus/safeline-2/management/webserver 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.3 6 | 7 | require ( 8 | chaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6 9 | chaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c 10 | chaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c 11 | github.com/gin-contrib/sessions v0.0.5 12 | github.com/gin-gonic/gin v1.10.0 13 | github.com/pquerna/otp v1.4.0 14 | github.com/robfig/cron/v3 v3.0.1 15 | github.com/rogpeppe/go-internal v1.10.0 16 | github.com/sirupsen/logrus v1.4.2 17 | google.golang.org/grpc v1.65.0 18 | gorm.io/datatypes v1.1.1 19 | gorm.io/driver/postgres v1.5.9 20 | gorm.io/gorm v1.25.10 21 | ) 22 | 23 | require ( 24 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect 25 | github.com/bytedance/sonic v1.11.6 // indirect 26 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 27 | github.com/gin-contrib/sse v0.1.0 // indirect 28 | github.com/go-playground/locales v0.14.1 // indirect 29 | github.com/go-playground/universal-translator v0.18.1 // indirect 30 | github.com/go-playground/validator/v10 v10.20.0 // indirect 31 | github.com/go-sql-driver/mysql v1.7.0 // indirect 32 | github.com/goccy/go-json v0.10.2 // indirect 33 | github.com/golang/protobuf v1.5.4 // indirect 34 | github.com/gorilla/context v1.1.1 // indirect 35 | github.com/gorilla/securecookie v1.1.1 // indirect 36 | github.com/gorilla/sessions v1.2.1 // indirect 37 | github.com/jackc/pgpassfile v1.0.0 // indirect 38 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 39 | github.com/jackc/pgx/v5 v5.5.5 // indirect 40 | github.com/jinzhu/inflection v1.0.0 // indirect 41 | github.com/jinzhu/now v1.1.5 // indirect 42 | github.com/json-iterator/go v1.1.12 // indirect 43 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 44 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 45 | github.com/leodido/go-urn v1.4.0 // indirect 46 | github.com/mattn/go-isatty v0.0.20 // indirect 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 48 | github.com/modern-go/reflect2 v1.0.2 // indirect 49 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 50 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 51 | github.com/ugorji/go/codec v1.2.12 // indirect 52 | golang.org/x/arch v0.8.0 // indirect 53 | golang.org/x/sys v0.20.0 // indirect 54 | golang.org/x/text v0.15.0 // indirect 55 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 56 | google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | gorm.io/driver/mysql v1.4.7 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /management/webserver/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-contrib/sessions" 7 | "github.com/gin-gonic/gin" 8 | 9 | "chaitin.cn/patronus/safeline-2/management/webserver/api/response" 10 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants" 11 | ) 12 | 13 | func AuthRequired(c *gin.Context) { 14 | session := sessions.Default(c) 15 | 16 | user := session.Get(constants.DefaultSessionUserKey) 17 | if user == nil { 18 | response.Error(c, response.ErrorLoginRequired, http.StatusUnauthorized) 19 | c.Abort() 20 | return 21 | } 22 | 23 | // extend session expired time 24 | session.Options(sessions.Options{ 25 | Path: "/", 26 | MaxAge: 3600 * 24 * 7, 27 | //Domain: options.Domain, 28 | //HttpOnly: true, 29 | //SameSite: http.SameSiteLaxMode, 30 | //Secure: false, 31 | }) 32 | 33 | if err := session.Save(); err != nil { 34 | response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Error occurred when creating sessions"}, http.StatusInternalServerError) 35 | return 36 | } 37 | 38 | c.Next() 39 | } 40 | 41 | func ReadOnly(c *gin.Context) { 42 | if c.Request.Method != "GET" && c.Request.Method != "HEAD" && c.Request.Method != "OPTIONS" { 43 | response.Error(c, response.ErrorReadOnly, http.StatusBadRequest) 44 | c.Abort() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /management/webserver/model/base.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/log" 7 | ) 8 | 9 | // Base is a replacement for gorm.Model without DeletedAt, which is considered to be not good. 10 | type Base struct { 11 | ID uint `gorm:"primarykey" json:"id"` 12 | CreatedAt time.Time ` json:"created_at"` 13 | UpdatedAt time.Time ` json:"updated_at"` 14 | } 15 | 16 | var logger = log.GetLogger("model") 17 | -------------------------------------------------------------------------------- /management/webserver/model/behaviour.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Behaviour struct { 4 | Base 5 | SrcRouter string `json:"src_router"` 6 | DstRouter string `json:"dst_router"` 7 | } 8 | -------------------------------------------------------------------------------- /management/webserver/model/db_patch_1_4_0.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | 6 | "chaitin.cn/dev/go/errors" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type sqlResult struct { 11 | Ids string `json:"ids"` 12 | } 13 | 14 | func DBPatch140(tx *gorm.DB) error { 15 | if !tx.Migrator().HasTable(&SystemStatistics{}) { 16 | return nil 17 | } 18 | 19 | var result []sqlResult 20 | //SELECT string_agg(id::text, ',') as ids FROM mgt_system_statistics GROUP BY (type, website, created_at) HAVING COUNT(*) > 1) as tmp 21 | res := tx.Model(&SystemStatistics{}).Select("string_agg(id::text, ',') as ids").Group("type, website, created_at").Having("COUNT(*) > 1").Find(&result) 22 | if res.Error != nil { 23 | return errors.Wrap(res.Error, "Failed to select data") 24 | } 25 | 26 | if len(result) <= 0 { 27 | return nil 28 | } 29 | 30 | var deleteIds []string 31 | for _, s := range result { 32 | tmpIds := strings.Split(s.Ids, ",") 33 | deleteIds = append(deleteIds, tmpIds...) 34 | } 35 | deleteRes := tx.Delete(&SystemStatistics{}, deleteIds) 36 | 37 | return errors.Wrap(deleteRes.Error, "Failed to delete") 38 | } 39 | -------------------------------------------------------------------------------- /management/webserver/model/init.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | 6 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/database" 7 | ) 8 | 9 | func InitModels() error { 10 | db := database.GetDB() 11 | err := db.Transaction(func(tx *gorm.DB) error { 12 | // 13 | if err := DBPatch140(tx); err != nil { 14 | return err 15 | } 16 | return nil 17 | }) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | if err := db.AutoMigrate(&User{}, &DetectLogBasic{}, &DetectLogDetail{}, &Behaviour{}, &Options{}, &Website{}, &PolicyRule{}, &SystemStatistics{}); err != nil { 23 | return err 24 | } 25 | 26 | if err := initAdminUser(); err != nil { 27 | return err 28 | } 29 | 30 | if err := initOptions(); err != nil { 31 | return err 32 | } 33 | 34 | if err := initPolicyGroupGlobal(); err != nil { 35 | return err 36 | } 37 | 38 | if err := initSrcIPConfig(); err != nil { 39 | return err 40 | } 41 | 42 | //InitDetectLogSamples() 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /management/webserver/model/option.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | 8 | "gorm.io/gorm/clause" 9 | 10 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/config" 11 | 12 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg" 13 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants" 14 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/database" 15 | "chaitin.cn/patronus/safeline-2/management/webserver/utils" 16 | ) 17 | 18 | type Options struct { 19 | Base 20 | Key string `gorm:"column:key;uniqueIndex"` 21 | Value string `gorm:"column:value;"` 22 | } 23 | 24 | func initOptions() error { 25 | db := database.GetDB() 26 | 27 | secretKey := Options{Key: constants.SecretKey, Value: utils.RandStr(32)} 28 | db.Clauses(clause.OnConflict{DoNothing: true}).Create(&secretKey) 29 | 30 | machineId := Options{Key: constants.MachineID, Value: utils.RandStr(32)} 31 | _ = db.Clauses(clause.OnConflict{DoNothing: true}).Create(&machineId) 32 | go NotifyInstallation(machineId.Value) 33 | 34 | return nil 35 | } 36 | 37 | func NotifyInstallation(machineId string) { 38 | logger.Info("Notify installation") 39 | tr := pkg.TelemetryRequest{ 40 | Telemetry: pkg.TelemetryInfo{ 41 | Id: constants.TelemetryId, 42 | }, 43 | Safeline: pkg.SafelineInfo{ 44 | Id: machineId, 45 | Type: constants.Installation, 46 | Version: constants.Version, 47 | }, 48 | } 49 | data, err := json.Marshal(tr) 50 | if err != nil { 51 | logger.Error(err) 52 | return 53 | } 54 | 55 | reader := bytes.NewReader(data) 56 | rsp, err := pkg.DoPostTelemetry(utils.GetHTTPClient(), config.GlobalConfig.Telemetry.Addr, reader) 57 | if err != nil { 58 | logger.Error(err) 59 | return 60 | } 61 | 62 | if rsp.StatusCode != http.StatusOK && rsp.StatusCode != http.StatusCreated { 63 | logger.Errorf("transfer telemetry %s failed, status code = %d", constants.Installation, rsp.StatusCode) 64 | return 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /management/webserver/model/policyrule.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/datatypes" 4 | 5 | type PolicyRule struct { 6 | Base 7 | Action int `gorm:"action" json:"action"` 8 | Comment string `gorm:"comment" json:"comment"` 9 | Pattern datatypes.JSON `gorm:"pattern" json:"pattern"` 10 | IsEnabled bool `gorm:"is_enabled;default=true" json:"is_enabled"` 11 | } 12 | 13 | type PolicyRulePattern struct { 14 | K string `json:"k"` 15 | Op string `json:"op"` 16 | V string `json:"v"` 17 | } 18 | 19 | const ( 20 | KeySrcIP = "src_ip" 21 | KeyURI = "uri" 22 | KeyHost = "host" 23 | 24 | OpEq = "eq" // 完全相等 25 | OpMatch = "match" // 模糊匹配 26 | OpCIDR = "cidr" // CIDR 27 | OpHas = "has" // 关键字 28 | OpPrefix = "prefix" // 前缀关键字 29 | OpRe = "re" // 正则 30 | ) 31 | -------------------------------------------------------------------------------- /management/webserver/model/statistics.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | type SystemStatistics struct { 6 | ID uint `json:"id" gorm:"primarykey"` 7 | Type string `json:"type" gorm:"index;uniqueIndex:type_website_createdat"` 8 | Value int64 `json:"value"` 9 | CreatedAt time.Time `json:"created_at" gorm:"index;uniqueIndex:type_website_createdat"` 10 | Website string `json:"website" gorm:"index;uniqueIndex:type_website_createdat"` 11 | } 12 | -------------------------------------------------------------------------------- /management/webserver/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "gorm.io/gorm/clause" 5 | 6 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants" 7 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/database" 8 | ) 9 | 10 | type User struct { 11 | Base 12 | Username string `gorm:"uniqueIndex;not null"` 13 | Password string 14 | Comment string 15 | 16 | TFAEnabled bool `gorm:"column:tfa_enabled;default:true"` 17 | TFASecret string `gorm:"column:tfa_secret"` 18 | LastLoginTime int64 `gorm:"default:0"` 19 | IsEnabled bool `gorm:"default:true"` 20 | } 21 | 22 | func initAdminUser() error { 23 | db := database.GetDB() 24 | user := User{ 25 | Username: constants.SuperUser, 26 | } 27 | db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user) 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /management/webserver/model/website.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/datatypes" 4 | 5 | type Website struct { 6 | Base 7 | Comment string `gorm:"comment" json:"comment"` 8 | ServerNames datatypes.JSON `gorm:"server_names" json:"server_names"` 9 | Ports datatypes.JSON `gorm:"ports" json:"ports"` 10 | Upstreams datatypes.JSON `gorm:"upstreams" json:"upstreams"` 11 | 12 | CertFilename string `gorm:"cert_filename" json:"cert_filename"` 13 | KeyFilename string `gorm:"key_filename" json:"key_filename"` 14 | 15 | IsEnabled bool `gorm:"is_enabled;default=true" json:"is_enabled"` 16 | } 17 | -------------------------------------------------------------------------------- /management/webserver/pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "chaitin.cn/dev/go/settings" 7 | ) 8 | 9 | var ( 10 | GlobalConfig = DefaultGlobalConfig() 11 | ) 12 | 13 | func InitConfigs(configFilePath string) error { 14 | s, err := settings.New(configFilePath) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | if err = GlobalConfig.DB.Load(s); err != nil { 20 | return err 21 | } 22 | 23 | if err = GlobalConfig.Log.Load(s); err != nil { 24 | return err 25 | } 26 | 27 | if err = GlobalConfig.Server.Load(s); err != nil { 28 | return err 29 | } 30 | 31 | if err = GlobalConfig.Detector.Load(s); err != nil { 32 | return err 33 | } 34 | 35 | if err = GlobalConfig.Telemetry.Load(s); err != nil { 36 | return err 37 | } 38 | 39 | if err = GlobalConfig.GPRC.Load(s); err != nil { 40 | return err 41 | } 42 | 43 | if err := settings.Unmarshal("platform_addr", &GlobalConfig.PlatformAddr); err != nil { 44 | return err 45 | } 46 | 47 | if v, ok := os.LookupEnv("MANAGEMENT_RESOURCES_DIR"); ok { 48 | GlobalConfig.MgtResDir = v 49 | } 50 | 51 | if v, ok := os.LookupEnv("NGINX_RESOURCES_DIR"); ok { 52 | GlobalConfig.NgxResDir = v 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /management/webserver/pkg/config/db.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | 7 | "chaitin.cn/dev/go/settings" 8 | ) 9 | 10 | type DBConfig struct { 11 | URL string `yaml:"url"` 12 | LogSQL bool `yaml:"log_sql"` 13 | SSLMode bool `yaml:"ssl_mode"` 14 | } 15 | 16 | func DefaultDBConfig() DBConfig { 17 | return DBConfig{ 18 | URL: "postgres://safeline-ce:safeline-ce@127.0.0.1/safeline-ce", 19 | LogSQL: false, 20 | SSLMode: false, 21 | } 22 | } 23 | 24 | func (dbc *DBConfig) Load(setting *settings.Setting) error { 25 | if err := setting.Unmarshal("db", dbc); err != nil { 26 | return err 27 | } 28 | 29 | if v, ok := os.LookupEnv("DATABASE_URL"); ok { 30 | dbc.URL = v 31 | } 32 | 33 | dbURL, err := url.Parse(dbc.URL) 34 | if err != nil { 35 | return err 36 | } 37 | q := dbURL.Query() 38 | if !dbc.SSLMode { 39 | q.Set("sslmode", "disable") 40 | } 41 | dbURL.RawQuery = q.Encode() 42 | dbc.URL = dbURL.String() 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /management/webserver/pkg/config/detector.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "chaitin.cn/dev/go/settings" 5 | ) 6 | 7 | type DetectorConfig struct { 8 | Addr string `yaml:"addr"` 9 | FslBytecode string `yaml:"fsl_bytecode"` 10 | } 11 | 12 | func DefaultDetectorConfig() DetectorConfig { 13 | return DetectorConfig{ 14 | Addr: "http://127.0.0.1:8001", 15 | FslBytecode: "bytecode", 16 | } 17 | } 18 | 19 | func (d *DetectorConfig) Load(setting *settings.Setting) error { 20 | if err := setting.Unmarshal("detector", d); err != nil { 21 | return err 22 | } 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /management/webserver/pkg/config/global.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Log LogConfig 5 | DB DBConfig 6 | Server ServerConfig 7 | Detector DetectorConfig 8 | Telemetry TelemetryConfig 9 | GPRC GRPCConfig 10 | PlatformAddr string 11 | MgtResDir string 12 | NgxResDir string 13 | } 14 | 15 | func DefaultGlobalConfig() Config { 16 | return Config{ 17 | Log: DefaultLogConfig(), 18 | DB: DefaultDBConfig(), 19 | Server: DefaultServerConfig(), 20 | Detector: DefaultDetectorConfig(), 21 | Telemetry: DefaultTelemetryConfig(), 22 | GPRC: DefaultGRPCConfig(), 23 | PlatformAddr: "https://waf-ce.chaitin.cn", 24 | MgtResDir: "/resources/management", 25 | NgxResDir: "/resources/nginx", 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /management/webserver/pkg/config/grpc.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "chaitin.cn/dev/go/settings" 5 | ) 6 | 7 | type GRPCConfig struct { 8 | ListenAddr string `yaml:"listen_addr"` 9 | } 10 | 11 | func DefaultGRPCConfig() GRPCConfig { 12 | return GRPCConfig{ 13 | ListenAddr: ":9002", 14 | } 15 | } 16 | 17 | func (sc *GRPCConfig) Load(setting *settings.Setting) error { 18 | if err := setting.Unmarshal("grpc_server", sc); err != nil { 19 | return err 20 | } 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /management/webserver/pkg/config/log.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "chaitin.cn/dev/go/settings" 5 | ) 6 | 7 | type LogConfig struct { 8 | Output string `yaml:"output"` 9 | Level string `yaml:"level"` 10 | } 11 | 12 | func DefaultLogConfig() LogConfig { 13 | return LogConfig{ 14 | Output: "stdout", 15 | Level: "info", 16 | } 17 | } 18 | 19 | func (lc *LogConfig) Load(setting *settings.Setting) error { 20 | if err := setting.Unmarshal("log", lc); err != nil { 21 | return err 22 | } 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /management/webserver/pkg/config/server.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "chaitin.cn/dev/go/settings" 5 | ) 6 | 7 | type ServerConfig struct { 8 | ListenAddr string `yaml:"listen_addr"` 9 | DevMode bool `yaml:"dev_mode"` 10 | IntenseMode bool `yaml:"intense_mode"` 11 | } 12 | 13 | func DefaultServerConfig() ServerConfig { 14 | return ServerConfig{ 15 | ListenAddr: ":9001", 16 | DevMode: false, 17 | } 18 | } 19 | 20 | func (sc *ServerConfig) Load(setting *settings.Setting) error { 21 | if err := setting.Unmarshal("server", sc); err != nil { 22 | return err 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /management/webserver/pkg/config/telemetry.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "chaitin.cn/dev/go/settings" 5 | ) 6 | 7 | type TelemetryConfig struct { 8 | Addr string `yaml:"addr"` 9 | } 10 | 11 | func DefaultTelemetryConfig() TelemetryConfig { 12 | return TelemetryConfig{ 13 | Addr: "rivers-telemetry:10086", 14 | } 15 | } 16 | 17 | func (t *TelemetryConfig) Load(setting *settings.Setting) error { 18 | if err := setting.Unmarshal("telemetry", t); err != nil { 19 | return err 20 | } 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /management/webserver/pkg/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | SuperUser = "admin" 5 | ProductName = "长亭雷池 WAF 社区版" 6 | ProductVersion = "" 7 | ConfigFilePath = "config.yml" 8 | CertsPath = "certs" 9 | ) 10 | 11 | const ( 12 | NotUpgrade int = iota 13 | RecommendedUpgrade 14 | MustUpgrade 15 | ) 16 | -------------------------------------------------------------------------------- /management/webserver/pkg/constants/detector.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | ContentType = "application/octet-stream" 5 | UpdateEntrypoint = "/update/policy" 6 | StatEntrypoint = "/stat" 7 | DefaultPolicyVersion = "1" 8 | ) 9 | -------------------------------------------------------------------------------- /management/webserver/pkg/constants/option.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | SecretKey = "secret_key" 5 | MachineID = "machine_id" 6 | PolicyGroupGlobal = "policy_group_global" 7 | SrcIPConfig = "src_ip_config" 8 | ) 9 | -------------------------------------------------------------------------------- /management/webserver/pkg/constants/session.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | DefaultSessionUserKey = "user" 5 | ) 6 | -------------------------------------------------------------------------------- /management/webserver/pkg/constants/telemetry.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | ApplicationJson = "application/json" 5 | TelemetryEntryPoint = "/telemetry" 6 | TelemetryId = "9e88109c-ebbf-4d82-8a25-f97f2ac5f3c4" 7 | Behaviour = "behaviour" 8 | FalsePositives = "false_positives" 9 | Installation = "Installation" 10 | ) 11 | 12 | var Version string 13 | -------------------------------------------------------------------------------- /management/webserver/pkg/cron/cron.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/robfig/cron/v3" 5 | 6 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/log" 7 | ) 8 | 9 | var logger = log.GetLogger("cron") 10 | 11 | func newCronWithSeconds() *cron.Cron { 12 | secondParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | 13 | cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor) 14 | return cron.New(cron.WithParser(secondParser), cron.WithChain()) 15 | } 16 | 17 | func StartCron() error { 18 | cronInstance := newCronWithSeconds() 19 | _, err := cronInstance.AddFunc(SpecUpdatePolicy, CheckAndUpdatePolicy) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | cronInstance.Start() 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /management/webserver/pkg/cron/update_policy.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "time" 9 | 10 | "chaitin.cn/patronus/safeline-2/management/webserver/utils" 11 | 12 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/config" 13 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants" 14 | ) 15 | 16 | // SpecUpdatePolicy http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-06.html 17 | // Seconds Minutes Hours Day-of-Month Month Day-of-Week Year (optional field) 18 | // every 5 second (starting from 0s) 19 | const SpecUpdatePolicy = "0/5 * * * * ?" 20 | 21 | type statResponseBody struct { 22 | PolicyVersion string `json:"policy_version"` 23 | } 24 | 25 | func getBytecode() ([]byte, error) { 26 | buff, err := ioutil.ReadFile(config.GlobalConfig.Detector.FslBytecode) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return buff, nil 31 | } 32 | 33 | func CheckAndUpdatePolicy() { 34 | if config.GlobalConfig.Detector.Addr == "" { 35 | return 36 | } 37 | 38 | exist, err := utils.FileExist(config.GlobalConfig.Detector.FslBytecode) 39 | if !exist || err != nil { 40 | return 41 | } 42 | 43 | data, err := getBytecode() 44 | if err != nil { 45 | logger.Error(err) 46 | return 47 | } 48 | 49 | reader := bytes.NewReader(data) 50 | 51 | tr := &http.Transport{ 52 | MaxIdleConns: 10, 53 | IdleConnTimeout: 30 * time.Second, 54 | } 55 | client := &http.Client{Transport: tr} 56 | 57 | addr := config.GlobalConfig.Detector.Addr 58 | statReq, err := http.NewRequest(http.MethodGet, addr+constants.StatEntrypoint, nil) 59 | if err != nil { 60 | logger.Error(err) 61 | return 62 | } 63 | 64 | updateReq, err := http.NewRequest(http.MethodPost, addr+constants.UpdateEntrypoint, reader) 65 | if err != nil { 66 | logger.Error(err) 67 | return 68 | } 69 | 70 | updateReq.Header.Set("Content-Type", constants.ContentType) 71 | 72 | statRspData := statResponseBody{} 73 | statRsp, err := client.Do(statReq) 74 | if err != nil || statRsp.StatusCode != http.StatusOK { 75 | logger.Warn(err) 76 | return 77 | } 78 | 79 | body, err := ioutil.ReadAll(statRsp.Body) 80 | if err != nil { 81 | logger.Warn(err) 82 | return 83 | } 84 | 85 | err = json.Unmarshal(body, &statRspData) 86 | if err != nil { 87 | logger.Warn(err) 88 | return 89 | } 90 | 91 | if statRspData.PolicyVersion == constants.DefaultPolicyVersion { 92 | return 93 | } 94 | 95 | err = statRsp.Body.Close() 96 | if err != nil { 97 | logger.Warn(err) 98 | } 99 | 100 | logger.Info("Update fsl bytecode") 101 | updateRsp, err := client.Do(updateReq) 102 | if err != nil { 103 | logger.Warn(err) 104 | return 105 | } 106 | 107 | if updateRsp.StatusCode != http.StatusOK { 108 | logger.Warnf("%s update policy, return %d", addr, updateRsp.StatusCode) 109 | } 110 | err = updateRsp.Body.Close() 111 | if err != nil { 112 | logger.Warn(err) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /management/webserver/pkg/database/postgres.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "gorm.io/driver/postgres" 5 | "gorm.io/gorm" 6 | "gorm.io/gorm/logger" 7 | "gorm.io/gorm/schema" 8 | 9 | "chaitin.cn/dev/go/errors" 10 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/config" 11 | ) 12 | 13 | type PostgresDB struct { 14 | *gorm.DB 15 | } 16 | 17 | var ( 18 | // db is not designed to be used directly, use method `GetDB` instead. 19 | db PostgresDB 20 | ) 21 | 22 | func GetDB() *PostgresDB { 23 | return &db 24 | } 25 | 26 | func InitDB() error { 27 | dbConfig := config.GlobalConfig.DB 28 | 29 | if dbConfig.URL == "" { 30 | return errors.New("empty database url") 31 | } 32 | // URL also works, see https://godoc.org/github.com/lib/pq 33 | var ( 34 | gormDB *gorm.DB 35 | err error 36 | ) 37 | 38 | gormConfig := &gorm.Config{ 39 | NamingStrategy: schema.NamingStrategy{ 40 | TablePrefix: "mgt_", // table name prefix, table for `User` would be `t_users` 41 | SingularTable: true, // use singular table name, table for `User` would be `user` with this option enabled 42 | }, 43 | AllowGlobalUpdate: false, 44 | Logger: logger.Default.LogMode(logger.Silent), 45 | } 46 | if dbConfig.LogSQL { 47 | gormConfig.Logger = logger.Default.LogMode(logger.Info) 48 | } 49 | 50 | gormDB, err = gorm.Open(postgres.Open(dbConfig.URL), gormConfig) 51 | 52 | if err != nil { 53 | return err 54 | } 55 | 56 | db.DB = gormDB 57 | 58 | return nil 59 | } 60 | 61 | func (db *PostgresDB) SetDB(gormDB *gorm.DB) { 62 | db.DB = gormDB 63 | } 64 | -------------------------------------------------------------------------------- /management/webserver/pkg/fvm/fsl/action.go: -------------------------------------------------------------------------------- 1 | package fsl 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func Goto(m string) string { 9 | return fmt.Sprintf("GOTO %s", m) 10 | } 11 | 12 | func Wheres(items ...string) string { 13 | return strings.Join(items, " AND ") 14 | } 15 | 16 | func Actions(items ...string) string { 17 | return strings.Join(items, ", ") 18 | } 19 | 20 | func Set(k, t, v string) string { 21 | return fmt.Sprintf("SET %s::%s = %s", k, t, v) 22 | } 23 | -------------------------------------------------------------------------------- /management/webserver/pkg/fvm/fsl/quote.go: -------------------------------------------------------------------------------- 1 | package fsl 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // see : https://chaitin.cn/patronus/fvm/-/blob/master/src/util/StrUtil.cpp#L18 8 | 9 | func appendEscapedByte(buf []byte, b byte) []byte { 10 | if b >= 0x20 && b <= 0x7e && b != '\'' && b != '"' && b != '\\' { 11 | return append(buf, b) 12 | } 13 | switch b { 14 | case '\'': 15 | return append(buf, `\'`...) 16 | case '"': 17 | return append(buf, `\"`...) 18 | case '\n': 19 | return append(buf, `\n`...) 20 | case '\t': 21 | return append(buf, `\t`...) 22 | case '\r': 23 | return append(buf, `\r`...) 24 | case '\b': 25 | return append(buf, `\b`...) 26 | case '\f': 27 | return append(buf, `\f`...) 28 | case '\\': 29 | return append(buf, `\\`...) 30 | default: 31 | return append(buf, fmt.Sprintf("\\x%x", b)...) 32 | } 33 | } 34 | 35 | func appendQuotedWith(buf []byte, s string, quote byte) []byte { 36 | buf = append(buf, quote) 37 | for i := 0; i < len(s); i++ { 38 | buf = appendEscapedByte(buf, s[i]) 39 | } 40 | buf = append(buf, quote) 41 | return buf 42 | } 43 | 44 | func quoteWith(s string, quote byte) string { 45 | return string(appendQuotedWith(make([]byte, 0, 3*len(s)/2), s, quote)) 46 | } 47 | 48 | func Quote(s string) string { 49 | return quoteWith(s, '\'') 50 | } 51 | -------------------------------------------------------------------------------- /management/webserver/pkg/fvm/fsl/selector.go: -------------------------------------------------------------------------------- 1 | package fsl 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Selector struct { 8 | TableName string 9 | ID string 10 | Where string 11 | Action string 12 | } 13 | 14 | func NewSelector(tableName, id, where, action string) *Selector { 15 | return &Selector{ 16 | TableName: tableName, 17 | ID: id, 18 | Where: where, 19 | Action: action, 20 | } 21 | } 22 | 23 | func (s *Selector) String() string { 24 | ret := fmt.Sprintf("ID %s", Quote(s.ID)) 25 | if s.Where != "" { 26 | ret += " WHERE " + s.Where 27 | } 28 | ret += fmt.Sprintf(" ACTION %s", s.Action) 29 | return ret 30 | } 31 | 32 | func (s *Selector) AppendInto() string { 33 | return fmt.Sprintf("APPEND INTO %s %s;", s.TableName, s.String()) 34 | } 35 | 36 | func AppendInto(tableName, id, where, action string) string { 37 | return NewSelector(tableName, id, where, action).AppendInto() 38 | } 39 | -------------------------------------------------------------------------------- /management/webserver/pkg/fvm/fsl/state.go: -------------------------------------------------------------------------------- 1 | package fsl 2 | 3 | import ( 4 | "chaitin.cn/dev/go/errors" 5 | ) 6 | 7 | type State int 8 | 9 | const ( 10 | S_NOP State = iota 11 | S_ABORT 12 | S_RETURN 13 | ) 14 | 15 | func (s State) String() string { 16 | switch s { 17 | case S_NOP: 18 | return "NOP" 19 | case S_ABORT: 20 | return "ABORT" 21 | case S_RETURN: 22 | return "RETURN" 23 | default: 24 | panic(errors.New("wrong State")) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /management/webserver/pkg/fvm/fsl/table.go: -------------------------------------------------------------------------------- 1 | package fsl 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Table struct { 9 | Name string 10 | Mappings map[State]State 11 | } 12 | 13 | func NewTable(name string, mappings map[State]State) *Table { 14 | return &Table{ 15 | Name: name, 16 | Mappings: mappings, 17 | } 18 | } 19 | 20 | func (t *Table) String() string { 21 | ret := fmt.Sprintf("TABLE %s", t.Name) 22 | if len(t.Mappings) > 0 { 23 | var mappings []string 24 | for from, to := range t.Mappings { 25 | mappings = append(mappings, fmt.Sprintf("%s TO %s", from.String(), to.String())) 26 | } 27 | ret += fmt.Sprintf(" MAPPING %s", strings.Join(mappings, ", ")) 28 | } 29 | return ret 30 | } 31 | 32 | func (t *Table) Create() string { 33 | return fmt.Sprintf("CREATE %s;", t.String()) 34 | } 35 | 36 | func CreateTable(name string, mappings map[State]State) string { 37 | return NewTable(name, mappings).Create() 38 | } 39 | -------------------------------------------------------------------------------- /management/webserver/pkg/fvm/fsl/target.go: -------------------------------------------------------------------------------- 1 | package fsl 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Target struct { 8 | ID string 9 | Type string 10 | Args string 11 | } 12 | 13 | func NewTarget(id string, typ string, args string) *Target { 14 | return &Target{ 15 | ID: id, 16 | Type: typ, 17 | Args: args, 18 | } 19 | } 20 | 21 | func NewSkynetTarget(id, configJSONStr string) *Target { 22 | return NewTarget(id, "skynet", configJSONStr) 23 | } 24 | 25 | func (t *Target) String() string { 26 | return fmt.Sprintf("%s TYPE %s ARGS (%s)", t.ID, t.Type, t.Args) 27 | } 28 | 29 | func (t *Target) Create() string { 30 | return fmt.Sprintf("CREATE TARGET %s;", t.String()) 31 | } 32 | 33 | func CreateTarget(id, configJSONStr string) string { 34 | return NewSkynetTarget(id, configJSONStr).Create() 35 | } 36 | -------------------------------------------------------------------------------- /management/webserver/pkg/fvm/helper.go: -------------------------------------------------------------------------------- 1 | package fvm 2 | 3 | import ( 4 | "chaitin.cn/dev/go/errors" 5 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/config" 6 | "chaitin.cn/patronus/safeline-2/management/webserver/utils" 7 | ) 8 | 9 | var GlobalFVM *FVM 10 | 11 | func init() { 12 | var err error 13 | GlobalFVM, err = New() 14 | if err != nil { 15 | panic(err) 16 | } 17 | } 18 | 19 | func CompileAndSave(text string) error { 20 | output, _, err := GlobalFVM.Compile(text, nil) 21 | if err != nil { 22 | return errors.Annotate(err, "failed to compile rules") 23 | } 24 | defer ReleaseOutput(output) 25 | 26 | update := Serialize(output, false, 0, 1) 27 | defer update.Release() 28 | 29 | // save to file 30 | if err = utils.EnsureWriteFile(config.GlobalConfig.Detector.FslBytecode, update.ToBytes(), 0666); err != nil { 31 | return errors.Annotate(err, "failed to save bytecode") 32 | } 33 | 34 | return nil 35 | } 36 | 37 | func CompileAndPush(text, serverAddr string) error { 38 | output, _, err := GlobalFVM.Compile(text, nil) 39 | if err != nil { 40 | return errors.Annotate(err, "failed to compile rules") 41 | } 42 | defer ReleaseOutput(output) 43 | 44 | update := Serialize(output, false, 0, 1) 45 | defer update.Release() 46 | 47 | // save to file 48 | if err = utils.EnsureWriteFile(config.GlobalConfig.Detector.FslBytecode, update.ToBytes(), 0666); err != nil { 49 | return errors.Annotate(err, "failed to save bytecode") 50 | } 51 | 52 | if err := GlobalFVM.PushFsl(serverAddr, update); err != nil { 53 | return errors.Annotatef(err, "failed to push rule to '%s'", serverAddr) 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /management/webserver/pkg/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/sirupsen/logrus" 10 | 11 | "chaitin.cn/dev/go/log" 12 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/config" 13 | "chaitin.cn/patronus/safeline-2/management/webserver/utils" 14 | ) 15 | 16 | func GetLogger(name string) *log.Logger { 17 | return log.GetLogger(name) 18 | } 19 | 20 | func LoadLogLevel() { 21 | lv, _ := log.ParseLevel(config.GlobalConfig.Log.Level) 22 | log.SetLevel(log.AllLoggers, lv) 23 | } 24 | 25 | func SetLogFormatter() { 26 | // format 27 | formatter := new(log.TextFormatter) 28 | formatter.FullTimestamp = true 29 | formatter.TimestampFormat = "2006/01/02 15:04:05" 30 | log.SetFormatter(log.AllLoggers, formatter) 31 | } 32 | 33 | func InitLogger() error { 34 | // output 35 | switch config.GlobalConfig.Log.Output { 36 | case "stdout": 37 | log.SetOutput(log.AllLoggers, os.Stdout) 38 | case "stderr": 39 | log.SetOutput(log.AllLoggers, os.Stderr) 40 | default: 41 | exist, err := utils.FileExist(config.GlobalConfig.Log.Output) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | fileFlag := os.O_WRONLY | os.O_APPEND | os.O_SYNC 47 | if !exist { 48 | if err := utils.EnsureFileDir(config.GlobalConfig.Log.Output); err != nil { 49 | return err 50 | } 51 | fileFlag = fileFlag | os.O_CREATE 52 | } 53 | 54 | if fp, err := os.OpenFile(config.GlobalConfig.Log.Output, fileFlag, os.ModePerm); err != nil { 55 | return fmt.Errorf("failed to open log file: %s", err.Error()) 56 | } else { 57 | log.SetOutput(log.AllLoggers, log.NewLockOutput(fp)) 58 | } 59 | } 60 | 61 | // hook 62 | log.AddHook(log.AllLoggers, NewRuntimeHook()) 63 | log.AddHook(log.AllLoggers, log.NewErrorStackHook(true)) 64 | // level 65 | LoadLogLevel() 66 | 67 | return nil 68 | } 69 | 70 | type RuntimeHook struct{} 71 | 72 | func (h *RuntimeHook) Levels() []logrus.Level { 73 | return logrus.AllLevels 74 | } 75 | 76 | func (h *RuntimeHook) Fire(entry *logrus.Entry) error { 77 | file := "???" 78 | funcName := "???" 79 | line := 0 80 | 81 | pc := make([]uintptr, 64) 82 | // Skip runtime.Callers, self, and another call from logrus 83 | n := runtime.Callers(3, pc) 84 | if n != 0 { 85 | pc = pc[:n] // pass only valid pcs to runtime.CallersFrames 86 | frames := runtime.CallersFrames(pc) 87 | 88 | // Loop to get frames. 89 | // A fixed number of pcs can expand to an indefinite number of Frames. 90 | for { 91 | frame, more := frames.Next() 92 | if !strings.Contains(frame.File, "github.com/sirupsen/logrus") && !strings.Contains(frame.Function, "chaitin.cn/dev/go") { 93 | file = frame.File 94 | funcName = frame.Function 95 | line = frame.Line 96 | break 97 | } 98 | if !more { 99 | break 100 | } 101 | } 102 | } 103 | 104 | slices := strings.Split(file, "/") 105 | file = slices[len(slices)-1] 106 | 107 | funcName = strings.ReplaceAll(funcName, "chaitin.cn", "") 108 | 109 | entry.Data["file"] = file 110 | entry.Data["func"] = funcName 111 | entry.Data["line"] = line 112 | return nil 113 | } 114 | 115 | func NewRuntimeHook() *RuntimeHook { 116 | return &RuntimeHook{} 117 | } 118 | -------------------------------------------------------------------------------- /management/webserver/pkg/telemetry.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "strconv" 8 | "time" 9 | 10 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants" 11 | "chaitin.cn/patronus/safeline-2/management/webserver/utils" 12 | ) 13 | 14 | type WebsiteResult struct { 15 | Id uint 16 | UpdatedAt time.Time 17 | } 18 | 19 | type TelemetryInfo struct { 20 | Id string `json:"id"` 21 | } 22 | 23 | type SafelineInfo struct { 24 | Id string `json:"id"` 25 | 26 | Type string `json:"type"` 27 | Version string `json:"version"` 28 | 29 | ReqCnt int `json:"req_cnt"` 30 | DetectLogCnt int `json:"detect_log_cnt"` 31 | SiteCnt int `json:"site_cnt"` 32 | HealthySiteCnt int `json:"healthy_site_cnt"` 33 | RuleCnt int `json:"rule_cnt"` 34 | IsHealthy bool `json:"is_healthy"` 35 | 36 | Behavior map[string]int `json:"behavior"` 37 | Websites []WebsiteResult `json:"websites"` 38 | WebsitesCnt int `json:"websites_cnt"` 39 | } 40 | 41 | type TelemetryRequest struct { 42 | Telemetry TelemetryInfo `json:"telemetry"` 43 | Safeline SafelineInfo `json:"safeline"` 44 | } 45 | 46 | func GetUploadTimestamp() string { 47 | yesterday := time.Now().AddDate(0, 0, -1) 48 | yesterdayNoon := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 12, 0, 0, 0, yesterday.Location()) 49 | return strconv.FormatInt(yesterdayNoon.Unix(), 10) 50 | } 51 | 52 | func GetUploadNonce() string { 53 | now := strconv.FormatInt(time.Now().Unix(), 10) 54 | randStr := utils.RandStr(6) 55 | return fmt.Sprintf("%s%s", now, randStr) 56 | } 57 | 58 | func DoPostTelemetry(client *http.Client, addr string, reader io.Reader) (*http.Response, error) { 59 | req, err := http.NewRequest(http.MethodPost, "https://"+addr+constants.TelemetryEntryPoint, reader) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | req.Header.Set("accept", constants.ApplicationJson) 65 | req.Header.Set("Content-Type", constants.ApplicationJson) 66 | 67 | rsp, err := client.Do(req) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return rsp, nil 73 | } 74 | -------------------------------------------------------------------------------- /management/webserver/proto/website/website.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package website; 3 | option go_package = "proto/website"; 4 | 5 | service Website { 6 | rpc Subscribe(stream Response) returns (stream Event) {} 7 | } 8 | 9 | // From client-side, may be "pong" 10 | message Response { 11 | string type = 1; 12 | bytes msg = 2; 13 | bool err = 3; 14 | } 15 | 16 | // From server-side, may be "ping" 17 | message Event { 18 | string type = 1; // ping/website 19 | bytes msg = 2; 20 | } 21 | -------------------------------------------------------------------------------- /management/webserver/rpc/main.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "google.golang.org/grpc" 8 | 9 | "chaitin.cn/dev/go/errors" 10 | "chaitin.cn/dev/go/log" 11 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/config" 12 | pb "chaitin.cn/patronus/safeline-2/management/webserver/proto/website" 13 | ) 14 | 15 | const ( 16 | WaitRspTimeout = 30 * time.Second 17 | KeepaliveTime = 5 * time.Second 18 | KeepaliveTimeout = 30 * time.Second 19 | 20 | Ping = "ping" 21 | Pong = "pong" 22 | EventTypeWebsite = "website" 23 | EventTypeDeleteWebsite = "deleteWebsite" 24 | EventTypeFullWebsite = "fullWebsite" 25 | ) 26 | 27 | var logger = log.GetLogger("grpc") 28 | 29 | func StartGRPCSever() error { 30 | lis, err := net.Listen("tcp", config.GlobalConfig.GPRC.ListenAddr) 31 | if err != nil { 32 | return errors.Wrap(err, "Failed to listen") 33 | } 34 | var opts []grpc.ServerOption 35 | grpcServer := grpc.NewServer(opts...) 36 | pb.RegisterWebsiteServer(grpcServer, GetWebsiteServer()) 37 | go func() { 38 | err := grpcServer.Serve(lis) 39 | if err != nil { 40 | logger.Fatalln("Failed to server") 41 | } 42 | }() 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /management/webserver/tools/init_db.sh: -------------------------------------------------------------------------------- 1 | docker rm -f mgt-postgres-dev 2 | 3 | #docker run -it -d -e POSTGRES_DB=safeline-ce -e POSTGRES_USER=safeline-ce -e POSTGRES_PASSWORD=safeline-ce -p 127.0.0.1:5432:5432 --name mgt-postgres-dev chaitin.cn/library/postgres:15.2 4 | docker run -it -d -e POSTGRES_DB=safeline-ce -e POSTGRES_USER=safeline-ce -e POSTGRES_PASSWORD=safeline-ce -p 127.0.0.1:5432:5432 --name mgt-postgres-dev postgres:15.2 -------------------------------------------------------------------------------- /management/webserver/utils/cert.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/pem" 10 | "io/ioutil" 11 | "math/big" 12 | "time" 13 | ) 14 | 15 | func GenerateCert(hostnames []string, days int64, keyBits int, subject *pkix.Name, isCA bool) ([]byte, []byte, error) { 16 | privateKey, err := rsa.GenerateKey(rand.Reader, keyBits) 17 | if err != nil { 18 | return nil, nil, err 19 | } 20 | 21 | notBefore := time.Now() 22 | duration := int64(time.Hour) * 24 * days 23 | notAfter := notBefore.Add(time.Duration(duration)) 24 | 25 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 26 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 27 | if err != nil { 28 | return nil, nil, err 29 | } 30 | 31 | var keyUsage x509.KeyUsage 32 | if isCA { 33 | keyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign 34 | } else { 35 | keyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment 36 | } 37 | 38 | template := x509.Certificate{ 39 | SerialNumber: serialNumber, 40 | NotBefore: notBefore, 41 | NotAfter: notAfter, 42 | DNSNames: hostnames, 43 | BasicConstraintsValid: true, 44 | IsCA: isCA, 45 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, 46 | KeyUsage: keyUsage, 47 | } 48 | if subject != nil { 49 | template.Subject = *subject 50 | } 51 | 52 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) 53 | if err != nil { 54 | return nil, nil, err 55 | } 56 | 57 | var certBuffer bytes.Buffer 58 | err = pem.Encode(&certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) 59 | if err != nil { 60 | return nil, nil, err 61 | } 62 | 63 | var keyBuffer bytes.Buffer 64 | err = pem.Encode(&keyBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}) 65 | if err != nil { 66 | return nil, nil, err 67 | } 68 | 69 | return certBuffer.Bytes(), keyBuffer.Bytes(), nil 70 | } 71 | 72 | func WriteCertIfNotExist(certFilePath, keyFilePath string, generator func() ([]byte, []byte, error)) error { 73 | cert, key, err := genCertIfNotExist(certFilePath, keyFilePath, generator) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | if err = EnsureRenameWriteFile(certFilePath, cert, 0644); err != nil { 79 | return err 80 | } 81 | 82 | if err = EnsureRenameWriteFile(keyFilePath, key, 0644); err != nil { 83 | return err 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func genCertIfNotExist(certFilePath, keyFilePath string, generator func() ([]byte, []byte, error)) ([]byte, []byte, error) { 90 | exist, err := FilesExist(certFilePath, keyFilePath) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | 95 | if !exist { 96 | return generator() 97 | } else { 98 | cert, err := ioutil.ReadFile(certFilePath) 99 | if err != nil { 100 | return nil, nil, err 101 | } 102 | 103 | key, err := ioutil.ReadFile(keyFilePath) 104 | if err != nil { 105 | return nil, nil, err 106 | } 107 | return cert, key, nil 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /management/webserver/utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func EnsureDir(dir string) error { 11 | if _, err := os.Stat(dir); os.IsNotExist(err) { 12 | return os.MkdirAll(dir, os.FileMode(0755)) 13 | } 14 | return nil 15 | } 16 | 17 | func EnsureFileDir(path string) error { 18 | return EnsureDir(filepath.Dir(path)) 19 | } 20 | 21 | func FileExist(path string) (bool, error) { 22 | stat, err := os.Stat(path) 23 | if err != nil { 24 | if os.IsNotExist(err) { 25 | return false, nil 26 | } else { 27 | return false, err 28 | } 29 | } else { 30 | if stat.IsDir() { 31 | return false, fmt.Errorf("%s is dir", path) 32 | } else { 33 | return true, nil 34 | } 35 | } 36 | } 37 | 38 | func FilesExist(paths ...string) (bool, error) { 39 | for _, path := range paths { 40 | exist, err := FileExist(path) 41 | if err != nil { 42 | return false, err 43 | } 44 | if !exist { 45 | return false, nil 46 | } 47 | } 48 | return true, nil 49 | } 50 | 51 | func RenameWriteFile(filename string, data []byte, perm os.FileMode) error { 52 | randFileName := filename + ".tmp." + RandStr(8) 53 | if err := ioutil.WriteFile(randFileName, data, perm); err != nil { 54 | return err 55 | } 56 | return os.Rename(randFileName, filename) 57 | } 58 | 59 | func EnsureRenameWriteFile(path string, data []byte, mode os.FileMode) error { 60 | err := EnsureFileDir(path) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | return RenameWriteFile(path, data, mode) 66 | } 67 | 68 | func EnsureWriteFile(path string, data []byte, mode os.FileMode) error { 69 | err := EnsureFileDir(path) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | return ioutil.WriteFile(path, data, mode) 75 | } 76 | -------------------------------------------------------------------------------- /management/webserver/utils/healthy.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | func CheckHealthy(url string) bool { 9 | method := "GET" 10 | 11 | client := &http.Client{} 12 | req, err := http.NewRequest(method, url, nil) 13 | 14 | if err != nil { 15 | fmt.Println(err) 16 | return false 17 | } 18 | res, err := client.Do(req) 19 | if err != nil { 20 | fmt.Println(err) 21 | return false 22 | } 23 | defer res.Body.Close() 24 | 25 | return res.StatusCode == 200 26 | } 27 | 28 | func CheckWafHealthy() bool { 29 | // todo: not check the healthy status 30 | return CheckHealthy("http://safeline-mario:3335") && CheckHealthy("http://safeline-detector:8001/stat") 31 | } 32 | -------------------------------------------------------------------------------- /management/webserver/utils/httpclient.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "time" 9 | ) 10 | 11 | const proxyName = "HTTPS_PROXY" 12 | 13 | var httpClient *http.Client 14 | 15 | func GetHTTPClient() *http.Client { 16 | if httpClient == nil { 17 | tr := &http.Transport{ 18 | MaxIdleConns: 10, 19 | IdleConnTimeout: 30 * time.Second, 20 | TLSClientConfig: &tls.Config{ 21 | InsecureSkipVerify: true, 22 | }, 23 | } 24 | 25 | proxyUrl, existed := os.LookupEnv(proxyName) 26 | if existed { 27 | uri, _ := url.Parse(proxyUrl) 28 | tr.Proxy = http.ProxyURL(uri) 29 | } 30 | 31 | httpClient = &http.Client{Transport: tr} 32 | } 33 | 34 | return httpClient 35 | } 36 | -------------------------------------------------------------------------------- /management/webserver/utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 9 | 10 | func RandStr(n int) string { 11 | rand.Seed(time.Now().UnixNano()) 12 | 13 | b := make([]rune, n) 14 | for i := range b { 15 | b[i] = letters[rand.Intn(len(letters))] 16 | } 17 | return string(b) 18 | } 19 | -------------------------------------------------------------------------------- /management/webserver/utils/url.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | 8 | "chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants" 9 | ) 10 | 11 | func IsIPv6(str string) bool { 12 | ip := net.ParseIP(str) 13 | return ip != nil && strings.Contains(str, ":") 14 | } 15 | 16 | func BuildUrl(protocol int, host string, port uint, path string) string { 17 | portStr := "" 18 | if len(host) > 0 && IsIPv6(host) && host[:1] != "[" { 19 | host = fmt.Sprintf("[%s]", host) 20 | } 21 | if protocol == constants.ProtocolHTTP { 22 | if port != 80 { 23 | portStr = fmt.Sprintf(":%d", port) 24 | } 25 | } else if protocol == constants.ProtocolHTTPS || protocol == constants.ProtocolHTTP2 { 26 | if port != 443 { 27 | portStr = fmt.Sprintf(":%d", port) 28 | } 29 | } else { 30 | // use HTTP as default protocol 31 | protocol = constants.ProtocolHTTP 32 | } 33 | return fmt.Sprintf("%s://%s%s%s", constants.HTTPProtocol[protocol], host, portStr, path) 34 | } 35 | -------------------------------------------------------------------------------- /mcp_server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | RUN apk add --no-cache git 6 | 7 | COPY go.mod go.sum ./ 8 | 9 | RUN go mod download 10 | 11 | COPY . . 12 | 13 | RUN CGO_ENABLED=0 go build -o mcp-server . 14 | 15 | FROM alpine:latest 16 | 17 | WORKDIR /app 18 | 19 | RUN apk add --no-cache ca-certificates tzdata 20 | 21 | COPY --from=builder /app/mcp-server . 22 | COPY --from=builder /app/config.yaml . 23 | 24 | ENV TZ=Asia/Shanghai 25 | 26 | EXPOSE 5678 27 | 28 | CMD ["./mcp-server"] -------------------------------------------------------------------------------- /mcp_server/config.yaml: -------------------------------------------------------------------------------- 1 | # Server Configuration 2 | server: 3 | name: "SafeLine MCP Server" 4 | version: "1.0.0" 5 | # Can be overridden by environment variable LISTEN_PORT 6 | port: 5678 7 | # Can be overridden by environment variable LISTEN_ADDRESS 8 | host: "0.0.0.0" 9 | # Can be overridden by environment variable SAFELINE_SECRET 10 | secret: "" # Secret for SSE server 11 | # Logger Configuration 12 | logger: 13 | level: "info" # Log level: debug, info, warn, error 14 | file_path: "" # Log file path 15 | console: true # Whether to output to console 16 | caller: false # Whether to record caller information 17 | development: true # Whether to use development mode 18 | 19 | # API Configuration 20 | api: 21 | # Can be overridden by environment variable SAFELINE_ADDRESS 22 | base_url: "" # API service address 23 | # Can be overridden by environment variable SAFELINE_API_TOKEN 24 | token: "" # Authentication token 25 | timeout: 30 # Timeout in seconds 26 | debug: false # Whether to enable debug mode 27 | insecure_skip_verify: true # Whether to skip certificate verification 28 | -------------------------------------------------------------------------------- /mcp_server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | mcp_server: 5 | image: chaitin/safeline-mcp:latest 6 | container_name: mcp_server 7 | restart: always 8 | ports: 9 | - "5678:5678" 10 | environment: 11 | - SAFELINE_SECRET=your_secret_key # optional, if you want to use secret key to authenticate 12 | - SAFELINE_ADDRESS=https://your_safeline_ip:9443 # required, your SafeLine WAF address 13 | - SAFELINE_API_TOKEN=your_safeline_api_token # required, your SafeLine WAF api token 14 | - LISTEN_PORT=5678 # optional, default is 5678 15 | - LISTEN_ADDRESS=0.0.0.0 # optional, default is 0.0.0.0 -------------------------------------------------------------------------------- /mcp_server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chaitin/SafeLine/mcp_server 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | github.com/mark3labs/mcp-go v0.18.0 7 | go.uber.org/zap v1.27.0 8 | gopkg.in/yaml.v3 v3.0.1 9 | ) 10 | 11 | require ( 12 | github.com/google/uuid v1.6.0 // indirect 13 | github.com/mcuadros/go-defaults v1.2.0 14 | github.com/yosida95/uritemplate/v3 v3.0.2 // indirect 15 | go.uber.org/multierr v1.11.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /mcp_server/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 6 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 7 | github.com/mark3labs/mcp-go v0.18.0 h1:YuhgIVjNlTG2ZOwmrkORWyPTp0dz1opPEqvsPtySXao= 8 | github.com/mark3labs/mcp-go v0.18.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= 9 | github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc= 10 | github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY= 11 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 15 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 16 | github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= 17 | github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= 18 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 19 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 20 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 21 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 22 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 23 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 26 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /mcp_server/internal/api/analyze/get_event_list.go: -------------------------------------------------------------------------------- 1 | package analyze 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/chaitin/SafeLine/mcp_server/internal/api" 8 | ) 9 | 10 | type GetEventListRequest struct { 11 | Page int `json:"page"` 12 | PageSize int `json:"page_size"` 13 | IP string `json:"ip"` 14 | Start int64 `json:"start"` 15 | End int64 `json:"end"` 16 | } 17 | 18 | type GetEventListResponse struct { 19 | Nodes []Event `json:"nodes"` 20 | Total int64 `json:"total"` 21 | } 22 | 23 | type Event struct { 24 | ID uint `json:"id"` 25 | IP string `json:"ip"` 26 | Protocol int `json:"protocol"` 27 | Host string `json:"host"` 28 | DstPort uint64 `json:"dst_port"` 29 | UpdatedAt int64 `json:"updated_at"` 30 | StartAt int64 `json:"start_at"` 31 | EndAt int64 `json:"end_at"` 32 | DenyCount int64 `json:"deny_count"` 33 | PassCount int64 `json:"pass_count"` 34 | Finished bool `json:"finished"` 35 | Country string `json:"country"` 36 | Province string `json:"province"` 37 | City string `json:"city"` 38 | } 39 | 40 | func GetEventList(ctx context.Context, req *GetEventListRequest) (*GetEventListResponse, error) { 41 | var resp api.Response[GetEventListResponse] 42 | err := api.Service().Get(ctx, fmt.Sprintf("/api/open/events?page=%d&page_size=%d&ip=%s&start=%d&end=%d", req.Page, req.PageSize, req.IP, req.Start, req.End), &resp) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return &resp.Data, nil 47 | } 48 | -------------------------------------------------------------------------------- /mcp_server/internal/api/app/create_application.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/chaitin/SafeLine/mcp_server/internal/api" 7 | "github.com/chaitin/SafeLine/mcp_server/pkg/errors" 8 | ) 9 | 10 | type CreateAppRequest struct { 11 | ServerNames []string `json:"server_names"` 12 | Ports []string `json:"ports"` 13 | Upstreams []string `json:"upstreams"` 14 | Comment string `json:"comment"` 15 | } 16 | 17 | // CreateApp Create new website or app 18 | func CreateApp(ctx context.Context, req *CreateAppRequest) (int64, error) { 19 | if req == nil { 20 | return 0, errors.New("request is required") 21 | } 22 | 23 | var resp api.Response[int64] 24 | err := api.Service().Post(ctx, "/api/open/site", req, &resp) 25 | if err != nil { 26 | return 0, errors.Wrap(err, "failed to create app") 27 | } 28 | 29 | if resp.Err != nil { 30 | return 0, errors.New(resp.Msg) 31 | } 32 | 33 | return resp.Data, nil 34 | } 35 | -------------------------------------------------------------------------------- /mcp_server/internal/api/response.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Response Common API response structure 4 | type Response[T any] struct { 5 | // Response data 6 | Data T `json:"data"` 7 | // Error message 8 | Err any `json:"err"` 9 | // Prompt message 10 | Msg string `json:"msg"` 11 | } 12 | -------------------------------------------------------------------------------- /mcp_server/internal/api/rule/create_rule.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/chaitin/SafeLine/mcp_server/internal/api" 7 | "github.com/chaitin/SafeLine/mcp_server/pkg/errors" 8 | ) 9 | 10 | type CreateRuleRequest struct { 11 | Name string `json:"name"` 12 | IP []string `json:"ip"` 13 | IsEnabled bool `json:"is_enabled"` 14 | Pattern [][]api.Pattern `json:"pattern"` 15 | Action int `json:"action"` 16 | } 17 | 18 | // CreateRule Create new rule 19 | func CreateRule(ctx context.Context, req *CreateRuleRequest) (int64, error) { 20 | if req == nil { 21 | return 0, errors.New("request is required") 22 | } 23 | 24 | var resp api.Response[int64] 25 | err := api.Service().Post(ctx, "/api/open/policy", req, &resp) 26 | if err != nil { 27 | return 0, errors.Wrap(err, "failed to create policy rule") 28 | } 29 | 30 | if resp.Err != nil { 31 | return 0, errors.New(resp.Msg) 32 | } 33 | 34 | return resp.Data, nil 35 | } 36 | -------------------------------------------------------------------------------- /mcp_server/internal/api/service.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/chaitin/SafeLine/mcp_server/internal/config" 9 | "github.com/chaitin/SafeLine/mcp_server/pkg/errors" 10 | "github.com/chaitin/SafeLine/mcp_server/pkg/logger" 11 | ) 12 | 13 | // APIClient API client implementation 14 | type APIClient struct { 15 | client *Client 16 | config *config.APIConfig 17 | } 18 | 19 | var ( 20 | instance *APIClient 21 | once sync.Once 22 | ) 23 | 24 | // Init Initialize API service 25 | func Init(cfg *config.APIConfig) error { 26 | var err error 27 | once.Do(func() { 28 | instance, err = newAPIClient(cfg) 29 | if err != nil { 30 | logger.With("error", err).Error("failed to initialize API service") 31 | return 32 | } 33 | logger.Info("API service initialized successfully") 34 | }) 35 | return err 36 | } 37 | 38 | // Service Get API service instance 39 | func Service() *APIClient { 40 | if instance == nil { 41 | logger.Error("API service not initialized") 42 | panic("API service not initialized") 43 | } 44 | return instance 45 | } 46 | 47 | // newAPIClient Create new API client 48 | func newAPIClient(config *config.APIConfig) (*APIClient, error) { 49 | if config == nil { 50 | return nil, errors.New("config is required") 51 | } 52 | 53 | if config.BaseURL == "" { 54 | return nil, errors.New("base_url is required") 55 | } 56 | 57 | timeout := 30 58 | if config.Timeout > 0 { 59 | timeout = config.Timeout 60 | } 61 | 62 | opts := []ClientOption{ 63 | WithBaseURL(config.BaseURL), 64 | WithTimeout(time.Duration(timeout) * time.Second), 65 | WithHeader("User-Agent", "SafeLine-MCP/1.0"), 66 | WithInsecureSkipVerify(config.InsecureSkipVerify), 67 | } 68 | 69 | // If token is configured, add authentication header 70 | if config.Token != "" { 71 | opts = append(opts, WithHeader("X-SLCE-API-TOKEN", config.Token)) 72 | } 73 | 74 | client := NewClient(opts...) 75 | 76 | return &APIClient{ 77 | client: client, 78 | config: config, 79 | }, nil 80 | } 81 | 82 | // Post Send POST request 83 | func (c *APIClient) Post(ctx context.Context, path string, body interface{}, result interface{}) error { 84 | return c.client.Request(ctx, "POST", path, body, result) 85 | } 86 | 87 | // Get Send GET request 88 | func (c *APIClient) Get(ctx context.Context, path string, result interface{}) error { 89 | return c.client.Request(ctx, "GET", path, nil, result) 90 | } 91 | 92 | // Put Send PUT request 93 | func (c *APIClient) Put(ctx context.Context, path string, body interface{}, result interface{}) error { 94 | return c.client.Request(ctx, "PUT", path, body, result) 95 | } 96 | 97 | // Delete Send DELETE request 98 | func (c *APIClient) Delete(ctx context.Context, path string, result interface{}) error { 99 | return c.client.Request(ctx, "DELETE", path, nil, result) 100 | } 101 | -------------------------------------------------------------------------------- /mcp_server/internal/api/types.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type PolicyRuleAction int 4 | 5 | const ( 6 | PolicyRuleActionAllow PolicyRuleAction = iota 7 | PolicyRuleActionDeny 8 | PolicyRuleActionMax 9 | ) 10 | 11 | type Key = string 12 | 13 | const ( 14 | KeySrcIP Key = "src_ip" 15 | KeyURI Key = "uri" 16 | KeyURINoQuery Key = "uri_no_query" 17 | KeyHost Key = "host" 18 | KeyMethod Key = "method" 19 | KeyReqHeader Key = "req_header" 20 | KeyReqBody Key = "req_body" 21 | KeyGetParam Key = "get_param" 22 | KeyPostParam Key = "post_param" 23 | ) 24 | 25 | type Op = string 26 | 27 | const ( 28 | OpEq Op = "eq" // equal 29 | OpNotEq Op = "not_eq" // not equal 30 | OpMatch Op = "match" // match 31 | OpCIDR Op = "cidr" // cidr 32 | OpHas Op = "has" // has 33 | OpNotHas Op = "not_has" // not has 34 | OpPrefix Op = "prefix" // prefix 35 | OpRe Op = "re" // regex 36 | OpIn Op = "in" // in 37 | OpNotIn Op = "not_in" // not in 38 | OpNotCIDR Op = "not_cidr" // not cidr 39 | OpExist Op = "exist" // exist 40 | OpNotExist Op = "not_exist" // not exist 41 | OpGeoEq Op = "geo_eq" // geo equal 42 | OpGeoNotEq Op = "geo_not_eq" // geo not equal 43 | ) 44 | 45 | type Pattern struct { 46 | K Key `json:"k"` 47 | Op Op `json:"op"` 48 | V []string `json:"v"` 49 | SubK string `json:"sub_k"` 50 | } 51 | -------------------------------------------------------------------------------- /mcp_server/internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | 7 | "github.com/chaitin/SafeLine/mcp_server/pkg/errors" 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | // Config Global configuration structure 12 | type Config struct { 13 | Server *ServerConfig `yaml:"server"` 14 | Logger *LoggerConfig `yaml:"logger"` 15 | API *APIConfig `yaml:"api"` 16 | } 17 | 18 | // APIConfig API configuration 19 | type APIConfig struct { 20 | // API base URL 21 | BaseURL string `yaml:"base_url"` 22 | // API token 23 | Token string `yaml:"token"` 24 | // API timeout 25 | Timeout int `yaml:"timeout"` 26 | // API debug mode 27 | Debug bool `yaml:"debug"` 28 | // API insecure skip verify 29 | InsecureSkipVerify bool `yaml:"insecure_skip_verify"` 30 | } 31 | 32 | // ServerConfig Server configuration 33 | type ServerConfig struct { 34 | Name string `yaml:"name"` 35 | Version string `yaml:"version"` 36 | Port int `yaml:"port"` 37 | Host string `yaml:"host"` 38 | Secret string `yaml:"secret"` 39 | } 40 | 41 | // LoggerConfig Logger configuration 42 | type LoggerConfig struct { 43 | Level string `yaml:"level"` 44 | FilePath string `yaml:"file_path"` 45 | Console bool `yaml:"console"` 46 | Caller bool `yaml:"caller"` 47 | Development bool `yaml:"development"` 48 | } 49 | 50 | var config *Config 51 | 52 | // getEnvString Get string value from environment variable, return default value if not exists 53 | func getEnvString(key, defaultValue string) string { 54 | if value, exists := os.LookupEnv(key); exists { 55 | return value 56 | } 57 | return defaultValue 58 | } 59 | 60 | // getEnvInt Get integer value from environment variable, return default value if not exists or cannot be parsed 61 | func getEnvInt(key string, defaultValue int) int { 62 | if value, exists := os.LookupEnv(key); exists { 63 | if intValue, err := strconv.Atoi(value); err == nil { 64 | return intValue 65 | } 66 | } 67 | return defaultValue 68 | } 69 | 70 | // Load Load configuration file 71 | func Load(path string) error { 72 | data, err := os.ReadFile(path) 73 | if err != nil { 74 | return errors.Wrap(err, "read config file failed") 75 | } 76 | 77 | config = &Config{} 78 | if err := yaml.Unmarshal(data, config); err != nil { 79 | return errors.Wrap(err, "unmarshal config failed") 80 | } 81 | 82 | // Override configuration from environment variables 83 | if config.Server != nil { 84 | config.Server.Host = getEnvString("LISTEN_ADDRESS", config.Server.Host) 85 | config.Server.Port = getEnvInt("LISTEN_PORT", config.Server.Port) 86 | } 87 | 88 | if config.API != nil { 89 | config.API.BaseURL = getEnvString("SAFELINE_ADDRESS", config.API.BaseURL) 90 | config.API.Token = getEnvString("SAFELINE_API_TOKEN", config.API.Token) 91 | } 92 | 93 | return nil 94 | } 95 | 96 | // GetServer Get server configuration 97 | func GetServer() *ServerConfig { 98 | if config == nil { 99 | return nil 100 | } 101 | return config.Server 102 | } 103 | 104 | // GetLogger Get logger configuration 105 | func GetLogger() *LoggerConfig { 106 | if config == nil { 107 | return nil 108 | } 109 | return config.Logger 110 | } 111 | 112 | // GetAPI Get API configuration 113 | func GetAPI() *APIConfig { 114 | if config == nil { 115 | return nil 116 | } 117 | return config.API 118 | } 119 | -------------------------------------------------------------------------------- /mcp_server/internal/tools/analyze/get_atttack_events.go: -------------------------------------------------------------------------------- 1 | package analyze 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/chaitin/SafeLine/mcp_server/internal/api/analyze" 7 | "github.com/chaitin/SafeLine/mcp_server/pkg/logger" 8 | ) 9 | 10 | type GetAttackEventsParams struct { 11 | IP string `json:"ip" desc:"ip" required:"false"` 12 | Page int `json:"page" desc:"page" required:"false" default:"1"` 13 | PageSize int `json:"page_size" desc:"page size" required:"false" default:"10"` 14 | Start int64 `json:"start" desc:"start unix timestamp in milliseconds" required:"false"` 15 | End int64 `json:"end" desc:"end unix timestamp in milliseconds" required:"false"` 16 | } 17 | 18 | type GetAttackEvents struct{} 19 | 20 | func (t *GetAttackEvents) Name() string { 21 | return "get_attack_events" 22 | } 23 | 24 | func (t *GetAttackEvents) Description() string { 25 | return "get attack events" 26 | } 27 | 28 | func (t *GetAttackEvents) Validate(params GetAttackEventsParams) error { 29 | return nil 30 | } 31 | 32 | func (t *GetAttackEvents) Execute(ctx context.Context, params GetAttackEventsParams) (analyze.GetEventListResponse, error) { 33 | resp, err := analyze.GetEventList(ctx, &analyze.GetEventListRequest{ 34 | IP: params.IP, 35 | PageSize: params.PageSize, 36 | Page: params.Page, 37 | Start: params.Start, 38 | End: params.End, 39 | }) 40 | if err != nil { 41 | return analyze.GetEventListResponse{}, err 42 | } 43 | logger.With("total", resp.Total).Info("get attack events") 44 | return *resp, nil 45 | } 46 | -------------------------------------------------------------------------------- /mcp_server/internal/tools/app/create_application.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/chaitin/SafeLine/mcp_server/internal/api/app" 7 | "github.com/chaitin/SafeLine/mcp_server/pkg/logger" 8 | ) 9 | 10 | type CreateApp struct{} 11 | 12 | type CreateAppParams struct { 13 | ServerNames []string `json:"server_names" desc:"domain list" required:"true"` 14 | Ports []string `json:"ports" desc:"port list" required:"true"` 15 | Upstreams []string `json:"upstreams" desc:"upstream list" required:"true"` 16 | } 17 | 18 | func (t *CreateApp) Name() string { 19 | return "create_http_application" 20 | } 21 | 22 | func (t *CreateApp) Description() string { 23 | return "create a new website or app" 24 | } 25 | 26 | func (t *CreateApp) Validate(params CreateAppParams) error { 27 | return nil 28 | } 29 | 30 | func (t *CreateApp) Execute(ctx context.Context, params CreateAppParams) (int64, error) { 31 | id, err := app.CreateApp(ctx, &app.CreateAppRequest{ 32 | ServerNames: params.ServerNames, 33 | Ports: params.Ports, 34 | Upstreams: params.Upstreams, 35 | }) 36 | if err != nil { 37 | return 0, err 38 | } 39 | logger.Info("create app success", logger.Int64("id", id)) 40 | return id, nil 41 | } 42 | -------------------------------------------------------------------------------- /mcp_server/internal/tools/example.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/chaitin/SafeLine/mcp_server/pkg/errors" 7 | "github.com/chaitin/SafeLine/mcp_server/pkg/logger" 8 | ) 9 | 10 | type CalculateSum struct{} 11 | 12 | func (t *CalculateSum) Name() string { 13 | return "calculate_sum" 14 | } 15 | 16 | func (t *CalculateSum) Description() string { 17 | return "Add two numbers together" 18 | } 19 | 20 | type MyToolInput struct { 21 | A int `json:"a" desc:"number a" required:"true"` 22 | B int `json:"b" desc:"number b" required:"true"` 23 | } 24 | 25 | type MyToolOutput struct { 26 | C int `json:"c"` 27 | } 28 | 29 | func (t *CalculateSum) Validate(params MyToolInput) error { 30 | return nil 31 | } 32 | 33 | func (t *CalculateSum) Execute(ctx context.Context, params MyToolInput) (MyToolOutput, error) { 34 | logger.With("a", params.A). 35 | With("b", params.B). 36 | Debug("Executing calculation") 37 | 38 | result := MyToolOutput{ 39 | C: params.A + params.B, 40 | } 41 | 42 | logger.With("result", result.C). 43 | Debug("Calculation completed") 44 | 45 | return result, errors.New("test error") 46 | } 47 | -------------------------------------------------------------------------------- /mcp_server/internal/tools/init.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "github.com/chaitin/SafeLine/mcp_server/internal/tools/analyze" 5 | "github.com/chaitin/SafeLine/mcp_server/internal/tools/app" 6 | "github.com/chaitin/SafeLine/mcp_server/internal/tools/rule" 7 | ) 8 | 9 | func init() { 10 | // app 11 | AppendTool(&app.CreateApp{}) 12 | 13 | // rule 14 | AppendTool(&rule.CreateBlacklistRule{}) 15 | AppendTool(&rule.CreateWhitelistRule{}) 16 | 17 | // analyze 18 | AppendTool(&analyze.GetAttackEvents{}) 19 | } 20 | -------------------------------------------------------------------------------- /mcp_server/internal/tools/rule/create_blacklist_rule.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/chaitin/SafeLine/mcp_server/internal/api" 7 | "github.com/chaitin/SafeLine/mcp_server/internal/api/rule" 8 | "github.com/chaitin/SafeLine/mcp_server/pkg/logger" 9 | ) 10 | 11 | type CreateBlacklistRule struct{} 12 | 13 | type CreateBlacklistRuleParams struct { 14 | Name string `json:"name" desc:"name" required:"true"` 15 | IP []string `json:"ip" desc:"ip" required:"false"` 16 | URINoQuery []string `json:"uri_no_query" desc:"uri_no_query" required:"false"` 17 | } 18 | 19 | func (t *CreateBlacklistRule) Name() string { 20 | return "create_blacklist_rule" 21 | } 22 | 23 | func (t *CreateBlacklistRule) Description() string { 24 | return "create a new blacklist rule" 25 | } 26 | 27 | func (t *CreateBlacklistRule) Validate(params CreateBlacklistRuleParams) error { 28 | return nil 29 | } 30 | 31 | func (t *CreateBlacklistRule) Execute(ctx context.Context, params CreateBlacklistRuleParams) (int64, error) { 32 | var pattern [][]api.Pattern 33 | if len(params.IP) > 0 { 34 | pattern = append(pattern, []api.Pattern{ 35 | { 36 | K: api.KeySrcIP, 37 | Op: api.OpEq, 38 | V: params.IP, 39 | SubK: "", 40 | }, 41 | }) 42 | } 43 | if len(params.URINoQuery) > 0 { 44 | pattern = append(pattern, []api.Pattern{ 45 | { 46 | K: api.KeyURINoQuery, 47 | Op: api.OpEq, 48 | V: params.URINoQuery, 49 | SubK: "", 50 | }, 51 | }) 52 | } 53 | 54 | id, err := rule.CreateRule(ctx, &rule.CreateRuleRequest{ 55 | Name: params.Name, 56 | IP: params.IP, 57 | IsEnabled: true, 58 | Action: int(api.PolicyRuleActionDeny), 59 | Pattern: pattern, 60 | }) 61 | if err != nil { 62 | return 0, err 63 | } 64 | logger.With("id", id).Info("create blacklist rule success") 65 | return id, nil 66 | } 67 | -------------------------------------------------------------------------------- /mcp_server/internal/tools/rule/create_whitelist_rule.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/chaitin/SafeLine/mcp_server/internal/api" 7 | "github.com/chaitin/SafeLine/mcp_server/internal/api/rule" 8 | "github.com/chaitin/SafeLine/mcp_server/pkg/logger" 9 | ) 10 | 11 | type CreateWhitelistRule struct{} 12 | 13 | type CreateWhitelistRuleParams struct { 14 | Name string `json:"name" desc:"name" required:"true"` 15 | IP []string `json:"ip" desc:"ip" required:"false"` 16 | URINoQuery []string `json:"uri_no_query" desc:"uri_no_query" required:"false"` 17 | } 18 | 19 | func (t *CreateWhitelistRule) Name() string { 20 | return "create_whitelist_rule" 21 | } 22 | 23 | func (t *CreateWhitelistRule) Description() string { 24 | return "create a new whitelist rule" 25 | } 26 | 27 | func (t *CreateWhitelistRule) Validate(params CreateWhitelistRuleParams) error { 28 | return nil 29 | } 30 | 31 | func (t *CreateWhitelistRule) Execute(ctx context.Context, params CreateWhitelistRuleParams) (int64, error) { 32 | var pattern [][]api.Pattern 33 | if len(params.IP) > 0 { 34 | pattern = append(pattern, []api.Pattern{ 35 | { 36 | K: api.KeySrcIP, 37 | Op: api.OpEq, 38 | V: params.IP, 39 | SubK: "", 40 | }, 41 | }) 42 | } 43 | if len(params.URINoQuery) > 0 { 44 | pattern = append(pattern, []api.Pattern{ 45 | { 46 | K: api.KeyURINoQuery, 47 | Op: api.OpEq, 48 | V: params.URINoQuery, 49 | SubK: "", 50 | }, 51 | }) 52 | } 53 | 54 | id, err := rule.CreateRule(ctx, &rule.CreateRuleRequest{ 55 | Name: params.Name, 56 | IP: params.IP, 57 | IsEnabled: true, 58 | Action: int(api.PolicyRuleActionAllow), 59 | Pattern: pattern, 60 | }) 61 | if err != nil { 62 | return 0, err 63 | } 64 | logger.With("id", id).Info("create whitelist rule success") 65 | return id, nil 66 | } 67 | -------------------------------------------------------------------------------- /mcp_server/internal/tools/tool.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "github.com/chaitin/SafeLine/mcp_server/pkg/logger" 5 | "github.com/chaitin/SafeLine/mcp_server/pkg/mcp" 6 | ) 7 | 8 | // By deferring the concretization of generic types to the Register method, 9 | // we avoid type inference issues. 10 | 11 | // Each Tool is wrapped in a toolWrapper that knows its concrete type, 12 | // allowing correct passing of generic parameters during registration. 13 | type ToolWrapper interface { 14 | Register(s *mcp.MCPServer) error 15 | } 16 | 17 | var ( 18 | tools = []ToolWrapper{} 19 | ) 20 | 21 | func AppendTool[T any, R any](tool ...mcp.Tool[T, R]) { 22 | for _, t := range tool { 23 | tools = append(tools, &toolWrapper[T, R]{tool: t}) 24 | } 25 | } 26 | 27 | func Tools() []ToolWrapper { 28 | return tools 29 | } 30 | 31 | type toolWrapper[T any, R any] struct { 32 | tool mcp.Tool[T, R] 33 | } 34 | 35 | func (w *toolWrapper[T, R]) Register(s *mcp.MCPServer) error { 36 | logger.Info("Registering tool", 37 | logger.String("name", w.tool.Name()), 38 | logger.String("description", w.tool.Description()), 39 | ) 40 | return mcp.RegisterTool(s, w.tool) 41 | } 42 | -------------------------------------------------------------------------------- /mcp_server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/chaitin/SafeLine/mcp_server/internal/api" 8 | "github.com/chaitin/SafeLine/mcp_server/internal/config" 9 | "github.com/chaitin/SafeLine/mcp_server/internal/tools" 10 | "github.com/chaitin/SafeLine/mcp_server/pkg/logger" 11 | "github.com/chaitin/SafeLine/mcp_server/pkg/mcp" 12 | ) 13 | 14 | func main() { 15 | configPath := flag.String("config", "config.yaml", "path to config file") 16 | flag.Parse() 17 | 18 | if err := config.Load(*configPath); err != nil { 19 | panic(fmt.Errorf("failed to load config: %v", err)) 20 | } 21 | 22 | logConfig := config.GetLogger() 23 | if err := logger.Init(&logger.Config{ 24 | Level: logConfig.Level, 25 | FilePath: logConfig.FilePath, 26 | Console: logConfig.Console, 27 | Caller: logConfig.Caller, 28 | Development: logConfig.Development, 29 | }); err != nil { 30 | panic(fmt.Errorf("failed to init logger: %v", err)) 31 | } 32 | 33 | logger.With("base_url", config.GetAPI().BaseURL).Info("Initializing API service...") 34 | if err := api.Init(config.GetAPI()); err != nil { 35 | panic(fmt.Errorf("failed to init API service: %v", err)) 36 | } 37 | 38 | logger.Info("Starting MCP Server...") 39 | serverConfig := config.GetServer() 40 | s := mcp.NewMCPServer( 41 | serverConfig.Name, 42 | serverConfig.Version, 43 | serverConfig.Secret, 44 | ) 45 | 46 | logger.Info("Registering tools...") 47 | for _, tool := range tools.Tools() { 48 | if err := tool.Register(s); err != nil { 49 | logger.With("error", err). 50 | Error("Failed to register tool") 51 | panic(err) 52 | } 53 | } 54 | 55 | addr := fmt.Sprintf("%s:%d", serverConfig.Host, serverConfig.Port) 56 | logger.With("addr", addr).Info("Starting server") 57 | if err := s.Start(addr); err != nil { 58 | logger.With("error", err). 59 | Error("Server failed to start") 60 | panic(err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /mcp_server/pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | // Config Global configuration structure 10 | type Config struct { 11 | Server ServerConfig `yaml:"server"` 12 | Logger LoggerConfig `yaml:"logger"` 13 | } 14 | 15 | // ServerConfig Server configuration 16 | type ServerConfig struct { 17 | Name string `yaml:"name"` 18 | Version string `yaml:"version"` 19 | Port int `yaml:"port"` 20 | Host string `yaml:"host"` 21 | Secret string `yaml:"secret"` 22 | } 23 | 24 | // LoggerConfig Logger configuration 25 | type LoggerConfig struct { 26 | Level string `yaml:"level"` 27 | FilePath string `yaml:"file_path"` 28 | Console bool `yaml:"console"` 29 | Caller bool `yaml:"caller"` 30 | Development bool `yaml:"development"` 31 | } 32 | 33 | var ( 34 | globalConfig *Config 35 | ) 36 | 37 | // Load Load configuration from file 38 | func Load(filename string) error { 39 | data, err := os.ReadFile(filename) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | config := &Config{} 45 | if err := yaml.Unmarshal(data, config); err != nil { 46 | return err 47 | } 48 | 49 | globalConfig = config 50 | return nil 51 | } 52 | 53 | // Get Get global configuration 54 | func Get() *Config { 55 | return globalConfig 56 | } 57 | 58 | // GetServer Get server configuration 59 | func GetServer() ServerConfig { 60 | if globalConfig == nil { 61 | return ServerConfig{} 62 | } 63 | return globalConfig.Server 64 | } 65 | 66 | // GetLogger Get logger configuration 67 | func GetLogger() LoggerConfig { 68 | if globalConfig == nil { 69 | return LoggerConfig{} 70 | } 71 | return globalConfig.Logger 72 | } 73 | -------------------------------------------------------------------------------- /mcp_server/pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/chaitin/SafeLine/mcp_server/pkg/logger" 10 | ) 11 | 12 | var ( 13 | // Common errors 14 | ErrInternal = New("internal error") 15 | ErrInvalidParam = New("invalid parameter") 16 | ErrNotFound = New("resource not found") 17 | ErrUnauthorized = New("unauthorized") 18 | ErrForbidden = New("forbidden") 19 | ErrTimeout = New("timeout") 20 | ) 21 | 22 | // Error Custom error structure 23 | type Error struct { 24 | err error 25 | stack []string 26 | msg string 27 | location string 28 | } 29 | 30 | // Error Implement error interface 31 | func (e *Error) Error() string { 32 | if e.msg != "" { 33 | return fmt.Sprintf("%s: %v (at %s)", e.msg, e.err, e.location) 34 | } 35 | return fmt.Sprintf("%v (at %s)", e.err, e.location) 36 | } 37 | 38 | // Unwrap Return original error 39 | func (e *Error) Unwrap() error { 40 | if e.err == nil { 41 | return nil 42 | } 43 | if wrapped, ok := e.err.(*Error); ok { 44 | return wrapped.Unwrap() 45 | } 46 | return e.err 47 | } 48 | 49 | // Stack Return error stack 50 | func (e *Error) Stack() []string { 51 | return e.stack 52 | } 53 | 54 | // Location Return error location 55 | func (e *Error) Location() string { 56 | return e.location 57 | } 58 | 59 | // getCallerLocation Get caller location 60 | func getCallerLocation(skip int) string { 61 | _, file, line, ok := runtime.Caller(skip) 62 | if !ok { 63 | return "unknown" 64 | } 65 | return fmt.Sprintf("%s:%d", file, line) 66 | } 67 | 68 | // WrapL Wrap error and print log 69 | func WrapL(err error, msg string) error { 70 | if err == nil { 71 | return nil 72 | } 73 | 74 | // Get stack trace information 75 | var stack []string 76 | for i := 1; i < 32; i++ { 77 | pc, file, line, ok := runtime.Caller(i) 78 | if !ok { 79 | break 80 | } 81 | fn := runtime.FuncForPC(pc) 82 | if fn == nil { 83 | break 84 | } 85 | name := fn.Name() 86 | if strings.Contains(name, "runtime.") { 87 | break 88 | } 89 | stack = append(stack, fmt.Sprintf("%s:%d", file, line)) 90 | } 91 | 92 | wrappedErr := &Error{ 93 | err: err, 94 | stack: stack, 95 | msg: msg, 96 | location: getCallerLocation(2), 97 | } 98 | 99 | // Print error information and stack using logger 100 | logger.With("error", err). 101 | With("location", wrappedErr.location). 102 | With("stack", strings.Join(stack, "\n")). 103 | Error(msg) 104 | 105 | return wrappedErr 106 | } 107 | 108 | // Is Check error type 109 | func Is(err, target error) bool { 110 | return errors.Is(err, target) 111 | } 112 | 113 | // As Type assertion 114 | func As(err error, target interface{}) bool { 115 | return errors.As(err, target) 116 | } 117 | 118 | // Wrap Wrap error without printing log 119 | func Wrap(err error, msg string) error { 120 | if err == nil { 121 | return nil 122 | } 123 | return &Error{ 124 | err: err, 125 | msg: msg, 126 | location: getCallerLocation(2), 127 | } 128 | } 129 | 130 | // New Create new error 131 | func New(text string) error { 132 | return &Error{ 133 | err: errors.New(text), 134 | location: getCallerLocation(2), 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /mcp_server/pkg/logger/field.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | ) 7 | 8 | // Field 日志字段 9 | type Field = zapcore.Field 10 | 11 | // String 创建字符串字段 12 | func String(key string, val string) Field { 13 | return zap.String(key, val) 14 | } 15 | 16 | // Int 创建整数字段 17 | func Int(key string, val int) Field { 18 | return zap.Int(key, val) 19 | } 20 | 21 | // Int64 创建 int64 字段 22 | func Int64(key string, val int64) Field { 23 | return zap.Int64(key, val) 24 | } 25 | 26 | // Float64 创建浮点数字段 27 | func Float64(key string, val float64) Field { 28 | return zap.Float64(key, val) 29 | } 30 | 31 | // Bool 创建布尔字段 32 | func Bool(key string, val bool) Field { 33 | return zap.Bool(key, val) 34 | } 35 | 36 | // Err 创建错误字段 37 | func Err(err error) Field { 38 | return zap.Error(err) 39 | } 40 | 41 | // Any 创建任意类型字段 42 | func Any(key string, val interface{}) Field { 43 | return zap.Any(key, val) 44 | } 45 | 46 | // Duration 创建时间段字段 47 | func Duration(key string, val float64) Field { 48 | return zap.Float64(key, val) 49 | } 50 | -------------------------------------------------------------------------------- /sdk/ingress-nginx/ingress-nginx-safeline-1.0.2-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "ingress-nginx-safeline" 2 | version = "1.0.2-1" 3 | source = { 4 | url = "git://github.com/xbingW/ingress-nginx-safeline.git" 5 | } 6 | description = { 7 | summary = "Ingress-Nginx plugin for Chaitin SafeLine Web Application Firewall", 8 | homepage = "https://github.com/xbingW/ingress-nginx-safeline", 9 | license = "Apache License 2.0", 10 | maintainer = "Xiaobing Wang " 11 | } 12 | dependencies = { 13 | "lua-resty-t1k" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["safeline.main"] = "lib/safeline/main.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sdk/ingress-nginx/ingress-nginx-safeline-1.0.3-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "ingress-nginx-safeline" 2 | version = "1.0.3-1" 3 | source = { 4 | url = "git://github.com/xbingW/ingress-nginx-safeline.git" 5 | } 6 | description = { 7 | summary = "Ingress-Nginx plugin for Chaitin SafeLine Web Application Firewall", 8 | homepage = "https://github.com/xbingW/ingress-nginx-safeline", 9 | license = "Apache License 2.0", 10 | maintainer = "Xiaobing Wang " 11 | } 12 | dependencies = { 13 | "lua-resty-t1k >= 1.1.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["safeline.main"] = "lib/safeline/main.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sdk/ingress-nginx/ingress-nginx-safeline-1.0.4-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "ingress-nginx-safeline" 2 | version = "1.0.4-1" 3 | source = { 4 | url = "git://github.com/chaitin/ingress-nginx-safeline.git" 5 | } 6 | description = { 7 | summary = "Ingress-Nginx plugin for Chaitin SafeLine Web Application Firewall", 8 | homepage = "https://github.com/chaitin/ingress-nginx-safeline", 9 | license = "Apache License 2.0", 10 | maintainer = "Xiaobing Wang " 11 | } 12 | dependencies = { 13 | "lua-resty-t1k >= 1.1.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["safeline.main"] = "lib/safeline/main.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sdk/ingress-nginx/lib/safeline/main.lua: -------------------------------------------------------------------------------- 1 | local t1k = require "resty.t1k" 2 | local t1k_constants = require "resty.t1k.constants" 3 | 4 | local ngx = ngx 5 | local fmt = string.format 6 | 7 | local blocked_message = [[{"code": %s, "success":false, ]] .. 8 | [["message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "%s"}]] 9 | 10 | local _M = {} 11 | 12 | local mode = os.getenv("SAFELINE_MODE") 13 | local host = os.getenv("SAFELINE_HOST") 14 | local port = os.getenv("SAFELINE_PORT") 15 | local connect_timeout = os.getenv("SAFELINE_CONNECT_TIMEOUT") 16 | local send_timeout = os.getenv("SAFELINE_SEND_TIMEOUT") 17 | local read_timeout = os.getenv("SAFELINE_READ_TIMEOUT") 18 | local req_body_size = os.getenv("SAFELINE_REQ_BODY_SIZE") 19 | local keepalive_size = os.getenv("SAFELINE_KEEPALIVE_SIZE") 20 | local keepalive_timeout = os.getenv("SAFELINE_KEEPALIVE_TIMEOUT") 21 | local remote_addr = os.getenv("SAFELINE_REMOTE_ADDR") 22 | 23 | local function get_conf() 24 | local t = { 25 | mode = mode or "block", 26 | host = host, 27 | port = port, 28 | connect_timeout = connect_timeout or 1000, 29 | send_timeout = send_timeout or 1000, 30 | read_timeout = read_timeout or 1000, 31 | req_body_size = req_body_size or 1024, 32 | keepalive_size = keepalive_size or 256, 33 | keepalive_timeout = keepalive_timeout or 60000, 34 | remote_addr = remote_addr 35 | } 36 | return t 37 | end 38 | 39 | function _M.rewrite() 40 | local t = get_conf() 41 | if not t.host then 42 | ngx.log(ngx.ERR, "safeline host is required") 43 | return 44 | end 45 | local ok, err, result = t1k.do_access(t, false) 46 | if not ok then 47 | ngx.log(ngx.ERR, "failed to detector req: ", err) 48 | return 49 | end 50 | if result then 51 | if result.action == t1k_constants.ACTION_BLOCKED then 52 | local msg = fmt(blocked_message, result.status, result.event_id) 53 | ngx.log(ngx.ERR, "blocked by safeline waf: ",msg) 54 | ngx.status = tonumber(result.status,10) 55 | ngx.say(msg) 56 | return ngx.exit(ngx.HTTP_OK) 57 | end 58 | end 59 | end 60 | 61 | return _M -------------------------------------------------------------------------------- /sdk/kong/Readme.md: -------------------------------------------------------------------------------- 1 | # Kong Safeline Plugin 2 | Kong plugin for Chaitin SafeLine Web Application Firewall (WAF). This plugin is used to protect your API from malicious requests. It can be used to block requests that contain malicious content in the request body, query parameters, headers, or URI. 3 | 4 | ## Safeline Prepare 5 | The detection engine of the SafeLine provides services by default via Unix socket. We need to modify it to use TCP, so it can be called by the t1k plugin. 6 | 7 | 1.Navigate to the configuration directory of the SafeLine detection engine: 8 | ```shell 9 | cd /data/safeline/resources/detector/ 10 | ``` 11 | 2.Open the `detector.yml` file in a text editor. Modify the bind configuration from Unix socket to TCP by adding the following settings: 12 | ```yaml 13 | bind_addr: 0.0.0.0 14 | listen_port: 8000 15 | ``` 16 | These configuration values will override the default settings in the container, making the SafeLine engine listen on port 8000. 17 | 18 | 3.Next, map the container’s port 8000 to the host machine. First, navigate to the SafeLine installation directory: 19 | ```shell 20 | cd /data/safeline 21 | ``` 22 | 23 | 4.Open the compose.yaml file in a text editor and add the ports field to the detector container to expose port 8000: 24 | ```yaml 25 | ... 26 | detect: 27 | ports: 28 | - 8000:8000 29 | ... 30 | ``` 31 | 32 | 5.Save the changes and restart SafeLine with the following commands: 33 | ```shell 34 | docker-compose down 35 | docker-compose up -d 36 | ``` 37 | This will apply the changes and activate the new configuration. 38 | 39 | ## Plugin Installation 40 | To install the plugin, run the following command in your Kong server: 41 | 42 | ```shell 43 | $ luarocks install kong-safeline 44 | ``` 45 | 46 | ## Plugin Configuration 47 | You can add the plugin to your API by making the following request: 48 | 49 | ```shell 50 | # if your detector is running on tcp port 51 | $ curl -X POST http://kong:8001/services/{name}/plugins \ 52 | --data "name=safeline" \ 53 | --data "config.host=your_detector_host" \ 54 | --data "config.port=your_detector_port" 55 | # if your detector is running on unix socket 56 | $ curl -X POST http://kong:8001/services/{name}/plugins \ 57 | --data "name=safeline" \ 58 | --data "config.host=unix:/path/to/your/unix/socket" 59 | ``` 60 | 61 | ## Test 62 | You can test the plugin by sending a request to your API with malicious content. If the request is blocked, you will receive a `403 Forbidden` response. 63 | 64 | ```shell 65 | $ curl -X POST http://kong:8000?1=1%20and%202=2 66 | 67 | # you will receive a 403 Forbidden response 68 | {"code": 403, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "8b41a021ea9541c89bb88f3773b4da24"} 69 | ``` 70 | -------------------------------------------------------------------------------- /sdk/kong/kong-safeline-1.0.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "kong-safeline" 2 | version = "1.0.0-1" 3 | source = { 4 | url = "git://github.com/xbingW/kong-safeline.git" 5 | } 6 | description = { 7 | summary = "Kong plugin for Chaitin SafeLine Web Application Firewall", 8 | homepage = "https://github.com/xbingW/kong-safeline", 9 | license = "Apache License 2.0", 10 | maintainer = "Xiaobing Wang " 11 | } 12 | dependencies = { 13 | "lua-resty-t1k" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua", 19 | ["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sdk/kong/kong-safeline-1.0.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "kong-safeline" 2 | version = "1.0.1-1" 3 | source = { 4 | url = "git://github.com/xbingW/kong-safeline.git" 5 | } 6 | description = { 7 | summary = "Kong plugin for Chaitin SafeLine Web Application Firewall", 8 | homepage = "https://github.com/xbingW/kong-safeline", 9 | license = "Apache License 2.0", 10 | maintainer = "Xiaobing Wang " 11 | } 12 | dependencies = { 13 | "lua-resty-t1k" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua", 19 | ["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sdk/kong/kong-safeline-1.0.2-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "kong-safeline" 2 | version = "1.0.2-1" 3 | source = { 4 | url = "git://github.com/xbingW/kong-safeline.git" 5 | } 6 | description = { 7 | summary = "Kong plugin for Chaitin SafeLine Web Application Firewall", 8 | homepage = "https://github.com/xbingW/kong-safeline", 9 | license = "Apache License 2.0", 10 | maintainer = "Xiaobing Wang " 11 | } 12 | dependencies = { 13 | "lua-resty-t1k >= 1.1.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua", 19 | ["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sdk/kong/kong-safeline-1.0.3-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "kong-safeline" 2 | version = "1.0.3-1" 3 | source = { 4 | url = "file://kong-safeline-1.0.3.tar.gz", 5 | } 6 | build = { 7 | type = "script", 8 | rockspec = { 9 | build = { 10 | "git clone https://github.com/chaitin/SafeLine.git", 11 | "cp -r sdk/kong .", 12 | "rm -rf SafeLine" 13 | } 14 | } 15 | } 16 | description = { 17 | summary = "Kong plugin for Chaitin SafeLine Web Application Firewall", 18 | homepage = "https://github.com/chaitin/SafeLine", 19 | license = "Apache License 2.0", 20 | maintainer = "Xiaobing Wang " 21 | } 22 | dependencies = { 23 | "lua-resty-t1k >= 1.1.5" 24 | } 25 | build = { 26 | type = "builtin", 27 | modules = { 28 | ["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua", 29 | ["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sdk/kong/kong-safeline-1.0.4-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "kong-safeline" 2 | version = "1.0.4-1" 3 | source = { 4 | url = "git://github.com/chaitin/SafeLine.git", 5 | } 6 | build = { 7 | type = "script", 8 | rockspec = { 9 | build = { 10 | "git clone https://github.com/chaitin/SafeLine.git", 11 | "cp -r sdk/kong .", 12 | "rm -rf SafeLine" 13 | } 14 | } 15 | } 16 | description = { 17 | summary = "Kong plugin for Chaitin SafeLine Web Application Firewall", 18 | homepage = "https://github.com/chaitin/SafeLine", 19 | license = "Apache License 2.0", 20 | maintainer = "Xiaobing Wang " 21 | } 22 | dependencies = { 23 | "lua-resty-t1k >= 1.1.5" 24 | } 25 | build = { 26 | type = "builtin", 27 | modules = { 28 | ["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua", 29 | ["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sdk/kong/kong-safeline-1.0.5-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "kong-safeline" 2 | version = "1.0.5-1" 3 | source = { 4 | url = "git://github.com/chaitin/SafeLine.git", 5 | } 6 | build = { 7 | type = "script", 8 | rockspec = { 9 | build = { 10 | "git clone https://github.com/chaitin/SafeLine.git", 11 | "cp -r sdk/kong .", 12 | "rm -rf SafeLine" 13 | } 14 | } 15 | } 16 | description = { 17 | summary = "Kong plugin for Chaitin SafeLine Web Application Firewall", 18 | homepage = "https://github.com/chaitin/SafeLine", 19 | license = "Apache License 2.0", 20 | maintainer = "Xiaobing Wang " 21 | } 22 | dependencies = { 23 | "lua-resty-t1k >= 1.1.5" 24 | } 25 | build = { 26 | type = "builtin", 27 | modules = { 28 | ["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua", 29 | ["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sdk/kong/kong-safeline-1.0.6-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "kong-safeline" 2 | version = "1.0.6-1" 3 | source = { 4 | url = "git://github.com/xbingW/kong-safeline.git", 5 | } 6 | description = { 7 | summary = "Kong plugin for Chaitin SafeLine Web Application Firewall", 8 | homepage = "https://github.com/chaitin/SafeLine", 9 | license = "Apache License 2.0", 10 | maintainer = "Xiaobing Wang " 11 | } 12 | dependencies = { 13 | "lua-resty-t1k >= 1.1.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua", 19 | ["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sdk/kong/kong-safeline-1.0.7-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "kong-safeline" 2 | version = "1.0.7-1" 3 | source = { 4 | url = "git://github.com/chaitin/kong-safeline.git", 5 | } 6 | description = { 7 | summary = "Kong plugin for Chaitin SafeLine Web Application Firewall", 8 | homepage = "https://github.com/chaitin/SafeLine", 9 | license = "Apache License 2.0", 10 | maintainer = "Xiaobing Wang " 11 | } 12 | dependencies = { 13 | "lua-resty-t1k >= 1.1.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua", 19 | ["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sdk/kong/kong/plugins/safeline/handler.lua: -------------------------------------------------------------------------------- 1 | local t1k = require "resty.t1k" 2 | local t1k_constants = require "resty.t1k.constants" 3 | 4 | local fmt = string.format 5 | 6 | local SafelineHandler = { 7 | VERSION = "0.0.1", 8 | PRIORITY = 1000 9 | } 10 | 11 | local blocked_message = [[{"code": %s, "success":false, ]] .. 12 | [["message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "%s"}]] 13 | 14 | local function get_conf(conf) 15 | local t = { 16 | mode = conf.mode, 17 | host = conf.host, 18 | port = conf.port, 19 | connect_timeout = conf.connect_timeout, 20 | send_timeout = conf.send_timeout, 21 | read_timeout = conf.read_timeout, 22 | req_body_size = conf.req_body_size, 23 | keepalive_size = conf.keepalive_size, 24 | keepalive_timeout = conf.keepalive_timeout, 25 | remote_addr = conf.remote_addr 26 | } 27 | return t 28 | end 29 | 30 | function SafelineHandler:access(conf) 31 | -- your custom code here 32 | local t = get_conf(conf) 33 | local ok, err, result = t1k.do_access(t, false) 34 | if not ok then 35 | kong.log.err("failed to detector req: ", err) 36 | end 37 | if result and result.status then 38 | if result.action == t1k_constants.ACTION_BLOCKED then 39 | local msg = fmt(blocked_message, result.status, result.event_id) 40 | kong.log.debug("blocked by safeline: ",msg) 41 | return kong.response.exit(tonumber(result.status), msg) 42 | end 43 | end 44 | end 45 | 46 | 47 | return SafelineHandler 48 | -------------------------------------------------------------------------------- /sdk/kong/kong/plugins/safeline/schema.lua: -------------------------------------------------------------------------------- 1 | local typedefs = require "kong.db.schema.typedefs" 2 | 3 | return { 4 | name = "kong-safeline", 5 | fields = {{ 6 | consumer = typedefs.no_consumer 7 | }, { 8 | protocols = typedefs.protocols_http 9 | }, { 10 | config = { 11 | type = "record", 12 | fields = {{ 13 | host = { 14 | type = "string", 15 | required = true 16 | } 17 | }, { 18 | port = { 19 | type = "number", 20 | required = false 21 | } 22 | }, { 23 | mode = { 24 | type = "string", 25 | required = false, 26 | default = "block", 27 | one_of = {"monitor", "block", "off"} 28 | } 29 | }, { 30 | connect_timeout = { 31 | type = "number", 32 | required = false, 33 | default = 1000 34 | } 35 | }, { 36 | send_timeout = { 37 | type = "number", 38 | required = false, 39 | default = 1000 40 | } 41 | }, { 42 | read_timeout = { 43 | type = "number", 44 | required = false, 45 | default = 1000 46 | } 47 | }, { 48 | req_body_size = { 49 | type = "number", 50 | required = false, 51 | default = 1000 52 | } 53 | }, { 54 | keepalive_size = { 55 | type = "number", 56 | required = false, 57 | default = 1000 58 | } 59 | }, { 60 | keepalive_timeout = { 61 | type = "number", 62 | required = false, 63 | default = 1000 64 | } 65 | }, { 66 | remote_addr = { 67 | type = "string", 68 | required = false 69 | } 70 | }} 71 | } 72 | }} 73 | } 74 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Install Lua 17 | uses: leafo/gh-actions-lua@v10 18 | 19 | - name: Install Luarocks 20 | uses: leafo/gh-actions-luarocks@v4 21 | 22 | - name: Extract release tag 23 | id: release_tag 24 | run: | 25 | # Extract the tag name from the ref 26 | tag="${GITHUB_REF#refs/tags/}" 27 | version_without_v="${tag#v}" 28 | echo "version=${tag}" >> $GITHUB_ENV 29 | echo "version_without_v=${version_without_v}" >> $GITHUB_ENV 30 | 31 | - name: Create Release 32 | uses: actions/create-release@v1 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | with: 36 | tag_name: ${{ env.version }} 37 | release_name: ${{ env.version }} 38 | draft: false 39 | prerelease: false 40 | 41 | - name: Upload to luarocks 42 | env: 43 | LUAROCKS_TOKEN: ${{ secrets.LUAROCKS_TOKEN }} 44 | run: | 45 | luarocks install dkjson 46 | luarocks upload rockspec/lua-resty-t1k-${{ env.version_without_v }}-0.rockspec --api-key=${{ secrets.LUAROCKS_API_KEY }} 47 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | luacheck: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: leafo/gh-actions-lua@v10 11 | with: 12 | luaVersion: "luajit-openresty" 13 | - uses: leafo/gh-actions-luarocks@v4 14 | - run: luarocks install luacheck 15 | - run: luacheck lib 16 | 17 | run_tests: 18 | strategy: 19 | matrix: 20 | openresty_version: 21 | - 1.17.8.2 22 | - 1.19.9.1 23 | - 1.21.4.3 24 | - 1.25.3.1 25 | 26 | services: 27 | detector: 28 | image: chaitin/safeline-detector:t1k-ci-1.6.0 29 | 30 | runs-on: ubuntu-latest 31 | container: 32 | image: openresty/openresty:${{ matrix.openresty_version }}-alpine-fat 33 | # --init runs tinit as PID 1 and prevents the 'WARNING: killing the child process' spam from the test suite 34 | options: --init 35 | 36 | steps: 37 | - name: Install deps 38 | run: | 39 | apk add --no-cache bash bind-tools curl git git-lfs libarchive-tools perl perl-dev wget 40 | ln -s /usr/bin/bsdtar /usr/bin/tar 41 | 42 | - name: Install CPAN 43 | run: curl -s -L http://xrl.us/cpanm > /bin/cpanm && chmod +x /bin/cpanm 44 | 45 | - name: Cache 46 | uses: actions/cache@v3 47 | with: 48 | path: | 49 | ~/.cpan 50 | ~/.cache 51 | key: ${{ runner.os }}-${{ matrix.openresty_version }}-cache 52 | 53 | - name: Install Test::Nginx 54 | run: cpanm -q -n Test::Nginx 55 | 56 | - uses: actions/checkout@v4 57 | with: 58 | lfs: true 59 | 60 | - name: Run tests 61 | run: | 62 | curl -fs -X POST -H "Content-Type: application/octet-stream" --data-binary "@ci/bytecode" "http://detector:8001/update/policy" 63 | env DETECTOR_IP=$(dig detector +short) prove -r t/ 64 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | *.iml 4 | out 5 | gen 6 | 7 | # Visual Studio Code 8 | .vscode/* 9 | !.vscode/settings.json 10 | !.vscode/tasks.json 11 | !.vscode/launch.json 12 | !.vscode/extensions.json 13 | !.vscode/*.code-snippets 14 | 15 | # Local History for Visual Studio Code 16 | .history/ 17 | 18 | # Built Visual Studio Code Extensions 19 | *.vsix 20 | 21 | # Test::Nginx files 22 | t/servroot 23 | 24 | # luarocks build files 25 | *.src.rock 26 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "ngx_lua" 2 | redefined = false 3 | max_line_length = 130 4 | max_code_line_length = 130 5 | max_string_line_length = 130 6 | max_comment_line_length = 130 7 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/ci/.dockerignore: -------------------------------------------------------------------------------- 1 | /bytecode 2 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/ci/Dockerfile: -------------------------------------------------------------------------------- 1 | # Usage: 2 | # docker build -t chaitin/safeline-detector:t1k-ci-1.6.0 . 3 | # docker push chaitin/safeline-detector:t1k-ci-1.6.0 4 | FROM chaitin/safeline-detector:1.6.0 5 | 6 | RUN sed -i "s/^# bind_addr/bind_addr/; s/^# listen_port/listen_port/; s/^bind_addr: unix/# bind_addr: unix/;" /detector/snserver.yml 7 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/ci/bytecode: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaitin/SafeLine/24e2bac1bdba9a5208763ed0eff045cffc8ad71a/sdk/lua-resty-t1k/ci/bytecode -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/lib/resty/t1k.lua: -------------------------------------------------------------------------------- 1 | local consts = require "resty.t1k.constants" 2 | local filter = require "resty.t1k.filter" 3 | local handler = require "resty.t1k.handler" 4 | local log = require "resty.t1k.log" 5 | local request = require "resty.t1k.request" 6 | local utils = require "resty.t1k.utils" 7 | 8 | local lower = string.lower 9 | 10 | local ngx = ngx 11 | local nlog = ngx.log 12 | 13 | local log_fmt = log.fmt 14 | local debug_fmt = log.debug_fmt 15 | 16 | local _M = { 17 | _VERSION = '1.0.0' 18 | } 19 | 20 | local DEFAULT_T1K_CONNECT_TIMEOUT = 1000 -- 1s 21 | local DEFAULT_T1K_SEND_TIMEOUT = 1000 -- 1s 22 | local DEFAULT_T1K_READ_TIMEOUT = 1000 -- 1s 23 | local DEFAULT_T1K_REQ_BODY_SIZE = 1024 -- 1024 KB 24 | local DEFAULT_T1K_KEEPALIVE_SIZE = 256 25 | local DEFAULT_T1K_KEEPALIVE_TIMEOUT = 60 * 1000 -- 60s 26 | 27 | function _M.do_access(t, handle) 28 | local ok, err, result 29 | local opts = {} 30 | t = t or {} 31 | 32 | if not t.mode then 33 | return true, nil, nil 34 | end 35 | 36 | opts.mode = lower(t.mode) 37 | if opts.mode == consts.MODE_OFF then 38 | nlog(debug_fmt("t1k is not enabled")) 39 | return true, nil, nil 40 | end 41 | 42 | if opts.mode ~= consts.MODE_OFF and opts.mode ~= consts.MODE_BLOCK and opts.mode ~= consts.MODE_MONITOR then 43 | err = log_fmt("invalid t1k mode: %s", t.mode) 44 | return nil, err, nil 45 | end 46 | 47 | if not t.host then 48 | err = log_fmt("invalid t1k host: %s", t.host) 49 | return nil, err, nil 50 | end 51 | opts.host = t.host 52 | 53 | if utils.starts_with(opts.host, consts.UNIX_SOCK_PREFIX) then 54 | opts.uds = true 55 | else 56 | if not tonumber(t.port) then 57 | err = log_fmt("invalid t1k port: %s", t.port) 58 | return nil, err, nil 59 | end 60 | opts.port = tonumber(t.port) 61 | end 62 | 63 | opts.connect_timeout = t.connect_timeout or DEFAULT_T1K_CONNECT_TIMEOUT 64 | opts.send_timeout = t.send_timeout or DEFAULT_T1K_SEND_TIMEOUT 65 | opts.read_timeout = t.read_timeout or DEFAULT_T1K_READ_TIMEOUT 66 | opts.req_body_size = t.req_body_size or DEFAULT_T1K_REQ_BODY_SIZE 67 | opts.keepalive_size = t.keepalive_size or DEFAULT_T1K_KEEPALIVE_SIZE 68 | opts.keepalive_timeout = t.keepalive_timeout or DEFAULT_T1K_KEEPALIVE_TIMEOUT 69 | 70 | if t.remote_addr then 71 | local var, idx = utils.to_var_idx(t.remote_addr) 72 | opts.remote_addr_var = var 73 | opts.remote_addr_idx = idx 74 | end 75 | 76 | ok, err, result = request.do_request(opts) 77 | if not ok then 78 | return ok, err, result 79 | end 80 | 81 | if handle and opts.mode == consts.MODE_BLOCK then 82 | ok, err = _M.do_handle(result) 83 | end 84 | 85 | return ok, err, result 86 | end 87 | 88 | function _M.do_handle(t) 89 | local ok, err = handler.handle(t) 90 | return ok, err 91 | end 92 | 93 | function _M.do_header_filter() 94 | filter.do_header_filter() 95 | end 96 | 97 | return _M 98 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/lib/resty/t1k/buffer.lua: -------------------------------------------------------------------------------- 1 | local _M = { 2 | _VERSION = '1.0.0', 3 | } 4 | 5 | function _M:new(o) 6 | o = o or {} 7 | setmetatable(o, self) 8 | self.__index = self 9 | return o 10 | end 11 | 12 | function _M:add(v) 13 | self[#self + 1] = v 14 | end 15 | 16 | function _M:len() 17 | local len = 0 18 | for _, v in ipairs(self) do 19 | len = len + #v 20 | end 21 | return len 22 | end 23 | 24 | return _M 25 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/lib/resty/t1k/constants.lua: -------------------------------------------------------------------------------- 1 | local t = {} 2 | 3 | t.ACTION_PASSED = "." 4 | t.ACTION_BLOCKED = "?" 5 | 6 | t.MODE_OFF = "off" 7 | t.MODE_BLOCK = "block" 8 | t.MODE_MONITOR = "monitor" 9 | 10 | t.T1K_HEADER_SIZE = 5 11 | 12 | t.TAG_HEAD = 0x01 13 | t.TAG_BODY = 0x02 14 | t.TAG_EXTRA = 0x03 15 | t.TAG_VERSION = 0x20 16 | t.TAG_EXTRA_HEADER = 0x23 17 | t.TAG_EXTRA_BODY = 0x24 18 | 19 | t.MASK_FIRST = 0x40 20 | t.MASK_LAST = 0x80 21 | 22 | t.NGX_HTTP_HEADER_PREFIX = "http_" 23 | 24 | t.BLOCK_CONTENT_TYPE = "application/json" 25 | t.BLOCK_CONTENT_FORMAT = [[ 26 | {"code": %s, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "%s"}]] 27 | 28 | t.UNIX_SOCK_PREFIX = "unix:" 29 | 30 | return t 31 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/lib/resty/t1k/file.lua: -------------------------------------------------------------------------------- 1 | local buffer = require "resty.t1k.buffer" 2 | 3 | local _M = { 4 | _VERSION = '1.0.0' 5 | } 6 | 7 | local buffer_size = 2 ^ 13 8 | 9 | function _M.read(p, size) 10 | size = (not size or size < 0) and 0 or size 11 | 12 | local f, err = io.open(p, "rb") 13 | if not f or err then 14 | return nil, err, nil 15 | end 16 | 17 | local left = size 18 | local buf = buffer:new() 19 | 20 | while left ~= 0 do 21 | local block_size = math.min(left, buffer_size) 22 | local block = f:read(block_size) 23 | if not block then 24 | break 25 | end 26 | buf:add(block) 27 | left = math.max(left - block_size, 0) 28 | end 29 | f:close() 30 | 31 | return true, nil, buf 32 | end 33 | 34 | return _M 35 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/lib/resty/t1k/filter.lua: -------------------------------------------------------------------------------- 1 | local _M = { 2 | _VERSION = '1.0.0' 3 | } 4 | 5 | local find = string.find 6 | local sub = string.sub 7 | 8 | local ngx = ngx 9 | 10 | local function parse_extra_header(extra_header) 11 | local t = {} 12 | local idx = 1 13 | 14 | while (idx <= #extra_header) do 15 | local key, val 16 | local _, to = find(extra_header, ":", idx) 17 | if to == 0 then 18 | break 19 | else 20 | key = sub(extra_header, idx, to - 1) 21 | end 22 | 23 | idx = to + 1 24 | _, to = find(extra_header, "\n", idx) 25 | if to == 0 then 26 | break 27 | else 28 | val = sub(extra_header, idx, to - 1) 29 | end 30 | 31 | t[key] = val 32 | idx = to + 1 33 | end 34 | 35 | return t 36 | end 37 | 38 | function _M.do_header_filter() 39 | local extra_header = ngx.ctx.t1k_extra_header 40 | if extra_header ~= nil then 41 | local header_table = parse_extra_header(extra_header) 42 | for k, v in pairs(header_table) do 43 | if k ~= nil and v ~= nil then 44 | ngx.header[k] = v 45 | end 46 | end 47 | end 48 | end 49 | 50 | return _M 51 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/lib/resty/t1k/handler.lua: -------------------------------------------------------------------------------- 1 | local consts = require "resty.t1k.constants" 2 | local log = require "resty.t1k.log" 3 | 4 | local fmt = string.format 5 | 6 | local ngx = ngx 7 | 8 | local log_fmt = log.fmt 9 | 10 | local _M = { 11 | _VERSION = '1.0.0' 12 | } 13 | 14 | function _M.handle(t) 15 | local t_type = type(t) 16 | if t_type ~= "table" then 17 | local err = log_fmt("invalid result type: %s", t_type) 18 | return nil, err 19 | end 20 | 21 | local action = t["action"] 22 | if action == consts.ACTION_PASSED then 23 | return true, nil 24 | elseif action == consts.ACTION_BLOCKED then 25 | ngx.status = t["status"] or ngx.HTTP_FORBIDDEN 26 | ngx.header.content_type = consts.BLOCK_CONTENT_TYPE 27 | ngx.say(fmt(consts.BLOCK_CONTENT_FORMAT, ngx.status, t["event_id"])) 28 | 29 | return ngx.exit(ngx.status) 30 | else 31 | local err = log_fmt("unknown action from t1k server: %s", action) 32 | return nil, err 33 | end 34 | end 35 | 36 | return _M 37 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/lib/resty/t1k/log.lua: -------------------------------------------------------------------------------- 1 | local _M = { 2 | _VERSION = '1.0.0' 3 | } 4 | 5 | local fmt = string.format 6 | 7 | local ERR = ngx.ERR 8 | local WARN = ngx.WARN 9 | local DEBUG = ngx.DEBUG 10 | 11 | function _M.fmt(formatstring, ...) 12 | return fmt("lua-resty-t1k: " .. formatstring, ...) 13 | end 14 | 15 | function _M.err_fmt(formatstring, ...) 16 | return ERR, _M.fmt(formatstring, ...) 17 | end 18 | 19 | function _M.warn_fmt(formatstring, ...) 20 | return WARN, _M.fmt(formatstring, ...) 21 | end 22 | 23 | function _M.debug_fmt(formatstring, ...) 24 | return DEBUG, _M.fmt(formatstring, ...) 25 | end 26 | 27 | return _M 28 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/lib/resty/t1k/utils.lua: -------------------------------------------------------------------------------- 1 | local consts = require "resty.t1k.constants" 2 | 3 | local _M = { 4 | _VERSION = '1.0.0' 5 | } 6 | 7 | local band = bit.band 8 | local bnot = bit.bnot 9 | local lshift = bit.lshift 10 | local rshift = bit.rshift 11 | 12 | local abs = math.abs 13 | local char = string.char 14 | 15 | local ngx = ngx 16 | local ngx_re = ngx.re 17 | local re_match = ngx_re.match 18 | local re_gmatch = ngx_re.gmatch 19 | local re_gsub = ngx_re.gsub 20 | 21 | local NOT_MASK_FIRST = bnot(consts.MASK_FIRST) 22 | local NOT_MASK_LAST = bnot(consts.MASK_LAST) 23 | 24 | function _M.int_to_char_length(x) 25 | return char(band(x, 0xff)) .. char(band(rshift(x, 8), 0xff)) .. 26 | char(band(rshift(x, 16), 0xff)) .. char(band(rshift(x, 24), 0xff)) 27 | end 28 | 29 | function _M.char_to_int_length(l) 30 | return l:byte(1, 1) + lshift(l:byte(2, 2), 8) + lshift(l:byte(3, 3), 16) + lshift(l:byte(4, 4), 24) 31 | end 32 | 33 | function _M.is_mask_first(b) 34 | return band(b, consts.MASK_FIRST) == consts.MASK_FIRST 35 | end 36 | 37 | function _M.is_mask_last(b) 38 | return band(b, consts.MASK_LAST) == consts.MASK_LAST 39 | end 40 | 41 | local function tag_parser(tag) 42 | return band(band(tag, NOT_MASK_FIRST), NOT_MASK_LAST) 43 | end 44 | 45 | function _M.packet_parser(header) 46 | if #header ~= consts.T1K_HEADER_SIZE then 47 | return true, nil, 0 48 | end 49 | 50 | local fb = header:byte(1, 1) 51 | local finished = _M.is_mask_last(fb) 52 | local tag = tag_parser(fb) 53 | local length = _M.char_to_int_length(header:sub(2, 5)) 54 | 55 | return finished, tag, length 56 | end 57 | 58 | function _M.starts_with(str, start) 59 | return str:sub(1, #start) == start 60 | end 61 | 62 | function _M.to_var_idx(o) 63 | local var = o 64 | local idx 65 | 66 | local _, p = o:find(":") 67 | if p then 68 | var = o:sub(1, p - 1) 69 | idx = tonumber(o:sub(p + 1)) 70 | end 71 | 72 | var = re_gsub(var:lower(), "-", "_") 73 | if not _M.starts_with(var, consts.NGX_HTTP_HEADER_PREFIX) then 74 | var = consts.NGX_HTTP_HEADER_PREFIX .. var 75 | end 76 | 77 | return var, idx 78 | end 79 | 80 | function _M.get_indexed_element(str, idx) 81 | if not str or not idx or idx == 0 then 82 | return str 83 | end 84 | 85 | local it, err = re_gmatch(str, [[([^,\s]+)]], "jo") 86 | if err then 87 | return nil 88 | end 89 | 90 | local t = {} 91 | for m, e in it do 92 | if e then 93 | return nil 94 | end 95 | table.insert(t, m[1]) 96 | end 97 | 98 | local len = #t 99 | if len < abs(idx) then 100 | return nil 101 | end 102 | 103 | return t[idx > 0 and idx or (len + idx + 1)] 104 | end 105 | 106 | function _M.get_event_id(str) 107 | if not str then 108 | return nil 109 | end 110 | 111 | local m, err = re_match(str, [[]], "jo") 112 | if err then 113 | return nil 114 | end 115 | 116 | if m then 117 | return m[1] 118 | end 119 | 120 | return nil 121 | end 122 | 123 | return _M 124 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/lib/resty/t1k/uuid.lua: -------------------------------------------------------------------------------- 1 | local bit = require "bit" 2 | local ffi = require "ffi" 3 | 4 | local log = require "resty.t1k.log" 5 | 6 | local _M = { 7 | _VERSION = '1.0.0' 8 | } 9 | 10 | local C = ffi.C 11 | local N_BYTES = 32 12 | local random = math.random 13 | 14 | local nlog = ngx.log 15 | 16 | local warn_fmt = log.warn_fmt 17 | 18 | ffi.cdef [[ 19 | int RAND_bytes(unsigned char *buf, int num); 20 | ]] 21 | 22 | local function _rand_bytes(buf, len) 23 | return C.RAND_bytes(buf, len) 24 | end 25 | 26 | local function rand_bytes(len) 27 | local buf = ffi.new("char[?]", len) 28 | local ok, ret = pcall(_rand_bytes, buf, len) 29 | 30 | if not ok or ret ~= 1 then 31 | nlog(warn_fmt("call RAND_bytes failed: %s", ret)) 32 | return nil 33 | end 34 | 35 | return ffi.string(buf, len) 36 | end 37 | 38 | do 39 | local band = bit.band 40 | local bor = bit.bor 41 | local tohex = bit.tohex 42 | 43 | local fmt = string.format 44 | local byte = string.byte 45 | 46 | function _M.generate_v4() 47 | local bytes = rand_bytes(N_BYTES) 48 | 49 | -- fallback to math.random based method 50 | if not bytes then 51 | return (fmt('%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s', 52 | tohex(random(0, 255), 2), 53 | tohex(random(0, 255), 2), 54 | tohex(random(0, 255), 2), 55 | tohex(random(0, 255), 2), 56 | 57 | tohex(random(0, 255), 2), 58 | tohex(random(0, 255), 2), 59 | 60 | tohex(bor(band(random(0, 255), 0x0F), 0x40), 2), 61 | tohex(random(0, 255), 2), 62 | 63 | tohex(bor(band(random(0, 255), 0x3F), 0x80), 2), 64 | tohex(random(0, 255), 2), 65 | 66 | tohex(random(0, 255), 2), 67 | tohex(random(0, 255), 2), 68 | tohex(random(0, 255), 2), 69 | tohex(random(0, 255), 2), 70 | tohex(random(0, 255), 2), 71 | tohex(random(0, 255), 2))) 72 | end 73 | 74 | return fmt('%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s', 75 | tohex(byte(bytes, 1, 2), 2), 76 | tohex(byte(bytes, 3, 4), 2), 77 | tohex(byte(bytes, 5, 6), 2), 78 | tohex(byte(bytes, 7, 8), 2), 79 | 80 | tohex(byte(bytes, 9, 10), 2), 81 | tohex(byte(bytes, 11, 12), 2), 82 | 83 | tohex(bor(band(byte(bytes, 13, 14), 0x0F), 0x40), 2), 84 | tohex(byte(bytes, 15, 16), 2), 85 | 86 | tohex(bor(band(byte(bytes, 17, 18), 0x3F), 0x80), 2), 87 | tohex(byte(bytes, 19, 20), 2), 88 | 89 | tohex(byte(bytes, 21, 22), 2), 90 | tohex(byte(bytes, 23, 24), 2), 91 | tohex(byte(bytes, 25, 26), 2), 92 | tohex(byte(bytes, 27, 28), 2), 93 | tohex(byte(bytes, 29, 30), 2), 94 | tohex(byte(bytes, 31, 32), 2)) 95 | end 96 | end 97 | 98 | return setmetatable(_M, { 99 | __call = _M.generate_v4 100 | }) 101 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/mainspec/lua-resty-t1k-main-0-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-t1k-main" 2 | version = "0-0" 3 | source = { 4 | url = "git://github.com/chaitin/lua-resty-t1k", 5 | branch = "main", 6 | } 7 | 8 | description = { 9 | summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall", 10 | detailed = [[ 11 | Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall. 12 | ]], 13 | homepage = "https://github.com/chaitin/lua-resty-t1k", 14 | license = "Apache License 2.0", 15 | maintainer = "Xudong Wang " 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["resty.t1k"] = "lib/resty/t1k.lua", 22 | ["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua", 23 | ["resty.t1k.constants"] = "lib/resty/t1k/constants.lua", 24 | ["resty.t1k.file"] = "lib/resty/t1k/file.lua", 25 | ["resty.t1k.filter"] = "lib/resty/t1k/filter.lua", 26 | ["resty.t1k.handler"] = "lib/resty/t1k/handler.lua", 27 | ["resty.t1k.log"] = "lib/resty/t1k/log.lua", 28 | ["resty.t1k.request"] = "lib/resty/t1k/request.lua", 29 | ["resty.t1k.utils"] = "lib/resty/t1k/utils.lua", 30 | ["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua", 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.0.0-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-t1k" 2 | version = "1.0.0-0" 3 | source = { 4 | url = "git://github.com/chaitin/lua-resty-t1k", 5 | tag = "v1.0.0" 6 | } 7 | 8 | description = { 9 | summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall", 10 | detailed = [[ 11 | Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall. 12 | ]], 13 | homepage = "https://github.com/chaitin/lua-resty-t1k", 14 | license = "Apache License 2.0", 15 | maintainer = "Xudong Wang " 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["resty.t1k"] = "lib/resty/t1k.lua", 22 | ["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua", 23 | ["resty.t1k.constants"] = "lib/resty/t1k/constants.lua", 24 | ["resty.t1k.file"] = "lib/resty/t1k/file.lua", 25 | ["resty.t1k.filter"] = "lib/resty/t1k/filter.lua", 26 | ["resty.t1k.handler"] = "lib/resty/t1k/handler.lua", 27 | ["resty.t1k.log"] = "lib/resty/t1k/log.lua", 28 | ["resty.t1k.request"] = "lib/resty/t1k/request.lua", 29 | ["resty.t1k.utils"] = "lib/resty/t1k/utils.lua", 30 | ["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua", 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.0.1-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-t1k" 2 | version = "1.0.1-0" 3 | source = { 4 | url = "git://github.com/chaitin/lua-resty-t1k", 5 | tag = "v1.0.1" 6 | } 7 | 8 | description = { 9 | summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall", 10 | detailed = [[ 11 | Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall. 12 | ]], 13 | homepage = "https://github.com/chaitin/lua-resty-t1k", 14 | license = "Apache License 2.0", 15 | maintainer = "Xudong Wang " 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["resty.t1k"] = "lib/resty/t1k.lua", 22 | ["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua", 23 | ["resty.t1k.constants"] = "lib/resty/t1k/constants.lua", 24 | ["resty.t1k.file"] = "lib/resty/t1k/file.lua", 25 | ["resty.t1k.filter"] = "lib/resty/t1k/filter.lua", 26 | ["resty.t1k.handler"] = "lib/resty/t1k/handler.lua", 27 | ["resty.t1k.log"] = "lib/resty/t1k/log.lua", 28 | ["resty.t1k.request"] = "lib/resty/t1k/request.lua", 29 | ["resty.t1k.utils"] = "lib/resty/t1k/utils.lua", 30 | ["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua", 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.0.2-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-t1k" 2 | version = "1.0.2-0" 3 | source = { 4 | url = "git://github.com/chaitin/lua-resty-t1k", 5 | tag = "v1.0.2" 6 | } 7 | 8 | description = { 9 | summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall", 10 | detailed = [[ 11 | Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall. 12 | ]], 13 | homepage = "https://github.com/chaitin/lua-resty-t1k", 14 | license = "Apache License 2.0", 15 | maintainer = "Xudong Wang " 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["resty.t1k"] = "lib/resty/t1k.lua", 22 | ["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua", 23 | ["resty.t1k.constants"] = "lib/resty/t1k/constants.lua", 24 | ["resty.t1k.file"] = "lib/resty/t1k/file.lua", 25 | ["resty.t1k.filter"] = "lib/resty/t1k/filter.lua", 26 | ["resty.t1k.handler"] = "lib/resty/t1k/handler.lua", 27 | ["resty.t1k.log"] = "lib/resty/t1k/log.lua", 28 | ["resty.t1k.request"] = "lib/resty/t1k/request.lua", 29 | ["resty.t1k.utils"] = "lib/resty/t1k/utils.lua", 30 | ["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua", 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.0.3-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-t1k" 2 | version = "1.0.3-0" 3 | source = { 4 | url = "git://github.com/chaitin/lua-resty-t1k", 5 | tag = "v1.0.3" 6 | } 7 | 8 | description = { 9 | summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall", 10 | detailed = [[ 11 | Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall. 12 | ]], 13 | homepage = "https://github.com/chaitin/lua-resty-t1k", 14 | license = "Apache License 2.0", 15 | maintainer = "Xudong Wang " 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["resty.t1k"] = "lib/resty/t1k.lua", 22 | ["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua", 23 | ["resty.t1k.constants"] = "lib/resty/t1k/constants.lua", 24 | ["resty.t1k.file"] = "lib/resty/t1k/file.lua", 25 | ["resty.t1k.filter"] = "lib/resty/t1k/filter.lua", 26 | ["resty.t1k.handler"] = "lib/resty/t1k/handler.lua", 27 | ["resty.t1k.log"] = "lib/resty/t1k/log.lua", 28 | ["resty.t1k.request"] = "lib/resty/t1k/request.lua", 29 | ["resty.t1k.utils"] = "lib/resty/t1k/utils.lua", 30 | ["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua", 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.0-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-t1k" 2 | version = "1.1.0-0" 3 | source = { 4 | url = "git://github.com/chaitin/lua-resty-t1k", 5 | tag = "v1.1.0" 6 | } 7 | 8 | description = { 9 | summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall", 10 | detailed = [[ 11 | Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall. 12 | ]], 13 | homepage = "https://github.com/chaitin/lua-resty-t1k", 14 | license = "Apache License 2.0", 15 | maintainer = "Xudong Wang " 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["resty.t1k"] = "lib/resty/t1k.lua", 22 | ["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua", 23 | ["resty.t1k.constants"] = "lib/resty/t1k/constants.lua", 24 | ["resty.t1k.file"] = "lib/resty/t1k/file.lua", 25 | ["resty.t1k.filter"] = "lib/resty/t1k/filter.lua", 26 | ["resty.t1k.handler"] = "lib/resty/t1k/handler.lua", 27 | ["resty.t1k.log"] = "lib/resty/t1k/log.lua", 28 | ["resty.t1k.request"] = "lib/resty/t1k/request.lua", 29 | ["resty.t1k.utils"] = "lib/resty/t1k/utils.lua", 30 | ["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua", 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.1-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-t1k" 2 | version = "1.1.1-0" 3 | source = { 4 | url = "git://github.com/chaitin/lua-resty-t1k", 5 | tag = "v1.1.1" 6 | } 7 | 8 | description = { 9 | summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall", 10 | detailed = [[ 11 | Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall. 12 | ]], 13 | homepage = "https://github.com/chaitin/lua-resty-t1k", 14 | license = "Apache License 2.0", 15 | maintainer = "Xudong Wang " 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["resty.t1k"] = "lib/resty/t1k.lua", 22 | ["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua", 23 | ["resty.t1k.constants"] = "lib/resty/t1k/constants.lua", 24 | ["resty.t1k.file"] = "lib/resty/t1k/file.lua", 25 | ["resty.t1k.filter"] = "lib/resty/t1k/filter.lua", 26 | ["resty.t1k.handler"] = "lib/resty/t1k/handler.lua", 27 | ["resty.t1k.log"] = "lib/resty/t1k/log.lua", 28 | ["resty.t1k.request"] = "lib/resty/t1k/request.lua", 29 | ["resty.t1k.utils"] = "lib/resty/t1k/utils.lua", 30 | ["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua", 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.2-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-t1k" 2 | version = "1.1.2-0" 3 | source = { 4 | url = "git://github.com/chaitin/lua-resty-t1k", 5 | tag = "v1.1.2" 6 | } 7 | 8 | description = { 9 | summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall", 10 | detailed = [[ 11 | Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall. 12 | ]], 13 | homepage = "https://github.com/chaitin/lua-resty-t1k", 14 | license = "Apache License 2.0", 15 | maintainer = "Xudong Wang " 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["resty.t1k"] = "lib/resty/t1k.lua", 22 | ["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua", 23 | ["resty.t1k.constants"] = "lib/resty/t1k/constants.lua", 24 | ["resty.t1k.file"] = "lib/resty/t1k/file.lua", 25 | ["resty.t1k.filter"] = "lib/resty/t1k/filter.lua", 26 | ["resty.t1k.handler"] = "lib/resty/t1k/handler.lua", 27 | ["resty.t1k.log"] = "lib/resty/t1k/log.lua", 28 | ["resty.t1k.request"] = "lib/resty/t1k/request.lua", 29 | ["resty.t1k.utils"] = "lib/resty/t1k/utils.lua", 30 | ["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua", 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.3-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-t1k" 2 | version = "1.1.3-0" 3 | source = { 4 | url = "git://github.com/chaitin/lua-resty-t1k", 5 | tag = "v1.1.3" 6 | } 7 | 8 | description = { 9 | summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall", 10 | detailed = [[ 11 | Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall. 12 | ]], 13 | homepage = "https://github.com/chaitin/lua-resty-t1k", 14 | license = "Apache License 2.0", 15 | maintainer = "Xudong Wang " 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["resty.t1k"] = "lib/resty/t1k.lua", 22 | ["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua", 23 | ["resty.t1k.constants"] = "lib/resty/t1k/constants.lua", 24 | ["resty.t1k.file"] = "lib/resty/t1k/file.lua", 25 | ["resty.t1k.filter"] = "lib/resty/t1k/filter.lua", 26 | ["resty.t1k.handler"] = "lib/resty/t1k/handler.lua", 27 | ["resty.t1k.log"] = "lib/resty/t1k/log.lua", 28 | ["resty.t1k.request"] = "lib/resty/t1k/request.lua", 29 | ["resty.t1k.utils"] = "lib/resty/t1k/utils.lua", 30 | ["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua", 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.4-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-t1k" 2 | version = "1.1.4-0" 3 | source = { 4 | url = "git://github.com/chaitin/lua-resty-t1k", 5 | tag = "v1.1.4" 6 | } 7 | 8 | description = { 9 | summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall", 10 | detailed = [[ 11 | Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall. 12 | ]], 13 | homepage = "https://github.com/chaitin/lua-resty-t1k", 14 | license = "Apache License 2.0", 15 | maintainer = "Xudong Wang " 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["resty.t1k"] = "lib/resty/t1k.lua", 22 | ["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua", 23 | ["resty.t1k.constants"] = "lib/resty/t1k/constants.lua", 24 | ["resty.t1k.file"] = "lib/resty/t1k/file.lua", 25 | ["resty.t1k.filter"] = "lib/resty/t1k/filter.lua", 26 | ["resty.t1k.handler"] = "lib/resty/t1k/handler.lua", 27 | ["resty.t1k.log"] = "lib/resty/t1k/log.lua", 28 | ["resty.t1k.request"] = "lib/resty/t1k/request.lua", 29 | ["resty.t1k.utils"] = "lib/resty/t1k/utils.lua", 30 | ["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua", 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.5-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-t1k" 2 | version = "1.1.5-0" 3 | source = { 4 | url = "git://github.com/chaitin/lua-resty-t1k", 5 | tag = "v1.1.5" 6 | } 7 | 8 | description = { 9 | summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall", 10 | detailed = [[ 11 | Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall. 12 | ]], 13 | homepage = "https://github.com/chaitin/lua-resty-t1k", 14 | license = "Apache License 2.0", 15 | maintainer = "Xudong Wang " 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["resty.t1k"] = "lib/resty/t1k.lua", 22 | ["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua", 23 | ["resty.t1k.constants"] = "lib/resty/t1k/constants.lua", 24 | ["resty.t1k.file"] = "lib/resty/t1k/file.lua", 25 | ["resty.t1k.filter"] = "lib/resty/t1k/filter.lua", 26 | ["resty.t1k.handler"] = "lib/resty/t1k/handler.lua", 27 | ["resty.t1k.log"] = "lib/resty/t1k/log.lua", 28 | ["resty.t1k.request"] = "lib/resty/t1k/request.lua", 29 | ["resty.t1k.utils"] = "lib/resty/t1k/utils.lua", 30 | ["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua", 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/t/buffer.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket; 2 | 3 | our $HttpConfig = <<'_EOC_'; 4 | lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; 5 | _EOC_ 6 | 7 | repeat_each(3); 8 | 9 | plan tests => repeat_each() * (blocks() * 3); 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: buffer add 16 | --- http_config eval: $::HttpConfig 17 | --- config 18 | location /t { 19 | content_by_lua_block { 20 | local buffer = require "resty.t1k.buffer" 21 | local b = buffer:new() 22 | b:add("hello") 23 | b:add(" ") 24 | b:add("world") 25 | b:add("!") 26 | ngx.say(b[1], b[2], b[3], b[4]) 27 | ngx.say(b:len()) 28 | } 29 | } 30 | --- request 31 | GET /t 32 | --- response_body 33 | hello world! 34 | 12 35 | --- no_error_log 36 | [error] 37 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/t/file.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket; 2 | 3 | our $HttpConfig = <<'_EOC_'; 4 | lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; 5 | _EOC_ 6 | 7 | repeat_each(3); 8 | 9 | plan tests => repeat_each() * (blocks() * 3); 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: read full file 16 | --- http_config eval: $::HttpConfig 17 | --- config 18 | location /t { 19 | content_by_lua_block { 20 | local file = require "resty.t1k.file" 21 | local path = ngx.var.document_root .. "/foo.bar" 22 | local ok, err, content = file.read(path, 2 ^ 15) 23 | if not ok then 24 | ngx.say(err) 25 | end 26 | ngx.print(table.concat(content)) 27 | } 28 | } 29 | --- user_files eval 30 | [ 31 | ["foo.bar" => "a" x (2 ** 14) ], 32 | ] 33 | --- request 34 | GET /t 35 | --- response_body eval 36 | "a" x (2 ** 14) 37 | --- no_error_log 38 | [error] 39 | 40 | 41 | 42 | === TEST 2: read partial file 43 | --- http_config eval: $::HttpConfig 44 | --- config 45 | location /t { 46 | content_by_lua_block { 47 | local file = require "resty.t1k.file" 48 | local path = ngx.var.document_root .. "/foo.bar" 49 | local ok, err, content = file.read(path, 2 ^ 13) 50 | if not ok then 51 | ngx.say(err) 52 | end 53 | ngx.print(table.concat(content)) 54 | } 55 | } 56 | --- user_files eval 57 | [ 58 | ["foo.bar" => "a" x (2 ** 14) ], 59 | ] 60 | --- request 61 | GET /t 62 | --- response_body eval 63 | "a" x (2 ** 13) 64 | --- no_error_log 65 | [error] 66 | 67 | 68 | 69 | === TEST 3: read negative bytes 70 | --- http_config eval: $::HttpConfig 71 | --- config 72 | location /t { 73 | content_by_lua_block { 74 | local file = require "resty.t1k.file" 75 | local path = ngx.var.document_root .. "/foo.bar" 76 | local ok, err, content = file.read(path, -1) 77 | if not ok then 78 | ngx.say(err) 79 | end 80 | ngx.print(table.concat(content)) 81 | } 82 | } 83 | --- user_files 84 | >>> foo.bar 85 | --- request 86 | GET /t 87 | --- response_body eval 88 | "" 89 | --- no_error_log 90 | [error] 91 | 92 | 93 | 94 | === TEST 4: read empty file 95 | --- http_config eval: $::HttpConfig 96 | --- config 97 | location /t { 98 | content_by_lua_block { 99 | local file = require "resty.t1k.file" 100 | local path = ngx.var.document_root .. "/foo.bar" 101 | local ok, err, content = file.read(path, 1) 102 | if not ok then 103 | ngx.say(err) 104 | end 105 | ngx.print(table.concat(content)) 106 | } 107 | } 108 | --- user_files 109 | >>> foo.bar 110 | --- request 111 | GET /t 112 | --- response_body eval 113 | "" 114 | --- no_error_log 115 | [error] 116 | 117 | 118 | 119 | === TEST 5: read non-existent file 120 | --- http_config eval: $::HttpConfig 121 | --- config 122 | location /t { 123 | content_by_lua_block { 124 | local file = require "resty.t1k.file" 125 | local ok, err, buffer = file.read("/opt/non_existent_file", 0) 126 | if not ok then 127 | ngx.say(err) 128 | end 129 | } 130 | } 131 | --- request 132 | GET /t 133 | --- response_body 134 | /opt/non_existent_file: No such file or directory 135 | --- no_error_log 136 | [error] 137 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/t/filter.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket; 2 | 3 | our $HttpConfig = <<'_EOC_'; 4 | lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; 5 | _EOC_ 6 | 7 | repeat_each(3); 8 | 9 | plan tests => repeat_each() * (blocks() * 5); 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: do_header_filter 16 | --- http_config eval: $::HttpConfig 17 | --- config 18 | location /t { 19 | access_by_lua_block { 20 | ngx.ctx.t1k_extra_header = "k1:v1\nk2:v2\nk3:v3\n" 21 | } 22 | 23 | header_filter_by_lua_block { 24 | local filter = require "resty.t1k.filter" 25 | filter.do_header_filter() 26 | } 27 | 28 | content_by_lua_block { 29 | ngx.say("hi") 30 | } 31 | } 32 | --- request 33 | GET /t 34 | --- response_headers 35 | k1: v1 36 | k2: v2 37 | k3: v3 38 | --- no_error_log 39 | [error] 40 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/t/handler.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket; 2 | 3 | our $HttpConfig = <<'_EOC_'; 4 | lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; 5 | _EOC_ 6 | 7 | repeat_each(3); 8 | 9 | plan tests => repeat_each() * (blocks() * 3); 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: handle passed action 16 | --- http_config eval: $::HttpConfig 17 | --- config 18 | location /t { 19 | access_by_lua_block { 20 | local handler = require "resty.t1k.handler" 21 | 22 | local t = { 23 | action = ".", 24 | } 25 | 26 | local ok, err = handler.handle(t) 27 | if not ok then 28 | ngx.log(ngx.ERR, err) 29 | end 30 | } 31 | 32 | content_by_lua_block { 33 | ngx.say("passed") 34 | } 35 | } 36 | --- request 37 | GET /t 38 | --- response_body 39 | passed 40 | --- no_error_log 41 | [error] 42 | 43 | 44 | 45 | === TEST 2: handle blocked action 46 | --- http_config eval: $::HttpConfig 47 | --- config 48 | location /t { 49 | access_by_lua_block { 50 | local handler = require "resty.t1k.handler" 51 | 52 | local t = { 53 | action = "?", 54 | status = 405, 55 | event_id = "c0c039a7c348486eaffd9e2f9846b66b", 56 | } 57 | 58 | local ok, err = handler.handle(t) 59 | if not ok then 60 | ngx.log(ngx.ERR, err) 61 | end 62 | } 63 | 64 | header_filter_by_lua_block { 65 | local filter = require "resty.t1k.filter" 66 | filter.do_header_filter() 67 | } 68 | 69 | content_by_lua_block { 70 | ngx.say("passed") 71 | } 72 | } 73 | --- request 74 | GET /t 75 | --- response_body 76 | {"code": 405, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "c0c039a7c348486eaffd9e2f9846b66b"} 77 | --- error_code eval 78 | "405" 79 | --- no_error_log 80 | [error] 81 | --- no_error_log 82 | [error] 83 | 84 | 85 | 86 | === TEST 3: handle unknown action 87 | --- http_config eval: $::HttpConfig 88 | --- config 89 | location /t { 90 | access_by_lua_block { 91 | local handler = require "resty.t1k.handler" 92 | 93 | local t = { 94 | action = "~" 95 | } 96 | 97 | local ok, err = handler.handle(t) 98 | if not ok then 99 | ngx.log(ngx.ERR, err) 100 | end 101 | } 102 | 103 | content_by_lua_block { 104 | ngx.say("passed") 105 | } 106 | } 107 | --- request 108 | GET /t 109 | --- response_body 110 | passed 111 | --- error_log 112 | lua-resty-t1k: unknown action from t1k server: ~ 113 | 114 | 115 | 116 | === TEST 4: handle nil result 117 | --- http_config eval: $::HttpConfig 118 | --- config 119 | location /t { 120 | access_by_lua_block { 121 | local handler = require "resty.t1k.handler" 122 | 123 | local ok, err = handler.handle(nil) 124 | if not ok then 125 | ngx.log(ngx.ERR, err) 126 | end 127 | } 128 | 129 | content_by_lua_block { 130 | ngx.say("passed") 131 | } 132 | } 133 | --- request 134 | GET /t 135 | --- response_body 136 | passed 137 | --- error_log 138 | lua-resty-t1k: invalid result type: nil 139 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/t/log.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket; 2 | 3 | our $HttpConfig = <<'_EOC_'; 4 | lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; 5 | _EOC_ 6 | 7 | repeat_each(3); 8 | 9 | plan tests => repeat_each() * (blocks() * 2 + 2); 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: err_fmt 16 | --- http_config eval: $::HttpConfig 17 | --- config 18 | location /t { 19 | content_by_lua_block { 20 | local log = require "resty.t1k.log" 21 | ngx.log(log.err_fmt("%s - %04d - %.4f", "test", 1, 1)) 22 | } 23 | } 24 | --- request 25 | GET /t 26 | [error] 27 | --- error_log 28 | lua-resty-t1k: test - 0001 - 1.0000 29 | --- log_level: error 30 | 31 | 32 | 33 | === TEST 2: warn_fmt 34 | --- http_config eval: $::HttpConfig 35 | --- config 36 | location /t { 37 | content_by_lua_block { 38 | local log = require "resty.t1k.log" 39 | ngx.log(log.warn_fmt("%s - %04d - %.4f", "test", 1, 1)) 40 | } 41 | } 42 | --- request 43 | GET /t 44 | --- no_error_log 45 | [error] 46 | --- error_log 47 | lua-resty-t1k: test - 0001 - 1.0000 48 | --- log_level: warn 49 | 50 | 51 | 52 | === TEST 3: debug_fmt 53 | --- http_config eval: $::HttpConfig 54 | --- config 55 | location /t { 56 | content_by_lua_block { 57 | local log = require "resty.t1k.log" 58 | ngx.log(log.debug_fmt("%s - %04d - %.4f", "test", 1, 1)) 59 | } 60 | } 61 | --- request 62 | GET /t 63 | --- no_error_log 64 | [error] 65 | --- error_log 66 | lua-resty-t1k: test - 0001 - 1.0000 67 | --- log_level: debug 68 | -------------------------------------------------------------------------------- /sdk/lua-resty-t1k/t/uuid.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket; 2 | 3 | our $HttpConfig = <<'_EOC_'; 4 | lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; 5 | _EOC_ 6 | 7 | repeat_each(3); 8 | 9 | plan tests => repeat_each() * (blocks() * 3); 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: generate_v4 16 | --- http_config eval: $::HttpConfig 17 | --- config 18 | location /t { 19 | content_by_lua_block { 20 | local uuid = require "resty.t1k.uuid" 21 | ngx.say(uuid.generate_v4()) 22 | } 23 | } 24 | --- request 25 | GET /t 26 | --- response_body_like 27 | ^[0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}$ 28 | --- no_error_log 29 | [error] 30 | -------------------------------------------------------------------------------- /yanshi/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.dot 3 | *.output 4 | /build 5 | /src/parser.cc 6 | /src/parser.hh 7 | /src/lexer.cc 8 | /src/lexer.hh 9 | -------------------------------------------------------------------------------- /yanshi/Makefile: -------------------------------------------------------------------------------- 1 | CPPFLAGS := -g3 -std=c++1y -Isrc -I. -DHAVE_READLINE 2 | 3 | ifeq ($(build),release) 4 | BUILD := release 5 | CPPFLAGS += -Os 6 | else 7 | BUILD := build 8 | CPPFLAGS += -fsanitize=undefined,address -DDEBUG 9 | LDLIBS := -lasan -lubsan 10 | endif 11 | 12 | LDLIBS += -licuuc -lreadline 13 | SRC := $(filter-out src/lexer.cc src/parser.cc, $(wildcard src/*.cc)) src/lexer.cc src/parser.cc 14 | OBJ := $(addprefix $(BUILD)/,$(subst src/,,$(SRC:.cc=.o))) 15 | UNITTEST_SRC := $(wildcard unittest/*.cc) 16 | UNITTEST_EXE := $(subst unittest/,,$(UNITTEST_SRC:.cc=)) 17 | 18 | all: $(BUILD)/yanshi # unittest 19 | 20 | unittest: $(addprefix $(BUILD)/unittest/,$(UNITTEST_EXE)) 21 | $(foreach x,$(addprefix $(BUILD)/unittest/,$(UNITTEST_EXE)),$x && ) : 22 | 23 | sinclude $(OBJ:.o=.d) 24 | 25 | # FIXME 26 | $(BUILD)/repl.o: src/lexer.hh 27 | 28 | $(BUILD) $(BUILD)/unittest: 29 | mkdir -p $@ 30 | 31 | $(BUILD)/yanshi: $(OBJ) 32 | $(LINK.cc) $^ $(LDLIBS) -o $@ 33 | 34 | $(BUILD)/%.o: src/%.cc | $(BUILD) 35 | $(CXX) $(CPPFLAGS) -MM -MP -MT $@ -MF $(@:.o=.d) $< 36 | $(COMPILE.cc) $< -o $@ 37 | 38 | $(BUILD)/unittest/%: unittest/%.cc $(wildcard unittest/*.hh) $(filter-out $(BUILD)/main.o,$(OBJ)) | $(BUILD)/unittest 39 | $(CXX) $(CPPFLAGS) -MM -MP -MT $@ -MF $(@:.o=.d) $< 40 | $(LINK.cc) $(filter-out %.hh,$^) $(LDLIBS) -o $@ 41 | 42 | src/lexer.cc src/lexer.hh: src/lexer.l 43 | flex --header-file=src/lexer.hh -o src/lexer.cc $< 44 | 45 | src/parser.cc src/parser.hh: src/parser.y src/common.hh src/location.hh src/option.hh src/syntax.hh 46 | bison --defines=src/parser.hh -o src/parser.cc $< 47 | 48 | $(BUILD)/loader.o: src/parser.hh 49 | $(BUILD)/parser.o: src/lexer.hh 50 | $(BUILD)/lexer.o: src/parser.hh 51 | 52 | clean: 53 | $(RM) -r build release 54 | 55 | distclean: clean 56 | $(RM) src/{lexer,parser}.{cc,hh} 57 | 58 | .PHONY: all clean distclean 59 | -------------------------------------------------------------------------------- /yanshi/contrib/vim/compiler/yanshi.vim: -------------------------------------------------------------------------------- 1 | "if exists('current_compiler') 2 | " finish 3 | "endif 4 | let current_compiler = 'yanshi' 5 | 6 | if exists(':CompilerSet') != 2 7 | command -nargs=* CompilerSet setlocal 8 | endif 9 | CompilerSet errorformat= 10 | \%E%f\ %l:%c-%*\\d\ error\ %m, 11 | \%E%f\ %l-%*\\d:%c-%*\\d\ error\ %m, 12 | \%W%f\ %l:%c-%*\\d\ warning\ %m, 13 | \%W%f\ %l-%*\\d:%c-%*\\d\ warning\ %m, 14 | \%C%.%# 15 | CompilerSet makeprg=yanshi\ -d0\ -c\ $*\ % 16 | -------------------------------------------------------------------------------- /yanshi/contrib/vim/ftdetect/yanshi.vim: -------------------------------------------------------------------------------- 1 | au BufRead,BufNewFile *.yanshi setf yanshi 2 | au BufRead,BufNewFile *.ys setf yanshi 3 | -------------------------------------------------------------------------------- /yanshi/contrib/vim/ftplugin/yanshi.vim: -------------------------------------------------------------------------------- 1 | if exists('b:did_ftplugin') 2 | finish 3 | endif 4 | let b:did_ftplugin = 1 5 | 6 | compiler yanshi 7 | -------------------------------------------------------------------------------- /yanshi/contrib/vim/syntax_checkers/yanshi/yanshi.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_syntastic_yanshi_yanshi_checker') 2 | finish 3 | endif 4 | let g:loaded_syntastic_yanshi_yanshi_checker = 1 5 | 6 | let s:save_cpo = &cpo 7 | set cpo&vim 8 | 9 | fu! SyntaxCheckers_yanshi_yanshi_GetLocList() dict 10 | let makeprg = self.makeprgBuild({ 'args': '-d0 -c' }) 11 | 12 | let errorformat = 13 | \ '%C %.%#,'. 14 | \ '%E%f %l:%c-%*\d error %m,'. 15 | \ '%E%f %l-%*\d:%c-%*\d error %m,'. 16 | \ '%W%f %l:%c-%*\d warning %m,'. 17 | \ '%W%f %l-%*\d:%c-%*\d warning %m' 18 | 19 | return SyntasticMake({ 20 | \ 'makeprg': makeprg, 21 | \ 'errorformat': errorformat }) 22 | endf 23 | 24 | call g:SyntasticRegistry.CreateAndRegisterChecker({ 25 | \ 'filetype': 'yanshi', 26 | \ 'name': 'yanshi'}) 27 | 28 | let &cpo = s:save_cpo 29 | unlet s:save_cpo 30 | -------------------------------------------------------------------------------- /yanshi/contrib/zsh/_yanshi: -------------------------------------------------------------------------------- 1 | #compdef yanshi 2 | 3 | _arguments \ 4 | '(-b --bytes)'{-b,--bytes}'[make labels range over \[0,256), Unicode literals will be treated as UTF-8 bytes]' \ 5 | '(-c --check)'{-c,--check}'[check syntax & use/def]' \ 6 | '-C[generate C source code (default: C++)]' \ 7 | '(-d --debug)'{-d,--debug}'+[debug level]:level:(0 1 2 3 4 5)' \ 8 | '--dump-action[dump associated actions for each edge]' \ 9 | '--dump-assoc[dump associated AST Expr for each state]' \ 10 | '--dump-automaton[dump automata]' \ 11 | '--dump-embed[dump statistics of EmbedExpr]' \ 12 | '--dump-module[dump module use/def/...]' \ 13 | '--dump-tree[dump AST]' \ 14 | '(-G --graph)'{-G,--graph}'[output a Graphviz dot file]' \ 15 | '(-I --import)'{-I,--import}'=[add to search path for "import"]' \ 16 | '(-i --interactive)'{-i,--interactive}'[interactive mode]' \ 17 | '(-k --keep-inaccessible)'{-k,--keep-inaccessible}'[do not perform accessible/co-accessible]' \ 18 | '(-l --debug-output)'{-l,--debug-output}'=[filename for debug output]:file:_files' \ 19 | '--max-return-stack=[max length of return stack in C generator]:len:' \ 20 | '(-o --output)'{-o,--output}'=[.cc output filename]:file:_files' \ 21 | '(-O --output-header)'{-O,--output-header}'=[.hh output filename]:file:_files' \ 22 | '(-s --substring-grammar)'{-s,--substring-grammar}'[construct regular approximation of the substring grammar. Inner states of nonterminals labeled 'intact' are not connected to start/final]' \ 23 | '(-h --help)'{-h,--help}'[display this help]' \ 24 | '1:file:_files -g "*.{ys,yanshi}"'\ 25 | -------------------------------------------------------------------------------- /yanshi/src/compiler.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "fsa_anno.hh" 3 | #include "syntax.hh" 4 | 5 | #include 6 | using std::unordered_map; 7 | 8 | void print_assoc(const FsaAnno& anno); 9 | void print_automaton(const Fsa& fsa); 10 | void compile(DefineStmt*); 11 | bool compile_export(DefineStmt* stmt); 12 | void generate_cxx(Module* mo); 13 | void generate_graphviz(Module* mo); 14 | extern unordered_map compiled; 15 | -------------------------------------------------------------------------------- /yanshi/src/fsa.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | using std::function; 6 | using std::pair; 7 | using std::vector; 8 | 9 | typedef pair Label; 10 | typedef pair Edge; 11 | 12 | const Label epsilon{-1L, 0L}; 13 | 14 | struct Fsa { 15 | long start; 16 | vector finals; // sorted 17 | vector> adj; // sorted 18 | 19 | void check() const; 20 | long n() const { return adj.size(); } 21 | bool is_final(long x) const; 22 | bool has(long u, long c) const; 23 | bool has_call(long u) const; 24 | bool has_call_or_collapse(long u) const; 25 | long transit(long u, long c) const; 26 | void epsilon_closure(vector& src) const; 27 | Fsa operator~() const; 28 | // a -> a 29 | void accessible(const vector* starts, function relate); 30 | // a -> a 31 | void co_accessible(const vector* final, function relate); 32 | // DFA -> DFA -> DFA 33 | Fsa intersect(const Fsa& rhs, function relate) const; 34 | // DFA -> DFA -> DFA 35 | Fsa difference(const Fsa& rhs, function relate) const; 36 | // DFA -> DFA 37 | Fsa distinguish(function&)> relate) const; 38 | // * -> DFA 39 | Fsa determinize(const vector* starts, function&)> relate) const; 40 | }; 41 | -------------------------------------------------------------------------------- /yanshi/src/fsa_anno.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "fsa.hh" 3 | #include "syntax.hh" 4 | 5 | enum class ExprTag { 6 | start = 1, 7 | inner = 2, 8 | final = 4, 9 | }; 10 | 11 | extern inline bool has_start(ExprTag x) { return long(x) & long(ExprTag::start); } 12 | extern inline bool has_inner(ExprTag x) { return long(x) & long(ExprTag::inner); } 13 | extern inline bool has_final(ExprTag x) { return long(x) & long(ExprTag::final); } 14 | 15 | bool operator<(ExprTag x, ExprTag y); 16 | bool assoc_has_expr(vector>& as, const Expr* x); 17 | 18 | struct FsaAnno { 19 | bool deterministic; 20 | Fsa fsa; 21 | vector>> assoc; 22 | void accessible(const vector* starts, vector& mapping); 23 | void add_assoc(Expr& expr); 24 | void complement(ComplementExpr* expr); 25 | void co_accessible(const vector* final, vector& mapping); 26 | void concat(FsaAnno& rhs, ConcatExpr* expr); 27 | void determinize(const vector* starts, vector>* mapping); 28 | void difference(FsaAnno& rhs, DifferenceExpr* expr); 29 | void intersect(FsaAnno& rhs, IntersectExpr* expr); 30 | void minimize(vector>* mapping); 31 | void plus(PlusExpr* expr); 32 | void question(QuestionExpr* expr); 33 | void repeat(RepeatExpr& expr); 34 | void star(StarExpr* expr); 35 | void substring_grammar(); 36 | void union_(FsaAnno& rhs, UnionExpr* expr); 37 | static FsaAnno bracket(BracketExpr& expr); 38 | static FsaAnno call(CallExpr& expr); 39 | static FsaAnno collapse(CollapseExpr& expr); 40 | static FsaAnno dot(DotExpr* expr); 41 | static FsaAnno embed(EmbedExpr& expr); 42 | static FsaAnno epsilon_fsa(EpsilonExpr* expr); 43 | static FsaAnno literal(LiteralExpr& expr); 44 | }; 45 | -------------------------------------------------------------------------------- /yanshi/src/lexer_helper.cc: -------------------------------------------------------------------------------- 1 | #include "common.hh" 2 | 3 | #include 4 | #include 5 | using namespace std; 6 | 7 | char* aprintf(const char* fmt, ...) 8 | { 9 | va_list va; 10 | va_start(va, fmt); 11 | char* r = NULL; 12 | vasprintf(&r, fmt, va); 13 | va_end(va); 14 | return r; 15 | } 16 | -------------------------------------------------------------------------------- /yanshi/src/lexer_helper.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | char* aprintf(const char* fmt, ...) 4 | __attribute__((format(printf, 1, 2))); 5 | -------------------------------------------------------------------------------- /yanshi/src/loader.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "syntax.hh" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | using std::map; 10 | using std::set; 11 | using std::string; 12 | using std::unordered_map; 13 | using std::vector; 14 | 15 | enum ModuleStatus { UNPROCESSED = 0, BAD, GOOD }; 16 | 17 | struct Module { 18 | ModuleStatus status; 19 | LocationFile locfile; 20 | string filename; 21 | Stmt* toplevel; 22 | unordered_map defined; 23 | vector unqualified_import; 24 | unordered_map qualified_import; 25 | unordered_map defined_action; 26 | unordered_map macro; 27 | }; 28 | 29 | Stmt* resolve(Module& mo, const string qualified, const string& ident); 30 | long load(const string& filename); 31 | Module* load_module(long& n_errors, const string& filename); 32 | void unload_all(); 33 | extern Module* main_module; 34 | extern FILE *output, *output_header; 35 | extern map> used_as_call, used_as_collapse, used_as_embed; 36 | -------------------------------------------------------------------------------- /yanshi/src/location.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "common.hh" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | struct Location { long start, end; }; 9 | 10 | struct LocationFile { 11 | std::string filename, data; 12 | std::vector linemap; 13 | 14 | LocationFile() = default; 15 | LocationFile(const std::string& filename, const std::string& data); 16 | LocationFile& operator=(const LocationFile&) = default; 17 | void context(const Location& loc) const; 18 | void locate(const Location& loc, long& line1, long& col1, long& line2, long& col2) const; 19 | void report_location(const Location& loc) const; 20 | void error(const Location& loc, const char* fmt, ...) const; 21 | void warning(const Location& loc, const char* fmt, ...) const; 22 | template 23 | void error_context(const Location& loc, const char* fmt, Args&&... args) const { 24 | error(loc, fmt, std::forward(args)...); 25 | context(loc); 26 | } 27 | template 28 | void warning_context(const Location& loc, const char* fmt, Args&&... args) const { 29 | warning(loc, fmt, std::forward(args)...); 30 | context(loc); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /yanshi/src/option.cc: -------------------------------------------------------------------------------- 1 | #include "common.hh" 2 | #include "option.hh" 3 | #include 4 | 5 | bool opt_bytes, opt_check, opt_dump_action, opt_dump_assoc, opt_dump_automaton, opt_dump_embed, opt_dump_module, opt_dump_tree, opt_gen_c, opt_gen_extern_c, opt_keep_inaccessible, opt_standalone, opt_substring_grammar; 6 | 7 | long AB = MAX_CODEPOINT+1, opt_max_return_stack = 100; 8 | long debug_level = 3; 9 | FILE* debug_file; 10 | const char* opt_output_filename = "-"; 11 | const char* opt_output_header_filename; 12 | Mode opt_mode = Mode::cxx; 13 | vector opt_include_paths; 14 | -------------------------------------------------------------------------------- /yanshi/src/option.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | using std::string; 5 | using std::vector; 6 | 7 | extern bool opt_bytes, opt_check, opt_dump_action, opt_dump_assoc, opt_dump_automaton, opt_dump_embed, opt_dump_module, opt_dump_tree, opt_gen_c, opt_gen_extern_c, opt_keep_inaccessible, opt_standalone, opt_substring_grammar; 8 | extern long AB, opt_max_return_stack; 9 | extern const char* opt_output_filename; 10 | extern const char* opt_output_header_filename; 11 | enum class Mode {cxx, graphviz, interactive}; 12 | extern Mode opt_mode; 13 | extern vector opt_include_paths; 14 | -------------------------------------------------------------------------------- /yanshi/src/repl.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "syntax.hh" 3 | 4 | void repl(DefineStmt* stmt); 5 | -------------------------------------------------------------------------------- /yanshi/src/syntax.cc: -------------------------------------------------------------------------------- 1 | #include "syntax.hh" 2 | 3 | void stmt_free(Stmt* stmt) 4 | { 5 | while (stmt) { 6 | auto x = stmt->next; 7 | delete stmt; 8 | stmt = x; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /yanshi/unittest/determinize_test.cc: -------------------------------------------------------------------------------- 1 | #include "fsa.hh" 2 | #include "unittest/unittest_helper.hh" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | using namespace std; 11 | 12 | const char test[] = 13 | "4 7 2\n" 14 | "2 3 \n" 15 | "0 0 1\n" 16 | "0 -1 2\n" 17 | "1 1 1\n" 18 | "1 1 3\n" 19 | "2 -1 1\n" 20 | "2 0 3\n" 21 | "3 0 2\n" 22 | ; 23 | 24 | int main(int argc, char *argv[]) 25 | { 26 | if (argc == 1) { 27 | char filename[] = "/tmp/XXXXXX"; 28 | int fd = mkstemp(filename); 29 | write(fd, test, sizeof test-1); 30 | close(fd); 31 | freopen(filename, "r", stdin); 32 | unlink(filename); 33 | } 34 | 35 | auto relate = [](const vector&) {}; 36 | Fsa fsa = read_nfa().determinize(relate); 37 | print_fsa(fsa); 38 | 39 | if (argc == 1) 40 | return fsa.n() == 4 ? 0 : 1; 41 | } 42 | -------------------------------------------------------------------------------- /yanshi/unittest/difference_test.cc: -------------------------------------------------------------------------------- 1 | #include "fsa.hh" 2 | #include "unittest/unittest_helper.hh" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | using namespace std; 10 | 11 | const char test[] = 12 | "4 4 1\n" 13 | "3 \n" 14 | "0 0 1\n" 15 | "0 1 2\n" 16 | "1 0 3\n" 17 | "2 1 3\n" 18 | 19 | "4 4 1\n" 20 | "3 \n" 21 | "0 0 1\n" 22 | "0 1 2\n" 23 | "1 1 3\n" 24 | "2 1 3\n" 25 | ; 26 | 27 | int main(int argc, char *argv[]) 28 | { 29 | if (argc == 1) { 30 | char filename[] = "/tmp/XXXXXX"; 31 | int fd = mkstemp(filename); 32 | write(fd, test, sizeof test-1); 33 | close(fd); 34 | freopen(filename, "r", stdin); 35 | unlink(filename); 36 | } 37 | 38 | auto relate = [](long u, long v) {}; 39 | Fsa a = read_dfa(), b = read_dfa(), fsa = a.difference(b, relate); 40 | print_fsa(fsa); 41 | 42 | if (argc == 1) 43 | return 0; // fsa.n() == 4 ? 0 : 1; 44 | } 45 | -------------------------------------------------------------------------------- /yanshi/unittest/intersection_test.cc: -------------------------------------------------------------------------------- 1 | #include "fsa.hh" 2 | #include "unittest/unittest_helper.hh" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | using namespace std; 10 | 11 | const char test[] = 12 | "4 4 1\n" 13 | "3 \n" 14 | "0 0 1\n" 15 | "0 1 2\n" 16 | "1 0 3\n" 17 | "2 1 3\n" 18 | 19 | "4 4 1\n" 20 | "3 \n" 21 | "0 0 1\n" 22 | "0 1 2\n" 23 | "1 1 3\n" 24 | "2 1 3\n" 25 | ; 26 | 27 | int main(int argc, char *argv[]) 28 | { 29 | if (argc == 1) { 30 | char filename[] = "/tmp/XXXXXX"; 31 | int fd = mkstemp(filename); 32 | write(fd, test, sizeof test-1); 33 | close(fd); 34 | freopen(filename, "r", stdin); 35 | unlink(filename); 36 | } 37 | 38 | auto relate = [](long u, long v) {}; 39 | Fsa a = read_dfa(), b = read_dfa(), fsa = a.intersect(b, relate); 40 | print_fsa(fsa); 41 | 42 | if (argc == 1) 43 | return 0; // fsa.n() == 4 ? 0 : 1; 44 | } 45 | -------------------------------------------------------------------------------- /yanshi/unittest/minimize_test.cc: -------------------------------------------------------------------------------- 1 | #include "fsa.hh" 2 | #include "unittest/unittest_helper.hh" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | using namespace std; 12 | 13 | const char test[] = 14 | "4 4 1\n" 15 | "3 \n" 16 | "0 0 1\n" 17 | "0 1 2\n" 18 | "1 0 3\n" 19 | "2 0 3\n" 20 | ; 21 | 22 | int main(int argc, char *argv[]) 23 | { 24 | if (argc == 1) { 25 | char filename[] = "/tmp/XXXXXX"; 26 | int fd = mkstemp(filename); 27 | write(fd, test, sizeof test-1); 28 | close(fd); 29 | freopen(filename, "r", stdin); 30 | unlink(filename); 31 | } 32 | 33 | auto relate = [](const vector&) {}; 34 | Fsa fsa = read_dfa().minimize(relate); 35 | print_fsa(fsa); 36 | 37 | if (argc == 1) 38 | return fsa.n() == 3 ? 0 : 1; 39 | } 40 | -------------------------------------------------------------------------------- /yanshi/unittest/union_test.cc: -------------------------------------------------------------------------------- 1 | #include "fsa.hh" 2 | #include "unittest/unittest_helper.hh" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | using namespace std; 12 | 13 | const char test[] = 14 | "4 4 1\n" 15 | "3 \n" 16 | "0 0 1\n" 17 | "0 1 2\n" 18 | "1 0 3\n" 19 | "2 1 3\n" 20 | 21 | "4 4 1\n" 22 | "3 \n" 23 | "0 0 1\n" 24 | "0 1 2\n" 25 | "1 1 3\n" 26 | "2 1 3\n" 27 | ; 28 | 29 | int main(int argc, char *argv[]) 30 | { 31 | if (argc == 1) { 32 | char filename[] = "/tmp/XXXXXX"; 33 | int fd = mkstemp(filename); 34 | write(fd, test, sizeof test-1); 35 | close(fd); 36 | freopen(filename, "r", stdin); 37 | unlink(filename); 38 | } 39 | 40 | auto relate = [](long u, long v) {}; 41 | Fsa a = read_dfa(), b = read_dfa(), fsa = a.union_(b, relate); 42 | print_fsa(fsa); 43 | 44 | if (argc == 1) 45 | return fsa.n() == 4 ? 0 : 1; 46 | } 47 | -------------------------------------------------------------------------------- /yanshi/unittest/unittest_helper.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "common.hh" 3 | #include "fsa.hh" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | using namespace std; 11 | 12 | static Fsa read_nfa() 13 | { 14 | long n, m, k, u, a, v; 15 | cin >> n >> m >> k; 16 | Fsa r; 17 | r.start = 0; 18 | r.adj.resize(n); 19 | while (k--) { 20 | cin >> u; 21 | r.finals.push_back(u); 22 | } 23 | sort(ALL(r.finals)); 24 | while (m--) { 25 | cin >> u >> a >> v; 26 | if (u < 0 || u >= n) 27 | errx(EX_DATAERR, "%ld: 0 <= u < n", u); 28 | if (a < -1 || a >= 256) 29 | errx(EX_DATAERR, "%ld: -1 <= c < 256", a); 30 | if (v < 0 || v >= n) 31 | errx(EX_DATAERR, "%ld: 0 <= v < n", v); 32 | r.adj[u].emplace_back(a, v); 33 | } 34 | assert(cin.good()); 35 | REP(i, n) 36 | sort(ALL(r.adj[i])); 37 | return r; 38 | } 39 | 40 | static Fsa read_dfa() 41 | { 42 | Fsa r = read_nfa(); 43 | REP(i, r.n()) 44 | if (r.adj[i].size()) { 45 | if (r.adj[i][0].first < 0) 46 | errx(EX_DATAERR, "epsilon edge found for %ld", i); 47 | REP(j, r.adj[i].size()-1) 48 | if (r.adj[i][j].first == r.adj[i][j+1].first) 49 | errx(EX_DATAERR, "duplicate labels %ld found for %ld", r.adj[i][j].first, i); 50 | } 51 | assert(cin.good()); 52 | return r; 53 | } 54 | 55 | static void print_fsa(const Fsa& fsa) 56 | { 57 | printf("finals:"); 58 | for (long i: fsa.finals) 59 | printf(" %ld", i); 60 | puts(""); 61 | puts("edges:"); 62 | REP(i, fsa.n()) { 63 | printf("%ld:", i); 64 | for (auto& x: fsa.adj[i]) 65 | printf(" (%ld,%ld)", x.first, x.second); 66 | puts(""); 67 | } 68 | } 69 | --------------------------------------------------------------------------------