Repository: chaitin/SafeLine
Branch: main
Commit: 6d871e638bf8
Files: 236
Total size: 649.6 KB
Directory structure:
gitextract_r0ep21tc/
├── .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
├── 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/
│ │ │ └── plugins/
│ │ │ └── safeline/
│ │ │ ├── handler.lua
│ │ │ └── schema.lua
│ │ ├── 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
│ └── lua-resty-t1k/
│ ├── .github/
│ │ └── workflows/
│ │ ├── release.yml
│ │ └── test.yml
│ ├── .gitignore
│ ├── .luacheckrc
│ ├── LICENSE
│ ├── README.md
│ ├── ci/
│ │ ├── .dockerignore
│ │ ├── Dockerfile
│ │ └── bytecode
│ ├── lib/
│ │ └── resty/
│ │ ├── t1k/
│ │ │ ├── buffer.lua
│ │ │ ├── constants.lua
│ │ │ ├── file.lua
│ │ │ ├── filter.lua
│ │ │ ├── handler.lua
│ │ │ ├── log.lua
│ │ │ ├── request.lua
│ │ │ ├── utils.lua
│ │ │ └── uuid.lua
│ │ └── t1k.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
├── version.json
└── 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
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.yaml
================================================
name: Bug
description: Report a bug
# Create a report to help us improve
title: "[Bug] "
body:
- type: markdown
attributes:
value: |
Please search the [open issues](https://github.com/chaitin/SafeLine/issues) and [discussion](https://github.com/chaitin/SafeLine/discussions) for duplicate issue first.
The more information you share, the faster we can identify and fix the bug.
# Please check for duplicate issue first.
- type: textarea
id: Description
attributes:
label: What happened?
# Describe the bug
validations:
required: false
- type: textarea
id: Reproduce
attributes:
label: How we reproduce?
description: |
Reports cannot be reproduced will Most likely be closed.
# To Reproduce
value: |
1. ...
2. ...
3. ...
- type: textarea
id: Expected
attributes:
label: Expected behavior
# placeholder: |
# Descript what you expected to happen.
# Expected behavior. Descript what you expected to happen.
- type: textarea
id: Errorlog
attributes:
label: Error log
placeholder: |
Paste the error logs if any.
# Expected behavior. Descript what you expected to happen.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Discord
url: https://discord.gg/wyshSVuvxC
about: Ask questions and discuss with other SafeLine users in real time.
- name: Home page 官网
url: https://waf.chaitin.com/
about: Get feature descriptions, technical documentation and more information. 获取功能描述、技术文档和更多信息。
- name: 绕过反馈
url: https://stack.chaitin.com/security-challenge/safeline/index
about: Waf 绕过可在 CT Stack 安全挑战赛提交细节
================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.yaml
================================================
name: Suggestion
# Feature request
description: New feature or improvements.
title: "[Suggestion] "
body:
- type: markdown
attributes:
value: |
Please search the [open issues](https://github.com/chaitin/SafeLine/issues) and [discussion](https://github.com/chaitin/SafeLine/discussions) for duplicate issue first.
Please rise only one suggestion in an issue.
- type: textarea
id: solution
attributes:
label: What would you like to be added or improved?
# Describe the solution you'd like
# placeholder: |
#
- type: textarea
id: problem
attributes:
label: Why is it needed?
# Background and the specific problem that frustrates you
placeholder: |
Background and the problem that frustrates you
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/other.yaml
================================================
name: Other
description: Other issues such as Doc
body:
- type: markdown
attributes:
value: |
Please search the [open issues](https://github.com/chaitin/SafeLine/issues) and [discussion](https://github.com/chaitin/SafeLine/discussions) for duplicate issue first.
Please rise only one suggestion in an issue.
- type: textarea
id: content
attributes:
label: Content
================================================
FILE: .github/workflows/slmcp-docker.yml
================================================
name: MCP Docker
on:
push:
branches:
- main
tags:
- "v*"
paths:
- "mcp_server/**"
- ".github/workflows/slmcp-docker.yml"
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERIO_USERNAME }}
password: ${{ secrets.DOCKERIO_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./mcp_server
push: true
platforms: linux/amd64,linux/arm64
tags: |
chaitin/safeline-mcp:latest
chaitin/safeline-mcp:${{ github.ref_name }}
cache-from: type=registry,ref=chaitin/safeline-mcp:buildcache
cache-to: type=registry,ref=chaitin/safeline-mcp:buildcache,mode=max
================================================
FILE: .gitignore
================================================
*.Zone.Identifier
.DS_Store
*.zip
*.tar
*.tar.gz
build.sh
compose.yml
__pycache__
.cursor
.vscode
================================================
FILE: .gitmodules
================================================
[submodule "blazehttp"]
path = blazehttp
url = https://github.com/chaitin/blazehttp
[submodule "sdk/traefik-safeline"]
path = sdk/traefik-safeline
url = https://github.com/chaitin/traefik-safeline
================================================
FILE: LICENSE.md
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
================================================
FILE: README.md
================================================
## 👋 INTRODUCTION
SafeLine is a self-hosted **`WAF(Web Application Firewall)`** to protect your web apps from attacks and exploits.
A web application firewall helps protect web apps by filtering and monitoring HTTP traffic between a web application and the Internet. It typically protects web apps from attacks such as `SQL injection`, `XSS`, `code injection`, `os command injection`, `CRLF injection`, `ldap injection`, `xpath injection`, `RCE`, `XXE`, `SSRF`, `path traversal`, `backdoor`, `bruteforce`, `http-flood`, `bot abused`, among others.
#### 💡 How It Works
By deploying a WAF in front of a web application, a shield is placed between the web application and the Internet. While a proxy server protects a client machine’s identity by using an intermediary, a WAF is a type of reverse-proxy, protecting the server from exposure by having clients pass through the WAF before reaching the server.
A WAF protects your web apps by filtering, monitoring, and blocking any malicious HTTP/S traffic traveling to the web application, and prevents any unauthorized data from leaving the app. It does this by adhering to a set of policies that help determine what traffic is malicious and what traffic is safe. Just as a proxy server acts as an intermediary to protect the identity of a client, a WAF operates in similar fashion but acting as a reverse proxy intermediary that protects the web app server from a potentially malicious client.
its core capabilities include:
- Defenses for web attacks
- Proactive bot abused defense
- HTML & JS code encryption
- IP-based rate limiting
- Web Access Control List
#### ⚡️ Screenshots
| | |
| ------------------------------------------------- | ------------------------------------------------- |
| | |
Get [Live Demo](https://demo.waf.chaitin.com:9443/)
## 🔥 FEATURES
List of the main features as follows:
- **`Block Web Attacks`**
- It defenses for all of web attacks, such as `SQL injection`, `XSS`, `code injection`, `os command injection`, `CRLF injection`, `XXE`, `SSRF`, `path traversal` and so on.
- **`Rate Limiting`**
- Defend your web apps against `DoS attacks`, `bruteforce attempts`, `traffic surges`, and other types of abuse by throttling traffic that exceeds defined limits.
- **`Anti-Bot Challenge`**
- Anti-Bot challenges to protect your website from `bot attacks`, humen users will be allowed, crawlers and bots will be blocked.
- **`Authentication Challenge`**
- When authentication challenge turned on, visitors need to enter the password, otherwise they will be blocked.
- **`Dynamic Protection`**
- When dynamic protection turned on, html and js codes in your web server will be dynamically encrypted by each time you visit.
#### 🧩 Showcases
| | Legitimate User | Malicious User |
| ----------------------------- | --------------------------------------------------- | ---------------------------------------------------------------- |
| **`Block Web Attacks`** | | |
| **`Rate Limiting`** | | |
| **`Anti-Bot Challenge`** | | |
| **`Auth Challenge`** | | |
| **`HTML Dynamic Protection`** | | |
| **`JS Dynamic Protection`** | | |
## 🚀 Quickstart
> [!WARNING]
> 中国大陆用户安装国际版可能会导致无法连接云服务,请查看 [中文版安装文档](https://docs.waf-ce.chaitin.cn/zh/%E4%B8%8A%E6%89%8B%E6%8C%87%E5%8D%97/%E5%AE%89%E8%A3%85%E9%9B%B7%E6%B1%A0)
#### 📦 Installing
Information on how to install SafeLine can be found in the [Install Guide](https://docs.waf.chaitin.com/en/GetStarted/Deploy)
#### ⚙️ Protecting Web Apps
to see [Configuration](https://docs.waf.chaitin.com/en/GetStarted/AddApplication)
## 📋 More Informations
#### Effect Evaluation
| Metric | ModSecurity, Level 1 | CloudFlare, Free | SafeLine, Balance | SafeLine, Strict |
| ----------------- | -------------------- | -------------------- | ---------------------- | --------------------- |
| Total Samples | 33669 | 33669 | 33669 | 33669 |
| **Detection** | 69.74% | 10.70% | 71.65% | **76.17%** |
| **False Positive**| 17.58% | 0.07% | **0.07%** | 0.22% |
| **Accuracy** | 82.20% | 98.40% | **99.45%** | 99.38% |
#### Is SafeLine Production-Ready?
Yes, SafeLine is production-ready.
- Over 180,000 installations worldwide
- Protecting over 1,000,000 Websites
- Handling over 30,000,000,000 HTTP Requests Daily
#### 🙋♂️ Community
Join our [Discord](https://discord.gg/SVnZGzHFvn) to get community support, the core team members are identified by the STAFF role in Discord.
- channel [#feedback](https://discord.com/channels/1243085666485534830/1243120292822253598): for new features discussion.
- channel [#FAQ](https://discord.com/channels/1243085666485534830/1263761679619981413): for FAQ.
- channel [#general](https://discord.com/channels/1243085666485534830/1243115843919806486): for any other questions.
Several contact options exist for our community, the primary one being Discord. These are in addition to GitHub issues for creating a new issue.
#### 💪 PRO Edition
Coming soon!
#### 📝 License
See [LICENSE](/LICENSE.md) for details.
================================================
FILE: README_CN.md
================================================
#### 💪 专业版
查看 [社区版 vs 专业版](https://waf-ce.chaitin.cn/version)
================================================
FILE: compose.yaml
================================================
networks:
safeline-ce:
name: safeline-ce
driver: bridge
ipam:
driver: default
config:
- gateway: ${SUBNET_PREFIX:?SUBNET_PREFIX required}.1
subnet: ${SUBNET_PREFIX}.0/24
driver_opts:
com.docker.network.bridge.name: safeline-ce
services:
postgres:
container_name: safeline-pg
restart: always
image: ${IMAGE_PREFIX}/safeline-postgres${ARCH_SUFFIX}:15.2
volumes:
- ${SAFELINE_DIR}/resources/postgres/data:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
environment:
- POSTGRES_USER=safeline-ce
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?postgres password required}
networks:
safeline-ce:
ipv4_address: ${SUBNET_PREFIX}.2
command: [postgres, -c, max_connections=600]
healthcheck:
test: pg_isready -U safeline-ce -d safeline-ce
mgt:
container_name: safeline-mgt
restart: always
image: ${IMAGE_PREFIX}/safeline-mgt${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG:?image tag required}
volumes:
- /etc/localtime:/etc/localtime:ro
- ${SAFELINE_DIR}/resources/mgt:/app/data
- ${SAFELINE_DIR}/logs/nginx:/app/log/nginx:z
- ${SAFELINE_DIR}/resources/sock:/app/sock
- /var/run:/app/run
ports:
- ${MGT_PORT:-9443}:1443
healthcheck:
test: curl -k -f https://localhost:1443/api/open/health
environment:
- MGT_PG=postgres://safeline-ce:${POSTGRES_PASSWORD}@safeline-pg/safeline-ce?sslmode=disable
depends_on:
- postgres
- fvm
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
networks:
safeline-ce:
ipv4_address: ${SUBNET_PREFIX}.4
detect:
container_name: safeline-detector
restart: always
image: ${IMAGE_PREFIX}/safeline-detector${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}
volumes:
- ${SAFELINE_DIR}/resources/detector:/resources/detector
- ${SAFELINE_DIR}/logs/detector:/logs/detector
- /etc/localtime:/etc/localtime:ro
environment:
- LOG_DIR=/logs/detector
networks:
safeline-ce:
ipv4_address: ${SUBNET_PREFIX}.5
tengine:
container_name: safeline-tengine
restart: always
image: ${IMAGE_PREFIX}/safeline-tengine${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/resolv.conf:/etc/resolv.conf:ro
- ${SAFELINE_DIR}/resources/nginx:/etc/nginx
- ${SAFELINE_DIR}/resources/detector:/resources/detector
- ${SAFELINE_DIR}/resources/chaos:/resources/chaos
- ${SAFELINE_DIR}/logs/nginx:/var/log/nginx:z
- ${SAFELINE_DIR}/resources/cache:/usr/local/nginx/cache
- ${SAFELINE_DIR}/resources/sock:/app/sock
environment:
- TCD_MGT_API=https://${SUBNET_PREFIX}.4:1443/api/open/publish/server
- TCD_SNSERVER=${SUBNET_PREFIX}.5:8000
# deprecated
- SNSERVER_ADDR=${SUBNET_PREFIX}.5:8000
- CHAOS_ADDR=${SUBNET_PREFIX}.10
ulimits:
nofile: 131072
network_mode: host
luigi:
container_name: safeline-luigi
restart: always
image: ${IMAGE_PREFIX}/safeline-luigi${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}
environment:
- MGT_IP=${SUBNET_PREFIX}.4
- LUIGI_PG=postgres://safeline-ce:${POSTGRES_PASSWORD}@safeline-pg/safeline-ce?sslmode=disable
volumes:
- /etc/localtime:/etc/localtime:ro
- ${SAFELINE_DIR}/resources/luigi:/app/data
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
depends_on:
- detect
- mgt
networks:
safeline-ce:
ipv4_address: ${SUBNET_PREFIX}.7
fvm:
container_name: safeline-fvm
restart: always
image: ${IMAGE_PREFIX}/safeline-fvm${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}
volumes:
- /etc/localtime:/etc/localtime:ro
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
networks:
safeline-ce:
ipv4_address: ${SUBNET_PREFIX}.8
chaos:
container_name: safeline-chaos
restart: always
image: ${IMAGE_PREFIX}/safeline-chaos${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "10"
environment:
- DB_ADDR=postgres://safeline-ce:${POSTGRES_PASSWORD}@safeline-pg/safeline-ce?sslmode=disable
volumes:
- ${SAFELINE_DIR}/resources/sock:/app/sock
- ${SAFELINE_DIR}/resources/chaos:/app/chaos
networks:
safeline-ce:
ipv4_address: ${SUBNET_PREFIX}.10
================================================
FILE: management/.gitignore
================================================
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
**/.idea
*.iml
#OSX system files.
**/.DS_Store
/build
*.pb.go
submodule/
================================================
FILE: management/.golangci.yml
================================================
linters:
disable-all: true
enable:
- deadcode
- errcheck
- gofmt
- goimports
- gosimple
- govet
- ineffassign
- staticcheck
- structcheck
- typecheck
- unused
- varcheck
================================================
FILE: management/Makefile
================================================
GO = GO111MODULE=on go
#GO = GO111MODULE=on GOOS=linux GOARCH=amd64 go
GOBUILD = $(GO) build -mod=readonly
GOTEST = $(GO) test -v -p 1 -coverprofile=coverage-management.out
STAMP = $(shell date +%s)
GITHASH = $(shell git rev-parse --short=8 HEAD)
GITTAG = $(shell git describe --tags --abbrev=0)
BUILDFLAGS := -ldflags "-X main.buildstamp=$(STAMP) -X main.githash=$(GITHASH) -X main.version=$(GITTAG)"
pkgs = ./...
all: build-all
.PHONY: build-all
build-all: proto build-webserver build-tcd
.PHONY: build-webserver
build-webserver:
cd webserver && $(GOBUILD) $(BUILDFLAGS) -o ../build/webserver main.go
.PHONY: build-tcd
build-tcd:
cd tcontrollerd && CGO_ENABLED=0 $(GOBUILD) $(BUILDFLAGS) -o ../build/tcontrollerd main.go
.PHONY: test
test:
$(GOTEST) -failfast $(pkgs)
.PHONY: proto
proto:
@./scripts/genproto.sh
.PHONY: lint
lint:
# 'go list' needs to be executed before staticcheck to prepopulate the modules cache.
# Otherwise staticcheck might fail randomly for some reason not yet explained.
$(GO) list -e -compiled -test=true -export=false -deps=true -find=false -tags= -- ./... > /dev/null
goimports -local chaitin.cn -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
golangci-lint version
cd webserver && golangci-lint run -v --skip-dirs vendor --deadline 10m
.PHONY: clean
clean:
rm -rf build
================================================
FILE: management/README.md
================================================
# Management Micro Service
## Requirements
Go 1.18+
================================================
FILE: management/scripts/genproto.sh
================================================
#!/usr/bin/env bash
#
# Generate all protobuf bindings.
# Run from repository root.
set -u
if ! [[ "$0" =~ scripts/genproto.sh ]]; then
echo "must be run from repository root"
exit 255
fi
DIRS="webserver/proto tcontrollerd/proto"
echo "generating code"
protoc --version
for dir in ${DIRS}; do
pushd "${dir}" || return
find . -type d -print0 | while IFS= read -r -d '' sdir ; do
pushd "${sdir}" || return
# shellcheck disable=SC2010
FS=$(ls | grep "\.proto\$")
if [ -n "${FS}" ] ; then
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
"${FS}"
goimports -local chaitin.cn -w ./*.pb.go
fi
popd || return
done
popd || return
done
================================================
FILE: management/tcontrollerd/README.md
================================================
# TControllerD
Tengine Controller Daemon (abbr. TCD) will be running in the Tengine container,
designed to be in place with minion on the host machine.
## Requirements
Go 1.18+
## Development
### init protobuf
```shell
# Refer: https://grpc.io/docs/languages/go/quickstart/
# 1. Install protoc
# https://grpc.io/docs/protoc-installation/
# 2. Install Go plugins
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30.0
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0
# 3. Update your PATH so that the protoc compiler can find the plugins
# export PATH="$PATH:$(go env GOPATH)/bin"
# 4. Generate proto go code
# cd /path/to/management
./scripts/genproto.sh
```
================================================
FILE: management/tcontrollerd/config.yml
================================================
# develop use only. For production, refer to `package/build/tengine/tcontrollerd/config.yml`
log:
output: stdout # "stdout", "stderr" or file path
level: debug # "debug", "info", "warn" or "error"
mgt_addr: 169.254.0.4:9002 # gRPC addr of mgt-api webserver
================================================
FILE: management/tcontrollerd/controller/controller.go
================================================
package controller
import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/config"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log"
pb "chaitin.cn/patronus/safeline-2/management/tcontrollerd/proto/website"
)
var (
logger = log.GetLogger("controller")
)
func Handle() error {
logger.Infof("Connect mgt-webserver at %s", config.GlobalConfig.MgtWebserver)
gRPCConn, err := grpc.Dial(config.GlobalConfig.MgtWebserver, []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
}...)
if err != nil {
logger.Errorf("Fail to dial: %v", err)
return err
}
wsClient := pb.NewWebsiteClient(gRPCConn)
defer func(conn *grpc.ClientConn) {
err := conn.Close()
if err != nil {
logger.Errorf("Fail to close: %v", err)
return
}
}(gRPCConn)
if err = websiteHandler(wsClient); err != nil {
return err
}
return nil
}
================================================
FILE: management/tcontrollerd/controller/template.go
================================================
package controller
var nginxConfigTpl = `
upstream %s {
%s
keepalive 128;
keepalive_timeout 75;
}
server {
%s
%s
%s
%s
location = /forbidden_page {
internal;
root /etc/nginx/forbidden_pages;
try_files /default_forbidden_page.html =403;
}
location ^~ / {
proxy_pass %s://%s;
include proxy_params;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Accept-Encoding "";
t1k_append_header SL-CE-SUID %d;
t1k_body_size 1024k;
tx_body_size 4k;
t1k_error_page 403 /forbidden_page;
tx_error_page 403 /forbidden_page;
}
}`
var upstreamAddrTpl = `server %s;`
var serverListenTpl = `listen 0.0.0.0:%s%s%s;`
var addrAnyPropertiesTpl = " default_server backlog=65536 reuseport"
var serverNameTpl = `server_name %s;`
var certTpl = `ssl_certificate /etc/nginx/certs/%s;`
var certKeyTpl = `ssl_certificate_key /etc/nginx/certs/%s;`
var backendTpl = `backend_%d`
================================================
FILE: management/tcontrollerd/controller/website.go
================================================
package controller
import (
"context"
"encoding/json"
"fmt"
"io"
"io/fs"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/model"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/ngcmd"
pb "chaitin.cn/patronus/safeline-2/management/tcontrollerd/proto/website"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/utils"
)
const (
Ping = "ping"
Pong = "pong"
EventTypeWebsite = "website"
EventTypeDeleteWebsite = "deleteWebsite"
EventTypeFullWebsite = "fullWebsite"
nginxConfigPath = "/etc/nginx/sites-enabled/" // 修改的为 host /resources/nginx/ 中的文件
nginxFilePrefix = "IF_backend_"
nginxBackupFilePrefix = "BAK_IF_backend_"
nginxFileMode = 0644
HttpsScheme = "https"
DefaultHttpsPort = "443"
)
var pong = pb.Response{Type: Pong, Msg: nil, Err: false}
func generateNginxConfig(website *model.WebsiteConfig) (string, error) {
// only ONE upstream supported in v1.0
var upstreamAddr string
scheme := "http"
for _, upstream := range website.Upstreams {
urlInfo, err := url.Parse(upstream)
if err != nil {
return "", err
}
if urlInfo.Scheme == HttpsScheme && urlInfo.Port() == "" {
urlInfo.Host = urlInfo.Host + ":" + DefaultHttpsPort
}
upstreamAddr = fmt.Sprintf(upstreamAddrTpl, urlInfo.Host)
if len(urlInfo.Scheme) > 0 {
scheme = urlInfo.Scheme
}
}
sslFlag := ""
sslCertFilename := ""
sslCertKeyFilename := ""
if website.KeyFilename != "" && website.CertFilename != "" {
sslFlag = " ssl" // with a blank ahead
sslCertFilename = fmt.Sprintf(certTpl, website.CertFilename)
sslCertKeyFilename = fmt.Sprintf(certKeyTpl, website.KeyFilename)
}
// only ONE server name supported in v1.0
var serverName string
var addrAnyProperties string
for _, sn := range website.ServerNames {
if sn == "*" || sn == "" {
sn = "_"
addrAnyProperties = addrAnyPropertiesTpl
}
serverName = fmt.Sprintf(serverNameTpl, sn)
}
// only ONE port supported in v1.0
var serverListen string
for _, port := range website.Ports {
serverListen = fmt.Sprintf(serverListenTpl, port, sslFlag, addrAnyProperties)
}
upstreamName := fmt.Sprintf(backendTpl, website.Id)
nginxConfig := strings.TrimSpace(fmt.Sprintf(nginxConfigTpl, upstreamName, upstreamAddr, serverListen, serverName, sslCertFilename, sslCertKeyFilename, scheme, upstreamName, website.Id))
return nginxConfig, nil
}
func nginxTestAndReload() error {
err := ngcmd.NginxConfTest()
if err != nil {
return err
}
err = ngcmd.NginxConfReload()
if err != nil {
return err
}
return nil
}
func generateFullConfigAndReload(msg []byte) error {
var websites []model.WebsiteConfig
if err := json.Unmarshal(msg, &websites); err != nil {
return err
}
configFilename := make(map[string]struct{})
for _, website := range websites {
configFilename[fmt.Sprintf("%s%d", nginxFilePrefix, website.Id)] = struct{}{}
}
filepath.Walk(nginxConfigPath, func(path string, info fs.FileInfo, err error) error {
_, ok := configFilename[info.Name()]
if !ok && strings.HasPrefix(info.Name(), nginxFilePrefix) {
if err := os.Remove(path); err != nil {
// not return error only logged error in order to delete not exist website nginx conf
logger.Warn(err)
}
}
return nil
})
for _, website := range websites {
if err := generateConfigAndReload(&website); err != nil {
// trigger a full site push when tcd starts, ignore some site errors, and push as much as possible
logger.Warn(err)
}
}
return nil
}
func generateConfigAndReload(website *model.WebsiteConfig) error {
nginxConfig, err := generateNginxConfig(website)
if err != nil {
return err
}
configPath := filepath.Join(nginxConfigPath, fmt.Sprintf("%s%d", nginxFilePrefix, website.Id))
backupPath := filepath.Join(nginxConfigPath, fmt.Sprintf("%s%d", nginxBackupFilePrefix, website.Id))
oldConfigExists, err := utils.FileExist(configPath)
if err != nil {
return err
}
if oldConfigExists {
oldConfig, err := ioutil.ReadFile(configPath)
if err != nil {
return err
}
if string(oldConfig) == nginxConfig {
logger.Info("No changes in the new website config, skip nginx -s reload")
return nil
}
// tmp save old config to the backup path
if err = utils.CopyFile(configPath, backupPath); err != nil {
return err
}
}
if err = utils.EnsureWriteFile(configPath, []byte(nginxConfig), nginxFileMode); err != nil {
return err
}
if err = nginxTestAndReload(); err != nil {
nginxError := err
if err = os.Remove(configPath); err != nil {
return err
}
if oldConfigExists {
// new config err, restore old config
if err = utils.CopyFile(backupPath, configPath); err != nil {
return err
}
if err = os.Remove(backupPath); err != nil {
return err
}
}
return nginxError
}
if oldConfigExists {
if err = os.Remove(backupPath); err != nil {
return err
}
}
return nil
}
func deleteConfigAndReload(config []byte) error {
var websiteIds []uint
if err := json.Unmarshal(config, &websiteIds); err != nil {
return err
}
for _, id := range websiteIds {
configPath := filepath.Join(nginxConfigPath, fmt.Sprintf("%s%d", nginxFilePrefix, id))
exists, err := utils.FileExist(configPath)
if err != nil {
return err
}
if exists {
if err := os.Remove(configPath); err != nil {
return err
}
}
}
if err := nginxTestAndReload(); err != nil {
return err
}
return nil
}
func sendResponse(stream pb.Website_SubscribeClient, eventType string, errMsg []byte) error {
return stream.Send(&pb.Response{
Type: eventType,
Msg: errMsg,
Err: len(errMsg) != 0,
})
}
func websiteHandler(wc pb.WebsiteClient) error {
stream, err := wc.Subscribe(context.Background())
if err != nil {
logger.Errorf("Subscribe failed: %v", err)
return err
}
for {
event, err := stream.Recv()
if err != nil {
if err == io.EOF {
// read done.
logger.Infof("Recv EOF from webserver")
return nil
} else {
logger.Errorf("Handle failed: %v", err)
return err
}
}
logger.Debugf("Got message Type %s, Msg: %s", event.GetType(), event.GetMsg())
if event.Type == Ping {
if err = stream.Send(&pong); err != nil {
return err
}
} else if event.Type == EventTypeWebsite {
logger.Infof("Update website with config: %s", event.GetMsg())
var website model.WebsiteConfig
if err := json.Unmarshal(event.GetMsg(), &website); err != nil {
return err
}
if err = generateConfigAndReload(&website); err != nil {
logger.Error(err)
if err := sendResponse(stream, EventTypeDeleteWebsite, []byte(err.Error())); err != nil {
return err
}
} else {
if err = sendResponse(stream, EventTypeDeleteWebsite, nil); err != nil {
return err
}
}
} else if event.Type == EventTypeFullWebsite {
if err = generateFullConfigAndReload(event.Msg); err != nil {
logger.Error(err)
sendErr := sendResponse(stream, EventTypeFullWebsite, []byte(err.Error()))
if sendErr != nil {
return sendErr
}
} else {
if err = sendResponse(stream, EventTypeFullWebsite, nil); err != nil {
return err
}
}
} else if event.Type == EventTypeDeleteWebsite {
if err = deleteConfigAndReload(event.Msg); err != nil {
logger.Error(err)
sendErr := sendResponse(stream, EventTypeDeleteWebsite, []byte(err.Error()))
if sendErr != nil {
return sendErr
}
} else {
if err = sendResponse(stream, EventTypeDeleteWebsite, nil); err != nil {
return err
}
}
}
}
}
================================================
FILE: management/tcontrollerd/go.mod
================================================
module chaitin.cn/patronus/safeline-2/management/tcontrollerd
go 1.21
toolchain go1.21.3
require (
chaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6
chaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c
chaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.3
google.golang.org/grpc v1.65.0
)
require (
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
)
================================================
FILE: management/tcontrollerd/go.sum
================================================
chaitin.cn/dev/go/errors v0.0.0-20200717101723-df6132d53dc8/go.mod h1:u+ZD0shdyUt0UG9XfCgD1R5mSqXpTVoO5rwQXQeo3Eo=
chaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6 h1:1Qa9ABk907/9ZrOLbbRcS8Fqq9VhjAF/mLjbSP1qAJY=
chaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6/go.mod h1:u+ZD0shdyUt0UG9XfCgD1R5mSqXpTVoO5rwQXQeo3Eo=
chaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c h1:Xn9IYkxmnpDcEpV+7JIR5ufEIexd1dhqKwpOLG1mYOE=
chaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c/go.mod h1:xJIYwUoA2TX5mNg/RBrEPyE251BPwj+70/mM7UIhoxg=
chaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c h1:tXsraF7o9iUsQY6IwpDJusc6OFhB7iv/bBTfgR3MPUU=
chaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c/go.mod h1:fUvtmpG8Z8Zf5aciadL9a/vn5SB3knG7pdNJixDplPg=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.39.0-dev h1:K4VkkiYp4LCvQiW6OiGglzm5nO4Zyryf7pHhzP15cmI=
google.golang.org/grpc v1.39.0-dev/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
================================================
FILE: management/tcontrollerd/main.go
================================================
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/controller"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/cron"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/ngcmd"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/config"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log"
)
var (
logger = log.GetLogger("main")
version = "undefined"
githash = "undefined"
buildstamp = "undefined"
goVersion = "undefined"
)
func init() {
// do something that do not raise error
}
func handleLoop(ctx context.Context) {
for {
select {
case <-ctx.Done():
os.Exit(0)
default:
if err := controller.Handle(); err != nil {
logger.Error("Error occurred when handling controller: ", err)
}
time.Sleep(time.Second * 5)
}
}
}
func main() {
log.SetLogFormatter()
fs := flag.NewFlagSet("tcontrollerd", flag.ExitOnError)
showVersion := fs.Bool("v", false, "show version")
nginxTest := fs.Bool("t", false, "nginx -t")
nginxReload := fs.Bool("r", false, "nginx -s reload")
cfgFile := fs.String("c", constants.ConfigFilePath, "config file path")
if err := fs.Parse(os.Args[1:]); err != nil {
logger.Fatalln("Failed to parse args: ", err)
}
if *showVersion {
i, _ := strconv.Atoi(buildstamp)
t := time.Unix(int64(i), 0).Format("2006-01-02 15:04:05")
fmt.Println("Version: ", version)
fmt.Println("Githash: ", githash)
fmt.Println("Build: ", t)
fmt.Println("Go version: ", goVersion)
return
}
// init configs
if err := config.InitConfigs(*cfgFile); err != nil {
logger.Fatalln("Failed to init configs: ", err)
}
if err := log.InitLogger(); err != nil {
logger.Fatalln("Failed to init db: ", err)
}
if *nginxTest {
if err := ngcmd.NginxConfTest(); err != nil {
logger.Fatalln("Failed to test nginx conf: ", err)
}
return
}
if *nginxReload {
if err := ngcmd.NginxConfReload(); err != nil {
logger.Fatalln("Failed to reload nginx conf: ", err)
}
return
}
if err := cron.StartCron(); err != nil {
logger.Fatalln("Failed to start cron: ", err)
}
ctx, cancel := context.WithCancel(context.Background())
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sig
cancel()
}()
handleLoop(ctx)
}
================================================
FILE: management/tcontrollerd/model/website.go
================================================
package model
// WebsiteConfig is supposed to be same with webserver/model/website.go
type WebsiteConfig struct {
Id int `json:"id"`
ServerNames []string `json:"server_names"`
Ports []string `json:"ports"`
Upstreams []string `json:"upstreams"`
CertFilename string `json:"cert_filename"`
KeyFilename string `json:"key_filename"`
}
================================================
FILE: management/tcontrollerd/pkg/config/config.go
================================================
package config
import (
"os"
"chaitin.cn/dev/go/settings"
)
var (
GlobalConfig = DefaultGlobalConfig()
)
func InitConfigs(configFilePath string) error {
s, err := settings.New(configFilePath)
if err != nil {
return err
}
if err = GlobalConfig.Log.Load(s); err != nil {
return err
}
if err := s.Unmarshal("mgt_addr", &GlobalConfig.MgtWebserver); err != nil {
return err
}
if v, ok := os.LookupEnv("MGT_ADDR"); ok {
GlobalConfig.MgtWebserver = v
}
return nil
}
================================================
FILE: management/tcontrollerd/pkg/config/global.go
================================================
package config
type Config struct {
Log LogConfig
MgtWebserver string
}
func DefaultGlobalConfig() Config {
return Config{
Log: DefaultLogConfig(),
MgtWebserver: "169.254.0.4:9002",
}
}
================================================
FILE: management/tcontrollerd/pkg/config/log.go
================================================
package config
import (
"chaitin.cn/dev/go/settings"
)
type LogConfig struct {
Output string `yaml:"output"`
Level string `yaml:"level"`
}
func DefaultLogConfig() LogConfig {
return LogConfig{
Output: "stdout",
Level: "info",
}
}
func (lc *LogConfig) Load(setting *settings.Setting) error {
if err := setting.Unmarshal("log", lc); err != nil {
return err
}
return nil
}
================================================
FILE: management/tcontrollerd/pkg/constants/constants.go
================================================
package constants
const (
ConfigFilePath = "config.yml"
)
================================================
FILE: management/tcontrollerd/pkg/constants/forbidden_page.go
================================================
package constants
const (
DefaultForbiddenPageMd5 = "d9921f84f36a6cc92a6fc13946a18e98"
DefaultForbiddenPage = ``
)
================================================
FILE: management/tcontrollerd/pkg/cron/cron.go
================================================
package cron
import (
"github.com/robfig/cron/v3"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log"
)
var logger = log.GetLogger("cron")
func newCronWithSeconds() *cron.Cron {
secondParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour |
cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor)
return cron.New(cron.WithParser(secondParser), cron.WithChain())
}
func StartCron() error {
cronInstance := newCronWithSeconds()
_, err := cronInstance.AddFunc(specCheckForbiddenPage, checkAndUpdateForbiddenPage)
if err != nil {
return err
}
cronInstance.Start()
return nil
}
================================================
FILE: management/tcontrollerd/pkg/cron/forbidden_page.go
================================================
package cron
import (
"crypto/md5"
"encoding/hex"
"io/ioutil"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/utils"
)
const (
// SpecUpdatePolicy http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-06.html
// Seconds Minutes Hours Day-of-Month Month Day-of-Week Year (optional field)
// every 30 second (starting from 0s)
specCheckForbiddenPage = "0/30 * * * * ?"
forbiddenPagePath = "/etc/nginx/forbidden_pages/default_forbidden_page.html"
)
func checkAndUpdateForbiddenPage() {
existed, err := utils.FileExist(forbiddenPagePath)
if err != nil {
logger.Error(err)
return
}
if !existed {
err = utils.EnsureWriteFile(forbiddenPagePath, []byte(constants.DefaultForbiddenPage), 0644)
if err != nil {
logger.Error(err)
}
return
}
content, err := ioutil.ReadFile(forbiddenPagePath)
if err != nil {
logger.Error(err)
return
}
hash := md5.New()
hash.Write([]byte(content))
forbiddenMd5 := hex.EncodeToString(hash.Sum(nil))
if forbiddenMd5 == constants.DefaultForbiddenPageMd5 {
return
}
err = ioutil.WriteFile(forbiddenPagePath, []byte(constants.DefaultForbiddenPage), 0644)
if err != nil {
logger.Error(err)
return
}
}
================================================
FILE: management/tcontrollerd/pkg/log/log.go
================================================
package log
import (
"fmt"
"os"
"runtime"
"strings"
"github.com/sirupsen/logrus"
"chaitin.cn/dev/go/log"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/config"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/utils"
)
func GetLogger(name string) *log.Logger {
return log.GetLogger(name)
}
func LoadLogLevel() {
lv, _ := log.ParseLevel(config.GlobalConfig.Log.Level)
log.SetLevel(log.AllLoggers, lv)
}
func SetLogFormatter() {
// format
formatter := new(log.TextFormatter)
formatter.FullTimestamp = true
formatter.TimestampFormat = "2006/01/02 15:04:05"
log.SetFormatter(log.AllLoggers, formatter)
}
func InitLogger() error {
// output
switch config.GlobalConfig.Log.Output {
case "stdout":
log.SetOutput(log.AllLoggers, os.Stdout)
case "stderr":
log.SetOutput(log.AllLoggers, os.Stderr)
default:
exist, err := utils.FileExist(config.GlobalConfig.Log.Output)
if err != nil {
return err
}
fileFlag := os.O_WRONLY | os.O_APPEND | os.O_SYNC
if !exist {
if err := utils.EnsureFileDir(config.GlobalConfig.Log.Output); err != nil {
return err
}
fileFlag = fileFlag | os.O_CREATE
}
if fp, err := os.OpenFile(config.GlobalConfig.Log.Output, fileFlag, os.ModePerm); err != nil {
return fmt.Errorf("failed to open log file: %s", err.Error())
} else {
log.SetOutput(log.AllLoggers, log.NewLockOutput(fp))
}
}
// hook
log.AddHook(log.AllLoggers, NewRuntimeHook())
log.AddHook(log.AllLoggers, log.NewErrorStackHook(true))
// level
LoadLogLevel()
return nil
}
type RuntimeHook struct{}
func (h *RuntimeHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func (h *RuntimeHook) Fire(entry *logrus.Entry) error {
file := "???"
funcName := "???"
line := 0
pc := make([]uintptr, 64)
// Skip runtime.Callers, self, and another call from logrus
n := runtime.Callers(3, pc)
if n != 0 {
pc = pc[:n] // pass only valid pcs to runtime.CallersFrames
frames := runtime.CallersFrames(pc)
// Loop to get frames.
// A fixed number of pcs can expand to an indefinite number of Frames.
for {
frame, more := frames.Next()
if !strings.Contains(frame.File, "github.com/sirupsen/logrus") && !strings.Contains(frame.Function, "chaitin.cn/dev/go") {
file = frame.File
funcName = frame.Function
line = frame.Line
break
}
if !more {
break
}
}
}
slices := strings.Split(file, "/")
file = slices[len(slices)-1]
funcName = strings.ReplaceAll(funcName, "chaitin.cn", "")
entry.Data["file"] = file
entry.Data["func"] = funcName
entry.Data["line"] = line
return nil
}
func NewRuntimeHook() *RuntimeHook {
return &RuntimeHook{}
}
================================================
FILE: management/tcontrollerd/pkg/ngcmd/ngcmd.go
================================================
package ngcmd
import (
"fmt"
"os/exec"
"strings"
"chaitin.cn/dev/go/errors"
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log"
)
var logger = log.GetLogger("ngcmd")
// NginxConfTest exec nginx -t and return stderr
func NginxConfTest() error {
out, err := exec.Command("nginx", "-t").CombinedOutput()
logger.Debugf("nginx -t output: %v", string(out))
if err != nil {
return errors.Wrap(err, string(out))
}
//logger.Debugf("nginx -t output: %v", out)
if strings.Contains(string(out), "syntax is ok") && strings.Contains(string(out), "test is successful") {
return nil
} else {
return errors.New(fmt.Sprintf("nginx conf test error: %s", string(out)))
}
}
// NginxConfReload exec nginx -t and return stderr
func NginxConfReload() error {
out, err := exec.Command("nginx", "-s", "reload").CombinedOutput()
logger.Debugf("nginx -s reload output: %v", string(out))
if err != nil {
return errors.Wrap(err, string(out))
}
if len(out) == 0 {
return nil
} else {
return errors.New(fmt.Sprintf("nginx conf reload error: %s", string(out)))
}
}
================================================
FILE: management/tcontrollerd/proto/website/website.proto
================================================
syntax = "proto3";
package website;
option go_package = "proto/website";
service Website {
rpc Subscribe(stream Response) returns (stream Event) {}
}
// From client-side, may be "pong"
message Response {
string type = 1;
bytes msg = 2;
bool err = 3;
}
// From server-side, may be "ping"
message Event {
string type = 1; // ping/website
bytes msg = 2;
}
================================================
FILE: management/tcontrollerd/utils/file.go
================================================
package utils
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
)
func EnsureDir(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return os.MkdirAll(dir, os.FileMode(0755))
}
return nil
}
func EnsureFileDir(path string) error {
return EnsureDir(filepath.Dir(path))
}
func FileExist(path string) (bool, error) {
stat, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
} else {
return false, err
}
} else {
if stat.IsDir() {
return false, fmt.Errorf("%s is dir", path)
} else {
return true, nil
}
}
}
func FilesExist(paths ...string) (bool, error) {
for _, path := range paths {
exist, err := FileExist(path)
if err != nil {
return false, err
}
if !exist {
return false, nil
}
}
return true, nil
}
func RenameWriteFile(filename string, data []byte, perm os.FileMode) error {
randFileName := filename + ".tmp." + RandStr(8)
if err := ioutil.WriteFile(randFileName, data, perm); err != nil {
return err
}
return os.Rename(randFileName, filename)
}
func EnsureRenameWriteFile(path string, data []byte, mode os.FileMode) error {
err := EnsureFileDir(path)
if err != nil {
return err
}
return RenameWriteFile(path, data, mode)
}
func EnsureWriteFile(path string, data []byte, mode os.FileMode) error {
err := EnsureFileDir(path)
if err != nil {
return err
}
return ioutil.WriteFile(path, data, mode)
}
func CopyFile(srcPath, dstPath string) error {
srcFile, err := os.Open(srcPath)
if err != nil {
return err
}
defer func(srcFile *os.File) {
err := srcFile.Close()
if err != nil {
}
}(srcFile)
fileInfo, err := srcFile.Stat()
if err != nil {
return err
}
return CopyFileFromIO(srcFile, dstPath, fileInfo.Mode())
}
func CopyFileFromIO(src io.Reader, dstPath string, perm os.FileMode) error {
if err := EnsureFileDir(dstPath); err != nil {
return err
}
dstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
if err != nil {
return err
}
defer func(dstFile *os.File) {
err := dstFile.Close()
if err != nil {
}
}(dstFile)
_, err = io.Copy(dstFile, src)
return err
}
func CopyFileIfNotExist(srcPath, dstPath string) error {
if exist, err := FileExist(dstPath); err != nil {
return err
} else if !exist {
return CopyFile(srcPath, dstPath)
} else {
return nil
}
}
================================================
FILE: management/tcontrollerd/utils/random.go
================================================
package utils
import (
"math/rand"
"time"
)
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func RandStr(n int) string {
rand.Seed(time.Now().UnixNano())
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
================================================
FILE: management/webserver/.gitignore
================================================
tmp
Taskfile.yml
.air.toml
================================================
FILE: management/webserver/README.md
================================================
# Web Server
web server for mgt-api
## Requirements
Go 1.18+
## Development
### Init protobuf
```shell
# Refer: https://grpc.io/docs/languages/go/quickstart/
# 1. Install protoc
# https://grpc.io/docs/protoc-installation/
# 2. Install Go plugins
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30.0
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0
# 3. Update your PATH so that the protoc compiler can find the plugins
# export PATH="$PATH:$(go env GOPATH)/bin"
# 4. Generate proto go code
# cd /path/to/management
./scripts/genproto.sh
```
### Init fvm libs
```shell
# Due to the fvm c header files
mkdir -p management/webserver/submodule/fvm/
mkdir -p management/webserver/submodule/libct/
cd management/webserver/submodule/fvm/
# Download https://chaitin.cn/patronus/fvm/-/tags 1.8.21 release:release, https://chaitin.cn/patronus/fvm/-/jobs/6716645
unzip artifacts.zip
rm artifacts.zip
cd management/webserver/submodule/libct/
# Download https://chaitin.cn/patronus/libct/-/tags 1.1.1.0 release, https://chaitin.cn/patronus/libct/-/jobs/7229201
# rename
rm artifacts.zip
cd management/webserver/submodule/
# Download https://chaitin.cn/patronus/fusion-2/-/tags 5.3.9-r1 build:release, https://chaitin.cn/patronus/fusion-2/-/jobs/7326007
# rename
unzip artifacts.zip
mv artifacts/lib/libfusion.so libfvm.so
rm artifacts.zip
rm -r artifacts/
```
### Build
```shell
cd management/
docker run -it --rm -w="/mnt" --mount type=bind,source="$(pwd)",target=/mnt chaitin.cn/ci/golang:1.18 bash
cp webserver/submodule/libfvm.so /usr/lib/
make build-webserver
```
================================================
FILE: management/webserver/api/auth.go
================================================
package api
import (
"math"
"net/http"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
"chaitin.cn/patronus/safeline-2/management/webserver/model"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/log"
)
var logger = log.GetLogger("api")
var OtpOpts = totp.GenerateOpts{
Issuer: constants.ProductName,
AccountName: constants.SuperUser,
Period: 30, // seconds
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
}
type PostLoginRequest struct {
Passcode string `json:"passcode"`
Timestamp int64 `json:"timestamp"`
}
func PostLogin(c *gin.Context) {
var params PostLoginRequest
if err := c.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
// only SuperUser in v0.9
var user model.User
db.Where(&model.User{Username: constants.SuperUser}).First(&user)
valid := totp.Validate(params.Passcode, user.TFASecret)
if !valid {
millisecondTimestamp := params.Timestamp
localTimeStamp := time.Now()
if millisecondTimestamp > 0 {
logger.Debugf("will valid otp frontend timestamp:%v, local timestamp:%v", millisecondTimestamp, localTimeStamp)
otpTime := time.Unix(millisecondTimestamp/1000, (millisecondTimestamp%1000)*int64(time.Millisecond))
timeSub := localTimeStamp.Sub(otpTime)
seconds := math.Abs(timeSub.Seconds())
if seconds >= 60 {
logger.Errorf("otp timestamp gap is more than a minute")
response.Error(c, response.JSONBody{Err: response.ErrWrongTimeGap, Msg: "otp timestamp gap is more than a minute"}, http.StatusUnauthorized)
return
}
}
response.Error(c, response.JSONBody{Err: response.ErrWrongPasscode, Msg: "Failed to verify your passcode"}, http.StatusUnauthorized)
return
}
user.LastLoginTime = time.Now().Unix()
user.IsEnabled = true
db.Save(&user)
session := sessions.Default(c)
session.Options(sessions.Options{
Path: "/",
MaxAge: 3600 * 24 * 7,
//Domain: options.Domain,
//HttpOnly: true,
//SameSite: http.SameSiteLaxMode,
//Secure: false,
})
session.Set(constants.DefaultSessionUserKey, user.ID)
if err := session.Save(); err != nil {
logger.Error(err)
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Error occurred when creating sessions"}, http.StatusInternalServerError)
return
}
response.Success(c, nil)
}
func PostLogout(c *gin.Context) {
session := sessions.Default(c)
session.Clear()
if err := session.Save(); err != nil {
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Error occurred when creating sessions"}, http.StatusInternalServerError)
return
}
response.Success(c, nil)
}
func GetOTPUrl(c *gin.Context) {
otpKey, err := totp.Generate(OtpOpts)
if err != nil {
logger.Error(err)
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Error occurred when generating otp qrcode"}, http.StatusInternalServerError)
return
}
db := database.GetDB()
// only SuperUser in v0.9
user := model.User{Username: constants.SuperUser}
db.First(&user)
if user.LastLoginTime > 0 {
// already bind tfa, because tfa binding is mandatory when login.
response.Success(c, gin.H{"url": ""})
return
}
user.TFASecret = otpKey.Secret()
db.Save(&user)
response.Success(c, gin.H{"url": otpKey.URL()})
}
func GetUser(c *gin.Context) {
db := database.GetDB()
user := model.User{Username: constants.SuperUser}
db.First(&user)
response.Success(c, gin.H{"id": user.ID, "username": user.Username})
}
================================================
FILE: management/webserver/api/behaviour.go
================================================
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
"chaitin.cn/patronus/safeline-2/management/webserver/model"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
)
type PostBehaviourRequest struct {
model.Behaviour
}
func PostBehaviour(c *gin.Context) {
var params PostBehaviourRequest
if err := c.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
db.Create(&model.Behaviour{SrcRouter: params.SrcRouter, DstRouter: params.DstRouter})
response.Success(c, nil)
}
================================================
FILE: management/webserver/api/cert.go
================================================
package api
import (
"crypto/x509/pkix"
"fmt"
"net/http"
"path/filepath"
"github.com/gin-gonic/gin"
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
"chaitin.cn/patronus/safeline-2/management/webserver/utils"
)
type postSSLCertRequest struct {
Hostname string `json:"hostname"`
}
// SSLCertDir is the dir of tengine conf, not mgt-api nginx certs dir defined by constants.CertsPath
const (
CRT = ".crt"
PEM = ".pem"
KEY = ".key"
SSLCertDir = "certs"
)
func PostUploadSSLCert(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
logger.Error(err)
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
switch filepath.Ext(file.Filename) {
case CRT, PEM, KEY:
logger.Debugf("File: %v is valid", file.Filename)
default:
logger.Errorf("Filename: %s, ext: %s", file.Filename, filepath.Ext(file.Filename))
response.Error(c, response.JSONBody{Err: response.ErrWrongFileType, Msg: "Wrong file type, please upload a file of .crt or .key"}, http.StatusUnsupportedMediaType)
return
}
var dstPath string
filename := fmt.Sprintf("%s_%s", utils.RandStr(16), file.Filename)
if config.GlobalConfig.Server.DevMode {
dstPath = filepath.Join("./nginx", SSLCertDir, filename)
} else {
dstPath = filepath.Join(config.GlobalConfig.NgxResDir, SSLCertDir, filename)
}
if err = c.SaveUploadedFile(file, dstPath); err != nil {
logger.Error(err)
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Error occurred when saving file"}, http.StatusInternalServerError)
return
}
response.Success(c, gin.H{"filename": filename})
}
func PostSSLCert(c *gin.Context) {
var params postSSLCertRequest
if err := c.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
filePrefix := utils.RandStr(16)
certFilename := fmt.Sprintf("%s_backend.crt", filePrefix)
keyFilename := fmt.Sprintf("%s_backend.key", filePrefix)
var certPath, keyPath string
if config.GlobalConfig.Server.DevMode {
certPath = filepath.Join("./management", SSLCertDir, certFilename)
keyPath = filepath.Join("./management", SSLCertDir, keyFilename)
} else {
certPath = filepath.Join(config.GlobalConfig.NgxResDir, SSLCertDir, certFilename)
keyPath = filepath.Join(config.GlobalConfig.NgxResDir, SSLCertDir, keyFilename)
}
if err := utils.WriteCertIfNotExist(
certPath,
keyPath,
func() ([]byte, []byte, error) {
return utils.GenerateCert(
[]string{params.Hostname},
3650,
4096,
&pkix.Name{
Country: []string{},
Province: []string{},
Locality: []string{},
Organization: []string{},
OrganizationalUnit: []string{},
CommonName: "",
},
false,
)
}); err != nil {
logger.Error(err)
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Error occurred when generating certs"}, http.StatusInternalServerError)
return
}
response.Success(c, gin.H{"crt": certFilename, "key": keyFilename})
}
================================================
FILE: management/webserver/api/common.go
================================================
package api
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/rogpeppe/go-internal/semver"
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/webserver/utils"
)
const VersionInfoEntrypoint = "/release/latest/version.json"
type idsRequest struct {
IDs []uint `json:"ids" form:"ids"`
}
type pageRequest struct {
Page int `json:"page" form:"page,default=1" binding:"min=1"`
PageSize int `json:"page_size" form:"page_size,default=10" binding:"min=1"`
}
type versionInfoResponse struct {
LatestVersion string `json:"latest_version"`
RecVersion string `json:"rec_version"`
}
func GetVersion(c *gin.Context) {
response.Success(c, gin.H{"version": strings.TrimPrefix(constants.Version, "ce-")})
}
func GetUpgradeTips(ctx *gin.Context) {
client := utils.GetHTTPClient()
logger.Debugf("GetUpgradeTips: %s", config.GlobalConfig.PlatformAddr+VersionInfoEntrypoint)
versionInfoReq, err := http.NewRequest(http.MethodGet, config.GlobalConfig.PlatformAddr+VersionInfoEntrypoint, nil)
if err != nil {
logger.Warn(err)
response.Success(ctx, gin.H{"upgrade_tips": constants.NotUpgrade})
return
}
versionInfoRsp, err := client.Do(versionInfoReq)
if err != nil {
logger.Warn(err)
response.Success(ctx, gin.H{"upgrade_tips": constants.NotUpgrade})
return
}
body, err := ioutil.ReadAll(versionInfoRsp.Body)
if err != nil {
logger.Warn(err)
response.Success(ctx, gin.H{"upgrade_tips": constants.NotUpgrade})
return
}
versionInfo := &versionInfoResponse{}
err = json.Unmarshal(body, versionInfo)
if err != nil {
logger.Warnf("err: %v, body: %s", err, body)
response.Success(ctx, gin.H{"upgrade_tips": constants.NotUpgrade})
return
}
currentVersion := fmt.Sprintf("v%s", constants.Version)
latestVersionCmp := semver.Compare(currentVersion, versionInfo.LatestVersion)
recVersionCmp := semver.Compare(currentVersion, versionInfo.RecVersion)
if semver.Compare(versionInfo.LatestVersion, versionInfo.RecVersion) == -1 || latestVersionCmp == 1 {
logger.Warnf("The version number is invalid, current version: %s, latest version: %s, rec version: %s",
currentVersion, versionInfo.LatestVersion, versionInfo.RecVersion)
response.Success(ctx, gin.H{"upgrade_tips": constants.NotUpgrade})
return
}
var upgradeTips int
if recVersionCmp == -1 {
upgradeTips = constants.MustUpgrade
} else if recVersionCmp == 0 {
if latestVersionCmp == 0 {
upgradeTips = constants.NotUpgrade
} else {
upgradeTips = constants.RecommendedUpgrade
}
} else {
if latestVersionCmp < 0 {
upgradeTips = constants.RecommendedUpgrade
} else {
upgradeTips = constants.NotUpgrade
}
}
response.Success(ctx, gin.H{"upgrade_tips": upgradeTips})
}
================================================
FILE: management/webserver/api/dashboard.go
================================================
package api
import (
"math"
"time"
"github.com/gin-gonic/gin"
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
"chaitin.cn/patronus/safeline-2/management/webserver/model"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
)
func GetDashboardCounts(c *gin.Context) {
var requested int64 = 0
var intercepted int64 = 0
db := database.GetDB()
var statisticTotalReq model.SystemStatistics
r := db.Where("type = 'total-req'").Where("created_at >= date_trunc('day',now())").First(&statisticTotalReq)
if r.RowsAffected > 0 {
requested = statisticTotalReq.Value
}
var statisticTotalDenied model.SystemStatistics
r = db.Where("type = 'total-denied'").Where("created_at >= date_trunc('day',now())").First(&statisticTotalDenied)
if r.RowsAffected > 0 {
intercepted = statisticTotalDenied.Value
}
response.Success(c, gin.H{"requested": requested, "intercepted": intercepted})
}
func GetDashboardSites(c *gin.Context) {
response.Success(c, gin.H{"normal": 0, "abnormal": 0})
}
func GetDashboardQps(c *gin.Context) {
var statistics []model.SystemStatistics
db := database.GetDB()
db.Where("type = 'req'").Order("created_at desc").Limit(75).Find(&statistics)
type Node struct {
Label string `json:"label"`
Value int64 `json:"value"`
}
var nodes = make([]Node, 0)
for i := len(statistics) - 1; i >= 0; i-- {
nodes = append(nodes, Node{
Label: statistics[i].CreatedAt.Format("2006-01-02 15:04:05"),
Value: int64(math.Ceil(float64(statistics[i].Value) / 5)),
})
}
response.Success(c, gin.H{"nodes": nodes, "total": len(nodes)})
}
func GetDashboardRequests(c *gin.Context) {
var statistics []model.SystemStatistics
db := database.GetDB()
db.Where("type = 'total-req'").Order("created_at desc").Limit(30).Find(&statistics)
type Node struct {
Label string `json:"label"`
Value int64 `json:"value"`
}
var nodes = make([]Node, 0)
for i := 30; i > len(statistics); i-- {
nodes = append(nodes, Node{
Label: time.Now().Add(-time.Duration(i-1) * time.Hour * 24).Format("2006-01-02"),
Value: 0,
})
}
for i := len(statistics) - 1; i >= 0; i-- {
nodes = append(nodes, Node{
Label: statistics[i].CreatedAt.Format("2006-01-02"),
Value: statistics[i].Value,
})
}
response.Success(c, gin.H{"nodes": nodes, "total": len(nodes)})
}
func GetDashboardIntercepts(c *gin.Context) {
var statistics []model.SystemStatistics
db := database.GetDB()
db.Where("type = 'total-denied'").Order("created_at desc").Limit(30).Find(&statistics)
type Node struct {
Label string `json:"label"`
Value int64 `json:"value"`
}
var nodes = make([]Node, 0)
for i := 30; i > len(statistics); i-- {
nodes = append(nodes, Node{
Label: time.Now().Add(-time.Duration(i-1) * time.Hour * 24).Format("2006-01-02"),
Value: 0,
})
}
for i := len(statistics) - 1; i >= 0; i-- {
nodes = append(nodes, Node{
Label: statistics[i].CreatedAt.Format("2006-01-02"),
Value: statistics[i].Value,
})
}
response.Success(c, gin.H{"nodes": nodes, "total": len(nodes)})
}
================================================
FILE: management/webserver/api/detectlog.go
================================================
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"chaitin.cn/dev/go/errors"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg"
"github.com/gin-gonic/gin"
"chaitin.cn/dev/go/log"
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
"chaitin.cn/patronus/safeline-2/management/webserver/model"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
"chaitin.cn/patronus/safeline-2/management/webserver/utils"
)
type (
GetDetectLogDetailRequest struct {
EventId string `json:"event_id" form:"event_id"`
}
PostFalsePositivesRequest struct {
EventId string `json:"event_id"`
}
telemetryFalsePositives struct {
Telemetry struct {
Id string `json:"id"`
} `json:"telemetry"`
Safeline struct {
Id string `json:"id"`
Type string `json:"type"`
DetectLog model.DetectLog `json:"detect_log"`
} `json:"safeline"`
}
)
func getDetectLog(eventId string) (*model.DetectLog, error) {
db := database.GetDB()
var detectLogBasic model.DetectLogBasic
res := db.Where(&model.DetectLogBasic{EventId: eventId}).First(&detectLogBasic)
if res.RowsAffected == 0 {
return nil, errors.New("Data queried does not exist")
}
var detectLogDetail model.DetectLogDetail
db.Where(&model.DetectLogDetail{EventId: eventId}).First(&detectLogDetail)
detectLog, err := model.TransformDetectLog(&detectLogBasic, &detectLogDetail)
if err != nil {
return nil, err
}
return detectLog, nil
}
func GetDetectLogList(c *gin.Context) {
var params pageRequest
if err := c.BindQuery(¶ms); err != nil {
logger.Error(err)
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
tx := db.Where("")
// 按 ip 搜索条件
if ip := c.Query("ip"); ip != "" {
tx = tx.Where("src_ip = ?", ip)
}
// 按 url 搜索条件
if url := c.Query("url"); url != "" {
tx = tx.Where("url_path like ?", "%"+url+"%")
}
// 按 type 搜索条件
if at := c.Query("attack_type"); at != "" {
ns := make([]int, 0)
for _, s := range strings.Split(at, ",") {
n, err := strconv.Atoi(s)
if err == nil {
ns = append(ns, n)
}
}
tx = tx.Where("attack_type in (?)", ns)
}
var total int64
tx.Model(&model.DetectLogBasic{}).Count(&total)
var basicList []model.DetectLogBasic
tx.Limit(params.PageSize).Offset(params.PageSize * (params.Page - 1)).Order("id desc").Find(&basicList)
var dLogList []*model.DetectLog
for _, basic := range basicList {
dLog, err := model.TransformDetectLog(&basic, nil)
if err != nil {
logger.Warn(err)
continue
}
dLogList = append(dLogList, dLog)
}
response.Success(c, gin.H{"data": dLogList, "total": total})
}
func GetDetectLogDetail(c *gin.Context) {
var params GetDetectLogDetailRequest
if err := c.BindQuery(¶ms); err != nil {
logger.Error(err)
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
detectLog, err := getDetectLog(params.EventId)
if err != nil {
logger.Error(err)
response.Error(c, response.ErrorDataNotExist, http.StatusNotFound)
return
}
response.Success(c, detectLog)
}
func PostFalsePositives(c *gin.Context) {
var params PostFalsePositivesRequest
if err := c.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
detectLog, err := getDetectLog(params.EventId)
if err != nil {
logger.Error(err)
response.Error(c, response.ErrorDataNotExist, http.StatusNotFound)
return
}
db := database.GetDB()
var option model.Options
db.Where(&model.Options{Key: constants.MachineID}).First(&option)
var jsonData telemetryFalsePositives
jsonData.Telemetry.Id = constants.TelemetryId
jsonData.Safeline.Id = option.Value
jsonData.Safeline.Type = constants.FalsePositives
jsonData.Safeline.DetectLog = *detectLog
data, err := json.Marshal(jsonData)
if err != nil {
log.Warn(err)
response.Success(c, nil)
return
}
reader := bytes.NewReader(data)
client := utils.GetHTTPClient()
addr := config.GlobalConfig.Telemetry.Addr
rsp, err := pkg.DoPostTelemetry(client, addr, reader)
if err != nil {
log.Warn(err)
response.Success(c, nil)
return
}
if rsp.StatusCode != http.StatusOK && rsp.StatusCode != http.StatusCreated {
log.Warn(fmt.Sprintf("Transfer telemetry failed, status code = %d", rsp.StatusCode), err)
response.Success(c, nil)
return
}
response.Success(c, nil)
}
================================================
FILE: management/webserver/api/endpoints.go
================================================
package api
const (
Version = "/Version"
UpgradeTips = "/UpgradeTips"
Login = "/Login"
Logout = "/Logout"
OTPUrl = "/OTPUrl"
User = "/User"
DetectLogList = "/DetectLogList"
DetectLogDetail = "/DetectLogDetail"
Behaviour = "/Behaviour"
FalsePositives = "/FalsePositives"
Website = "/Website"
UploadSSLCert = "/UploadSSLCert"
SSLCert = "/SSLCert"
PolicyRule = "/PolicyRule"
SwitchPolicyRule = "/SwitchPolicyRule"
DashboardCounts = "/dashboard/counts"
DashboardSites = "/dashboard/sites"
DashboardQps = "/dashboard/qps"
DashboardRequests = "/dashboard/requests"
DashboardIntercepts = "/dashboard/intercepts"
PolicyGroupGlobal = "/PolicyGroupGlobal"
SrcIPConfig = "/SrcIPConfig"
)
================================================
FILE: management/webserver/api/policygroup.go
================================================
package api
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"chaitin.cn/dev/go/errors"
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
"chaitin.cn/patronus/safeline-2/management/webserver/model"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm"
)
func PutPolicyGroupGlobal(ctx *gin.Context) {
var params model.PolicyGroup
if err := ctx.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
err := db.Transaction(func(tx *gorm.DB) error {
var pggOption model.Options
res := tx.Where(&model.Options{Key: constants.PolicyGroupGlobal}).First(&pggOption)
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return errors.New("Data queried does not exist")
}
pggStr, err := json.Marshal(params)
if err != nil {
logger.Error(err)
response.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
return err
}
pggOption.Value = string(pggStr)
tx.Save(&pggOption)
if err := fvm.PushFSL(tx); err != nil {
return errors.New("Rules compile error, please check your params.")
}
return nil
})
if err != nil {
logger.Error(err)
response.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
return
}
response.Success(ctx, nil)
}
func GetPolicyGroupGlobal(ctx *gin.Context) {
var pggOption model.Options
database.GetDB().Where(&model.Options{Key: constants.PolicyGroupGlobal}).First(&pggOption)
var pgg model.PolicyGroup
err := json.Unmarshal([]byte(pggOption.Value), &pgg)
if err != nil {
logger.Error(err)
response.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
return
}
response.Success(ctx, gin.H{"data": pgg})
}
func PutSrcIPConfig(ctx *gin.Context) {
var params model.SrcIPConfig
if err := ctx.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
err := db.Transaction(func(tx *gorm.DB) error {
var scOption model.Options
res := tx.Where(&model.Options{Key: constants.SrcIPConfig}).First(&scOption)
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return errors.New("Data queried does not exist")
}
scStr, err := json.Marshal(params)
if err != nil {
logger.Error(err)
response.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
return err
}
scOption.Value = string(scStr)
tx.Save(&scOption)
if err := fvm.PushFSL(tx); err != nil {
return errors.New("Rules compile error, please check your params.")
}
return nil
})
if err != nil {
logger.Error(err)
response.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
return
}
response.Success(ctx, nil)
}
func GetSrcIPConfig(ctx *gin.Context) {
srcIPConfig, err := model.GetSrcIPConfig(database.GetDB().DB)
if err != nil {
return
}
response.Success(ctx, gin.H{"data": srcIPConfig})
}
================================================
FILE: management/webserver/api/policyrule.go
================================================
package api
import (
"net/http"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"chaitin.cn/dev/go/errors"
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
"chaitin.cn/patronus/safeline-2/management/webserver/model"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
)
type putSwitchRequest struct {
IDs []uint `json:"ids"`
IsEnabled bool `json:"is_enabled"`
}
func PostPolicyRule(ctx *gin.Context) {
var params model.PolicyRule
if err := ctx.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
err := db.Transaction(func(tx *gorm.DB) error {
policyRule := &model.PolicyRule{Action: params.Action, Comment: params.Comment, IsEnabled: params.IsEnabled, Pattern: params.Pattern}
res := tx.Create(policyRule)
if res.Error != nil {
return res.Error
}
if err := fvm.PushFSL(tx); err != nil {
return errors.New("Rules compile error, please check your params.")
}
return nil
})
if err != nil {
logger.Error(err)
response.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
return
}
response.Success(ctx, nil)
}
func PutSwitchPolicyRule(ctx *gin.Context) {
var params putSwitchRequest
if err := ctx.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
err := db.Transaction(func(tx *gorm.DB) error {
res := tx.Model(&model.PolicyRule{}).Where(params.IDs).Updates(model.PolicyRule{IsEnabled: params.IsEnabled})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return errors.New("Data queried does not exist")
}
if err := fvm.PushFSL(tx); err != nil {
return errors.New("Rules compile error, please check your params.")
}
return nil
})
if err != nil {
logger.Error(err)
response.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
return
}
}
func PutPolicyRule(ctx *gin.Context) {
var params model.PolicyRule
if err := ctx.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
err := db.Transaction(func(tx *gorm.DB) error {
var policyRule model.PolicyRule
res := tx.Where(params.ID).First(&policyRule)
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return errors.New("Data queried does not exist")
}
policyRule.Action = params.Action
policyRule.Comment = params.Comment
policyRule.IsEnabled = params.IsEnabled
policyRule.Pattern = params.Pattern
tx.Save(&policyRule)
if err := fvm.PushFSL(tx); err != nil {
return errors.New("Rules compile error, please check your params.")
}
return nil
})
if err != nil {
logger.Error(err)
response.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
return
}
response.Success(ctx, nil)
}
func DeletePolicyRule(ctx *gin.Context) {
var params idsRequest
if err := ctx.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
err := db.Transaction(func(tx *gorm.DB) error {
res := tx.Where(params.IDs).Delete(&model.PolicyRule{})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return errors.New("Data queried does not exist")
}
if err := fvm.PushFSL(tx); err != nil {
return errors.New("Rules compile error, please check your params.")
}
return nil
})
if err != nil {
logger.Error(err)
response.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
return
}
response.Success(ctx, nil)
}
func GetPolicyRule(ctx *gin.Context) {
var params pageRequest
if err := ctx.BindQuery(¶ms); err != nil {
logger.Error(err)
response.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
var policyRuleList []model.PolicyRule
db.Limit(params.PageSize).Offset(params.PageSize * (params.Page - 1)).Order("id desc").Find(&policyRuleList)
var total int64
db.Model(&model.PolicyRule{}).Count(&total)
response.Success(ctx, gin.H{"data": policyRuleList, "total": total})
}
================================================
FILE: management/webserver/api/response/error.go
================================================
package response
const (
ErrLoginRequired = "login-required"
ErrWrongPasscode = "wrong-passcode"
ErrWrongTimeGap = "wrong-time-gap"
ErrInternalError = "internal-error"
ErrDataNotExist = "data-not-exist"
ErrWrongFileType = "wrong-filetype"
ErrReadOnly = "read-only"
)
================================================
FILE: management/webserver/api/response/jsonbody.go
================================================
package response
import (
"net/http"
"github.com/gin-gonic/gin"
)
type JSONBody struct {
Err string
Msg string
Data interface{}
}
var (
ErrorLoginRequired = JSONBody{ErrLoginRequired, "Login required", nil}
ErrorParamNotOK = JSONBody{ErrInternalError, "Error occurred when extracting params", nil}
ErrorDataNotExist = JSONBody{ErrDataNotExist, "Data queried does not exist", nil}
ErrorReadOnly = JSONBody{ErrReadOnly, "This environment is read only", nil}
)
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, gin.H{
"data": data,
"msg": "",
"err": nil,
})
//c.Abort()
}
func SuccessWithList(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, gin.H{
"data": data,
"msg": "",
"err": nil,
})
//c.Abort()
}
func SuccessWithMsg(c *gin.Context, data interface{}, msg string) {
c.JSON(http.StatusOK, gin.H{
"data": data,
"msg": msg,
"err": nil,
})
//c.Abort()
}
func Error(c *gin.Context, rsp JSONBody, status int) {
c.JSON(status, gin.H{
"data": rsp.Data,
"msg": rsp.Msg,
"err": rsp.Err,
})
//c.Abort()
}
================================================
FILE: management/webserver/api/response/png.go
================================================
package response
import (
"net/http"
"github.com/gin-gonic/gin"
)
const StreamContentType = "application/octet-stream"
func PNG(c *gin.Context, bytes []byte) {
c.Data(http.StatusOK, StreamContentType, bytes)
}
================================================
FILE: management/webserver/api/website.go
================================================
package api
import (
"encoding/json"
"net/http"
"strconv"
"chaitin.cn/dev/go/errors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
"chaitin.cn/patronus/safeline-2/management/webserver/model"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
"chaitin.cn/patronus/safeline-2/management/webserver/rpc"
)
func publishWebsiteConfig(website *model.Website) error {
byteWebsite, err := json.Marshal(website)
if err != nil {
return err
}
err = rpc.Publish(byteWebsite, rpc.EventTypeWebsite)
if err != nil {
return err
}
return nil
}
func publishDeleteWebsiteConfig(id []uint) error {
byteId, err := json.Marshal(id)
if err != nil {
return err
}
err = rpc.Publish(byteId, rpc.EventTypeDeleteWebsite)
if err != nil {
return err
}
return nil
}
func PostWebsite(ctx *gin.Context) {
var params model.Website
if err := ctx.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
err := db.Transaction(func(tx *gorm.DB) error {
website := &model.Website{Comment: params.Comment, ServerNames: params.ServerNames, Upstreams: params.Upstreams, Ports: params.Ports,
CertFilename: params.CertFilename, KeyFilename: params.KeyFilename, IsEnabled: true}
res := tx.Create(website)
if res.Error != nil {
return res.Error
}
if config.GlobalConfig.Server.DevMode {
return nil
}
err := publishWebsiteConfig(website)
if err != nil {
return err
}
return nil
})
if err != nil {
logger.Error(err)
response.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
return
}
response.Success(ctx, nil)
}
func PutWebsite(ctx *gin.Context) {
var params model.Website
if err := ctx.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
err := db.Transaction(func(tx *gorm.DB) error {
var website model.Website
res := tx.Where(params.ID).First(&website)
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return errors.New("Data queried does not exist")
}
website.Comment = params.Comment
website.ServerNames = params.ServerNames
website.Upstreams = params.Upstreams
website.Ports = params.Ports
website.CertFilename = params.CertFilename
website.KeyFilename = params.KeyFilename
website.IsEnabled = true
tx.Save(&website)
if config.GlobalConfig.Server.DevMode {
return nil
}
err := publishWebsiteConfig(¶ms)
if err != nil {
return err
}
return nil
})
if err != nil {
logger.Error(err)
response.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
return
}
response.Success(ctx, nil)
}
func DeleteWebsite(ctx *gin.Context) {
var params idsRequest
if err := ctx.BindJSON(¶ms); err != nil {
logger.Error(err)
response.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
err := db.Transaction(func(tx *gorm.DB) error {
res := tx.Where(params.IDs).Delete(&model.Website{})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return errors.New("Data queried does not exist")
}
if config.GlobalConfig.Server.DevMode {
return nil
}
err := publishDeleteWebsiteConfig(params.IDs)
if err != nil {
return err
}
return nil
})
if err != nil {
logger.Error(err)
response.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
return
}
response.Success(ctx, nil)
}
func GetWebsite(ctx *gin.Context) {
var params pageRequest
if err := ctx.BindQuery(¶ms); err != nil {
logger.Error(err)
response.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
type Website struct {
model.Website
ReqValue int64 `json:"req_value"`
DeniedValue int64 `json:"denied_value"`
}
var websiteList []Website
db.Limit(params.PageSize).Offset(params.PageSize * (params.Page - 1)).Order("id desc").Find(&websiteList)
var statistics []model.SystemStatistics
var websiteIds []string
for _, i := range websiteList {
websiteIds = append(websiteIds, strconv.Itoa(int(i.ID)))
}
db.Where("created_at >= date_trunc('day',now())").Where("website in (?)", websiteIds).Find(&statistics)
for i, website := range websiteList {
for _, j := range statistics {
if strconv.Itoa(int(website.ID)) == j.Website {
if j.Type == "website-req" {
websiteList[i].ReqValue = j.Value
} else if j.Type == "website-denied" {
websiteList[i].DeniedValue = j.Value
}
}
}
}
var total int64
db.Model(&model.Website{}).Count(&total)
response.Success(ctx, gin.H{"data": websiteList, "total": total})
}
================================================
FILE: management/webserver/cmd/fake_logs.go
================================================
package cmd
import "chaitin.cn/patronus/safeline-2/management/webserver/model"
func FakeLogs() {
model.InitDetectLogSamples()
}
================================================
FILE: management/webserver/cmd/gen_certs.go
================================================
package cmd
import (
"crypto/x509/pkix"
"path/filepath"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/webserver/utils"
)
func GenCerts() error {
if err := genServerCert(); err != nil {
return err
}
if err := genClientCACert(); err != nil {
return err
}
return nil
}
func genServerCert() error {
certPath := filepath.Join(config.GlobalConfig.MgtResDir, constants.CertsPath, "server.crt")
keyPath := filepath.Join(config.GlobalConfig.MgtResDir, constants.CertsPath, "server.key")
if err := utils.WriteCertIfNotExist(
certPath,
keyPath,
func() ([]byte, []byte, error) {
return utils.GenerateCert(
[]string{},
3650,
4096,
&pkix.Name{
Country: []string{"CN"},
Province: []string{"Beijing"},
Locality: []string{"Beijing"},
Organization: []string{"Beijing WAF Technology Co., Ltd."},
OrganizationalUnit: []string{"Service Infrastructure Department"},
CommonName: "WAF Management Server",
},
false,
)
}); err != nil {
return err
}
return nil
}
func genClientCACert() error {
certPath := filepath.Join(config.GlobalConfig.MgtResDir, constants.CertsPath, "client_ca.crt")
keyPath := filepath.Join(config.GlobalConfig.MgtResDir, constants.CertsPath, "client_ca.key")
if err := utils.WriteCertIfNotExist(
certPath,
keyPath,
func() ([]byte, []byte, error) {
return utils.GenerateCert(
[]string{},
3650,
4096,
&pkix.Name{
Country: []string{"CN"},
Province: []string{"Beijing"},
Locality: []string{"Beijing"},
Organization: []string{"Beijing WAF Technology Co., Ltd."},
OrganizationalUnit: []string{"Service Infrastructure Department"},
CommonName: "WAF Client Certificate Authority",
},
true,
)
}); err != nil {
return err
}
return nil
}
================================================
FILE: management/webserver/cmd/push_fsl.go
================================================
package cmd
import (
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm"
)
func PushFSL() error {
return fvm.PushFSL(database.GetDB().DB)
}
================================================
FILE: management/webserver/cmd/reset_user.go
================================================
package cmd
import (
"chaitin.cn/patronus/safeline-2/management/webserver/model"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
)
func ResetUser(username string) {
db := database.GetDB()
var user model.User
db.Where(&model.User{Username: username}).First(&user)
user.LastLoginTime = 0
db.Save(&user)
}
================================================
FILE: management/webserver/cmd/show_fsl.go
================================================
package cmd
import (
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm"
)
func ShowFSL() (string, error) {
return fvm.GenerateFullFSL(database.GetDB().DB)
}
================================================
FILE: management/webserver/config.yml
================================================
# develop use only. For production, refer to `package/build/mgt-api/webserver/config.yml`
log:
output: stdout # "stdout", "stderr" or file path
level: debug # "debug", "info", "warn" or "error"
server:
listen_addr: :9001
dev_mode: true
db:
url: postgres://safeline-ce:safeline-ce@127.0.0.1/safeline-ce
log_sql: false
detector:
addr: ""
fsl_bytecode: fvm/bytecode
grpc_server:
listen_addr: :9002
================================================
FILE: management/webserver/go.mod
================================================
module chaitin.cn/patronus/safeline-2/management/webserver
go 1.21
toolchain go1.21.3
require (
chaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6
chaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c
chaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c
github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.10.0
github.com/pquerna/otp v1.4.0
github.com/robfig/cron/v3 v3.0.1
github.com/rogpeppe/go-internal v1.10.0
github.com/sirupsen/logrus v1.4.2
google.golang.org/grpc v1.65.0
gorm.io/datatypes v1.1.1
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.10
)
require (
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.4.7 // indirect
)
================================================
FILE: management/webserver/go.sum
================================================
chaitin.cn/dev/go/errors v0.0.0-20200717101723-df6132d53dc8/go.mod h1:u+ZD0shdyUt0UG9XfCgD1R5mSqXpTVoO5rwQXQeo3Eo=
chaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6 h1:1Qa9ABk907/9ZrOLbbRcS8Fqq9VhjAF/mLjbSP1qAJY=
chaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6/go.mod h1:u+ZD0shdyUt0UG9XfCgD1R5mSqXpTVoO5rwQXQeo3Eo=
chaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c h1:Xn9IYkxmnpDcEpV+7JIR5ufEIexd1dhqKwpOLG1mYOE=
chaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c/go.mod h1:xJIYwUoA2TX5mNg/RBrEPyE251BPwj+70/mM7UIhoxg=
chaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c h1:tXsraF7o9iUsQY6IwpDJusc6OFhB7iv/bBTfgR3MPUU=
chaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c/go.mod h1:fUvtmpG8Z8Zf5aciadL9a/vn5SB3knG7pdNJixDplPg=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA=
github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.1.1 h1:XAjO7NNfUKVUvnS3+BkqMrPXxCAcxDlpOYbjnizxNCw=
gorm.io/datatypes v1.1.1/go.mod h1:u8GEgFjJ+GpsGfgHmBUcQqHm/937t3sj/SO9dvbndTg=
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
================================================
FILE: management/webserver/main.go
================================================
package main
import (
"flag"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"chaitin.cn/patronus/safeline-2/management/webserver/api"
"chaitin.cn/patronus/safeline-2/management/webserver/cmd"
"chaitin.cn/patronus/safeline-2/management/webserver/middleware"
"chaitin.cn/patronus/safeline-2/management/webserver/model"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/cron"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/log"
"chaitin.cn/patronus/safeline-2/management/webserver/rpc"
)
var (
logger = log.GetLogger("main")
version = "undefined"
githash = "undefined"
buildstamp = "undefined"
goVersion = "undefined"
)
func init() {
// do something that do not raise error
}
func main() {
log.SetLogFormatter()
fs := flag.NewFlagSet("webserver", flag.ExitOnError)
showVersion := fs.Bool("v", false, "show version")
cfgFile := fs.String("c", constants.ConfigFilePath, "config file path")
genCerts := fs.Bool("gen_certs", false, "generate certs")
showFSL := fs.Bool("show_fsl", false, "show full selectors")
push_fsl := fs.Bool("push_fsl", false, "compile and push fsl")
fakeLogs := fs.Bool("fake_logs", false, "fake logs")
resetUsername := fs.String("reset_user", "", "reset user")
if err := fs.Parse(os.Args[1:]); err != nil {
logger.Fatalln("Failed to parse args: ", err)
}
if *showVersion {
i, _ := strconv.Atoi(buildstamp)
t := time.Unix(int64(i), 0).Format("2006-01-02 15:04:05")
fmt.Println("Version: ", version)
fmt.Println("Githash: ", githash)
fmt.Println("Build: ", t)
fmt.Println("Go version: ", goVersion)
return
}
constants.Version = strings.TrimPrefix(version, "ce-")
// init configs
if err := config.InitConfigs(*cfgFile); err != nil {
logger.Fatalln("Failed to init configs: ", err)
}
if err := log.InitLogger(); err != nil {
logger.Fatalln("Failed to init db: ", err)
}
if *genCerts {
if err := cmd.GenCerts(); err != nil {
logger.Fatalln("Failed to generate certs: ", err)
}
return
}
logger.Info("Init database")
if err := database.InitDB(); err != nil {
logger.Fatalln("Failed to init db: ", err)
}
if *showFSL {
if fullFSL, err := cmd.ShowFSL(); err != nil {
logger.Fatalln("Failed to generate fsl: ", err)
} else {
logger.Info(strings.ReplaceAll(strings.ReplaceAll(fullFSL, ";", ";\n"), "CREATE", "\nCREATE"))
}
return
}
logger.Info("Init models")
if err := model.InitModels(); err != nil {
logger.Fatalln("Failed to init models: ", err)
}
if len(*resetUsername) > 0 {
logger.Infoln("reset user:", *resetUsername)
cmd.ResetUser(*resetUsername)
logger.Infoln("success!")
return
}
if *fakeLogs {
logger.Infoln("faking logs...")
cmd.FakeLogs()
logger.Infoln("success!")
return
}
if *push_fsl {
logger.Infoln("push fsl...")
if err := cmd.PushFSL(); err != nil {
logger.Fatalln("Failed to generate fsl: ", err)
}
logger.Infoln("success!")
return
}
logger.Info("Init FVM bytecode")
fvm.InitFVMBytecode()
if err := cron.StartCron(); err != nil {
logger.Fatalln("Failed to start cron: ", err)
}
if err := rpc.StartGRPCSever(); err != nil {
logger.Fatalln("Failed to start gRPC server: ", err)
}
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
var option model.Options
database.GetDB().Where(&model.Options{Key: constants.SecretKey}).First(&option)
logger.Debugf("Secret: %s", option.Value)
store := cookie.NewStore([]byte(option.Value))
r.Use(sessions.Sessions("session", store))
publicRouters := r.Group("/api")
publicRouters.POST(api.Login, api.PostLogin)
publicRouters.POST(api.Logout, api.PostLogout)
publicRouters.POST(api.Behaviour, api.PostBehaviour)
publicRouters.POST(api.FalsePositives, api.PostFalsePositives)
publicRouters.GET(api.OTPUrl, api.GetOTPUrl)
publicRouters.GET(api.Version, api.GetVersion)
publicRouters.GET(api.UpgradeTips, api.GetUpgradeTips)
// test use
publicRouters.GET("/Ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
limitedRouters := r.Group("/api")
noAuth, existed := os.LookupEnv("NO_AUTH")
if existed && len(noAuth) >= 0 {
logger.Warn("No auth")
} else {
limitedRouters.Use(middleware.AuthRequired)
}
readOnly, existed := os.LookupEnv("READ_ONLY")
if existed && len(readOnly) >= 0 {
logger.Warn("Read only")
limitedRouters.Use(middleware.ReadOnly)
}
limitedRouters.GET(api.User, api.GetUser)
limitedRouters.GET(api.DetectLogList, api.GetDetectLogList)
limitedRouters.GET(api.DetectLogDetail, api.GetDetectLogDetail)
limitedRouters.POST(api.Website, api.PostWebsite)
limitedRouters.PUT(api.Website, api.PutWebsite)
limitedRouters.DELETE(api.Website, api.DeleteWebsite)
limitedRouters.GET(api.Website, api.GetWebsite)
limitedRouters.POST(api.UploadSSLCert, api.PostUploadSSLCert)
limitedRouters.POST(api.SSLCert, api.PostSSLCert)
limitedRouters.POST(api.PolicyRule, api.PostPolicyRule)
limitedRouters.PUT(api.PolicyRule, api.PutPolicyRule)
limitedRouters.DELETE(api.PolicyRule, api.DeletePolicyRule)
limitedRouters.GET(api.PolicyRule, api.GetPolicyRule)
limitedRouters.PUT(api.SwitchPolicyRule, api.PutSwitchPolicyRule)
// 仪表盘接口
limitedRouters.GET(api.DashboardCounts, api.GetDashboardCounts)
limitedRouters.GET(api.DashboardSites, api.GetDashboardSites)
limitedRouters.GET(api.DashboardQps, api.GetDashboardQps)
limitedRouters.GET(api.DashboardRequests, api.GetDashboardRequests)
limitedRouters.GET(api.DashboardIntercepts, api.GetDashboardIntercepts)
limitedRouters.GET(api.PolicyGroupGlobal, api.GetPolicyGroupGlobal)
limitedRouters.PUT(api.PolicyGroupGlobal, api.PutPolicyGroupGlobal)
limitedRouters.GET(api.SrcIPConfig, api.GetSrcIPConfig)
limitedRouters.PUT(api.SrcIPConfig, api.PutSrcIPConfig)
logger.Info("Staring...")
if err := r.Run(config.GlobalConfig.Server.ListenAddr); err != nil {
logger.Fatalln("Error occurred when running web server: ", err)
}
}
================================================
FILE: management/webserver/middleware/auth.go
================================================
package middleware
import (
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
)
func AuthRequired(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(constants.DefaultSessionUserKey)
if user == nil {
response.Error(c, response.ErrorLoginRequired, http.StatusUnauthorized)
c.Abort()
return
}
// extend session expired time
session.Options(sessions.Options{
Path: "/",
MaxAge: 3600 * 24 * 7,
//Domain: options.Domain,
//HttpOnly: true,
//SameSite: http.SameSiteLaxMode,
//Secure: false,
})
if err := session.Save(); err != nil {
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Error occurred when creating sessions"}, http.StatusInternalServerError)
return
}
c.Next()
}
func ReadOnly(c *gin.Context) {
if c.Request.Method != "GET" && c.Request.Method != "HEAD" && c.Request.Method != "OPTIONS" {
response.Error(c, response.ErrorReadOnly, http.StatusBadRequest)
c.Abort()
}
}
================================================
FILE: management/webserver/model/base.go
================================================
package model
import (
"time"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/log"
)
// Base is a replacement for gorm.Model without DeletedAt, which is considered to be not good.
type Base struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time ` json:"created_at"`
UpdatedAt time.Time ` json:"updated_at"`
}
var logger = log.GetLogger("model")
================================================
FILE: management/webserver/model/behaviour.go
================================================
package model
type Behaviour struct {
Base
SrcRouter string `json:"src_router"`
DstRouter string `json:"dst_router"`
}
================================================
FILE: management/webserver/model/db_patch_1_4_0.go
================================================
package model
import (
"strings"
"chaitin.cn/dev/go/errors"
"gorm.io/gorm"
)
type sqlResult struct {
Ids string `json:"ids"`
}
func DBPatch140(tx *gorm.DB) error {
if !tx.Migrator().HasTable(&SystemStatistics{}) {
return nil
}
var result []sqlResult
//SELECT string_agg(id::text, ',') as ids FROM mgt_system_statistics GROUP BY (type, website, created_at) HAVING COUNT(*) > 1) as tmp
res := tx.Model(&SystemStatistics{}).Select("string_agg(id::text, ',') as ids").Group("type, website, created_at").Having("COUNT(*) > 1").Find(&result)
if res.Error != nil {
return errors.Wrap(res.Error, "Failed to select data")
}
if len(result) <= 0 {
return nil
}
var deleteIds []string
for _, s := range result {
tmpIds := strings.Split(s.Ids, ",")
deleteIds = append(deleteIds, tmpIds...)
}
deleteRes := tx.Delete(&SystemStatistics{}, deleteIds)
return errors.Wrap(deleteRes.Error, "Failed to delete")
}
================================================
FILE: management/webserver/model/detectlog.go
================================================
package model
import (
"fmt"
"math/rand"
"strings"
"time"
"chaitin.cn/dev/go/errors"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
"chaitin.cn/patronus/safeline-2/management/webserver/utils"
)
// DetectLog is designed to be used in response, not a good naming.
type DetectLog struct {
DetectLogBasic
DetectLogDetail
EventId string `json:"event_id"` // to eliminate ambiguous
Website string `json:"website"`
AttackType string `json:"attack_type"`
Module string `json:"module"`
Reason string `json:"reason"`
}
func getRuleModule(ruleId string, attackType int) string {
var module string
if strings.HasPrefix(ruleId, "m_rule") { // `m_rule/65543`
module = "m_rule"
} else if strings.HasPrefix(ruleId, "/") {
if attackType == -2 {
module = "whitelist"
} else { // if attackType == -3
module = "blacklist"
}
} else {
module = ruleId
}
return constants.RuleModule[module]
}
func getRuleReason(ruleId string, attackType int) string {
var reason string
if strings.HasPrefix(ruleId, "m_rule") { // `m_rule/65543`
reason = constants.RuleReason[strings.ReplaceAll(ruleId, "m_rule/", "")]
} else if strings.HasPrefix(ruleId, "/") {
reason = ruleId[1:]
} else {
reason = fmt.Sprintf("检测到 %s 攻击", getAttackType(attackType))
}
return reason
}
func getAttackType(at int) string {
atStr, ok := constants.AttackType[at]
if !ok {
atStr = constants.AttackType[62] // unknown
}
return atStr
}
func getCountry(country string) string {
countryStr, ok := constants.CountryCode[country]
if !ok {
countryStr = ""
}
return countryStr
}
func TransformDetectLog(basic *DetectLogBasic, detail *DetectLogDetail) (*DetectLog, error) {
if basic == nil {
return nil, errors.New("basic *DetectLogBasic cannot be nil")
}
if detail != nil && basic.EventId != detail.EventId {
return nil, errors.New("EventId field should be the same for basic and detail")
}
basic.Country = getCountry(basic.Country)
dLog := DetectLog{
DetectLogBasic: *basic,
EventId: basic.EventId,
Website: utils.BuildUrl(basic.Protocol, basic.Host, basic.DstPort, basic.UrlPath),
AttackType: getAttackType(basic.AttackType),
Module: getRuleModule(basic.RuleId, basic.AttackType),
Reason: getRuleReason(basic.RuleId, basic.AttackType),
}
if detail != nil {
dLog.DetectLogDetail = *detail
}
return &dLog, nil
}
type DetectLogBasic struct {
ID uint `json:"id" gorm:"primarykey"`
EventId string `json:"event_id" gorm:"uniqueIndex;not null"`
SiteUUID string `json:"site_uuid" gorm:"column:site_uuid"`
SrcIp string `json:"src_ip" gorm:"index"`
SocketIp string `json:"socket_ip" gorm:"index"`
Protocol int `json:"protocol"`
Host string `json:"host"`
UrlPath string `json:"url_path"`
DstPort uint `json:"dst_port"`
Country string `json:"country"`
Province string `json:"province"`
City string `json:"city"`
AttackType int `json:"attack_type" gorm:"index"`
RiskLevel int `json:"risk_level" gorm:"index"`
Action int `json:"action" gorm:"index"`
RuleId string `json:"rule_id"`
Timestamp int64 `json:"timestamp" gorm:"index"`
}
type DetectLogDetail struct {
ID uint ` gorm:"primarykey"`
EventId string ` gorm:"uniqueIndex;not null"`
SrcPort uint `json:"src_port"`
DstIp string `json:"dst_ip"`
Method string `json:"method"`
QueryString string `json:"query_string"`
StatusCode uint `json:"status_code"`
ReqHeader string `json:"req_header"`
ReqBody string `json:"req_body"`
RspHeader string `json:"rsp_header"`
RspBody string `json:"rsp_body"`
Payload string `json:"payload"`
Location string `json:"location"`
DecodePath string `json:"decode_path"`
}
func InitDetectLogSamples() {
db := database.GetDB()
var detectLogBasicList []DetectLogBasic
var detectLogDetailList []DetectLogDetail
timestamp := time.Now().Unix()
detail := DetectLogDetail{
SrcPort: 58694,
DstIp: "10.2.35.143",
Method: "GET",
QueryString: "",
ReqHeader: "GET /webshell.php HTTP/1.1\nUser-Agent: curl/7.77.0\nAccept: */*\n\n\"",
ReqBody: "",
RspHeader: "",
RspBody: "",
Payload: "",
Location: "urlpath",
DecodePath: "",
}
protocolList := []int{constants.ProtocolHTTP, constants.ProtocolHTTPS}
portList := []uint{80, 443}
provinceList := []string{}
cityList := []string{}
ipList := []string{}
ruleIdList := []string{}
for i := 0; i < 100; i++ {
randInt := rand.Intn(1000)
eventId := utils.RandStr(32)
basic := DetectLogBasic{}
basic.EventId = eventId
basic.SrcIp = ipList[randInt%len(ipList)]
basic.SocketIp = ipList[randInt%len(ipList)]
basic.Protocol = protocolList[randInt%len(protocolList)]
basic.DstPort = portList[randInt%len(portList)]
basic.Host = fmt.Sprintf("%s.com", utils.RandStr(5))
basic.UrlPath = fmt.Sprintf("/%s", utils.RandStr(10))
basic.Country = "CN"
basic.Province = provinceList[randInt%len(provinceList)]
basic.City = cityList[randInt%len(cityList)]
basic.AttackType = randInt % 32
basic.RiskLevel = randInt % 4
basic.Action = randInt % 2
basic.RuleId = ruleIdList[randInt%len(ruleIdList)]
basic.Timestamp = timestamp - int64(randInt*100)
detailCopy := detail
detailCopy.EventId = eventId
detectLogBasicList = append(detectLogBasicList, basic)
detectLogDetailList = append(detectLogDetailList, detailCopy)
}
db.CreateInBatches(detectLogBasicList, 100)
db.CreateInBatches(detectLogDetailList, 100)
}
================================================
FILE: management/webserver/model/init.go
================================================
package model
import (
"gorm.io/gorm"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
)
func InitModels() error {
db := database.GetDB()
err := db.Transaction(func(tx *gorm.DB) error {
//
if err := DBPatch140(tx); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
if err := db.AutoMigrate(&User{}, &DetectLogBasic{}, &DetectLogDetail{}, &Behaviour{}, &Options{}, &Website{}, &PolicyRule{}, &SystemStatistics{}); err != nil {
return err
}
if err := initAdminUser(); err != nil {
return err
}
if err := initOptions(); err != nil {
return err
}
if err := initPolicyGroupGlobal(); err != nil {
return err
}
if err := initSrcIPConfig(); err != nil {
return err
}
//InitDetectLogSamples()
return nil
}
================================================
FILE: management/webserver/model/option.go
================================================
package model
import (
"bytes"
"encoding/json"
"net/http"
"gorm.io/gorm/clause"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
"chaitin.cn/patronus/safeline-2/management/webserver/utils"
)
type Options struct {
Base
Key string `gorm:"column:key;uniqueIndex"`
Value string `gorm:"column:value;"`
}
func initOptions() error {
db := database.GetDB()
secretKey := Options{Key: constants.SecretKey, Value: utils.RandStr(32)}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&secretKey)
machineId := Options{Key: constants.MachineID, Value: utils.RandStr(32)}
_ = db.Clauses(clause.OnConflict{DoNothing: true}).Create(&machineId)
go NotifyInstallation(machineId.Value)
return nil
}
func NotifyInstallation(machineId string) {
logger.Info("Notify installation")
tr := pkg.TelemetryRequest{
Telemetry: pkg.TelemetryInfo{
Id: constants.TelemetryId,
},
Safeline: pkg.SafelineInfo{
Id: machineId,
Type: constants.Installation,
Version: constants.Version,
},
}
data, err := json.Marshal(tr)
if err != nil {
logger.Error(err)
return
}
reader := bytes.NewReader(data)
rsp, err := pkg.DoPostTelemetry(utils.GetHTTPClient(), config.GlobalConfig.Telemetry.Addr, reader)
if err != nil {
logger.Error(err)
return
}
if rsp.StatusCode != http.StatusOK && rsp.StatusCode != http.StatusCreated {
logger.Errorf("transfer telemetry %s failed, status code = %d", constants.Installation, rsp.StatusCode)
return
}
}
================================================
FILE: management/webserver/model/policygroup.go
================================================
package model
import (
"encoding/json"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"chaitin.cn/dev/go/errors"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
)
type PolicyGroup map[string]string
const (
StrictMode = "strict"
DefaultMode = "default"
DisableMode = "disable"
SocketIP = "socket_ip"
HTTPHeader = "http_header"
)
type SrcIPConfig struct {
Source string `json:"source"` // socket_ip, http_header
Value string `json:"value"`
}
type SkynetConfigModule struct {
DetectConfig interface{} `json:"detect_config,omitempty"`
HighRiskAction string `json:"high_risk_action"`
MediumRiskAction string `json:"medium_risk_action"`
LowRiskAction string `json:"low_risk_action"`
HighRiskEnableLog int `json:"high_risk_enable_log"`
MediumRiskEnableLog int `json:"medium_risk_enable_log"`
LowRiskEnableLog int `json:"low_risk_enable_log"`
State string `json:"state"`
}
type SkynetConfig struct {
DetectConfig string `json:"detect_config"`
DeepDetect bool `json:"deep_detect"`
Timeout interface{} `json:"timeout"`
Modules map[string]*SkynetConfigModule `json:"modules"`
}
func GetSrcIPConfig(db *gorm.DB) (*SrcIPConfig, error) {
var scOption Options
db.Where(&Options{Key: constants.SrcIPConfig}).First(&scOption)
var sc SrcIPConfig
err := json.Unmarshal([]byte(scOption.Value), &sc)
if err != nil {
return nil, err
}
return &sc, nil
}
func initSrcIPConfig() error {
db := database.GetDB()
// policyGroupGlobal config, three mode: strict/default/disable
var srcIPConfig = SrcIPConfig{
Source: SocketIP,
Value: "",
}
scStr, err := json.Marshal(srcIPConfig)
if err != nil {
return err
}
pgg := Options{Key: constants.SrcIPConfig, Value: string(scStr)}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&pgg)
return nil
}
func GetSkynetConfig(db *gorm.DB) (string, error) {
// skynetConfigStr skynet config in 5.3.9
const skynetConfigStr = "{\"decode_config\":{\"decode_methods\":[\"url decode\",\"JSON\",\"base64\",\"hex\",\"eval\",\"XML\",\"PHP deserialize\",\"utf7\"]},\"deep_detect\":false,\"modules\":{\"m_asp_code_injection\":{\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_cmd_injection\":{\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_csrf\":{\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_file_include\":{\"detect_config\":{\"detect_suspicious_schema\":true},\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_file_upload\":{\"detect_config\":{\"action_of_handling_custome_file\":[\"deny transmission\"],\"detect_file_content\":true,\"detect_real_file_content\":false,\"enable_module_in_detecting_file_content\":[\"java\",\"asp_code_injection\",\"php_code_injection\"],\"file_type_of_custome_action\":[],\"forbidden_extra_token\":false,\"forbidden_multiple_filename\":true,\"forbidden_suspicious_filename\":true},\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_http\":{\"detect_config\":{\"warning_http_parse_failed\":false,\"warning_suspicious_http_version\":false},\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"disabled\"},\"m_java\":{\"detect_config\":{\"detect_lookup\":false},\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_java_unserialize\":{\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_php_code_injection\":{\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_php_unserialize\":{\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_response\":{\"detect_config\":{\"detection_configure\":[\"detect_jsp_code_leak\",\"detect_php_code_leak\",\"detect_webshell\"],\"error_types\":[\"directory indexing\",\"SQL execution error\",\"server exception\"]},\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"disabled\"},\"m_rule\":{\"detect_config\":{\"check_info_leak_by_rsp\":false,\"compatibility_mode\":false,\"detection_configure\":[\"detect_phpinfo\",\"detect_admin_page\",\"detect_backdoor\",\"detect_php_vuln\",\"detect_nginx\",\"detect_dedecms\",\"detect_apache\",\"detect_xml\",\"detect_struts2\",\"detect_java_vuln\",\"detect_php168\",\"detect_wordpress\",\"detect_directory_traversal\",\"detect_iis\",\"detect_gogs_gitea\",\"detect_thinkphp\",\"detect_jenkins\",\"detect_ecshop\",\"detect_nexus\",\"detect_drupal\",\"detect_ghostscript\",\"detect_atlassian\",\"detect_weblogic\",\"detect_coremail\",\"detect_phpcms\",\"detect_spring\",\"detect_southidc\",\"detect_fastjson\",\"detect_tbk_dvr\",\"detect_joomla\",\"detect_ecology_oa\",\"detect_jackson\",\"detect_xstream\",\"detect_activemq\",\"detect_solr\",\"detect_csii\",\"detect_big_ip\",\"detect_apisix\",\"detect_druid\",\"detect_log4j\"],\"info_leak_types\":[\"test file\",\"backup file\",\"code repository\",\"server sensitive file\"],\"rules_config\":{\"disable_ruleid\":[],\"enable_ruleid\":[],\"rules_status\":[]}},\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_scanner\":{\"detect_config\":{\"language_types\":[\"python_scanner\",\"go_scanner\"],\"other_types\":[\"normal_scanner\"],\"spider_types\":[]},\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"disabled\"},\"m_sqli\":{\"detect_config\":{\"detect_non_injection_sql\":true},\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_ssrf\":{\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_ssti\":{\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"},\"m_xss\":{\"detect_config\":{\"detect_complete_html\":true},\"high_risk_action\":\"deny\",\"high_risk_enable_log\":1,\"low_risk_action\":\"continue\",\"low_risk_enable_log\":1,\"medium_risk_action\":\"continue\",\"medium_risk_enable_log\":1,\"state\":\"enabled\"}},\"timeout\":{\"threshold\":1000,\"log\":\"enabled\"}}"
var sc SkynetConfig
err := json.Unmarshal([]byte(skynetConfigStr), &sc)
if err != nil {
return "", err
}
var pggOption Options
db.Where(&Options{Key: constants.PolicyGroupGlobal}).First(&pggOption)
var pgg PolicyGroup
err = json.Unmarshal([]byte(pggOption.Value), &pgg)
if err != nil {
return "", err
}
for module, mode := range pgg {
switch mode {
case StrictMode:
scm := sc.Modules[module]
scm.HighRiskAction = "deny"
scm.MediumRiskAction = "deny"
scm.LowRiskAction = "deny"
scm.State = "enabled"
sc.Modules[module] = scm
case DefaultMode:
scm := sc.Modules[module]
scm.HighRiskAction = "deny"
scm.MediumRiskAction = "continue"
scm.LowRiskAction = "continue"
scm.State = "enabled"
sc.Modules[module] = scm
case DisableMode:
scm := sc.Modules[module]
scm.HighRiskAction = "continue"
scm.MediumRiskAction = "continue"
scm.LowRiskAction = "continue"
scm.State = "disabled"
sc.Modules[module] = scm
default:
return "", errors.New("no such mode")
}
}
scStr, err := json.Marshal(sc)
if err != nil {
return "", err
}
return string(scStr), nil
}
func initPolicyGroupGlobal() error {
db := database.GetDB()
// policyGroupGlobal config, three mode: strict/default/disable
var policyGroupGlobal = PolicyGroup{
"m_asp_code_injection": DefaultMode,
"m_cmd_injection": DefaultMode,
"m_csrf": DefaultMode,
"m_file_include": DefaultMode,
"m_file_upload": DefaultMode,
"m_http": DefaultMode,
"m_java": DefaultMode,
"m_java_unserialize": DefaultMode,
"m_php_code_injection": DefaultMode,
"m_php_unserialize": DefaultMode,
"m_response": DefaultMode,
"m_rule": DefaultMode,
"m_scanner": DefaultMode,
"m_sqli": DefaultMode,
"m_ssrf": DefaultMode,
"m_ssti": DefaultMode,
"m_xss": DefaultMode,
}
pggStr, err := json.Marshal(policyGroupGlobal)
if err != nil {
return err
}
pgg := Options{Key: constants.PolicyGroupGlobal, Value: string(pggStr)}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&pgg)
return nil
}
================================================
FILE: management/webserver/model/policyrule.go
================================================
package model
import "gorm.io/datatypes"
type PolicyRule struct {
Base
Action int `gorm:"action" json:"action"`
Comment string `gorm:"comment" json:"comment"`
Pattern datatypes.JSON `gorm:"pattern" json:"pattern"`
IsEnabled bool `gorm:"is_enabled;default=true" json:"is_enabled"`
}
type PolicyRulePattern struct {
K string `json:"k"`
Op string `json:"op"`
V string `json:"v"`
}
const (
KeySrcIP = "src_ip"
KeyURI = "uri"
KeyHost = "host"
OpEq = "eq" // 完全相等
OpMatch = "match" // 模糊匹配
OpCIDR = "cidr" // CIDR
OpHas = "has" // 关键字
OpPrefix = "prefix" // 前缀关键字
OpRe = "re" // 正则
)
================================================
FILE: management/webserver/model/statistics.go
================================================
package model
import "time"
type SystemStatistics struct {
ID uint `json:"id" gorm:"primarykey"`
Type string `json:"type" gorm:"index;uniqueIndex:type_website_createdat"`
Value int64 `json:"value"`
CreatedAt time.Time `json:"created_at" gorm:"index;uniqueIndex:type_website_createdat"`
Website string `json:"website" gorm:"index;uniqueIndex:type_website_createdat"`
}
================================================
FILE: management/webserver/model/user.go
================================================
package model
import (
"gorm.io/gorm/clause"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
)
type User struct {
Base
Username string `gorm:"uniqueIndex;not null"`
Password string
Comment string
TFAEnabled bool `gorm:"column:tfa_enabled;default:true"`
TFASecret string `gorm:"column:tfa_secret"`
LastLoginTime int64 `gorm:"default:0"`
IsEnabled bool `gorm:"default:true"`
}
func initAdminUser() error {
db := database.GetDB()
user := User{
Username: constants.SuperUser,
}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)
return nil
}
================================================
FILE: management/webserver/model/website.go
================================================
package model
import "gorm.io/datatypes"
type Website struct {
Base
Comment string `gorm:"comment" json:"comment"`
ServerNames datatypes.JSON `gorm:"server_names" json:"server_names"`
Ports datatypes.JSON `gorm:"ports" json:"ports"`
Upstreams datatypes.JSON `gorm:"upstreams" json:"upstreams"`
CertFilename string `gorm:"cert_filename" json:"cert_filename"`
KeyFilename string `gorm:"key_filename" json:"key_filename"`
IsEnabled bool `gorm:"is_enabled;default=true" json:"is_enabled"`
}
================================================
FILE: management/webserver/pkg/config/config.go
================================================
package config
import (
"os"
"chaitin.cn/dev/go/settings"
)
var (
GlobalConfig = DefaultGlobalConfig()
)
func InitConfigs(configFilePath string) error {
s, err := settings.New(configFilePath)
if err != nil {
return err
}
if err = GlobalConfig.DB.Load(s); err != nil {
return err
}
if err = GlobalConfig.Log.Load(s); err != nil {
return err
}
if err = GlobalConfig.Server.Load(s); err != nil {
return err
}
if err = GlobalConfig.Detector.Load(s); err != nil {
return err
}
if err = GlobalConfig.Telemetry.Load(s); err != nil {
return err
}
if err = GlobalConfig.GPRC.Load(s); err != nil {
return err
}
if err := settings.Unmarshal("platform_addr", &GlobalConfig.PlatformAddr); err != nil {
return err
}
if v, ok := os.LookupEnv("MANAGEMENT_RESOURCES_DIR"); ok {
GlobalConfig.MgtResDir = v
}
if v, ok := os.LookupEnv("NGINX_RESOURCES_DIR"); ok {
GlobalConfig.NgxResDir = v
}
return nil
}
================================================
FILE: management/webserver/pkg/config/db.go
================================================
package config
import (
"net/url"
"os"
"chaitin.cn/dev/go/settings"
)
type DBConfig struct {
URL string `yaml:"url"`
LogSQL bool `yaml:"log_sql"`
SSLMode bool `yaml:"ssl_mode"`
}
func DefaultDBConfig() DBConfig {
return DBConfig{
URL: "postgres://safeline-ce:safeline-ce@127.0.0.1/safeline-ce",
LogSQL: false,
SSLMode: false,
}
}
func (dbc *DBConfig) Load(setting *settings.Setting) error {
if err := setting.Unmarshal("db", dbc); err != nil {
return err
}
if v, ok := os.LookupEnv("DATABASE_URL"); ok {
dbc.URL = v
}
dbURL, err := url.Parse(dbc.URL)
if err != nil {
return err
}
q := dbURL.Query()
if !dbc.SSLMode {
q.Set("sslmode", "disable")
}
dbURL.RawQuery = q.Encode()
dbc.URL = dbURL.String()
return nil
}
================================================
FILE: management/webserver/pkg/config/detector.go
================================================
package config
import (
"chaitin.cn/dev/go/settings"
)
type DetectorConfig struct {
Addr string `yaml:"addr"`
FslBytecode string `yaml:"fsl_bytecode"`
}
func DefaultDetectorConfig() DetectorConfig {
return DetectorConfig{
Addr: "http://127.0.0.1:8001",
FslBytecode: "bytecode",
}
}
func (d *DetectorConfig) Load(setting *settings.Setting) error {
if err := setting.Unmarshal("detector", d); err != nil {
return err
}
return nil
}
================================================
FILE: management/webserver/pkg/config/global.go
================================================
package config
type Config struct {
Log LogConfig
DB DBConfig
Server ServerConfig
Detector DetectorConfig
Telemetry TelemetryConfig
GPRC GRPCConfig
PlatformAddr string
MgtResDir string
NgxResDir string
}
func DefaultGlobalConfig() Config {
return Config{
Log: DefaultLogConfig(),
DB: DefaultDBConfig(),
Server: DefaultServerConfig(),
Detector: DefaultDetectorConfig(),
Telemetry: DefaultTelemetryConfig(),
GPRC: DefaultGRPCConfig(),
PlatformAddr: "https://waf-ce.chaitin.cn",
MgtResDir: "/resources/management",
NgxResDir: "/resources/nginx",
}
}
================================================
FILE: management/webserver/pkg/config/grpc.go
================================================
package config
import (
"chaitin.cn/dev/go/settings"
)
type GRPCConfig struct {
ListenAddr string `yaml:"listen_addr"`
}
func DefaultGRPCConfig() GRPCConfig {
return GRPCConfig{
ListenAddr: ":9002",
}
}
func (sc *GRPCConfig) Load(setting *settings.Setting) error {
if err := setting.Unmarshal("grpc_server", sc); err != nil {
return err
}
return nil
}
================================================
FILE: management/webserver/pkg/config/log.go
================================================
package config
import (
"chaitin.cn/dev/go/settings"
)
type LogConfig struct {
Output string `yaml:"output"`
Level string `yaml:"level"`
}
func DefaultLogConfig() LogConfig {
return LogConfig{
Output: "stdout",
Level: "info",
}
}
func (lc *LogConfig) Load(setting *settings.Setting) error {
if err := setting.Unmarshal("log", lc); err != nil {
return err
}
return nil
}
================================================
FILE: management/webserver/pkg/config/server.go
================================================
package config
import (
"chaitin.cn/dev/go/settings"
)
type ServerConfig struct {
ListenAddr string `yaml:"listen_addr"`
DevMode bool `yaml:"dev_mode"`
IntenseMode bool `yaml:"intense_mode"`
}
func DefaultServerConfig() ServerConfig {
return ServerConfig{
ListenAddr: ":9001",
DevMode: false,
}
}
func (sc *ServerConfig) Load(setting *settings.Setting) error {
if err := setting.Unmarshal("server", sc); err != nil {
return err
}
return nil
}
================================================
FILE: management/webserver/pkg/config/telemetry.go
================================================
package config
import (
"chaitin.cn/dev/go/settings"
)
type TelemetryConfig struct {
Addr string `yaml:"addr"`
}
func DefaultTelemetryConfig() TelemetryConfig {
return TelemetryConfig{
Addr: "rivers-telemetry:10086",
}
}
func (t *TelemetryConfig) Load(setting *settings.Setting) error {
if err := setting.Unmarshal("telemetry", t); err != nil {
return err
}
return nil
}
================================================
FILE: management/webserver/pkg/constants/constants.go
================================================
package constants
const (
SuperUser = "admin"
ProductName = "长亭雷池 WAF 社区版"
ProductVersion = ""
ConfigFilePath = "config.yml"
CertsPath = "certs"
)
const (
NotUpgrade int = iota
RecommendedUpgrade
MustUpgrade
)
================================================
FILE: management/webserver/pkg/constants/detectlog.go
================================================
package constants
const (
ProtocolHTTP = 0
ProtocolHTTPS = 1
ProtocolHTTP2 = 2
)
var (
HTTPProtocol = map[int]string{
ProtocolHTTP: "http",
ProtocolHTTPS: "https",
// 在 WAF 中开启 HTTP2 的前提是开启 TLS,所以这里先把 HTTP2 都兼容显示为 HTTPS
ProtocolHTTP2: "https",
}
AttackType = map[int]string{
-4: "超长数据",
-3: "黑名单",
-2: "白名单",
-1: "非攻击",
0: "SQL 注入",
1: "XSS",
2: "CSRF",
3: "SSRF",
4: "拒绝服务",
5: "后门",
6: "反序列化",
7: "代码执行",
8: "代码注入",
9: "命令注入",
10: "文件上传",
11: "文件包含",
12: "重定向",
13: "权限不当",
14: "信息泄露",
15: "未授权访问",
16: "不安全的配置",
17: "XXE",
18: "XPath 注入",
19: "LDAP 注入",
20: "目录穿越",
21: "扫描器",
22: "水平权限绕过",
23: "垂直权限绕过",
24: "文件修改",
25: "文件读取",
26: "文件删除",
27: "逻辑错误",
28: "CRLF 注入",
29: "模板注入",
30: "点击劫持",
31: "缓冲区溢出",
32: "整数溢出",
33: "格式化字符串",
34: "条件竞争",
35: "HTTP 协议违规",
61: "超时",
62: "未知",
63: "威胁情报",
64: "Cookie 篡改",
}
CountryCode = map[string]string{
"CN": "中国",
"AD": "安道尔",
"AE": "阿联酋",
"AF": "阿富汗",
"AG": "安提瓜和巴布达",
"AI": "安圭拉",
"AL": "阿尔巴尼亚",
"AM": "亚美尼亚",
"AO": "安哥拉",
"AQ": "南极洲",
"AR": "阿根廷",
"AS": "美属萨摩亚",
"AT": "奥地利",
"AU": "澳大利亚",
"AW": "阿鲁巴",
"AX": "奥兰",
"AZ": "阿塞拜疆",
"BA": "波斯尼亚和黑塞哥维那",
"BB": "巴巴多斯",
"BD": "孟加拉国",
"BE": "比利时",
"BF": "布基纳法索",
"BG": "保加利亚",
"BH": "巴林",
"BI": "布隆迪",
"BJ": "贝宁",
"BL": "圣巴泰勒米",
"BM": "百慕大",
"BN": "文莱",
"BO": "玻利维亚",
"BQ": "加勒比荷兰",
"BR": "巴西",
"BS": "巴哈马",
"BT": "不丹",
"BV": "布韦岛",
"BW": "博茨瓦纳",
"BY": "白俄罗斯",
"BZ": "伯利兹",
"CA": "加拿大",
"CC": "科科斯(基林)群岛",
"CD": "刚果(金)",
"CF": "中非",
"CG": "刚果(布)",
"CH": "瑞士",
"CI": "科特迪瓦",
"CK": "库克群岛",
"CL": "智利",
"CM": "喀麦隆",
"CO": "哥伦比亚",
"CR": "哥斯达黎加",
"CU": "古巴",
"CV": "佛得角",
"CW": "库拉索",
"CX": "圣诞岛",
"CY": "塞浦路斯",
"CZ": "捷克",
"DE": "德国",
"DJ": "吉布提",
"DK": "丹麦",
"DM": "多米尼克",
"DO": "多米尼加",
"DZ": "阿尔及利亚",
"EC": "厄瓜多尔",
"EE": "爱沙尼亚",
"EG": "埃及",
"EH": "阿拉伯撒哈拉民主共和国",
"ER": "厄立特里亚",
"ES": "西班牙",
"ET": "埃塞俄比亚",
"FI": "芬兰",
"FJ": "斐济",
"FK": "福克兰群岛",
"FM": "密克罗尼西亚联邦",
"FO": "法罗群岛",
"FR": "法国",
"GA": "加蓬",
"GB": "英国",
"GD": "格林纳达",
"GE": "格鲁吉亚",
"GF": "法属圭亚那",
"GG": "根西",
"GH": "加纳",
"GI": "直布罗陀",
"GL": "格陵兰",
"GM": "冈比亚",
"GN": "几内亚",
"GP": "瓜德罗普",
"GQ": "赤道几内亚",
"GR": "希腊",
"GS": "南乔治亚和南桑威奇群岛",
"GT": "危地马拉",
"GU": "关岛",
"GW": "几内亚比绍",
"GY": "圭亚那",
"HM": "赫德岛和麦克唐纳群岛",
"HN": "洪都拉斯",
"HR": "克罗地亚",
"HT": "海地",
"HU": "匈牙利",
"ID": "印尼",
"IE": "爱尔兰",
"IL": "以色列",
"IM": "马恩岛",
"IN": "印度",
"IO": "英属印度洋领地",
"IQ": "伊拉克",
"IR": "伊朗",
"IS": "冰岛",
"IT": "意大利",
"JE": "泽西",
"JM": "牙买加",
"JO": "约旦",
"JP": "日本",
"KE": "肯尼亚",
"KG": "吉尔吉斯斯坦",
"KH": "柬埔寨",
"KI": "基里巴斯",
"KM": "科摩罗",
"KN": "圣基茨和尼维斯",
"KP": "朝鲜",
"KR": "韩国",
"KW": "科威特",
"KY": "开曼群岛",
"KZ": "哈萨克斯坦",
"LA": "老挝",
"LB": "黎巴嫩",
"LC": "圣卢西亚",
"LI": "列支敦士登",
"LK": "斯里兰卡",
"LR": "利比里亚",
"LS": "莱索托",
"LT": "立陶宛",
"LU": "卢森堡",
"LV": "拉脱维亚",
"LY": "利比亚",
"MA": "摩洛哥",
"MC": "摩纳哥",
"MD": "摩尔多瓦",
"ME": "黑山",
"MF": "法属圣马丁",
"MG": "马达加斯加",
"MH": "马绍尔群岛",
"MK": "马其顿",
"ML": "马里",
"MM": "缅甸",
"MN": "蒙古",
"MP": "北马里亚纳群岛",
"MQ": "马提尼克",
"MR": "毛里塔尼亚",
"MS": "蒙特塞拉特",
"MT": "马耳他",
"MU": "毛里求斯",
"MV": "马尔代夫",
"MW": "马拉维",
"MX": "墨西哥",
"MY": "马来西亚",
"MZ": "莫桑比克",
"NA": "纳米比亚",
"NC": "新喀里多尼亚",
"NE": "尼日尔",
"NF": "诺福克岛",
"NG": "尼日利亚",
"NI": "尼加拉瓜",
"NL": "荷兰",
"NO": "挪威",
"NP": "尼泊尔",
"NR": "瑙鲁",
"NU": "纽埃",
"NZ": "新西兰",
"OM": "阿曼",
"PA": "巴拿马",
"PE": "秘鲁",
"PF": "法属波利尼西亚",
"PG": "巴布亚新几内亚",
"PH": "菲律宾",
"PK": "巴基斯坦",
"PL": "波兰",
"PM": "圣皮埃尔和密克隆",
"PN": "皮特凯恩群岛",
"PR": "波多黎各",
"PS": "巴勒斯坦",
"PT": "葡萄牙",
"PW": "帕劳",
"PY": "巴拉圭",
"QA": "卡塔尔",
"RE": "留尼汪",
"RO": "罗马尼亚",
"RS": "塞尔维亚",
"RU": "俄罗斯",
"RW": "卢旺达",
"SA": "沙特阿拉伯",
"SB": "所罗门群岛",
"SC": "塞舌尔",
"SD": "苏丹",
"SE": "瑞典",
"SG": "新加坡",
"SH": "圣赫勒拿",
"SI": "斯洛文尼亚",
"SJ": "挪威",
"SK": "斯洛伐克",
"SL": "塞拉利昂",
"SM": "圣马力诺",
"SN": "塞内加尔",
"SO": "索马里",
"SR": "苏里南",
"SS": "南苏丹",
"ST": "圣多美和普林西比",
"SV": "萨尔瓦多",
"SX": "荷属圣马丁",
"SY": "叙利亚",
"SZ": "斯威士兰",
"TC": "特克斯和凯科斯群岛",
"TD": "乍得",
"TF": "法属南方和南极洲领地",
"TG": "多哥",
"TH": "泰国",
"TJ": "塔吉克斯坦",
"TK": "托克劳",
"TL": "东帝汶",
"TM": "土库曼斯坦",
"TN": "突尼斯",
"TO": "汤加",
"TR": "土耳其",
"TT": "特立尼达和多巴哥",
"TV": "图瓦卢",
"TZ": "坦桑尼亚",
"UA": "乌克兰",
"UG": "乌干达",
"UM": "美国本土外小岛屿",
"US": "美国",
"UY": "乌拉圭",
"UZ": "乌兹别克斯坦",
"VA": "梵蒂冈",
"VC": "圣文森特和格林纳丁斯",
"VE": "委内瑞拉",
"VG": "英属维尔京群岛",
"VI": "美属维尔京群岛",
"VN": "越南",
"VU": "瓦努阿图",
"WF": "瓦利斯和富图纳",
"WS": "萨摩亚",
"YE": "也门",
"YT": "马约特",
"ZA": "南非",
"ZM": "赞比亚",
"ZW": "津巴布韦",
}
RuleModule = map[string]string{
"m_sqli": "SQL 注入检测模块",
"m_xss": "XSS 检测模块",
"m_csrf": "CSRF 检测模块",
"m_ssrf": "SSRF 检测模块",
"m_php_unserialize": "PHP 反序列化检测模块",
"m_java_unserialize": "Java 反序列化检测模块",
"m_file_upload": "文件上传攻击检测模块",
"m_file_include": "文件包含攻击检测模块",
"m_php_code_injection": "PHP 代码注入检测模块",
"m_java": "Java 代码注入检测模块",
"m_cmd_injection": "命令注入检测模块",
"m_response": "服务器响应检测模块",
"m_scanner": "机器人检测模块",
"m_http": "畸形 HTTP 协议检测模块",
"m_asp_code_injection": "ASP 代码注入检测模块",
"m_ssti": "模板注入检测模块",
"m_rule": "通用漏洞模块",
"m_timeout": "", // 检测超时
"whitelist": "白名单",
"blacklist": "黑名单",
}
// RuleReason TODO: get from libfusion.so, refer to skyview `Fusion.get_rule_detail_dict()`
RuleReason = map[string]string{
"6f4922f45568161a8cdf4ad2299f6d23": "访问测试文件的请求",
"c6b99e08e56911ecb18f00155d694977": "[HW2021] 深信服终端检测平台远程命令执行漏洞(CNVD-2020-46552)",
"c9a2f2f5b1035a1ca3a5aa776914e393": "[HW2020] GitLab 文件读写 (CVE-2017-0915, CVE-2016-9086)",
"97e264c2235c4ad0aa61ecd68fa53351": "jenkins 管理员权限开放 (CVE-2018-1999001)",
"ee41775801ab11ed90a200155ddb8f4e": "Apache HTTPD SSRF (CVE-2021-40438)",
"33e75ff09dd601bbe69f351039152189": "访问系统文件的请求",
"1c383cd30b7c298ab50293adfecb7b18": "Struts2 Java 代码注入漏洞",
"eff4046e0b0411ed976f00155dd454dd": "蓝凌OA漏洞",
"a1d0c6e83f027327d8461063f4ac58a6": "webshell",
"d92ebbf7683c5cafbc74456c73bc5d0c": "[HW2020] JIRA OAuth SSRF (CVE-2017-9506)",
"dbc22aa2fce011eca0e800163e345065": "[HW2020] Nginx range filter overflow (CVE-2017-7529)",
"ae971210f5f411ecae9200163e345065": "[HW2022] VMware 认证绕过漏洞 (CVE-2022-22972)",
"5af4619c46045568b4d701ad5208eee0": "[HW2020] Druid 未授权访问",
"fbd7939d674997cdb4692d34de8633c4": "Jackson 反序列化 (CVE-2017-17485)",
"63400573dc155253869bc90e341708e9": "[HW2020] Apache Spark 未授权访问",
"37274a94eadf11eca1a600155dcfc445": "[HW2022] Apache Solr SSRF漏洞 (CVE-2021-27905)",
"c4ca4238a0b923820dcc509a6f75849b": "PHP 代码泄露",
"3416a75f4cea9109507cacd8e2f2aefc": "phpinfo 泄露",
"c9f0f895fb98ab9159f51fd0297e236d": "访问敏感文件的请求",
"36564acfab005dcda413a0b489ce8a02": "[HW2020] ffmpeg SSRF (CVE-2016-1898)",
"31a7ad75521152eca43e7eb15065e850": "[HW2020] Kibana远程代码执行(CVE-2019-7609)",
"03d0598da679b4d68089730de888a945": "Laravel Debug 远程代码执行 (CVE-2021-3129)",
"34173cb38f07f89ddbebc2ac9128303f": "XML 实体注入漏洞",
"09afe350746411eda37500163e12b978": "Apache Kylin 未授权配置 (CVE-2020-13927)",
"4096d530f60c11eca0ad00163e345065": "[HW2020] Horde Groupware Webmail Edition 远程命令执行漏洞",
"c6b57d8ce56911ec980b00155d694977": "[HW2021] F5 BIG-IP 远程代码执行漏洞(CVE-2020-5902)",
"8964339e01b411ed9bdc00163e345065": "Spring Cloud Config 路径穿越漏洞 (CVE-2020-5405)",
"e336d501c94e556d8fa3f6df584e88e2": "[HW2020] Spring WebFlow RCE (CVE-2017-4971)",
"c6b9fa2ee56911ecb64f00155d694977": "[HW2021] 通达OA权限提升漏洞(11.5.200417 之前的版本)",
"8f1c07fe747c11ed8b7c00163e12b978": "Microsoft Exchange 远程代码执行 (CVE-2020-0688)",
"3ef815416f775098fe977004015c6193": "Apache Solr 远程代码执行漏洞 (CVE-2019-0193)",
"2e7f58dd541d5c478fc12bc8a64bef41": "[HW2020] S2-005 (2) (CVE-2010-1870)",
"1ff1de774005f8da13f42943881c655f": "DEDECMS add suffix",
"458c1b2af1f911ecafdb00163e345065": "[HW2021] Sitecore XP远程代码执行漏洞(CVE-2021-42237)",
"ac3d6ee2714b11ed87ba00163e12b978": "通达OA V11.x远程代码执行漏洞",
"9fa20cc8eabf11ec808600155dcfc445": "[HW2022] 360天擎信息泄露",
"2a1f6e4292ee30c1a92d3e8333471534": "Jackson 反序列化",
"e404c364f1f511ec9cc500163e345065": "[HW2021] Zabbix 5.0.17-Remote Code Execution (RCE) (Authenticated)",
"c6b8cd66e56911ec8cc100155d694977": "[HW2021] 宝塔面板数据库管理未授权访问漏洞(Linux正式版7.4.2、Linux测试版7.5.13、Windows正式版6.8)",
"3c27c530751511edbaf200163e12b978": "Citrix 未授权访问 ",
"22aaa784da604abeb19b0aeee2b3cc6d": "Jenkins 非预期方法调用漏洞(SECURITY-595)",
"a5bfc9e07964f8dddeb95fc584cd965d": "Java 畸形 double 数据 DOS 漏洞",
"c6babe8ce56911eca76200155d694977": "[HW2021] 致远OA 文件上传漏洞(V8.0、V7.1、V7.1SP1、V7.0、V7.0SP1、V7.0SP2、V7.0SP3、V6.0、V6.1SP1、V6.1SP2、V5.x)",
"d7e82c628f96541c924637f711555266": "[HW2020] PHPStudy 命令执行",
"7f39f8317fbdb1988ef4c628eba02591": "访问 IIS 短文件名 / 文件夹",
"44f683a84163b3523afe57c2e008bc8c": "扫描 IIS 短文件名 / 文件夹",
"c38555f8746411eda4a400163e12b978": "快排CMS 未授权访问 ",
"866cbe77d2884225a8fa19793cc8ad51": "ghostscript 命令执行 (CVE-2018-19475)",
"17400b535f9f5db5a8424265ed8cc38c": "[HW2020] JeeCMS SSRF",
"774b89ca853942cabc8b94bfff46d73c": "ECShop 2.x-3.x 远程代码执行漏洞",
"b801a4a10a22422dff885812a5d0d417": "科蓝反序列化漏洞",
"6c991e5e23215c589a86f13ae800c616": "[HW2020] Couchdb 垂直越权 (1) (CVE-2017-12635)",
"7d5a620cf69311ecbf8400163e345065": "[HW2020] ShardingShpere远程命令执行漏洞 (CVE-2020-1947)",
"a26afe82eade11ecb85f00155dcfc445": "[HW2022] Alibaba Nacos认证绕过",
"3343e2def60b11ec964400163e345065": "[HW2021] 用友ERP-NC 任意文件读取漏洞 (2021-?-?)",
"09189cf61c0e4774a4d8af7bfb0cf6de": "WebLogic wls9-async 和 wls-wsat 反序列化 (CNVD-C-2019-48814)",
"738430de6f054031a7597c00eb1fd061": "Drupal Mail 命令注入",
"2abf53869fd05cf5a69c11ea42d2cb54": "[HW2020] ActiveMQ 任意文件写入漏洞 (CVE-2016-3088)",
"e2c420d928d4bf8ce0ff2ec19b371514": "Drupal 内核远程代码执行漏洞 (CVE-2018-7602)",
"ac61a980eae411ec985100155dcfc445": "[HW2022] Zyxel NBG2105 身份验证绕过 (CVE-2021-3297)",
"d67d8ab4f4c10bf22aa353e27879133c": "PHP168 代码执行漏洞",
"e7629f3e750a11eda3a900163e12b978": "PbootCms v3.1.2 远程代码执行 ",
"6d85a117690c32af50f9b47f98abf408": "骑士 CMS 远程代码执行",
"4ebd2f6aee2811ec8e2600163e345065": "[HW2022] 齐治堡垒机任意用户登录漏洞",
"688142e95d6d446ee901de2b4d087a5d": "F5 BIG-IP漏洞",
"91d18ff6f2dc54618b1a28f15afe572f": "[HW2020] Couchdb 命令执行 (CVE-2017-12636)",
"70efdf2ec9b086079795c442636b55fb": "访问管理后台的请求",
"6d6bcd8ebc2c4d4a961ce2e8b4b3af67": "joomla 远程代码执行",
"c6b4be74e56911eca25c00155d694977": "[HW2021] Apache Shiro身份认证绕过漏洞(CVE-2020-17523)",
"1f0822d21d0a11edad0900163e345065": "[HW2020] Couchdb 垂直越权 (1) (CVE-2017-12635)",
"2024042cf11511ecb17e00163e345065": "[HW2021] GoAhead Server 环境变量注入漏洞(CVE-2021-42342)",
"abade038ebc211ec8a8800163e345065": "[HW2022] VMware SSRF XSS)",
"32ac3f49feb65bada13cdf85722d237a": "[HW2020] Spring RCE (CVE-2018-1270)",
"43ec517d68b6edd3015b3edc9a11367b": "Spring 框架漏洞",
"6a48a52c11424cd68e89d26d212556c4": "ThinkPHP5 任意代码执行漏洞",
"a5d9fe3b0172b4625a1fe6a41482aa8b": "Xstream 反序列化",
"072b030ba126b2f4b2374f342be9ed44": "路径穿越攻击",
"38529f8eebc211eca87c00163e345065": "[HW2022] VMware 任意文件读取",
"3a3128d0e7e811ec909000155db4c628": "Java 代码注入",
"e369853df766fa44e1ed0ff613f563bd": "Struts2 S2-020",
"d3d9446802a44259755d38e6d163e820": "访问 Git 仓库的请求",
"6e4b3bcf45295dcc9b1f3efef4353287": "[HW2020] JIRA SSRF (CVE-2019-8451)",
"b58a180cf11111ec8a4600163e345065": "[HW2022] VMware 服务端模板注入漏洞(CVE-2022-22954)",
"7cbbc409ec990f19c78c75bd1e06f215": "Jenkins 远程代码执行漏洞 (CVE-2018-1000861)",
"c6b6f9d2e56911ec8e2e00155d694977": "[HW2021] phpStudy nginx 解析漏洞(phptsuy8.1.07的Nginx1.5.11版本)",
"b4b143a12d0d5eeabef67b6597dc7cd9": "[HW2020] DedeCMS 密码重置漏洞",
"028c274cfc4f11ecaedd00163e345065": "[HW2022] Oracle Access Manager RCE (CVE-2021-35587)",
"1fc707c92da6519e9337f202ad1fd959": "[HW2020] 通达OA 前台任意用户登录",
"2efd7872752f11ed97f600163e12b978": "XStream 反序列化代码执行 (CVE-2021-29505)",
"9355da5d436e52cba6fd2012ad5a5832": "[HW2020] Spring Data Rest RCE (CVE-2017-8046)",
"d09bf41544a3365a46c9077ebb5e35c3": "Jackson 反序列化 (CVE-2017-7525)",
"c20ad4d76fe97759aa27a0c99bff6710": "访问 SVN 仓库的请求",
"e1c17c16169b58078be071b37eda9f3c": "[HW2020] HFS RCE",
"e4a2355fa844484bbdf799cc8795b415": "Apache Druid 远程命令执行漏洞",
"72c04a1fbd6e5641bb33d0185dc36b9b": "[HW2020] Nexus 后台表达式注入 (CVE-2020-10199, CVE-2019-7238)",
"c6b519e6e56911ecaa1c00155d694977": "[HW2021] Drupal任意PHP代码执行漏洞(CVE-2020-28948/28949)",
"c81e728d9d4c2f636f067f89cc14862c": "访问敏感文件的请求",
"a02582663945fa213788203624eb3b89": "Jackson-databind 反序列化 (CVE-2020-35790/CVE-2020-35491)",
"45c48cce2e2d7fbdea1afc51c7c6ad26": "访问 Git 仓库的请求",
"029e67ff376659daaea612ec5c3964ab": "[HW2020] Gitea LFS RCE",
"e52769085be711ec859e00155dae391c": "Apache Log4j 远程命令执行漏洞",
"19ca14e7ea6328a42e0eb13d585e4c22": "Java 代码注入漏洞 (针对 Struts 2 漏洞的通用防御)",
"3c59dc048e8850243be8079a5c74d079": "CVE-2012-1823 PHP FastCGI 远程代码执行漏洞",
"14766b63abf447ac808b92535dde9c09": "Gogs、Gitea 远程代码执行漏洞 (CVE-2018-18925)",
"9bf31c7ff062936a96d3c8bd1f8f2ff3": "后门",
"ae0196baf5f211ec98c400163e345065": "[HW2022] Sunlogin-11.0.0.33-RCE-1_获取token",
"2838023a778dfaecdc212708f721b788": "访问备份或临时文件的请求",
"110ad086eaff11ec87c500155d143f7b": "[HW2022] Gitlab exiftool 远程命令执行漏洞 (CVE-2021-22205)",
"e3959e0e30494d6caa1e9b259073d96c": "南方数据管理员添加未授权访问漏洞",
"98f13708210194c475687be6106a3b84": "Nginx code 解析漏洞",
"887d1820a85a43cfbeee578a02f6a914": "fastjson 反序列化代码执行",
"88a4a7227e81d064b551f52f7f51e6c8": "泛微 E-cology OA远程代码执行",
"4c15abcbe6e8410aac559817925bfd11": "TBK DVR 验证绕过漏洞 (CVE-2018-9995)",
"460072c82cca58999281247426c71bf8": "[HW2020] ffmpeg 文件读取 (CVE-2016-1897)",
"96c4bc6a6adf4434b93d714106f4c67f": "ghostscript 任意文件读写 (CVE-2018-17961)",
"9dbe327d8dd64f3a8d919cf19d5e3e00": "ThinkPHP 5.0.x _method 代码注入",
"6512bd43d9caa6e02c990b0a82652dca": "访问 SVN 仓库的请求",
"a12856ca51bb5ed69b6646dafd605e7c": "[HW2020] Apache Shiro 反序列化攻击 (CVE-2016-4437)",
"9473dea2eb144fee9ddeac33b49358b9": "Nexus Repository Manager 3 远程代码执行漏洞(CVE-2019-7238)",
"eccbc87e4b5ce2fe28308fd9f2a7baf3": "Apache 目录遍历",
"e799871cf6b653db94b759b79deefe5d": "[HW2020] ImageMagick RCE (CVE-2016-3714)",
"ca037bccfa7c5e7b822ea1889ba7be47": "[HW2020] MetInfo 前台 SQL 注入 (CNVD-2018-20024)",
"638d9bb801b611eda81e00163e345065": "Atlassian Jira 服务端请求伪造漏洞 (CVE-2022-26135)",
"8ef24d7501e85d1fa3ce4016dbf297d1": "[HW2020] Webmin RCE (CVE-2019-15107)",
"81f360f5033c55179d34b37b61389503": "[HW2020] Palo Alto GlobalProtect SSL RCE (CVE-2019-1579)",
"c374fb01134a4213be9ba67539523162": "IIS 扩展名漏洞 (CVE-2009-4444)",
"21203602eafd11ec8dc300155d143f7b": "[HW2022] F5 BIG-IP 远程代码执行漏洞 (CVE-2021-22986)",
"2cba1bd57c9603c07ff4f09f8e4fb0f9": "Apache AXIS 远程命令执行漏洞",
"a0ff28f2f5f111eca2de00163e345065": "[HW2022] Weblogic Server 信息泄漏漏洞利用",
"d645920e395fedad7bbbed0eca3fe2e0": "DEDECMS SQL 注入漏洞",
"2b6e15ac747811eda03000163e12b978": "Apache DolphinScheduler 远程代码执行 (CVE-2020-11974)",
"1f5449318bbc38d200fb9f3bcf87a50f": "WebLogic console 远程代码执行漏洞 (CVE-2020-14882)",
"02e74f10e0327ad868d138f2b4fdd6f0": "利用 Apache 解析漏洞来执行 PHP 脚本",
"3ff4aed51a7c506087c0b9df1b6ac0d3": "[HW2020] Couchdb 垂直越权 (2) (CVE-2017-12635)",
"933cbfbbc71c417ab9d757e5dc4be4d4": "Jenkins Accept-Language 信息泄露 (CVE-2018-1999002)",
"182be0c5cdcd5072bb1864cdee4d3d6e": "Struts2 S2-016",
"6af6b17ceae711ecbd5100155dcfc445": "[HW2022] 三星 WLAN AP WEA453e路由器 RCE (query)",
"7f41d19e081411edaca100163e345065": "[HW2022] 三星 WLAN AP WEA453e路由器 RCE (form)",
"cfcd208495d565ef66e7dff9f98764da": "JSP 代码泄露",
"3b57f3413df14b2380ae8e483cb0dcd5": "Drupalgeddon2 - Drupal核心远程代码执行",
"6364d3f0f495b6ab9dcf8d3b5c6e0b01": "Struts2 S2-005",
"a98e8c0cf12211ecb1d800163e345065": "[HW2021] VMware vCenter Server 远程代码执行漏洞(CVE-2021-21985)",
"b42530a1ea025fa78bb0c6c7d2e1ed95": "[HW2020] PHP FPM RCE (CVE-2019-11043)",
"bbe199e1efec469588fde16dce75df36": "coremail 信息泄露",
"c6b7547ce56911ec9d3d00155d694977": "[HW2021] SaltStack多个高危漏洞(CVE-2020-16846,CVE-2020-17490,CVE-2020-25592)",
"c6b7ae5ee56911ec9d1600155d694977": "[HW2021] SAP NetWeaver AS JAVA 高危漏洞(CVE-2020-6287)",
"65d86160f1f711ec971800163e345065": "[HW2021] Total.js框架远程代码执行漏洞(CVE-2021-23389)",
"aa4755eb0fdf47ec91e8f285613e7954": "Atlassian 漏洞",
"c6b5dc64e56911ecbe7300155d694977": "[HW2021] Microsoft Exchange远程代码执行漏洞(CVE-2020-16875)",
"b6f76f653ded54b494292eb6c285d464": "[HW2020] WebLogic 弱口令",
"9778d5d219c5080b9a6a17bef029331c": "Apache Solr 远程代码执行漏洞 (CVE-2019-0192)",
"c6bb1f3ae56911ec93a500155d694977": "[HW2021] WebSphere 权限提升漏洞(CVE-2020-4276)",
"c6ba5e10e56911ec8f6d00155d694977": "[HW2021] 微软 SQL Server 报表服务远程代码执行漏洞(CVE-2020-0618)",
"061de838eabf11ec8ee500155dcfc445": "[HW2022] 360天擎SQL注入漏洞",
"6ea9ab1baa0efb9e19094440c317e21b": "XML 实体注入漏洞",
"039c966635bd4ac286553f55d9457615": "dedecms 5.7 升级时间泄露",
"b6d767d2f8ed5d21a44b0e5886680cb9": "DEDECMS add suffix",
"f033ab37c30201f73f142449d037028d": "ActiveMQ 任意文件写入漏洞 (CVE-2016-3088)",
"558fce096ca65beb8f7ca7d4b2bf6b4f": "[HW2020] ElasticSearch 命令执行 (CVE-2015-1427)",
"b53b3a3d6ab90ce0268229151c9bde11": "代码注入漏洞 (针对 WordPress)",
"c6b4239ce56911ec8b8a00155d694977": "[HW2021] Apache Shiro 权限绕过漏洞(CVE-2020-13933)",
"2408e35abe40580286b6d9106b43357f": "[HW2020] S2-005 (1) (CVE-2010-1870)",
"724390fd0bf29ab3adb92ee026474fe1": "Jackson-databind 反序列化 (CVE-2020-36179~CVE-2020-36189)",
"ef268f2b31fd5943bd1882e2218e9924": "[HW2020] Tomcat RCE (CVE-2019-0232)",
"d49bb7ebe41146deb48138f28d220c7f": "可疑远程调用协议",
"aab3238922bcc25a6f606eb525ffdc56": "phpinfo 泄露",
"582e036cee2811ec90c700163e345065": "[HW2022] H3C SecPath运维审计系统任意用户登录漏洞",
"25d513abf19740b083f2262270c0a281": "PHPCMS v9.6.0 任意文件上传",
"ab299838fbe1584d843fc004b0034e81": "[HW2020] GoAhead RCE (CVE-2017-17562)",
"c6b81b78e56911ec989800155d694977": "[HW2021] SolarWinds 远程代码执行漏洞(CVE-2020-10148)",
"daf7d4a0116711ed8d9c00163e35b03e": "Seacms php代码注入",
"c6b93e5ee56911ec8e1500155d694977": "[HW2021] 深信服终端检测平台任意用户登录(CNVD-2020-46552)",
"95a4035353ed405ba3418698fc06b544": "spring 目录遍历 (cve-2014-3625)",
"eab53f221ee611edb57e00163e345065": "VMware Workspace ONE UEM SSRF (CVE-2021-22054)",
"fca34064ebc211ec9b9900163e345065": "[HW2022] VMware vRealize SSRF (CVE-2021-21975)",
"c51ce410c124a10e0db5e4b97fc2af39": "PHPSpy 后门",
"2f7e97b9204b5239b089c5ab83242519": "[HW2020] ElasticSearch 未授权访问",
"6e34a9cb6f5b43e4973b170e626a2f77": "Apache APISIX lua 远程代码执行",
"299ce1c43af85c7987d8ddef915888a8": "[HW2020] Zimbra RCE",
"529d97a8f5d711eca1e700163e345065": "[HW2022] Weblogic Server 信息泄漏漏洞利用 (CVE-2022-21371)",
"49234868ee1f11ec833b00155de98ae1": "[HW2022] AVTECH监控后台命令注入漏洞",
"1f0e3dad99908345f7439f8ffabdffc4": "PHP FastCGI 解析漏洞",
}
)
================================================
FILE: management/webserver/pkg/constants/detector.go
================================================
package constants
const (
ContentType = "application/octet-stream"
UpdateEntrypoint = "/update/policy"
StatEntrypoint = "/stat"
DefaultPolicyVersion = "1"
)
================================================
FILE: management/webserver/pkg/constants/option.go
================================================
package constants
const (
SecretKey = "secret_key"
MachineID = "machine_id"
PolicyGroupGlobal = "policy_group_global"
SrcIPConfig = "src_ip_config"
)
================================================
FILE: management/webserver/pkg/constants/session.go
================================================
package constants
const (
DefaultSessionUserKey = "user"
)
================================================
FILE: management/webserver/pkg/constants/telemetry.go
================================================
package constants
const (
ApplicationJson = "application/json"
TelemetryEntryPoint = "/telemetry"
TelemetryId = "9e88109c-ebbf-4d82-8a25-f97f2ac5f3c4"
Behaviour = "behaviour"
FalsePositives = "false_positives"
Installation = "Installation"
)
var Version string
================================================
FILE: management/webserver/pkg/cron/cron.go
================================================
package cron
import (
"github.com/robfig/cron/v3"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/log"
)
var logger = log.GetLogger("cron")
func newCronWithSeconds() *cron.Cron {
secondParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour |
cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor)
return cron.New(cron.WithParser(secondParser), cron.WithChain())
}
func StartCron() error {
cronInstance := newCronWithSeconds()
_, err := cronInstance.AddFunc(SpecUpdatePolicy, CheckAndUpdatePolicy)
if err != nil {
return err
}
cronInstance.Start()
return nil
}
================================================
FILE: management/webserver/pkg/cron/update_policy.go
================================================
package cron
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"time"
"chaitin.cn/patronus/safeline-2/management/webserver/utils"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
)
// SpecUpdatePolicy http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-06.html
// Seconds Minutes Hours Day-of-Month Month Day-of-Week Year (optional field)
// every 5 second (starting from 0s)
const SpecUpdatePolicy = "0/5 * * * * ?"
type statResponseBody struct {
PolicyVersion string `json:"policy_version"`
}
func getBytecode() ([]byte, error) {
buff, err := ioutil.ReadFile(config.GlobalConfig.Detector.FslBytecode)
if err != nil {
return nil, err
}
return buff, nil
}
func CheckAndUpdatePolicy() {
if config.GlobalConfig.Detector.Addr == "" {
return
}
exist, err := utils.FileExist(config.GlobalConfig.Detector.FslBytecode)
if !exist || err != nil {
return
}
data, err := getBytecode()
if err != nil {
logger.Error(err)
return
}
reader := bytes.NewReader(data)
tr := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
}
client := &http.Client{Transport: tr}
addr := config.GlobalConfig.Detector.Addr
statReq, err := http.NewRequest(http.MethodGet, addr+constants.StatEntrypoint, nil)
if err != nil {
logger.Error(err)
return
}
updateReq, err := http.NewRequest(http.MethodPost, addr+constants.UpdateEntrypoint, reader)
if err != nil {
logger.Error(err)
return
}
updateReq.Header.Set("Content-Type", constants.ContentType)
statRspData := statResponseBody{}
statRsp, err := client.Do(statReq)
if err != nil || statRsp.StatusCode != http.StatusOK {
logger.Warn(err)
return
}
body, err := ioutil.ReadAll(statRsp.Body)
if err != nil {
logger.Warn(err)
return
}
err = json.Unmarshal(body, &statRspData)
if err != nil {
logger.Warn(err)
return
}
if statRspData.PolicyVersion == constants.DefaultPolicyVersion {
return
}
err = statRsp.Body.Close()
if err != nil {
logger.Warn(err)
}
logger.Info("Update fsl bytecode")
updateRsp, err := client.Do(updateReq)
if err != nil {
logger.Warn(err)
return
}
if updateRsp.StatusCode != http.StatusOK {
logger.Warnf("%s update policy, return %d", addr, updateRsp.StatusCode)
}
err = updateRsp.Body.Close()
if err != nil {
logger.Warn(err)
}
}
================================================
FILE: management/webserver/pkg/database/postgres.go
================================================
package database
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"chaitin.cn/dev/go/errors"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
)
type PostgresDB struct {
*gorm.DB
}
var (
// db is not designed to be used directly, use method `GetDB` instead.
db PostgresDB
)
func GetDB() *PostgresDB {
return &db
}
func InitDB() error {
dbConfig := config.GlobalConfig.DB
if dbConfig.URL == "" {
return errors.New("empty database url")
}
// URL also works, see https://godoc.org/github.com/lib/pq
var (
gormDB *gorm.DB
err error
)
gormConfig := &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "mgt_", // table name prefix, table for `User` would be `t_users`
SingularTable: true, // use singular table name, table for `User` would be `user` with this option enabled
},
AllowGlobalUpdate: false,
Logger: logger.Default.LogMode(logger.Silent),
}
if dbConfig.LogSQL {
gormConfig.Logger = logger.Default.LogMode(logger.Info)
}
gormDB, err = gorm.Open(postgres.Open(dbConfig.URL), gormConfig)
if err != nil {
return err
}
db.DB = gormDB
return nil
}
func (db *PostgresDB) SetDB(gormDB *gorm.DB) {
db.DB = gormDB
}
================================================
FILE: management/webserver/pkg/fvm/fsl/action.go
================================================
package fsl
import (
"fmt"
"strings"
)
func Goto(m string) string {
return fmt.Sprintf("GOTO %s", m)
}
func Wheres(items ...string) string {
return strings.Join(items, " AND ")
}
func Actions(items ...string) string {
return strings.Join(items, ", ")
}
func Set(k, t, v string) string {
return fmt.Sprintf("SET %s::%s = %s", k, t, v)
}
================================================
FILE: management/webserver/pkg/fvm/fsl/quote.go
================================================
package fsl
import (
"fmt"
)
// see : https://chaitin.cn/patronus/fvm/-/blob/master/src/util/StrUtil.cpp#L18
func appendEscapedByte(buf []byte, b byte) []byte {
if b >= 0x20 && b <= 0x7e && b != '\'' && b != '"' && b != '\\' {
return append(buf, b)
}
switch b {
case '\'':
return append(buf, `\'`...)
case '"':
return append(buf, `\"`...)
case '\n':
return append(buf, `\n`...)
case '\t':
return append(buf, `\t`...)
case '\r':
return append(buf, `\r`...)
case '\b':
return append(buf, `\b`...)
case '\f':
return append(buf, `\f`...)
case '\\':
return append(buf, `\\`...)
default:
return append(buf, fmt.Sprintf("\\x%x", b)...)
}
}
func appendQuotedWith(buf []byte, s string, quote byte) []byte {
buf = append(buf, quote)
for i := 0; i < len(s); i++ {
buf = appendEscapedByte(buf, s[i])
}
buf = append(buf, quote)
return buf
}
func quoteWith(s string, quote byte) string {
return string(appendQuotedWith(make([]byte, 0, 3*len(s)/2), s, quote))
}
func Quote(s string) string {
return quoteWith(s, '\'')
}
================================================
FILE: management/webserver/pkg/fvm/fsl/selector.go
================================================
package fsl
import (
"fmt"
)
type Selector struct {
TableName string
ID string
Where string
Action string
}
func NewSelector(tableName, id, where, action string) *Selector {
return &Selector{
TableName: tableName,
ID: id,
Where: where,
Action: action,
}
}
func (s *Selector) String() string {
ret := fmt.Sprintf("ID %s", Quote(s.ID))
if s.Where != "" {
ret += " WHERE " + s.Where
}
ret += fmt.Sprintf(" ACTION %s", s.Action)
return ret
}
func (s *Selector) AppendInto() string {
return fmt.Sprintf("APPEND INTO %s %s;", s.TableName, s.String())
}
func AppendInto(tableName, id, where, action string) string {
return NewSelector(tableName, id, where, action).AppendInto()
}
================================================
FILE: management/webserver/pkg/fvm/fsl/state.go
================================================
package fsl
import (
"chaitin.cn/dev/go/errors"
)
type State int
const (
S_NOP State = iota
S_ABORT
S_RETURN
)
func (s State) String() string {
switch s {
case S_NOP:
return "NOP"
case S_ABORT:
return "ABORT"
case S_RETURN:
return "RETURN"
default:
panic(errors.New("wrong State"))
}
}
================================================
FILE: management/webserver/pkg/fvm/fsl/table.go
================================================
package fsl
import (
"fmt"
"strings"
)
type Table struct {
Name string
Mappings map[State]State
}
func NewTable(name string, mappings map[State]State) *Table {
return &Table{
Name: name,
Mappings: mappings,
}
}
func (t *Table) String() string {
ret := fmt.Sprintf("TABLE %s", t.Name)
if len(t.Mappings) > 0 {
var mappings []string
for from, to := range t.Mappings {
mappings = append(mappings, fmt.Sprintf("%s TO %s", from.String(), to.String()))
}
ret += fmt.Sprintf(" MAPPING %s", strings.Join(mappings, ", "))
}
return ret
}
func (t *Table) Create() string {
return fmt.Sprintf("CREATE %s;", t.String())
}
func CreateTable(name string, mappings map[State]State) string {
return NewTable(name, mappings).Create()
}
================================================
FILE: management/webserver/pkg/fvm/fsl/target.go
================================================
package fsl
import (
"fmt"
)
type Target struct {
ID string
Type string
Args string
}
func NewTarget(id string, typ string, args string) *Target {
return &Target{
ID: id,
Type: typ,
Args: args,
}
}
func NewSkynetTarget(id, configJSONStr string) *Target {
return NewTarget(id, "skynet", configJSONStr)
}
func (t *Target) String() string {
return fmt.Sprintf("%s TYPE %s ARGS (%s)", t.ID, t.Type, t.Args)
}
func (t *Target) Create() string {
return fmt.Sprintf("CREATE TARGET %s;", t.String())
}
func CreateTarget(id, configJSONStr string) string {
return NewSkynetTarget(id, configJSONStr).Create()
}
================================================
FILE: management/webserver/pkg/fvm/fvm.go
================================================
package fvm
/*
#cgo CFLAGS: -I../../submodule/fvm/include -I../../submodule/libct/include
#cgo LDFLAGS: -L../../submodule/fvm/lib -lfvm
#include
#include
#include
#include
#include
#include
static inline fvm_update_t *build_update(const void* buf, size_t len) {
ct_string_t tmp = CT_STRING_FROM_PTR_LENGTH(buf, len);
return fvm_update_create(tmp);
}
*/
import (
"C" //nolint:typecheck
)
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"net/url"
"unsafe"
"chaitin.cn/dev/go/errors"
"chaitin.cn/dev/go/log"
)
// Constant for Output
const (
OutputItemTable int = 0
OutputItemSelector int = 1
OutputItemTarget int = 2
)
// FVM wrapper
type FVM struct {
framework *C.fvm_framework_t
apiTable *C.char
version int64
}
type FVMUpdate struct {
update *C.fvm_update_t
}
type FVMRe struct {
data []byte
}
func maybePointer(array []byte) unsafe.Pointer {
if len(array) == 0 {
return nil
}
return unsafe.Pointer(&array[0])
}
func (u *FVMUpdate) ToBytes() []byte {
return C.GoBytes(unsafe.Pointer(u.update.buf.ptr), C.int(u.update.buf.length))
}
func (u *FVMUpdate) FromBytes(out []byte) error {
ptr := C.build_update(unsafe.Pointer(&out[0]), C.ulong(len(out)))
if ptr == nil {
return errors.New("failed to create update from db")
}
u.update = ptr
return nil
}
func (u *FVMUpdate) MergeUpdate(patch *FVMUpdate) error {
m := C.fvm_update_merge(u.update, patch.update)
if m == nil {
return errors.New("failed to merge two updates")
}
C.fvm_update_destroy(u.update)
u.update = m
return nil
}
func (u *FVMUpdate) Release() {
C.fvm_update_destroy(u.update)
}
func (r *FVMRe) ToBytes() []byte {
return r.data
}
func (r *FVMRe) FromBytes(o []byte) {
r.data = o
}
type FVMOutput struct {
output *C.fvm_output_t
}
// New creates FVM
func New() (*FVM, error) {
apiTable := C.fvm_api_table()
fw := C.fvm_framework_acquire(nil)
if fw == nil {
return nil, errors.New("fvm_framework_acquire returns a null pointer")
}
return &FVM{framework: fw, apiTable: apiTable, version: 0}, nil
}
func (f *FVM) Update(upd *FVMUpdate) error {
if upd == nil || upd.update == nil {
return errors.New("null pointer passed to update")
}
ret := bool(C.fvm_update(f.framework, upd.update))
if !ret {
return errors.New("update framework failed")
}
return nil
}
func (f *FVM) Compile(text string, re []byte) (*FVMOutput, *FVMRe, error) {
cplPtr := C.fvm_compiler_acquire(f.apiTable)
cText := C.CString(text)
defer C.fvm_compiler_release(cplPtr)
defer C.free(unsafe.Pointer(cText))
ok := C.fvm_compiler_load_re(cplPtr, (*C.char)(maybePointer(re)), C.ulong(len(re)))
if !ok {
return nil, nil, errors.New("failed to load re")
}
outputPtr := C.fvm_compile(cplPtr, cText)
if outputPtr == nil {
return nil, nil, errors.New("compiling FSL failed")
}
var cRePtr *C.char
var cReLen C.ulong
ok = C.fvm_compiler_dump_re(cplPtr, &cRePtr, &cReLen)
if !ok {
return nil, nil, errors.New("failed to dump re")
}
defer C.free(unsafe.Pointer(cRePtr))
cRe := &FVMRe{C.GoBytes(unsafe.Pointer(cRePtr), C.int(cReLen))}
return &FVMOutput{outputPtr}, cRe, nil
}
func (f *FVM) Plot() string {
plotP := C.fvm_plot(f.framework)
defer C.fvm_plot_destroy(plotP)
goBuf := C.GoBytes(unsafe.Pointer(plotP.buf.ptr), C.int(plotP.buf.length))
return string(goBuf)
}
func (f *FVM) Dump() *FVMUpdate {
return &FVMUpdate{C.fvm_dump(f.framework)}
}
func (f *FVM) Release() {
C.fvm_framework_release(f.framework)
}
func Serialize(out *FVMOutput, diff bool, base int64, target int64) *FVMUpdate {
updatePtr := C.fvm_serialize(out.output, C.bool(diff), C.long(base), C.long(target))
if updatePtr == nil {
return nil
}
ctStrPtr := updatePtr.buf.ptr
if ctStrPtr == nil {
return nil
}
return &FVMUpdate{updatePtr}
}
func ReleaseOutput(out *FVMOutput) {
C.fvm_output_destroy(out.output)
}
func (f *FVM) PushFsl(server string, update *FVMUpdate) error {
log.Infof("Push FSL to %s", server)
var length = uint32(update.update.buf.length)
var p = uintptr(unsafe.Pointer(update.update.buf.ptr))
log.Infof("Get update length %d, %d", length, C.int(update.update.buf.length))
u := &upload{
Length: length,
P: p,
}
base, _ := url.Parse(server)
base.Path = "/update/policy"
snURL := base.String()
req, err := http.NewRequest(
"POST",
snURL,
u,
)
if err != nil {
return errors.Annotatef(err, "create request failed")
}
req.Header.Add("Content-Type", "application/octet-stream")
// req.Header.Set("Connection", "close")
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
var (
respErr error
resp *http.Response
)
for i := 0; i < 3; i++ {
resp, respErr = httpClient.Do(req)
if respErr == nil {
break
}
}
if respErr != nil {
return errors.Annotatef(respErr, "Get response failed")
}
if resp != nil && resp.StatusCode != 200 {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
bodyString := string(bodyBytes)
return errors.New(fmt.Sprintf("Push FSL response(%d %s)", resp.StatusCode, bodyString))
}
return nil
}
type upload struct {
Length uint32
off uint32
P uintptr
}
func (u *upload) Read(p []byte) (n int, err error) {
if u.off >= u.Length {
if len(p) == 0 {
return 0, nil
}
return 0, io.EOF
}
plen := uint32(len(p))
var i uint32
for i = 0; i < plen; i++ {
if i+u.off >= u.Length {
break
}
p[i] = *(*byte)(unsafe.Pointer(u.P + unsafe.Sizeof(p[0])*uintptr(i+u.off)))
}
u.off = i + u.off
return int(i), nil
}
================================================
FILE: management/webserver/pkg/fvm/generator.go
================================================
package fvm
import (
"encoding/json"
"fmt"
"strings"
"gorm.io/gorm"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
"chaitin.cn/dev/go/errors"
"chaitin.cn/dev/go/log"
"chaitin.cn/patronus/safeline-2/management/webserver/model"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm/fsl"
)
const (
Main = "main"
Preprocess = "preprocess"
MassPackage = "mass_package"
PolicyRule = "policy_rule"
PolicyRuleGlobal = "policy_rule_global"
PolicyGroup = "policy_group"
PolicyGroupDetect = "policy_group_detect"
WeblogGenerate = "weblog_generate"
ReqWeblogGenerate = "req_weblog_generate"
RspWeblogGenerate = "rsp_weblog_generate"
CommonLog = "common_log"
STR = "STRING"
INT = "INTEGER"
INET = "INET"
BOOL = "BOOLEAN"
)
var logger = log.GetLogger("fvm")
func InitFVMBytecode() {
go func() {
fullFSL, err := GenerateFullFSL(database.GetDB().DB)
if err != nil {
logger.Fatalln("Failed to generate fsl", err)
}
if err = CompileAndSave(fullFSL); err != nil {
logger.Fatalln("Failed to compile and save fsl", err)
}
}()
}
func PushFSL(tx *gorm.DB) error {
fullFSL, err := GenerateFullFSL(tx)
if err != nil {
logger.Debugf("Full fsl: %s", fullFSL)
logger.Error(err)
return err
}
if err = CompileAndPush(fullFSL, config.GlobalConfig.Detector.Addr); err != nil {
logger.Error(err)
return err
}
return nil
}
func GenerateFullFSL(tx *gorm.DB) (fullFSL string, err error) {
logger.Info("Generate FSL")
pFSL, err := PreprocessTable(tx)
if err != nil {
return "", err
}
fullFSL += pFSL
fullFSL += MassPackageTable()
prFSL, err := PolicyRuleTable(tx)
if err != nil {
return "", err
}
fullFSL += prFSL
pgFSL, err := PolicyGroupTable(tx)
if err != nil {
return "", err
}
fullFSL += pgFSL
fullFSL += WeblogGenerateTable()
fullFSL += MainTable()
return fullFSL, err
}
func PreprocessTable(db *gorm.DB) (pFSL string, err error) {
pFSL += fsl.CreateTable(Preprocess, nil)
pFSL += fsl.AppendInto(Preprocess, "http_parse", "", "http_parse()")
pFSL += fsl.AppendInto(Preprocess, "init_variables", "", fsl.Actions(
fsl.Set("@site_uuid", STR, "get_http_all_headers('SL-CE-SUID')"),
fsl.Set("@src_ip", INET, "socket.src_ip"),
fsl.Set("@skip_remaining", BOOL, "false"),
fsl.Set("@rule_id", STR, fsl.Quote("")),
fsl.Set("@attack_type", INT, "-1"),
fsl.Set("@final_action", INT, "0"),
fsl.Set("@risk_level", INT, "0"),
fsl.Set("@host", STR, "http.decayed_host"),
fsl.Set("@start_time", INT, "now.timestamp_us"),
fsl.Set("@transport_scheme", STR, "transport.scheme"),
fsl.Set("@forward_log", INT, "0"),
fsl.Set("@convert_to_alog", INT, "1"),
fsl.Set("@alog_save_to_db", INT, "0"),
fsl.Set("@convert_to_blog", INT, "0"),
fsl.Set("@blog_save_to_db", INT, "0"),
fsl.Set("@convert_to_dlog", INT, "0"),
fsl.Set("@dlog_save_to_db", INT, "0"),
))
pFSL += fsl.AppendInto(Preprocess, "set_host", "@host::STRING = ''", fsl.Actions(fsl.Set("@host", STR, "inet_to_string(socket.dst_ip)")))
pFSL += fsl.AppendInto(Preprocess, "adapt_http2", "@transport_scheme::STRING = 'http2'", fsl.Actions(fsl.Set("@transport_scheme", STR, fsl.Quote("https"))))
// get src_ip
sc, err := model.GetSrcIPConfig(db)
if err != nil {
return "", err
}
if sc.Source == model.HTTPHeader {
key := fmt.Sprintf("string_to_inet(get_http_all_headers(%s))", fsl.Quote(sc.Value))
pFSL += fsl.AppendInto(Preprocess, "set_src_ip", fmt.Sprintf("%s != '::/0'::INET", key), fsl.Actions(fsl.Set("@src_ip", INET, key)))
}
return pFSL, nil
}
func MassPackageTable() (mpFSL string) {
mpFSL += fsl.CreateTable(MassPackage, nil)
mpFSL += fsl.AppendInto(MassPackage, "mass_package_detect", "string.length(http.raw_req_body) > 1048576", fsl.Actions(
fsl.Set("@skip_remaining", BOOL, "true"),
fsl.Set("@rule_id", STR, fsl.Quote("mass_package")),
fsl.Set("@final_action", INT, "0"),
fsl.Set("@convert_to_dlog", INT, "1"),
fsl.Set("@dlog_save_to_db", INT, "1"),
fsl.Set("@attack_type", INT, "-4"),
))
return mpFSL
}
func PolicyRuleTable(db *gorm.DB) (prFSL string, err error) {
prFSL += fsl.CreateTable(PolicyRuleGlobal, map[fsl.State]fsl.State{fsl.S_ABORT: fsl.S_RETURN})
var policyRuleList []model.PolicyRule
res := db.Model(&model.PolicyRule{}).Where(&model.PolicyRule{IsEnabled: true}).Order("id desc").Find(&policyRuleList)
if res.Error != nil {
err = errors.New(fmt.Sprintf("Error occurred when fetch PolicyRule: %s", res.Error))
}
for _, policyRule := range policyRuleList {
var patternBytes []byte
patternBytes, err = policyRule.Pattern.MarshalJSON()
if err != nil {
return "", err
}
var patterns []model.PolicyRulePattern
err = json.Unmarshal(patternBytes, &patterns)
if err != nil {
return "", err
}
var wheres []string
for _, pattern := range patterns {
// transform key to string
var key string
switch pattern.K {
case model.KeySrcIP:
key = "inet_to_string(@src_ip::INET)"
case model.KeyURI:
key = "@uri_decoded::STRING"
case model.KeyHost:
key = "http.host"
default:
return "", errors.New("wrong Key")
}
switch pattern.Op {
case model.OpEq:
wheres = append(wheres, fmt.Sprintf("string.equals_case(%s, %s)", key, fsl.Quote(pattern.V)))
case model.OpMatch:
// . -> \.
// * -> .*
valueQuote := strings.ReplaceAll(fsl.Quote(strings.ReplaceAll(pattern.V, ".", "\\.")), "*", ".*")
wheres = append(wheres, fmt.Sprintf("pcre_match(%s, %s)", valueQuote, key))
case model.OpCIDR:
// use INET for CIDR compare
key = "@src_ip::INET"
wheres = append(wheres, fmt.Sprintf("%s IN CIDR(%s)", key, fsl.Quote(pattern.V)))
case model.OpHas:
wheres = append(wheres, fmt.Sprintf("string.contains_case(%s, %s)", key, fsl.Quote(pattern.V)))
case model.OpPrefix:
l := len(pattern.V)
wheres = append(wheres, fmt.Sprintf("string.equals_case(string.substr(%s, 0, %d), %s)", key, l, fsl.Quote(pattern.V)))
case model.OpRe:
wheres = append(wheres, fmt.Sprintf("pcre_match(%s, %s)", fsl.Quote(pattern.V), key))
default:
return "", errors.New("wrong Operator")
}
}
// save log only when deny and dry_run
logOption := "1"
attackType := "-3"
// DROP or ACCEPT
appendix := "DROP"
if policyRule.Action == 0 {
logOption = "0"
attackType = "-2"
appendix = "set_ctx_allow(), ACCEPT" // this selector seems no use
}
prFSL += fsl.AppendInto(PolicyRuleGlobal, fmt.Sprintf("%s_%d", PolicyRuleGlobal, policyRule.ID), fsl.Wheres(wheres...), fsl.Actions(
fsl.Set("@skip_remaining", BOOL, "true"),
fsl.Set("@rule_id", STR, fsl.Quote(fmt.Sprintf("/%s", policyRule.Comment))),
fsl.Set("@final_action", INT, fmt.Sprintf("%d", policyRule.Action)),
fsl.Set("@convert_to_dlog", INT, logOption),
fsl.Set("@dlog_save_to_db", INT, logOption),
fsl.Set("@attack_type", INT, attackType),
appendix,
// no use in v1.1
//fsl.Set("@risk_level", INT, ""),
))
}
prFSL += fsl.CreateTable(PolicyRule, nil)
prFSL += fsl.AppendInto(PolicyRule, "preprocess", "", fsl.Actions(fsl.Set("@uri_decoded", STR, "url_decode(http.uri)")))
prFSL += fsl.AppendInto(PolicyRule, "policy_rule_global", "", fsl.Goto(PolicyRuleGlobal))
prFSL += fsl.AppendInto(PolicyRule, "set_status_code", "@final_action::INTEGER = 1", "set_status_code(403)")
return prFSL, err
}
func PolicyGroupTable(db *gorm.DB) (pgFSL string, err error) {
skynetConfigStr, err := model.GetSkynetConfig(db)
if err != nil {
return "", err
}
pgFSL += fsl.CreateTarget("skynet_config", fsl.Quote(skynetConfigStr))
pgFSL += fsl.CreateTable(PolicyGroupDetect, map[fsl.State]fsl.State{fsl.S_ABORT: fsl.S_RETURN})
pgFSL += fsl.AppendInto(PolicyGroupDetect, "do_detect", "", "target 'skynet_config' type skynet do detect()")
pgFSL += fsl.CreateTable(PolicyGroup, nil)
pgFSL += fsl.AppendInto(PolicyGroup, "do_detect", "", fsl.Goto(PolicyGroupDetect))
pgFSL += fsl.AppendInto(PolicyGroup, "dlog_persistence", "skynet.enable_log = true", fsl.Actions(
fsl.Set("@convert_to_dlog", INT, "1"),
fsl.Set("@dlog_save_to_db", INT, "1"),
))
pgFSL += fsl.AppendInto(PolicyGroup, "set_status_code", "skynet.action = 1", "set_status_code(403)")
pgFSL += fsl.AppendInto(PolicyGroup, "hit_policy_group", "skynet.rule_id != ''", fsl.Actions(
fsl.Set("@skip_remaining", BOOL, "true"),
fsl.Set("@rule_id", STR, "skynet.rule_id"),
fsl.Set("@attack_type", INT, "skynet.main_attack"),
fsl.Set("@final_action", INT, "skynet.action"),
fsl.Set("@risk_level", INT, "skynet.risk_level"),
fsl.Set("@convert_to_dlog", INT, "1"),
))
return pgFSL, nil
}
func CommonLogTable() (clFSL string) {
clFSL += fsl.CreateTable(CommonLog, nil)
clFSL += fsl.AppendInto(CommonLog, "batch_set_payload_string", "", "weblog_payload_set_string_batch({'event_id', 'scheme', 'dst_ip', 'socket_ip', 'src_ip'}::ARRAY(STRING), {extra.uuid, transport.scheme, inet_to_string(socket.dst_ip), inet_to_string(socket.src_ip), inet_to_string(@src_ip::INET)}::ARRAY(STRING))")
clFSL += fsl.AppendInto(CommonLog, "batch_set_payload_int", "", "weblog_payload_set_int_batch({'src_port', 'dst_port'}::ARRAY(STRING), {socket.src_port, socket.dst_port}::ARRAY(INTEGER))")
clFSL += fsl.AppendInto(CommonLog, "batch_set_extra_bool", "", "weblog_extra_set_bool_batch({'alog_save_to_db', 'convert_to_blog', 'forward_log', 'convert_to_alog', 'convert_to_dlog', 'dlog_save_to_db', 'blog_save_to_db'}::ARRAY(STRING), {@alog_save_to_db::INTEGER, @convert_to_blog::INTEGER, @forward_log::INTEGER, @convert_to_alog::INTEGER, @convert_to_dlog::INTEGER, @dlog_save_to_db::INTEGER, @blog_save_to_db::INTEGER}::ARRAY(INTEGER))")
clFSL += fsl.AppendInto(CommonLog, "generate", "", "weblog_generate()")
return clFSL
}
func ReqWeblogGenerateTable() (reqWGFSL string) {
reqWGFSL += fsl.CreateTable(ReqWeblogGenerate, nil)
reqWGFSL += fsl.AppendInto(ReqWeblogGenerate, "init_http_req_body", "", fsl.Actions(fsl.Set("@raw_req_body", STR, "http.raw_req_body")))
reqWGFSL += fsl.AppendInto(ReqWeblogGenerate, "cut_http_req_body", "string.length(@raw_req_body::STRING) > 1048576", fsl.Actions(fsl.Set("@raw_req_body", STR, "string.substr(http.raw_req_body, 0, 1048576)")))
reqWGFSL += fsl.AppendInto(ReqWeblogGenerate, "batch_set_payload_binary", "", "weblog_payload_set_binary_batch({'req_payload', 'req_body'}::ARRAY(STRING), {skynet.payload, @raw_req_body::STRING}::ARRAY(STRING))")
reqWGFSL += fsl.AppendInto(ReqWeblogGenerate, "batch_set_payload_string", "", "weblog_payload_set_string_batch({'site_uuid', 'req_header', 'req_detector_name', 'req_proxy_name', 'req_rule_id', 'req_location', 'req_decode_path', 'user_agent', 'query_string', 'method', 'host', 'referer', 'url_path', 'cookie'}::ARRAY(STRING), {@site_uuid::STRING, http.raw_req_header, fusion.node_id, extra.proxy_name, @rule_id::STRING, skynet.location, skynet.decode_path, http.user_agent, http.decoded_query, http.raw_method, @host::STRING, http.referer, http.path, http.raw_cookie}::ARRAY(STRING))")
reqWGFSL += fsl.AppendInto(ReqWeblogGenerate, "batch_set_payload_int", "", "weblog_payload_set_int_batch({'req_start_time', 'req_detect_time', 'req_block_reason', 'req_attack_type', 'req_risk_level', 'req_end_time', 'req_action'}::ARRAY(STRING), {extra.req_start_time, now.timestamp_us - @start_time::INTEGER, fusion.block_reason, @attack_type::INTEGER, @risk_level::INTEGER, extra.req_end_time, @final_action::INTEGER}::ARRAY(INTEGER))")
reqWGFSL += fsl.AppendInto(ReqWeblogGenerate, "common_log", "", fsl.Goto(CommonLog))
return reqWGFSL
}
func RspWeblogGenerateTable() (rspWGFSL string) {
rspWGFSL += fsl.CreateTable(RspWeblogGenerate, nil)
rspWGFSL += fsl.AppendInto(RspWeblogGenerate, "init_http_rsp_body", "", fsl.Actions(fsl.Set("@raw_rsp_body", STR, "http.raw_rsp_body")))
rspWGFSL += fsl.AppendInto(RspWeblogGenerate, "cut_http_rsp_body", "string.length(@raw_rsp_body::STRING) > 1048576", fsl.Actions(fsl.Set("@raw_rsp_body", STR, "string.substr(http.raw_rsp_body, 0, 1048576)")))
rspWGFSL += fsl.AppendInto(RspWeblogGenerate, "batch_set_payload_binary", "", "weblog_payload_set_binary_batch({'rsp_payload', 'rsp_body'}::ARRAY(STRING), {skynet.payload, @raw_rsp_body::STRING}::ARRAY(STRING))")
rspWGFSL += fsl.AppendInto(RspWeblogGenerate, "batch_set_payload_string", "", "weblog_payload_set_string_batch({'rsp_rule_id', 'rsp_location', 'rsp_decode_path', 'rsp_header', 'rsp_detector_name', 'rsp_proxy_name'}::ARRAY(STRING), {@rule_id::STRING, skynet.location, skynet.decode_path, http.raw_rsp_header, fusion.node_id, extra.proxy_name}::ARRAY(STRING))")
rspWGFSL += fsl.AppendInto(RspWeblogGenerate, "batch_set_payload_int", "", "weblog_payload_set_int_batch({'rsp_detect_time', 'status_code', 'rsp_start_time', 'rsp_end_time', 'rsp_action', 'rsp_attack_type', 'rsp_risk_level', 'rsp_block_reason'}::ARRAY(STRING), {now.timestamp_us - @start_time::INTEGER, http.status_code, extra.rsp_start_time, extra.rsp_end_time, @final_action::INTEGER, @attack_type::INTEGER, @risk_level::INTEGER, fusion.block_reason}::ARRAY(INTEGER))")
rspWGFSL += fsl.AppendInto(RspWeblogGenerate, "common_log", "", fsl.Goto(CommonLog))
return rspWGFSL
}
func WeblogGenerateTable() (wgFSL string) {
wgFSL += CommonLogTable()
wgFSL += ReqWeblogGenerateTable()
wgFSL += RspWeblogGenerateTable()
wgFSL += fsl.CreateTable(WeblogGenerate, nil)
wgFSL += fsl.AppendInto(WeblogGenerate, "req", "fusion.stage = 0", fsl.Goto(ReqWeblogGenerate))
wgFSL += fsl.AppendInto(WeblogGenerate, "rsp", "fusion.stage = 1", fsl.Goto(RspWeblogGenerate))
return wgFSL
}
func MainTable() (mainFSL string) {
mainFSL += fsl.CreateTable(Main, nil)
mainFSL += fsl.AppendInto(Main, "m_preprocess", "", fsl.Goto(Preprocess))
mainFSL += fsl.AppendInto(Main, "m_mass_package", "", fsl.Goto(MassPackage))
mainFSL += fsl.AppendInto(Main, "m_policy_rule", "@skip_remaining::BOOLEAN = false", fsl.Goto(PolicyRule))
mainFSL += fsl.AppendInto(Main, "m_policy_group", "@skip_remaining::BOOLEAN = false", fsl.Goto(PolicyGroup))
mainFSL += fsl.AppendInto(Main, "m_weblog_generate", "", fsl.Goto(WeblogGenerate))
mainFSL += "ENTRYPOINT TABLE main;"
return mainFSL
}
================================================
FILE: management/webserver/pkg/fvm/helper.go
================================================
package fvm
import (
"chaitin.cn/dev/go/errors"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
"chaitin.cn/patronus/safeline-2/management/webserver/utils"
)
var GlobalFVM *FVM
func init() {
var err error
GlobalFVM, err = New()
if err != nil {
panic(err)
}
}
func CompileAndSave(text string) error {
output, _, err := GlobalFVM.Compile(text, nil)
if err != nil {
return errors.Annotate(err, "failed to compile rules")
}
defer ReleaseOutput(output)
update := Serialize(output, false, 0, 1)
defer update.Release()
// save to file
if err = utils.EnsureWriteFile(config.GlobalConfig.Detector.FslBytecode, update.ToBytes(), 0666); err != nil {
return errors.Annotate(err, "failed to save bytecode")
}
return nil
}
func CompileAndPush(text, serverAddr string) error {
output, _, err := GlobalFVM.Compile(text, nil)
if err != nil {
return errors.Annotate(err, "failed to compile rules")
}
defer ReleaseOutput(output)
update := Serialize(output, false, 0, 1)
defer update.Release()
// save to file
if err = utils.EnsureWriteFile(config.GlobalConfig.Detector.FslBytecode, update.ToBytes(), 0666); err != nil {
return errors.Annotate(err, "failed to save bytecode")
}
if err := GlobalFVM.PushFsl(serverAddr, update); err != nil {
return errors.Annotatef(err, "failed to push rule to '%s'", serverAddr)
}
return nil
}
================================================
FILE: management/webserver/pkg/log/log.go
================================================
package log
import (
"fmt"
"os"
"runtime"
"strings"
"github.com/sirupsen/logrus"
"chaitin.cn/dev/go/log"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
"chaitin.cn/patronus/safeline-2/management/webserver/utils"
)
func GetLogger(name string) *log.Logger {
return log.GetLogger(name)
}
func LoadLogLevel() {
lv, _ := log.ParseLevel(config.GlobalConfig.Log.Level)
log.SetLevel(log.AllLoggers, lv)
}
func SetLogFormatter() {
// format
formatter := new(log.TextFormatter)
formatter.FullTimestamp = true
formatter.TimestampFormat = "2006/01/02 15:04:05"
log.SetFormatter(log.AllLoggers, formatter)
}
func InitLogger() error {
// output
switch config.GlobalConfig.Log.Output {
case "stdout":
log.SetOutput(log.AllLoggers, os.Stdout)
case "stderr":
log.SetOutput(log.AllLoggers, os.Stderr)
default:
exist, err := utils.FileExist(config.GlobalConfig.Log.Output)
if err != nil {
return err
}
fileFlag := os.O_WRONLY | os.O_APPEND | os.O_SYNC
if !exist {
if err := utils.EnsureFileDir(config.GlobalConfig.Log.Output); err != nil {
return err
}
fileFlag = fileFlag | os.O_CREATE
}
if fp, err := os.OpenFile(config.GlobalConfig.Log.Output, fileFlag, os.ModePerm); err != nil {
return fmt.Errorf("failed to open log file: %s", err.Error())
} else {
log.SetOutput(log.AllLoggers, log.NewLockOutput(fp))
}
}
// hook
log.AddHook(log.AllLoggers, NewRuntimeHook())
log.AddHook(log.AllLoggers, log.NewErrorStackHook(true))
// level
LoadLogLevel()
return nil
}
type RuntimeHook struct{}
func (h *RuntimeHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func (h *RuntimeHook) Fire(entry *logrus.Entry) error {
file := "???"
funcName := "???"
line := 0
pc := make([]uintptr, 64)
// Skip runtime.Callers, self, and another call from logrus
n := runtime.Callers(3, pc)
if n != 0 {
pc = pc[:n] // pass only valid pcs to runtime.CallersFrames
frames := runtime.CallersFrames(pc)
// Loop to get frames.
// A fixed number of pcs can expand to an indefinite number of Frames.
for {
frame, more := frames.Next()
if !strings.Contains(frame.File, "github.com/sirupsen/logrus") && !strings.Contains(frame.Function, "chaitin.cn/dev/go") {
file = frame.File
funcName = frame.Function
line = frame.Line
break
}
if !more {
break
}
}
}
slices := strings.Split(file, "/")
file = slices[len(slices)-1]
funcName = strings.ReplaceAll(funcName, "chaitin.cn", "")
entry.Data["file"] = file
entry.Data["func"] = funcName
entry.Data["line"] = line
return nil
}
func NewRuntimeHook() *RuntimeHook {
return &RuntimeHook{}
}
================================================
FILE: management/webserver/pkg/telemetry.go
================================================
package pkg
import (
"fmt"
"io"
"net/http"
"strconv"
"time"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/webserver/utils"
)
type WebsiteResult struct {
Id uint
UpdatedAt time.Time
}
type TelemetryInfo struct {
Id string `json:"id"`
}
type SafelineInfo struct {
Id string `json:"id"`
Type string `json:"type"`
Version string `json:"version"`
ReqCnt int `json:"req_cnt"`
DetectLogCnt int `json:"detect_log_cnt"`
SiteCnt int `json:"site_cnt"`
HealthySiteCnt int `json:"healthy_site_cnt"`
RuleCnt int `json:"rule_cnt"`
IsHealthy bool `json:"is_healthy"`
Behavior map[string]int `json:"behavior"`
Websites []WebsiteResult `json:"websites"`
WebsitesCnt int `json:"websites_cnt"`
}
type TelemetryRequest struct {
Telemetry TelemetryInfo `json:"telemetry"`
Safeline SafelineInfo `json:"safeline"`
}
func GetUploadTimestamp() string {
yesterday := time.Now().AddDate(0, 0, -1)
yesterdayNoon := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 12, 0, 0, 0, yesterday.Location())
return strconv.FormatInt(yesterdayNoon.Unix(), 10)
}
func GetUploadNonce() string {
now := strconv.FormatInt(time.Now().Unix(), 10)
randStr := utils.RandStr(6)
return fmt.Sprintf("%s%s", now, randStr)
}
func DoPostTelemetry(client *http.Client, addr string, reader io.Reader) (*http.Response, error) {
req, err := http.NewRequest(http.MethodPost, "https://"+addr+constants.TelemetryEntryPoint, reader)
if err != nil {
return nil, err
}
req.Header.Set("accept", constants.ApplicationJson)
req.Header.Set("Content-Type", constants.ApplicationJson)
rsp, err := client.Do(req)
if err != nil {
return nil, err
}
return rsp, nil
}
================================================
FILE: management/webserver/proto/website/website.proto
================================================
syntax = "proto3";
package website;
option go_package = "proto/website";
service Website {
rpc Subscribe(stream Response) returns (stream Event) {}
}
// From client-side, may be "pong"
message Response {
string type = 1;
bytes msg = 2;
bool err = 3;
}
// From server-side, may be "ping"
message Event {
string type = 1; // ping/website
bytes msg = 2;
}
================================================
FILE: management/webserver/rpc/main.go
================================================
package rpc
import (
"net"
"time"
"google.golang.org/grpc"
"chaitin.cn/dev/go/errors"
"chaitin.cn/dev/go/log"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config"
pb "chaitin.cn/patronus/safeline-2/management/webserver/proto/website"
)
const (
WaitRspTimeout = 30 * time.Second
KeepaliveTime = 5 * time.Second
KeepaliveTimeout = 30 * time.Second
Ping = "ping"
Pong = "pong"
EventTypeWebsite = "website"
EventTypeDeleteWebsite = "deleteWebsite"
EventTypeFullWebsite = "fullWebsite"
)
var logger = log.GetLogger("grpc")
func StartGRPCSever() error {
lis, err := net.Listen("tcp", config.GlobalConfig.GPRC.ListenAddr)
if err != nil {
return errors.Wrap(err, "Failed to listen")
}
var opts []grpc.ServerOption
grpcServer := grpc.NewServer(opts...)
pb.RegisterWebsiteServer(grpcServer, GetWebsiteServer())
go func() {
err := grpcServer.Serve(lis)
if err != nil {
logger.Fatalln("Failed to server")
}
}()
return nil
}
================================================
FILE: management/webserver/rpc/website.go
================================================
package rpc
import (
"encoding/json"
"io"
"time"
"chaitin.cn/dev/go/errors"
"chaitin.cn/patronus/safeline-2/management/webserver/model"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
pb "chaitin.cn/patronus/safeline-2/management/webserver/proto/website"
)
var (
ping = pb.Event{Type: Ping, Msg: nil}
Subscriber *StreamClient // only ONE sub
)
func Publish(msg []byte, eventType string) error {
if Subscriber == nil {
return errors.New("Service is abnormal, and the nginx conf cannot be updated for the time being. Please go to the shell to check the relevant logs")
}
err := Subscriber.stream.Send(&pb.Event{
Type: eventType,
Msg: msg,
})
if err != nil {
select {
case <-Subscriber.stream.Context().Done():
err = Subscriber.stream.Context().Err() // context canceled
Subscriber = nil
return err
case Subscriber.errCh <- errors.Wrapf(err, "Send event err: %s", msg):
return errors.Wrapf(err, "Send event err: %s", msg)
}
}
rspTimeoutTicker := time.NewTicker(WaitRspTimeout)
defer rspTimeoutTicker.Stop()
for {
select {
case rsp := <-Subscriber.rspCh:
if rsp.Err {
return errors.New(string(rsp.Msg))
} else {
// success
return nil
}
case <-rspTimeoutTicker.C:
return errors.New("Wait timeout for updating result")
}
}
}
// StreamClient is the instance for every client stream connected
type StreamClient struct {
stream pb.Website_SubscribeServer
timer *time.Timer // timer for timeout
errCh chan error
rspCh chan *pb.Response
quit chan struct{} // quit stream client gracefully
}
func newStreamClient(stream pb.Website_SubscribeServer) *StreamClient {
return &StreamClient{
stream: stream,
timer: time.NewTimer(KeepaliveTimeout),
errCh: make(chan error, 1),
rspCh: make(chan *pb.Response, 1),
quit: make(chan struct{}),
}
}
func (sc *StreamClient) pingLoop() {
pingTicker := time.NewTicker(KeepaliveTime)
defer pingTicker.Stop()
for {
if Subscriber != nil && sc != Subscriber {
// new subscriber in replace of the old one.
logger.Debug("New subscriber in replace of the old one")
return
}
select {
case <-sc.stream.Context().Done():
return
case <-pingTicker.C:
err := sc.stream.Send(&ping)
if err != nil {
select {
case <-sc.stream.Context().Done():
case sc.errCh <- errors.Wrapf(err, "Send ping err"):
}
return
}
}
}
}
func (sc *StreamClient) recvLoop() {
for {
if Subscriber != nil && sc != Subscriber {
// new subscriber in replace of the old one.
close(sc.quit)
logger.Debug("New subscriber in replace of the old one")
return
}
rsp, err := sc.stream.Recv()
if err != nil {
if err != io.EOF {
select {
case <-sc.stream.Context().Done():
case sc.errCh <- errors.Wrapf(err, "Recv pong err"):
}
}
return
}
logger.Debugf("Got message Type %s", rsp.GetType())
if rsp.Type != Pong {
// receive response
logger.Infof("Recv updating website rsp: err(%t), msg(%s)", rsp.GetErr(), rsp.GetMsg())
sc.rspCh <- rsp
continue
}
sc.timer.Reset(KeepaliveTimeout)
}
}
// WebsiteServer is the gRPC server implementation
type WebsiteServer struct {
*pb.UnimplementedWebsiteServer
}
func GetWebsiteServer() *WebsiteServer {
return &WebsiteServer{}
}
func publishFullWebsite() error {
var websites []model.Website
db := database.GetDB().DB
db.Model(&model.Website{}).Find(&websites)
byteWebsites, err := json.Marshal(&websites)
if err != nil {
return err
}
return Publish(byteWebsites, EventTypeFullWebsite)
}
// Subscribe is gRPC API entrypoint
func (ws *WebsiteServer) Subscribe(stream pb.Website_SubscribeServer) error {
Subscriber = newStreamClient(stream)
defer Subscriber.timer.Stop()
go Subscriber.pingLoop()
go Subscriber.recvLoop()
if err := publishFullWebsite(); err != nil {
// triggered when tcd starts, ignore push error messages
logger.Warn(err)
}
select {
case <-Subscriber.quit:
logger.Infof("Disconnected gracefully")
return nil
case <-Subscriber.timer.C:
logger.Error("Keepalive timeout")
case err := <-Subscriber.errCh:
logger.WithError(err).Error()
return err
case <-Subscriber.stream.Context().Done():
logger.Infof("Subscribe context done: %s", Subscriber.stream.Context().Err())
Subscriber = nil
}
return nil
}
================================================
FILE: management/webserver/tools/init_db.sh
================================================
docker rm -f mgt-postgres-dev
#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
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
================================================
FILE: management/webserver/utils/cert.go
================================================
package utils
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"io/ioutil"
"math/big"
"time"
)
func GenerateCert(hostnames []string, days int64, keyBits int, subject *pkix.Name, isCA bool) ([]byte, []byte, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, keyBits)
if err != nil {
return nil, nil, err
}
notBefore := time.Now()
duration := int64(time.Hour) * 24 * days
notAfter := notBefore.Add(time.Duration(duration))
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, nil, err
}
var keyUsage x509.KeyUsage
if isCA {
keyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign
} else {
keyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
}
template := x509.Certificate{
SerialNumber: serialNumber,
NotBefore: notBefore,
NotAfter: notAfter,
DNSNames: hostnames,
BasicConstraintsValid: true,
IsCA: isCA,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
KeyUsage: keyUsage,
}
if subject != nil {
template.Subject = *subject
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, nil, err
}
var certBuffer bytes.Buffer
err = pem.Encode(&certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil {
return nil, nil, err
}
var keyBuffer bytes.Buffer
err = pem.Encode(&keyBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
if err != nil {
return nil, nil, err
}
return certBuffer.Bytes(), keyBuffer.Bytes(), nil
}
func WriteCertIfNotExist(certFilePath, keyFilePath string, generator func() ([]byte, []byte, error)) error {
cert, key, err := genCertIfNotExist(certFilePath, keyFilePath, generator)
if err != nil {
return err
}
if err = EnsureRenameWriteFile(certFilePath, cert, 0644); err != nil {
return err
}
if err = EnsureRenameWriteFile(keyFilePath, key, 0644); err != nil {
return err
}
return nil
}
func genCertIfNotExist(certFilePath, keyFilePath string, generator func() ([]byte, []byte, error)) ([]byte, []byte, error) {
exist, err := FilesExist(certFilePath, keyFilePath)
if err != nil {
return nil, nil, err
}
if !exist {
return generator()
} else {
cert, err := ioutil.ReadFile(certFilePath)
if err != nil {
return nil, nil, err
}
key, err := ioutil.ReadFile(keyFilePath)
if err != nil {
return nil, nil, err
}
return cert, key, nil
}
}
================================================
FILE: management/webserver/utils/file.go
================================================
package utils
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
)
func EnsureDir(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return os.MkdirAll(dir, os.FileMode(0755))
}
return nil
}
func EnsureFileDir(path string) error {
return EnsureDir(filepath.Dir(path))
}
func FileExist(path string) (bool, error) {
stat, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
} else {
return false, err
}
} else {
if stat.IsDir() {
return false, fmt.Errorf("%s is dir", path)
} else {
return true, nil
}
}
}
func FilesExist(paths ...string) (bool, error) {
for _, path := range paths {
exist, err := FileExist(path)
if err != nil {
return false, err
}
if !exist {
return false, nil
}
}
return true, nil
}
func RenameWriteFile(filename string, data []byte, perm os.FileMode) error {
randFileName := filename + ".tmp." + RandStr(8)
if err := ioutil.WriteFile(randFileName, data, perm); err != nil {
return err
}
return os.Rename(randFileName, filename)
}
func EnsureRenameWriteFile(path string, data []byte, mode os.FileMode) error {
err := EnsureFileDir(path)
if err != nil {
return err
}
return RenameWriteFile(path, data, mode)
}
func EnsureWriteFile(path string, data []byte, mode os.FileMode) error {
err := EnsureFileDir(path)
if err != nil {
return err
}
return ioutil.WriteFile(path, data, mode)
}
================================================
FILE: management/webserver/utils/healthy.go
================================================
package utils
import (
"fmt"
"net/http"
)
func CheckHealthy(url string) bool {
method := "GET"
client := &http.Client{}
req, err := http.NewRequest(method, url, nil)
if err != nil {
fmt.Println(err)
return false
}
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return false
}
defer res.Body.Close()
return res.StatusCode == 200
}
func CheckWafHealthy() bool {
// todo: not check the healthy status
return CheckHealthy("http://safeline-mario:3335") && CheckHealthy("http://safeline-detector:8001/stat")
}
================================================
FILE: management/webserver/utils/httpclient.go
================================================
package utils
import (
"crypto/tls"
"net/http"
"net/url"
"os"
"time"
)
const proxyName = "HTTPS_PROXY"
var httpClient *http.Client
func GetHTTPClient() *http.Client {
if httpClient == nil {
tr := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
proxyUrl, existed := os.LookupEnv(proxyName)
if existed {
uri, _ := url.Parse(proxyUrl)
tr.Proxy = http.ProxyURL(uri)
}
httpClient = &http.Client{Transport: tr}
}
return httpClient
}
================================================
FILE: management/webserver/utils/random.go
================================================
package utils
import (
"math/rand"
"time"
)
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func RandStr(n int) string {
rand.Seed(time.Now().UnixNano())
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
================================================
FILE: management/webserver/utils/url.go
================================================
package utils
import (
"fmt"
"net"
"strings"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
)
func IsIPv6(str string) bool {
ip := net.ParseIP(str)
return ip != nil && strings.Contains(str, ":")
}
func BuildUrl(protocol int, host string, port uint, path string) string {
portStr := ""
if len(host) > 0 && IsIPv6(host) && host[:1] != "[" {
host = fmt.Sprintf("[%s]", host)
}
if protocol == constants.ProtocolHTTP {
if port != 80 {
portStr = fmt.Sprintf(":%d", port)
}
} else if protocol == constants.ProtocolHTTPS || protocol == constants.ProtocolHTTP2 {
if port != 443 {
portStr = fmt.Sprintf(":%d", port)
}
} else {
// use HTTP as default protocol
protocol = constants.ProtocolHTTP
}
return fmt.Sprintf("%s://%s%s%s", constants.HTTPProtocol[protocol], host, portStr, path)
}
================================================
FILE: mcp_server/Dockerfile
================================================
FROM golang:1.24-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o mcp-server .
FROM alpine:latest
WORKDIR /app
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app/mcp-server .
COPY --from=builder /app/config.yaml .
ENV TZ=Asia/Shanghai
EXPOSE 5678
CMD ["./mcp-server"]
================================================
FILE: mcp_server/README.md
================================================
# SafeLine MCP Server
SafeLine MCP Server is an implementation of the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) that provides complete management and control capabilities for SafeLine WAF.
[](docker-compose.yml)
[](go.mod)
## Use Cases
- Automated management and control of SafeLine WAF instances
- WAF configuration and policy management through API
- Building AI-based security protection tools and applications
## Prerequisites
1. Install [Docker](https://www.docker.com/) (if running in container)
2. Configure SafeLine API Token (obtained from SafeLine console)
## Features
- Complete MCP (Management Control Protocol) server implementation
- Support for SafeLine WAF instance management and control
- Flexible configuration system supporting file configuration and environment variables
- Docker containerization support
- Secure API communication
## Quick Start
### Environment Variables
| Environment Variable | Description | Default Value | Required |
|---------|------|--------|-----|
| LISTEN_PORT | Service listening port | 5678 | No |
| LISTEN_ADDRESS | Service listening address | 0.0.0.0 | No |
| SAFELINE_SECRET | SSE server secret | - | No |
| SAFELINE_ADDRESS | SafeLine API address | - | Yes |
| SAFELINE_API_TOKEN | SafeLine API authentication token | - | Yes |
### Using Docker
#### Method 1: Using docker run
```bash
docker run -d \
--name safeline-mcp \
-p 5678:5678 \
-e SAFELINE_API_TOKEN="your_api_token" \
-e SAFELINE_ADDRESS="https://your.safeline.com" \
-e LISTEN_PORT=5678 \
-e LISTEN_ADDRESS="0.0.0.0" \
chaitin/safeline-mcp:latest
```
#### Method 2: Using docker-compose
```bash
# 1. Clone repository
git clone https://github.com/chaitin/safeline-mcp.git
cd safeline-mcp
# 2. Edit docker-compose.yml to configure environment variables
# Example docker-compose.yml:
# version: '3'
# services:
# mcp:
# image: chaitin/safeline-mcp:latest
# container_name: safeline-mcp
# ports:
# - "5678:5678"
# environment:
# - SAFELINE_API_TOKEN=your_api_token
# - SAFELINE_ADDRESS=https://your.safeline.com
# - LISTEN_PORT=5678
# - LISTEN_ADDRESS=0.0.0.0
# 3. Start service
docker compose -f docker-compose.yml up -d
```
#### Method 3: Using Go
```bash
# 1. Clone repository
git clone https://github.com/chaitin/SafeLine.git
cd safeline-mcp
# 2. Install dependencies
go mod download
# 3. Configure config.yaml
cp config.yaml.example config.yaml
# Edit config.yaml with necessary configurations
# 4. Run service
go run main.go
```
For more API details, please refer to the [API Documentation](https://demo.waf.chaitin.com:9443/swagger/index.html).
## Tools
### Application Management
- **create_application**
### Rule Management
- **create_blacklist_rule**
- **create_whitelist_rule**
### Analyze
- **get_attack_events**
## Development Guide
The Go API in this project is currently under development, and APIs may change. If you have specific requirements, please submit an Issue for discussion.
### Directory Structure
```
internal/
├── api/ # API implementation
│ ├── app/ # Application-related APIs
│ │ └── create_application.go
│ └── rule/ # Rule-related APIs
│ └── create_rule.go
└── tools/ # MCP tool implementation
├── app/ # Application-related tools
│ └── create_application.go
└── rule/ # Rule-related tools
└── create_rule.go
```
### Adding New Tools
1. **Create Tool File**
- Create corresponding directory and file under `internal/tools`
- File name should match tool name
- Use separate file for each tool
- Example: `internal/tools/app/create_application.go`
2. **Tool Implementation Template**
```go
package app
type ToolName struct{}
type ToolParams struct {
// Parameter definitions
Param1 string `json:"param1" desc:"parameter description" required:"true"`
Param2 int `json:"param2" desc:"parameter description" required:"false"`
}
type ToolResult struct {
Field1 string `json:"field1"`
}
func (t *ToolName) Name() string {
return "tool_name"
}
func (t *ToolName) Description() string {
return "tool description"
}
func (t *ToolName) Validate(params ToolParams) error {
// Parameter validation logic
return nil
}
func (t *ToolName) Execute(ctx context.Context, params ToolParams) (result ToolResult, err error) {
// Tool execution logic
return result, nil
}
```
3. **[Optional]Create API Implementation**
If you need to use some APIs that have not been implemented yet, you need to create corresponding files in the api directory for implementation
- Create same directory structure under `internal/api`
- File name should match tool func
- Example: `internal/api/app/create_application.go`
**API Implementation Template**
```go
package app
type RequestType struct {
// Request parameter definitions
Param1 string `json:"param1"`
Param2 int `json:"param2"`
}
func APIName(ctx context.Context, req *RequestType) (ResultType, error) {
if req == nil {
return nil, errors.New("request is required")
}
var resp api.Response[ResultType]
err := api.Service().Post(ctx, "/api/path", req, &resp)
if err != nil {
return nil, errors.Wrap(err, "failed to execute")
}
if resp.Err != nil {
return nil, errors.New(resp.Msg)
}
return resp.Data, nil
}
```
4. **Tool Registration (init.go)**
The tool registration file `internal/tools/init.go` is used to centrally manage all tool registrations
- Register all tools uniformly in the `init()` function
- Use the `AppendTool()` method for registration
- Example:
```go
// Register create application tool
AppendTool(&app.CreateApp{})
// Register create blacklist rule tool
AppendTool(&rule.CreateBlacklistRule{})
```
### Development Standards
1. **Naming Conventions**
- Use lowercase letters and underscores for tool names
- File names should match tool names
2. **Directory Organization**
- Divide directories by functional modules (e.g., app, rule, etc.)
- Maintain consistent structure between tools and api directories
- Keep related functionality in the same directory
3. **Code Standards**
- Follow Go standard code conventions
- Add necessary parameter validation
- Use unified error handling approach
- Add appropriate logging
4. **Documentation Requirements**
- Provide clear functional description in tool Description
- Add detailed description for parameters
- Update API toolkit documentation in README
### Example
Refer to the implementation of the `create_application` tool:
- Tool implementation: `internal/tools/app/create_application.go`
- API implementation: `internal/api/app/create_application.go`
================================================
FILE: mcp_server/config.yaml
================================================
# Server Configuration
server:
name: "SafeLine MCP Server"
version: "1.0.0"
# Can be overridden by environment variable LISTEN_PORT
port: 5678
# Can be overridden by environment variable LISTEN_ADDRESS
host: "0.0.0.0"
# Can be overridden by environment variable SAFELINE_SECRET
secret: "" # Secret for SSE server
# Logger Configuration
logger:
level: "info" # Log level: debug, info, warn, error
file_path: "" # Log file path
console: true # Whether to output to console
caller: false # Whether to record caller information
development: true # Whether to use development mode
# API Configuration
api:
# Can be overridden by environment variable SAFELINE_ADDRESS
base_url: "" # API service address
# Can be overridden by environment variable SAFELINE_API_TOKEN
token: "" # Authentication token
timeout: 30 # Timeout in seconds
debug: false # Whether to enable debug mode
insecure_skip_verify: true # Whether to skip certificate verification
================================================
FILE: mcp_server/docker-compose.yml
================================================
version: '3.8'
services:
mcp_server:
image: chaitin/safeline-mcp:latest
container_name: mcp_server
restart: always
ports:
- "5678:5678"
environment:
- SAFELINE_SECRET=your_secret_key # optional, if you want to use secret key to authenticate
- SAFELINE_ADDRESS=https://your_safeline_ip:9443 # required, your SafeLine WAF address
- SAFELINE_API_TOKEN=your_safeline_api_token # required, your SafeLine WAF api token
- LISTEN_PORT=5678 # optional, default is 5678
- LISTEN_ADDRESS=0.0.0.0 # optional, default is 0.0.0.0
================================================
FILE: mcp_server/go.mod
================================================
module github.com/chaitin/SafeLine/mcp_server
go 1.24.1
require (
github.com/mark3labs/mcp-go v0.18.0
go.uber.org/zap v1.27.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/google/uuid v1.6.0 // indirect
github.com/mcuadros/go-defaults v1.2.0
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
)
================================================
FILE: mcp_server/go.sum
================================================
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mark3labs/mcp-go v0.18.0 h1:YuhgIVjNlTG2ZOwmrkORWyPTp0dz1opPEqvsPtySXao=
github.com/mark3labs/mcp-go v0.18.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc=
github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: mcp_server/internal/api/analyze/get_event_list.go
================================================
package analyze
import (
"context"
"fmt"
"github.com/chaitin/SafeLine/mcp_server/internal/api"
)
type GetEventListRequest struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
IP string `json:"ip"`
Start int64 `json:"start"`
End int64 `json:"end"`
}
type GetEventListResponse struct {
Nodes []Event `json:"nodes"`
Total int64 `json:"total"`
}
type Event struct {
ID uint `json:"id"`
IP string `json:"ip"`
Protocol int `json:"protocol"`
Host string `json:"host"`
DstPort uint64 `json:"dst_port"`
UpdatedAt int64 `json:"updated_at"`
StartAt int64 `json:"start_at"`
EndAt int64 `json:"end_at"`
DenyCount int64 `json:"deny_count"`
PassCount int64 `json:"pass_count"`
Finished bool `json:"finished"`
Country string `json:"country"`
Province string `json:"province"`
City string `json:"city"`
}
func GetEventList(ctx context.Context, req *GetEventListRequest) (*GetEventListResponse, error) {
var resp api.Response[GetEventListResponse]
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)
if err != nil {
return nil, err
}
return &resp.Data, nil
}
================================================
FILE: mcp_server/internal/api/app/create_application.go
================================================
package app
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/internal/api"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
)
type CreateAppRequest struct {
ServerNames []string `json:"server_names"`
Ports []string `json:"ports"`
Upstreams []string `json:"upstreams"`
Comment string `json:"comment"`
}
// CreateApp Create new website or app
func CreateApp(ctx context.Context, req *CreateAppRequest) (int64, error) {
if req == nil {
return 0, errors.New("request is required")
}
var resp api.Response[int64]
err := api.Service().Post(ctx, "/api/open/site", req, &resp)
if err != nil {
return 0, errors.Wrap(err, "failed to create app")
}
if resp.Err != nil {
return 0, errors.New(resp.Msg)
}
return resp.Data, nil
}
================================================
FILE: mcp_server/internal/api/client.go
================================================
package api
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
// Client API client
type Client struct {
baseURL string
httpClient *http.Client
headers map[string]string
}
// ClientOption Client configuration options
type ClientOption func(*Client)
// WithTimeout Set timeout duration
func WithTimeout(timeout time.Duration) ClientOption {
return func(c *Client) {
c.httpClient.Timeout = timeout
}
}
// WithHeader Set request header
func WithHeader(key, value string) ClientOption {
return func(c *Client) {
c.headers[key] = value
}
}
// WithBaseURL Set base URL
func WithBaseURL(baseURL string) ClientOption {
return func(c *Client) {
c.baseURL = baseURL
}
}
// WithInsecureSkipVerify Set whether to skip certificate verification
func WithInsecureSkipVerify(skip bool) ClientOption {
return func(c *Client) {
if transport, ok := c.httpClient.Transport.(*http.Transport); ok {
if transport.TLSClientConfig == nil {
transport.TLSClientConfig = &tls.Config{}
}
transport.TLSClientConfig.InsecureSkipVerify = skip
}
}
}
// NewClient Create new API client
func NewClient(opts ...ClientOption) *Client {
transport := &http.Transport{
TLSClientConfig: &tls.Config{},
}
c := &Client{
httpClient: &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
},
headers: make(map[string]string),
}
for _, opt := range opts {
opt(c)
}
return c
}
// Request Send request
func (c *Client) Request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
reqURL := fmt.Sprintf("%s%s", c.baseURL, path)
var bodyReader io.Reader
if body != nil {
bodyBytes, err := json.Marshal(body)
if err != nil {
return errors.Wrap(err, "marshal request body failed")
}
bodyReader = bytes.NewReader(bodyBytes)
}
logger.With("url", reqURL).Debug("request url")
req, err := http.NewRequestWithContext(ctx, method, reqURL, bodyReader)
if err != nil {
return errors.Wrap(err, "create request failed")
}
// Set common headers
req.Header.Set("Content-Type", "application/json")
for k, v := range c.headers {
req.Header.Set(k, v)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return errors.Wrap(err, "send request failed")
}
defer resp.Body.Close()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "read response body failed")
}
// Check status code
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return errors.New(fmt.Sprintf("request failed with status %d: %s", resp.StatusCode, string(respBody)))
}
// Parse response
if result != nil {
if err := json.Unmarshal(respBody, result); err == nil {
return nil
}
var respData map[string]interface{}
if err := json.Unmarshal(respBody, &respData); err != nil {
return errors.Wrap(err, "unmarshal response failed")
}
if respData["err"] != nil || respData["msg"] != nil {
return errors.New(respData["msg"].(string))
}
}
return nil
}
// Get Send GET request
func (c *Client) Get(ctx context.Context, path string, result interface{}) error {
return c.Request(ctx, http.MethodGet, path, nil, result)
}
// Post Send POST request
func (c *Client) Post(ctx context.Context, path string, body interface{}, result interface{}) error {
return c.Request(ctx, http.MethodPost, path, body, result)
}
// Put Send PUT request
func (c *Client) Put(ctx context.Context, path string, body interface{}, result interface{}) error {
return c.Request(ctx, http.MethodPut, path, body, result)
}
// Delete Send DELETE request
func (c *Client) Delete(ctx context.Context, path string, result interface{}) error {
return c.Request(ctx, http.MethodDelete, path, nil, result)
}
================================================
FILE: mcp_server/internal/api/response.go
================================================
package api
// Response Common API response structure
type Response[T any] struct {
// Response data
Data T `json:"data"`
// Error message
Err any `json:"err"`
// Prompt message
Msg string `json:"msg"`
}
================================================
FILE: mcp_server/internal/api/rule/create_rule.go
================================================
package rule
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/internal/api"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
)
type CreateRuleRequest struct {
Name string `json:"name"`
IP []string `json:"ip"`
IsEnabled bool `json:"is_enabled"`
Pattern [][]api.Pattern `json:"pattern"`
Action int `json:"action"`
}
// CreateRule Create new rule
func CreateRule(ctx context.Context, req *CreateRuleRequest) (int64, error) {
if req == nil {
return 0, errors.New("request is required")
}
var resp api.Response[int64]
err := api.Service().Post(ctx, "/api/open/policy", req, &resp)
if err != nil {
return 0, errors.Wrap(err, "failed to create policy rule")
}
if resp.Err != nil {
return 0, errors.New(resp.Msg)
}
return resp.Data, nil
}
================================================
FILE: mcp_server/internal/api/service.go
================================================
package api
import (
"context"
"sync"
"time"
"github.com/chaitin/SafeLine/mcp_server/internal/config"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
// APIClient API client implementation
type APIClient struct {
client *Client
config *config.APIConfig
}
var (
instance *APIClient
once sync.Once
)
// Init Initialize API service
func Init(cfg *config.APIConfig) error {
var err error
once.Do(func() {
instance, err = newAPIClient(cfg)
if err != nil {
logger.With("error", err).Error("failed to initialize API service")
return
}
logger.Info("API service initialized successfully")
})
return err
}
// Service Get API service instance
func Service() *APIClient {
if instance == nil {
logger.Error("API service not initialized")
panic("API service not initialized")
}
return instance
}
// newAPIClient Create new API client
func newAPIClient(config *config.APIConfig) (*APIClient, error) {
if config == nil {
return nil, errors.New("config is required")
}
if config.BaseURL == "" {
return nil, errors.New("base_url is required")
}
timeout := 30
if config.Timeout > 0 {
timeout = config.Timeout
}
opts := []ClientOption{
WithBaseURL(config.BaseURL),
WithTimeout(time.Duration(timeout) * time.Second),
WithHeader("User-Agent", "SafeLine-MCP/1.0"),
WithInsecureSkipVerify(config.InsecureSkipVerify),
}
// If token is configured, add authentication header
if config.Token != "" {
opts = append(opts, WithHeader("X-SLCE-API-TOKEN", config.Token))
}
client := NewClient(opts...)
return &APIClient{
client: client,
config: config,
}, nil
}
// Post Send POST request
func (c *APIClient) Post(ctx context.Context, path string, body interface{}, result interface{}) error {
return c.client.Request(ctx, "POST", path, body, result)
}
// Get Send GET request
func (c *APIClient) Get(ctx context.Context, path string, result interface{}) error {
return c.client.Request(ctx, "GET", path, nil, result)
}
// Put Send PUT request
func (c *APIClient) Put(ctx context.Context, path string, body interface{}, result interface{}) error {
return c.client.Request(ctx, "PUT", path, body, result)
}
// Delete Send DELETE request
func (c *APIClient) Delete(ctx context.Context, path string, result interface{}) error {
return c.client.Request(ctx, "DELETE", path, nil, result)
}
================================================
FILE: mcp_server/internal/api/types.go
================================================
package api
type PolicyRuleAction int
const (
PolicyRuleActionAllow PolicyRuleAction = iota
PolicyRuleActionDeny
PolicyRuleActionMax
)
type Key = string
const (
KeySrcIP Key = "src_ip"
KeyURI Key = "uri"
KeyURINoQuery Key = "uri_no_query"
KeyHost Key = "host"
KeyMethod Key = "method"
KeyReqHeader Key = "req_header"
KeyReqBody Key = "req_body"
KeyGetParam Key = "get_param"
KeyPostParam Key = "post_param"
)
type Op = string
const (
OpEq Op = "eq" // equal
OpNotEq Op = "not_eq" // not equal
OpMatch Op = "match" // match
OpCIDR Op = "cidr" // cidr
OpHas Op = "has" // has
OpNotHas Op = "not_has" // not has
OpPrefix Op = "prefix" // prefix
OpRe Op = "re" // regex
OpIn Op = "in" // in
OpNotIn Op = "not_in" // not in
OpNotCIDR Op = "not_cidr" // not cidr
OpExist Op = "exist" // exist
OpNotExist Op = "not_exist" // not exist
OpGeoEq Op = "geo_eq" // geo equal
OpGeoNotEq Op = "geo_not_eq" // geo not equal
)
type Pattern struct {
K Key `json:"k"`
Op Op `json:"op"`
V []string `json:"v"`
SubK string `json:"sub_k"`
}
================================================
FILE: mcp_server/internal/config/config.go
================================================
package config
import (
"os"
"strconv"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
"gopkg.in/yaml.v3"
)
// Config Global configuration structure
type Config struct {
Server *ServerConfig `yaml:"server"`
Logger *LoggerConfig `yaml:"logger"`
API *APIConfig `yaml:"api"`
}
// APIConfig API configuration
type APIConfig struct {
// API base URL
BaseURL string `yaml:"base_url"`
// API token
Token string `yaml:"token"`
// API timeout
Timeout int `yaml:"timeout"`
// API debug mode
Debug bool `yaml:"debug"`
// API insecure skip verify
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
}
// ServerConfig Server configuration
type ServerConfig struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Port int `yaml:"port"`
Host string `yaml:"host"`
Secret string `yaml:"secret"`
}
// LoggerConfig Logger configuration
type LoggerConfig struct {
Level string `yaml:"level"`
FilePath string `yaml:"file_path"`
Console bool `yaml:"console"`
Caller bool `yaml:"caller"`
Development bool `yaml:"development"`
}
var config *Config
// getEnvString Get string value from environment variable, return default value if not exists
func getEnvString(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultValue
}
// getEnvInt Get integer value from environment variable, return default value if not exists or cannot be parsed
func getEnvInt(key string, defaultValue int) int {
if value, exists := os.LookupEnv(key); exists {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
// Load Load configuration file
func Load(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return errors.Wrap(err, "read config file failed")
}
config = &Config{}
if err := yaml.Unmarshal(data, config); err != nil {
return errors.Wrap(err, "unmarshal config failed")
}
// Override configuration from environment variables
if config.Server != nil {
config.Server.Host = getEnvString("LISTEN_ADDRESS", config.Server.Host)
config.Server.Port = getEnvInt("LISTEN_PORT", config.Server.Port)
}
if config.API != nil {
config.API.BaseURL = getEnvString("SAFELINE_ADDRESS", config.API.BaseURL)
config.API.Token = getEnvString("SAFELINE_API_TOKEN", config.API.Token)
}
return nil
}
// GetServer Get server configuration
func GetServer() *ServerConfig {
if config == nil {
return nil
}
return config.Server
}
// GetLogger Get logger configuration
func GetLogger() *LoggerConfig {
if config == nil {
return nil
}
return config.Logger
}
// GetAPI Get API configuration
func GetAPI() *APIConfig {
if config == nil {
return nil
}
return config.API
}
================================================
FILE: mcp_server/internal/tools/analyze/get_atttack_events.go
================================================
package analyze
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/internal/api/analyze"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
type GetAttackEventsParams struct {
IP string `json:"ip" desc:"ip" required:"false"`
Page int `json:"page" desc:"page" required:"false" default:"1"`
PageSize int `json:"page_size" desc:"page size" required:"false" default:"10"`
Start int64 `json:"start" desc:"start unix timestamp in milliseconds" required:"false"`
End int64 `json:"end" desc:"end unix timestamp in milliseconds" required:"false"`
}
type GetAttackEvents struct{}
func (t *GetAttackEvents) Name() string {
return "get_attack_events"
}
func (t *GetAttackEvents) Description() string {
return "get attack events"
}
func (t *GetAttackEvents) Validate(params GetAttackEventsParams) error {
return nil
}
func (t *GetAttackEvents) Execute(ctx context.Context, params GetAttackEventsParams) (analyze.GetEventListResponse, error) {
resp, err := analyze.GetEventList(ctx, &analyze.GetEventListRequest{
IP: params.IP,
PageSize: params.PageSize,
Page: params.Page,
Start: params.Start,
End: params.End,
})
if err != nil {
return analyze.GetEventListResponse{}, err
}
logger.With("total", resp.Total).Info("get attack events")
return *resp, nil
}
================================================
FILE: mcp_server/internal/tools/app/create_application.go
================================================
package app
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/internal/api/app"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
type CreateApp struct{}
type CreateAppParams struct {
ServerNames []string `json:"server_names" desc:"domain list" required:"true"`
Ports []string `json:"ports" desc:"port list" required:"true"`
Upstreams []string `json:"upstreams" desc:"upstream list" required:"true"`
}
func (t *CreateApp) Name() string {
return "create_http_application"
}
func (t *CreateApp) Description() string {
return "create a new website or app"
}
func (t *CreateApp) Validate(params CreateAppParams) error {
return nil
}
func (t *CreateApp) Execute(ctx context.Context, params CreateAppParams) (int64, error) {
id, err := app.CreateApp(ctx, &app.CreateAppRequest{
ServerNames: params.ServerNames,
Ports: params.Ports,
Upstreams: params.Upstreams,
})
if err != nil {
return 0, err
}
logger.Info("create app success", logger.Int64("id", id))
return id, nil
}
================================================
FILE: mcp_server/internal/tools/example.go
================================================
package tools
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
type CalculateSum struct{}
func (t *CalculateSum) Name() string {
return "calculate_sum"
}
func (t *CalculateSum) Description() string {
return "Add two numbers together"
}
type MyToolInput struct {
A int `json:"a" desc:"number a" required:"true"`
B int `json:"b" desc:"number b" required:"true"`
}
type MyToolOutput struct {
C int `json:"c"`
}
func (t *CalculateSum) Validate(params MyToolInput) error {
return nil
}
func (t *CalculateSum) Execute(ctx context.Context, params MyToolInput) (MyToolOutput, error) {
logger.With("a", params.A).
With("b", params.B).
Debug("Executing calculation")
result := MyToolOutput{
C: params.A + params.B,
}
logger.With("result", result.C).
Debug("Calculation completed")
return result, errors.New("test error")
}
================================================
FILE: mcp_server/internal/tools/init.go
================================================
package tools
import (
"github.com/chaitin/SafeLine/mcp_server/internal/tools/analyze"
"github.com/chaitin/SafeLine/mcp_server/internal/tools/app"
"github.com/chaitin/SafeLine/mcp_server/internal/tools/rule"
)
func init() {
// app
AppendTool(&app.CreateApp{})
// rule
AppendTool(&rule.CreateBlacklistRule{})
AppendTool(&rule.CreateWhitelistRule{})
// analyze
AppendTool(&analyze.GetAttackEvents{})
}
================================================
FILE: mcp_server/internal/tools/rule/create_blacklist_rule.go
================================================
package rule
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/internal/api"
"github.com/chaitin/SafeLine/mcp_server/internal/api/rule"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
type CreateBlacklistRule struct{}
type CreateBlacklistRuleParams struct {
Name string `json:"name" desc:"name" required:"true"`
IP []string `json:"ip" desc:"ip" required:"false"`
URINoQuery []string `json:"uri_no_query" desc:"uri_no_query" required:"false"`
}
func (t *CreateBlacklistRule) Name() string {
return "create_blacklist_rule"
}
func (t *CreateBlacklistRule) Description() string {
return "create a new blacklist rule"
}
func (t *CreateBlacklistRule) Validate(params CreateBlacklistRuleParams) error {
return nil
}
func (t *CreateBlacklistRule) Execute(ctx context.Context, params CreateBlacklistRuleParams) (int64, error) {
var pattern [][]api.Pattern
if len(params.IP) > 0 {
pattern = append(pattern, []api.Pattern{
{
K: api.KeySrcIP,
Op: api.OpEq,
V: params.IP,
SubK: "",
},
})
}
if len(params.URINoQuery) > 0 {
pattern = append(pattern, []api.Pattern{
{
K: api.KeyURINoQuery,
Op: api.OpEq,
V: params.URINoQuery,
SubK: "",
},
})
}
id, err := rule.CreateRule(ctx, &rule.CreateRuleRequest{
Name: params.Name,
IP: params.IP,
IsEnabled: true,
Action: int(api.PolicyRuleActionDeny),
Pattern: pattern,
})
if err != nil {
return 0, err
}
logger.With("id", id).Info("create blacklist rule success")
return id, nil
}
================================================
FILE: mcp_server/internal/tools/rule/create_whitelist_rule.go
================================================
package rule
import (
"context"
"github.com/chaitin/SafeLine/mcp_server/internal/api"
"github.com/chaitin/SafeLine/mcp_server/internal/api/rule"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
type CreateWhitelistRule struct{}
type CreateWhitelistRuleParams struct {
Name string `json:"name" desc:"name" required:"true"`
IP []string `json:"ip" desc:"ip" required:"false"`
URINoQuery []string `json:"uri_no_query" desc:"uri_no_query" required:"false"`
}
func (t *CreateWhitelistRule) Name() string {
return "create_whitelist_rule"
}
func (t *CreateWhitelistRule) Description() string {
return "create a new whitelist rule"
}
func (t *CreateWhitelistRule) Validate(params CreateWhitelistRuleParams) error {
return nil
}
func (t *CreateWhitelistRule) Execute(ctx context.Context, params CreateWhitelistRuleParams) (int64, error) {
var pattern [][]api.Pattern
if len(params.IP) > 0 {
pattern = append(pattern, []api.Pattern{
{
K: api.KeySrcIP,
Op: api.OpEq,
V: params.IP,
SubK: "",
},
})
}
if len(params.URINoQuery) > 0 {
pattern = append(pattern, []api.Pattern{
{
K: api.KeyURINoQuery,
Op: api.OpEq,
V: params.URINoQuery,
SubK: "",
},
})
}
id, err := rule.CreateRule(ctx, &rule.CreateRuleRequest{
Name: params.Name,
IP: params.IP,
IsEnabled: true,
Action: int(api.PolicyRuleActionAllow),
Pattern: pattern,
})
if err != nil {
return 0, err
}
logger.With("id", id).Info("create whitelist rule success")
return id, nil
}
================================================
FILE: mcp_server/internal/tools/tool.go
================================================
package tools
import (
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
"github.com/chaitin/SafeLine/mcp_server/pkg/mcp"
)
// By deferring the concretization of generic types to the Register method,
// we avoid type inference issues.
// Each Tool is wrapped in a toolWrapper that knows its concrete type,
// allowing correct passing of generic parameters during registration.
type ToolWrapper interface {
Register(s *mcp.MCPServer) error
}
var (
tools = []ToolWrapper{}
)
func AppendTool[T any, R any](tool ...mcp.Tool[T, R]) {
for _, t := range tool {
tools = append(tools, &toolWrapper[T, R]{tool: t})
}
}
func Tools() []ToolWrapper {
return tools
}
type toolWrapper[T any, R any] struct {
tool mcp.Tool[T, R]
}
func (w *toolWrapper[T, R]) Register(s *mcp.MCPServer) error {
logger.Info("Registering tool",
logger.String("name", w.tool.Name()),
logger.String("description", w.tool.Description()),
)
return mcp.RegisterTool(s, w.tool)
}
================================================
FILE: mcp_server/main.go
================================================
package main
import (
"flag"
"fmt"
"github.com/chaitin/SafeLine/mcp_server/internal/api"
"github.com/chaitin/SafeLine/mcp_server/internal/config"
"github.com/chaitin/SafeLine/mcp_server/internal/tools"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
"github.com/chaitin/SafeLine/mcp_server/pkg/mcp"
)
func main() {
configPath := flag.String("config", "config.yaml", "path to config file")
flag.Parse()
if err := config.Load(*configPath); err != nil {
panic(fmt.Errorf("failed to load config: %v", err))
}
logConfig := config.GetLogger()
if err := logger.Init(&logger.Config{
Level: logConfig.Level,
FilePath: logConfig.FilePath,
Console: logConfig.Console,
Caller: logConfig.Caller,
Development: logConfig.Development,
}); err != nil {
panic(fmt.Errorf("failed to init logger: %v", err))
}
logger.With("base_url", config.GetAPI().BaseURL).Info("Initializing API service...")
if err := api.Init(config.GetAPI()); err != nil {
panic(fmt.Errorf("failed to init API service: %v", err))
}
logger.Info("Starting MCP Server...")
serverConfig := config.GetServer()
s := mcp.NewMCPServer(
serverConfig.Name,
serverConfig.Version,
serverConfig.Secret,
)
logger.Info("Registering tools...")
for _, tool := range tools.Tools() {
if err := tool.Register(s); err != nil {
logger.With("error", err).
Error("Failed to register tool")
panic(err)
}
}
addr := fmt.Sprintf("%s:%d", serverConfig.Host, serverConfig.Port)
logger.With("addr", addr).Info("Starting server")
if err := s.Start(addr); err != nil {
logger.With("error", err).
Error("Server failed to start")
panic(err)
}
}
================================================
FILE: mcp_server/pkg/config/config.go
================================================
package config
import (
"os"
"gopkg.in/yaml.v3"
)
// Config Global configuration structure
type Config struct {
Server ServerConfig `yaml:"server"`
Logger LoggerConfig `yaml:"logger"`
}
// ServerConfig Server configuration
type ServerConfig struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Port int `yaml:"port"`
Host string `yaml:"host"`
Secret string `yaml:"secret"`
}
// LoggerConfig Logger configuration
type LoggerConfig struct {
Level string `yaml:"level"`
FilePath string `yaml:"file_path"`
Console bool `yaml:"console"`
Caller bool `yaml:"caller"`
Development bool `yaml:"development"`
}
var (
globalConfig *Config
)
// Load Load configuration from file
func Load(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return err
}
config := &Config{}
if err := yaml.Unmarshal(data, config); err != nil {
return err
}
globalConfig = config
return nil
}
// Get Get global configuration
func Get() *Config {
return globalConfig
}
// GetServer Get server configuration
func GetServer() ServerConfig {
if globalConfig == nil {
return ServerConfig{}
}
return globalConfig.Server
}
// GetLogger Get logger configuration
func GetLogger() LoggerConfig {
if globalConfig == nil {
return LoggerConfig{}
}
return globalConfig.Logger
}
================================================
FILE: mcp_server/pkg/errors/errors.go
================================================
package errors
import (
"errors"
"fmt"
"runtime"
"strings"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
)
var (
// Common errors
ErrInternal = New("internal error")
ErrInvalidParam = New("invalid parameter")
ErrNotFound = New("resource not found")
ErrUnauthorized = New("unauthorized")
ErrForbidden = New("forbidden")
ErrTimeout = New("timeout")
)
// Error Custom error structure
type Error struct {
err error
stack []string
msg string
location string
}
// Error Implement error interface
func (e *Error) Error() string {
if e.msg != "" {
return fmt.Sprintf("%s: %v (at %s)", e.msg, e.err, e.location)
}
return fmt.Sprintf("%v (at %s)", e.err, e.location)
}
// Unwrap Return original error
func (e *Error) Unwrap() error {
if e.err == nil {
return nil
}
if wrapped, ok := e.err.(*Error); ok {
return wrapped.Unwrap()
}
return e.err
}
// Stack Return error stack
func (e *Error) Stack() []string {
return e.stack
}
// Location Return error location
func (e *Error) Location() string {
return e.location
}
// getCallerLocation Get caller location
func getCallerLocation(skip int) string {
_, file, line, ok := runtime.Caller(skip)
if !ok {
return "unknown"
}
return fmt.Sprintf("%s:%d", file, line)
}
// WrapL Wrap error and print log
func WrapL(err error, msg string) error {
if err == nil {
return nil
}
// Get stack trace information
var stack []string
for i := 1; i < 32; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
fn := runtime.FuncForPC(pc)
if fn == nil {
break
}
name := fn.Name()
if strings.Contains(name, "runtime.") {
break
}
stack = append(stack, fmt.Sprintf("%s:%d", file, line))
}
wrappedErr := &Error{
err: err,
stack: stack,
msg: msg,
location: getCallerLocation(2),
}
// Print error information and stack using logger
logger.With("error", err).
With("location", wrappedErr.location).
With("stack", strings.Join(stack, "\n")).
Error(msg)
return wrappedErr
}
// Is Check error type
func Is(err, target error) bool {
return errors.Is(err, target)
}
// As Type assertion
func As(err error, target interface{}) bool {
return errors.As(err, target)
}
// Wrap Wrap error without printing log
func Wrap(err error, msg string) error {
if err == nil {
return nil
}
return &Error{
err: err,
msg: msg,
location: getCallerLocation(2),
}
}
// New Create new error
func New(text string) error {
return &Error{
err: errors.New(text),
location: getCallerLocation(2),
}
}
================================================
FILE: mcp_server/pkg/logger/field.go
================================================
package logger
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Field 日志字段
type Field = zapcore.Field
// String 创建字符串字段
func String(key string, val string) Field {
return zap.String(key, val)
}
// Int 创建整数字段
func Int(key string, val int) Field {
return zap.Int(key, val)
}
// Int64 创建 int64 字段
func Int64(key string, val int64) Field {
return zap.Int64(key, val)
}
// Float64 创建浮点数字段
func Float64(key string, val float64) Field {
return zap.Float64(key, val)
}
// Bool 创建布尔字段
func Bool(key string, val bool) Field {
return zap.Bool(key, val)
}
// Err 创建错误字段
func Err(err error) Field {
return zap.Error(err)
}
// Any 创建任意类型字段
func Any(key string, val interface{}) Field {
return zap.Any(key, val)
}
// Duration 创建时间段字段
func Duration(key string, val float64) Field {
return zap.Float64(key, val)
}
================================================
FILE: mcp_server/pkg/logger/logger.go
================================================
package logger
import (
"os"
"path/filepath"
"sync"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Logger 封装 zap.Logger
type Logger struct {
zl *zap.Logger
}
var (
defaultLogger *Logger
once sync.Once
)
// Config 日志配置
type Config struct {
// 日志级别
Level string `json:"level" yaml:"level"`
// 日志文件路径
FilePath string `json:"file_path" yaml:"file_path"`
// 是否输出到控制台
Console bool `json:"console" yaml:"console"`
// 是否记录调用者信息
Caller bool `json:"caller" yaml:"caller"`
// 是否使用开发模式(更详细的日志)
Development bool `json:"development" yaml:"development"`
}
// 默认配置
var defaultConfig = Config{
Level: "info",
FilePath: "logs/mcp.log",
Console: true,
Caller: true,
Development: false,
}
// Init 初始化日志系统
func Init(cfg *Config) error {
var err error
once.Do(func() {
if cfg == nil {
cfg = &defaultConfig
}
// 确保日志目录存在
if cfg.FilePath != "" {
dir := filepath.Dir(cfg.FilePath)
if err = os.MkdirAll(dir, 0755); err != nil {
return
}
}
// 配置编码器
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05.000"))
},
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
// 设置日志级别
var level zapcore.Level
if err = level.UnmarshalText([]byte(cfg.Level)); err != nil {
level = zapcore.InfoLevel
}
// 创建Core
var cores []zapcore.Core
// 文件输出
if cfg.FilePath != "" {
fileWriter, err := os.OpenFile(cfg.FilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return
}
cores = append(cores, zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(fileWriter),
level,
))
}
// 控制台输出
if cfg.Console {
cores = append(cores, zapcore.NewCore(
zapcore.NewConsoleEncoder(encoderConfig),
zapcore.AddSync(os.Stdout),
level,
))
}
// 创建Logger
core := zapcore.NewTee(cores...)
zl := zap.New(core)
// 是否记录调用者信息
if cfg.Caller {
zl = zl.WithOptions(zap.AddCaller())
}
// 是否使用开发模式
if cfg.Development {
zl = zl.WithOptions(zap.Development())
}
defaultLogger = &Logger{zl: zl}
})
return err
}
// GetLogger 获取日志实例
func GetLogger() *Logger {
if defaultLogger == nil {
Init(nil)
}
return defaultLogger
}
// With 创建带有字段的新Logger
func (l *Logger) With(key string, value interface{}) *Logger {
return &Logger{zl: l.zl.With(Any(key, value))}
}
// Debug level
func (l *Logger) Debug(msg string, fields ...Field) {
l.zl.Debug(msg, fields...)
}
// Info level
func (l *Logger) Info(msg string, fields ...Field) {
l.zl.Info(msg, fields...)
}
// Warn level
func (l *Logger) Warn(msg string, fields ...Field) {
l.zl.Warn(msg, fields...)
}
// Error level
func (l *Logger) Error(msg string, fields ...Field) {
l.zl.Error(msg, fields...)
}
// Fatal level
func (l *Logger) Fatal(msg string, fields ...Field) {
l.zl.Fatal(msg, fields...)
}
// 全局函数
// With 创建带有字段的新Logger
func With(key string, value interface{}) *Logger {
l := GetLogger()
// 为链式调用创建新的logger实例,并添加caller skip
return &Logger{zl: l.zl.WithOptions(zap.AddCallerSkip(1)).With(Any(key, value))}
}
// Debug level
func Debug(msg string, fields ...Field) {
l := GetLogger()
l.zl.WithOptions(zap.AddCallerSkip(1)).Debug(msg, fields...)
}
// Info level
func Info(msg string, fields ...Field) {
l := GetLogger()
l.zl.WithOptions(zap.AddCallerSkip(1)).Info(msg, fields...)
}
// Warn level
func Warn(msg string, fields ...Field) {
l := GetLogger()
l.zl.WithOptions(zap.AddCallerSkip(1)).Warn(msg, fields...)
}
// Error level
func Error(msg string, fields ...Field) {
l := GetLogger()
l.zl.WithOptions(zap.AddCallerSkip(1)).Error(msg, fields...)
}
// Fatal level
func Fatal(msg string, fields ...Field) {
l := GetLogger()
l.zl.WithOptions(zap.AddCallerSkip(1)).Fatal(msg, fields...)
}
================================================
FILE: mcp_server/pkg/mcp/mcp.go
================================================
package mcp
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/mcuadros/go-defaults"
)
type Tool[T any, R any] interface {
Name() string
Description() string
Execute(ctx context.Context, params T) (R, error)
Validate(params T) error
}
type SSEServer struct {
sse *server.SSEServer
secret string
}
func (s *SSEServer) Start(addr string) error {
srv := &http.Server{
Addr: addr,
Handler: s,
}
return srv.ListenAndServe()
}
func (s *SSEServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.secret == "" {
s.sse.ServeHTTP(w, r)
return
}
messagePath := s.sse.CompleteMessagePath()
if messagePath != "" && r.URL.Path == messagePath {
secret := r.Header.Get("Secret")
if secret != s.secret {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
s.sse.ServeHTTP(w, r)
}
type MCPServer struct {
server *server.MCPServer
sse *SSEServer
}
func NewMCPServer(name, version string, secret string) *MCPServer {
s := server.NewMCPServer(
name,
version,
server.WithLogging(),
)
return &MCPServer{
server: s,
sse: &SSEServer{sse: server.NewSSEServer(s), secret: secret},
}
}
func (s *MCPServer) Start(addr string) error {
return s.sse.Start(addr)
}
func handleToolCall[T any, R any](ctx context.Context, request mcp.CallToolRequest, tool Tool[T, R]) (result *mcp.CallToolResult, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
var raw []byte
raw, err = json.Marshal(request.Params.Arguments)
if err != nil {
return nil, errors.Wrap(err, "marshal arguments failed")
}
var params T
defaults.SetDefaults(¶ms)
if err = json.Unmarshal(raw, ¶ms); err != nil {
return nil, errors.Wrap(err, "unmarshal parameters failed")
}
if err = tool.Validate(params); err != nil {
return nil, err
}
var execResult R
execResult, err = tool.Execute(ctx, params)
if err != nil {
return nil, err
}
v := any(execResult)
switch v := v.(type) {
case string:
return mcp.NewToolResultText(v), nil
case []byte:
return mcp.NewToolResultText(string(v)), nil
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
return mcp.NewToolResultText(json.Number(fmt.Sprint(v)).String()), nil
case bool:
return mcp.NewToolResultText(strconv.FormatBool(v)), nil
default:
bytes, err := json.Marshal(v)
if err != nil {
return nil, errors.New("invalid result type")
}
return mcp.NewToolResultText(string(bytes)), nil
}
}
func RegisterTool[T any, R any](s *MCPServer, tool Tool[T, R]) error {
var v T
opts, err := SchemaToOptions(v)
if err != nil {
return err
}
opts = append(opts, mcp.WithDescription(tool.Description()))
t := mcp.NewTool(tool.Name(),
opts...,
)
s.server.AddTool(t, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
result, err := handleToolCall(ctx, request, tool)
if err != nil {
logger.With("error", err).Error("handle tool call failed")
if wrapped, ok := err.(*errors.Error); ok {
return nil, wrapped.Unwrap()
}
return nil, err
}
return result, nil
})
return nil
}
================================================
FILE: mcp_server/pkg/mcp/schema.go
================================================
package mcp
import (
"encoding/json"
"reflect"
"strconv"
"strings"
"github.com/mark3labs/mcp-go/mcp"
)
// SchemaToOptions Convert struct to MCP ToolOption list
func SchemaToOptions(schema any) ([]mcp.ToolOption, error) {
t := reflect.TypeOf(schema)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
var options []mcp.ToolOption
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" {
continue
}
desc := field.Tag.Get("desc")
required := field.Tag.Get("required") == "true"
enumTag := field.Tag.Get("enum")
defaultTag := field.Tag.Get("default")
minTag := field.Tag.Get("min")
maxTag := field.Tag.Get("max")
opts := []mcp.PropertyOption{}
if desc != "" {
opts = append(opts, mcp.Description(desc))
}
if required {
opts = append(opts, mcp.Required())
}
if enumTag != "" && field.Type.Kind() == reflect.String {
enumValues := strings.Split(enumTag, ",")
opts = append(opts, mcp.Enum(enumValues...))
}
switch field.Type.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:
if defaultTag != "" {
if defaultValue, err := strconv.Atoi(defaultTag); err == nil {
opts = append(opts, mcp.DefaultNumber(float64(defaultValue)))
}
}
if minTag != "" {
if minValue, err := strconv.Atoi(minTag); err == nil {
opts = append(opts, mcp.Min(float64(minValue)))
}
}
if maxTag != "" {
if maxValue, err := strconv.Atoi(maxTag); err == nil {
opts = append(opts, mcp.Max(float64(maxValue)))
}
}
options = append(options, mcp.WithNumber(jsonTag, opts...))
case reflect.Bool:
if defaultTag != "" {
if defaultValue, err := strconv.ParseBool(defaultTag); err == nil {
opts = append(opts, mcp.DefaultBool(defaultValue))
}
}
options = append(options, mcp.WithBoolean(jsonTag, opts...))
case reflect.String:
if defaultTag != "" {
opts = append(opts, mcp.DefaultString(defaultTag))
}
options = append(options, mcp.WithString(jsonTag, opts...))
case reflect.Struct:
subSchema := reflect.New(field.Type).Interface()
subOptions, err := SchemaToOptions(subSchema)
if err != nil {
return nil, err
}
// Create a temporary Tool to get JSON Schema of sub-struct
tempTool := mcp.NewTool("temp", subOptions...)
tempJSON, _ := tempTool.MarshalJSON()
var tempMap map[string]any
if err := json.Unmarshal(tempJSON, &tempMap); err != nil {
continue
}
// Extract properties from temporary Tool
if inputSchema, ok := tempMap["inputSchema"].(map[string]any); ok {
if properties, ok := inputSchema["properties"].(map[string]any); ok {
// Check if there are required fields
if required, ok := inputSchema["required"].([]any); ok {
// Add required field information to corresponding properties
for _, req := range required {
if reqStr, ok := req.(string); ok {
if prop, ok := properties[reqStr].(map[string]any); ok {
prop["required"] = true
}
}
}
}
opts = append(opts, mcp.Properties(properties))
}
}
options = append(options, mcp.WithObject(jsonTag, opts...))
case reflect.Slice:
elemType := field.Type.Elem()
var items map[string]any
switch elemType.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:
items = map[string]any{
"type": "number",
}
case reflect.Bool:
items = map[string]any{
"type": "boolean",
}
case reflect.String:
items = map[string]any{
"type": "string",
}
case reflect.Struct:
subSchema := reflect.New(elemType).Interface()
subOptions, err := SchemaToOptions(subSchema)
if err != nil {
return nil, err
}
// Create a temporary Tool to get JSON Schema of sub-struct
tempTool := mcp.NewTool("temp", subOptions...)
tempJSON, _ := tempTool.MarshalJSON()
var tempMap map[string]any
if err := json.Unmarshal(tempJSON, &tempMap); err != nil {
continue
}
// Extract properties from temporary Tool
if inputSchema, ok := tempMap["inputSchema"].(map[string]any); ok {
if properties, ok := inputSchema["properties"].(map[string]any); ok {
// Check if there are required fields
if required, ok := inputSchema["required"].([]any); ok {
// Add required field information to corresponding properties
for _, req := range required {
if reqStr, ok := req.(string); ok {
if prop, ok := properties[reqStr].(map[string]any); ok {
prop["required"] = true
}
}
}
}
items = map[string]any{
"type": "object",
"properties": properties,
}
}
}
}
opts = append(opts, mcp.Items(items))
options = append(options, mcp.WithArray(jsonTag, opts...))
}
}
return options, nil
}
================================================
FILE: mcp_server/pkg/mcp/schema_test.go
================================================
package mcp
import (
"reflect"
"testing"
"github.com/mark3labs/mcp-go/mcp"
)
func TestSchemaToOptions(t *testing.T) {
tests := []struct {
name string
args any
want mcp.Tool
}{
{
name: "test number",
args: struct {
A int `json:"a" desc:"number a" required:"true"`
}{},
want: mcp.NewTool("test number",
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a")),
),
},
{
name: "test number int64",
args: struct {
A int64 `json:"a" desc:"number a" required:"true"`
}{},
want: mcp.NewTool("test number int64",
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a")),
),
},
{
name: "test number float64",
args: struct {
A float64 `json:"a" desc:"number a" required:"true"`
}{},
want: mcp.NewTool("test number float64",
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a")),
),
},
{
name: "test number default",
args: struct {
A int `json:"a" desc:"number a" required:"true" default:"10"`
}{},
want: mcp.NewTool("test number default",
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a"), mcp.DefaultNumber(10)),
),
},
{
name: "test number min max",
args: struct {
A int `json:"a" desc:"number a" required:"true" min:"10" max:"20"`
}{},
want: mcp.NewTool("test number min max",
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a"), mcp.Min(10), mcp.Max(20)),
),
},
{
name: "test number optional",
args: struct {
A int `json:"a" desc:"number a"`
}{},
want: mcp.NewTool("test number optional",
mcp.WithNumber("a", mcp.Description("number a")),
),
},
{
name: "test boolean",
args: struct {
A bool `json:"a" desc:"boolean a" required:"true"`
}{},
want: mcp.NewTool("test boolean",
mcp.WithBoolean("a", mcp.Required(), mcp.Description("boolean a")),
),
},
{
name: "test string",
args: struct {
A string `json:"a" desc:"string a" required:"true"`
}{},
want: mcp.NewTool("test string",
mcp.WithString("a", mcp.Required(), mcp.Description("string a")),
),
},
{
name: "test string default",
args: struct {
A string `json:"a" desc:"string a" required:"true" default:"hello"`
}{},
want: mcp.NewTool("test string default",
mcp.WithString("a", mcp.Required(), mcp.Description("string a"), mcp.DefaultString("hello")),
),
},
{
name: "test string enum",
args: struct {
A string `json:"a" desc:"string a" required:"true" enum:"1,2,3"`
}{},
want: mcp.NewTool("test string enum",
mcp.WithString("a", mcp.Required(), mcp.Description("string a"), mcp.Enum("1", "2", "3")),
),
},
{
name: "test object",
args: struct {
A struct {
B int `json:"b" desc:"number b" required:"true"`
} `json:"a" desc:"object a" required:"true"`
}{},
want: mcp.NewTool("test object",
mcp.WithObject("a", mcp.Required(), mcp.Description("object a"),
mcp.Properties(map[string]any{
"b": map[string]any{
"type": "number",
"description": "number b",
"required": true,
},
}),
),
),
},
{
name: "test object optional",
args: struct {
A struct {
B int `json:"b" desc:"number b" required:"true"`
C int `json:"c" desc:"number c"`
} `json:"a" desc:"object a" required:"true"`
}{},
want: mcp.NewTool("test object optional",
mcp.WithObject("a", mcp.Required(), mcp.Description("object a"),
mcp.Properties(map[string]any{
"b": map[string]any{
"type": "number",
"description": "number b",
"required": true,
},
"c": map[string]any{
"type": "number",
"description": "number c",
},
}),
),
),
},
{
name: "test nested object",
args: struct {
A struct {
B struct {
C int `json:"c" desc:"number c" required:"true"`
} `json:"b" desc:"object b" required:"true"`
} `json:"a" desc:"object a" required:"true"`
}{},
want: mcp.NewTool("test nested object",
mcp.WithObject("a", mcp.Required(), mcp.Description("object a"),
mcp.Properties(map[string]any{
"b": map[string]any{
"type": "object",
"description": "object b",
"required": true,
"properties": map[string]any{
"c": map[string]any{
"type": "number",
"description": "number c",
"required": true,
},
},
},
}),
),
),
},
{
name: "test array",
args: struct {
A []int `json:"a" desc:"array a" required:"true"`
}{},
want: mcp.NewTool("test array",
mcp.WithArray("a", mcp.Required(), mcp.Description("array a"),
mcp.Items(map[string]any{
"type": "number",
}),
),
),
},
{
name: "test array of object",
args: struct {
A []struct {
B int `json:"b" desc:"number b" required:"true"`
} `json:"a" desc:"array of object a" required:"true"`
}{},
want: mcp.NewTool("test array of object",
mcp.WithArray("a", mcp.Required(), mcp.Description("array of object a"),
mcp.Items(map[string]any{
"type": "object",
"properties": map[string]any{
"b": map[string]any{
"type": "number",
"description": "number b",
"required": true,
},
},
}),
),
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := SchemaToOptions(tt.args)
if err != nil {
t.Errorf("SchemaToOptions() error = %v", err)
return
}
s1, _ := mcp.NewTool(tt.name, got...).MarshalJSON()
s2, _ := tt.want.MarshalJSON()
if !reflect.DeepEqual(s1, s2) {
t.Errorf("\n got %v\n want %v", string(s1), string(s2))
}
})
}
}
================================================
FILE: scripts/manage.py
================================================
#!/usr/bin/env python3
import argparse
import base64
import json
import shutil
import ssl
import sys
import datetime
import platform
import os
from urllib.request import urlopen, HTTPError
import re
import subprocess
import string
import time
import random
import socket
texts = {
'hello1': {
'en': 'SafeLine is a self-hosted WAF(Web Application Firewall) to protect your web apps from attacks and exploits.',
'zh': 'SafeLine,中文名 "雷池",是一款简单好用, 效果突出的 Web 应用防火墙(WAF),可以保护 Web 服务不受黑客攻击。'
},
'hello2': {
'en': 'A web application firewall helps protect web apps by filtering and monitoring HTTP traffic between a web application and the Internet. It typically protects web apps from attacks such as SQL injection, XSS, code injection, os command injection, CRLF injection, ldap injection, xpath injection, RCE, XXE, SSRF, path traversal, backdoor, bruteforce, http-flood, bot abused, among others.',
'zh': '雷池通过过滤和监控 Web 应用与互 联网之间的 HTTP 流量来保护 Web 服务。可以保护 Web 服务免受 SQL 注入、XSS 、 代码注入、命令注入、CRLF 注入、ldap 注入、xpath 注入、RCE、XXE、SSRF、路径遍历、后门、暴力破解、CC、爬虫 等攻击。'
},
'talking-group': {
'en': '\n'
'https://discord.gg/SVnZGzHFvn\n'
'\n'
'Join discord group for more informations of SafeLine by above address',
'zh': '\n'
'▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄\n'
'█ ▄▄▄▄▄ █▀ █▀▀██▀▄▀▀▄▀▄▀▄██ ▄▄▄▄▄ █\n'
'█ █ █ █▀ ▄ █▀▄▄▀▀ ▄█▄ ▀█ █ █ █\n'
'█ █▄▄▄█ █▀█ █▄█▄▀▀▄▀▄ ▀▀▄▄█ █▄▄▄█ █\n'
'█▄▄▄▄▄▄▄█▄█▄█ █▄▀ █ ▀▄▀ █▄█▄▄▄▄▄▄▄█\n'
'█▄ ▄▄ █▄▄ ▄█▄▄▄▄▀▄▀▀▄██ ▄▄▀▄█▄▀ ▀█\n'
'█▄ ▄▀▄ ▄▀▄ ▀ ▄█▀ ▀▄ █▀▀ ▀█▀▄██▄▀▄██\n'
'██ ▀▄█ ▄ ▄▄▀▄▀▀█▄▀▄▄▀▄▀▄ ▄ ▀▄▄▄█▀▀█\n'
'█ █▀▄▀ ▄▀▄▄▀█▀ ▄▄ █▄█▀▀▄▀▀█▄█▄█▀▄██\n'
'█ █ ▀ ▄▀▀ ██▄█▄▄▄▄▄▀▄▀▀▀▄▄▀█▄▀█ ▀█\n'
'█ █ ▀▄ ▄██▀▀ ▄█▀ ▀███▄ ▀▄▀▄▄ ▄▀▄██\n'
'█▀▄▄█ ▄▀▄▀ ▄▀▀▀▄▀▄▀ ▄▀▄ ▄▀ ▄▀█ ▀█\n'
'█ █ █ █▄▀ █▄█▀ ▄▄███▀▀▀▄█▀▄ ▀ ▀▄██\n'
'█▄███▄█▄▄▀▄ █▄█▄▄▄▄▀▀▄█▀▀ ▄▄▄ ▀█ █\n'
'█ ▄▄▄▄▄ █▄▀█ ▄█▀▄ █▀█▄ ▀ █▄█ ▀▄▀█\n'
'█ █ █ █ █▄▀▀▀▄▄▄▀▀▀▀▀▀ ▄▄ ▀█ █\n'
'█ █▄▄▄█ █ ▀█▀ ▄▄▄▄ ▀█ ▀▀▄▀ ▀▀ ▀███\n'
'█▄▄▄▄▄▄▄█▄▄██▄█▄▄█▄██▄██▄▄█▄▄█▄█▄██\n'
'\n'
'微信扫描上方二维码加入雷池项目讨论组'
},
'input-target-path': {
'en': 'Input the path to install %s',
'zh': '请输入%s 的安装目录'
},
'input-mgt-port': {
'en': 'Input the %s mgt port',
'zh': '请输入%s 的管理端口'
},
'python-version-too-low': {
'en': 'The Python version is too low, Python 3.5 above is required',
'zh': 'Python 版本过低, 脚本无法运行, 需要 Python 3.5 以上'
},
'not-a-tty': {
'en': 'Stdin is not a standard TTY',
'zh': '运行脚本的方式不对, STDIN 不是标准的 TTY'
},
'not-root': {
'en': 'Requires root privileges to run',
'zh': '需要 root 权限才能运行'
},
'not-linux': {
'en': '%s does not support %s OS yet',
'zh': '%s 暂时不支持 %s 操作系统'
},
'unsupported-arch': {
'en': '%s does not support %s processor yet',
'zh': '%s 暂时不支持 %s 处理器'
},
'prepare-to-install': {
'en': 'Will be going to installing %s for you.',
'zh': '即将为您安装%s'
},
'choice-action': {
'en': 'Choice what do you want to do',
'zh': '选择你要执行的动作'
},
'default-value': {
'en': 'Keep blank default to',
'zh': '留空则为默认值'
},
'ssse3-not-support': {
'en': 'SSSE3 instruction set not enabled in your CPU',
'zh': '当前 CPU 未启用 SSSE3 指令集'
},
'precheck-failed': {
'en': 'The environment does not meet the installation conditions of %s',
'zh': '当前环境不符合%s 的安装条件'
},
'precheck-passed': {
'en': 'Installation environment check passed',
'zh': '检查安装环境已完成'
},
'insufficient-memory': {
'en': 'Remaining memory is less than 1 GB',
'zh': '剩余内存不足 1 GB'
},
'docker-not-installed': {
'en': 'Running %s requires Docker, but Docker is not installed',
'zh': '运行%s 依赖 Docker, 但是 Docker 没安装'
},
'docker-compose-not-installed': {
'en': 'Running %s requires Docker Compose, but Docker Compose is not installed',
'zh': '运行%s 依赖 Docker Compose, 但是 Docker Compose 没安装'
},
'docker-version-too-low': {
'en': 'Docker version is too low, it does not match %s',
'zh': 'Docker 版本太低, 不满足%s 的安装需求'
},
'if-install-docker': {
'en': 'Do you want the latest version of Docker to be automatically installed for you?',
'zh': '是否需要为你自动安装 Docker 的最新版本'
},
'if-restart-docker': {
'en': 'Do you want to restart %s Docker container',
'zh': '是否需要重启%s 的容器'
},
'if-update-docker': {
'en': 'Do you want to update your Docker version?',
'zh': '是否需要为你自动更新 Docker 版本'
},
'install-docker-failed': {
'en': 'Failed to install Docker. Please try to install Docker manually before installing %s',
'zh': '安装 Docker 失败, 请尝试手动安装 Docker 后再来安装%s'
},
'install-docker': {
'en': 'Docker is being installed for you. It will take a few minutes. Please wait patiently.',
'zh': '正在为你安装 Docker, 需要几分钟时间, 请耐心等待'
},
'get-space-failed': {
'en': 'Unable to query disk capacity of "%s"',
'zh': '无法查询 "%s" 的磁盘容量'
},
'remain-disk-capacity': {
'en': 'Disk capacity of "%s" has %s avaiable',
'zh': '"%s" 路径有 %s 的空间可用'
},
'insufficient-disk-capacity': {
'en': 'Insufficient disk capacity of "%s", at least 5 GB is required to install %s',
'zh': '"%s" 的磁盘容量不足,安装%s 至少需要 5 GB'
},
'pg-pass-contains-invalid-char': {
'en': 'The POSTGRES_PASSWORD variable contains special characters. Please choose repair to reset password',
'zh': 'POSTGRES_PASSWORD 变量包含特殊字符, 请选择修复重置密码'
},
'invalid-path': {
'en': '"%s" is not a valid absolute path',
'zh': '"%s" 不是合法的绝对路径'
},
'path-exists': {
'en': '"%s" already exists, please select a new directory',
'zh': '路径 "%s" 已存在,请选择一个全新的目录'
},
'fail-to-parse-route': {
'en': 'Unable to parse /proc/net/route file',
'zh': '无法解析 /proc/net/route 文件'
},
'fail-to-download-compose': {
'en': 'Failed to download docker compose script',
'zh': '下载 docker compose 脚本失败'
},
'fail-to-create-dir': {
'en': 'Unable to create the "%s" directory',
'zh': '无法创建 "%s" 目录'
},
'docker-pull': {
'en': 'Pulling Docker image',
'zh': '正在拉取 Docker 镜像'
},
'try-another-image-source': {
'en': 'Try another image source',
'zh': '尝试使用其他镜像源'
},
'image-clean': {
'en': 'Cleaning Docker image',
"zh": '正在清理 Docker 镜像'
},
'update-config': {
'en': 'Updating .env configuration files',
'zh': '正在更新 .env 配置文件'
},
'download-compose': {
'en': 'Downloading the docker-compose.yaml file',
'zh': '正在下载 docker-compose.yaml 文件'
},
'fail-to-pull-image': {
'en': 'Failed to pull Docker image',
'zh': '拉取 Docker 镜像失败'
},
'docker-up': {
'en': 'Starting Docker containers',
'zh': '正在启动 Docker 容器'
},
'fail-to-up': {
'en': 'Failed to start Docker containers',
'zh': '启动 Docker 容器失败'
},
'fail-to-down': {
'en': 'Failed to stop Docker containers',
'zh': '停止 Docker 容器失败'
},
'install-finish': {
'en': '%s installation completed',
'zh': '%s 安装完成'
},
'upgrade-finish': {
'en': '%s upgrade completed',
'zh': '%s 升级完成'
},
'go-to-panel': {
'en': '%s management panel: https://%s:%s/',
'zh': '%s 管理面板: https://%s:%s/'
}
,'install': {
'en': 'INSTALL',
'zh': '安装'
},
'repair': {
'en': 'REPAIR',
'zh': '修复'
},
'uninstall': {
'en': 'UNINSTALL',
'zh': '卸载'
},
'upgrade': {
'en': 'UPGRADE',
'zh': '升级'
},
'backup': {
'en': 'BACKUP',
'zh': '备份'
},
'yes': {
'en': 'Yes',
'zh': '是'
},
'no': {
'en': 'No',
'zh': '否'
},
'fail-to-get-installed-dir': {
'en': 'Failed to get installed dir',
'zh': '未能找到安装目录',
},
'fail-to-connect-image-source': {
'en': 'Failed to connect image source',
'zh': '无法连接到任何镜像源'
},
'fail-to-connect-docker-source': {
'en': 'Failed to connect docker source',
'zh': '无法连接到任何 docker 源'
},
'fail-to-download-docker-installation': {
'en': 'Failed to download docker installation',
"zh": '下载 docker 安装脚本失败'
},
'docker-source': {
'en': 'Docker source',
"zh": 'docker 源'
},
'reset-admin': {
'en': 'Setup admin',
"zh": '设置 admin'
},
'docker-version': {
'en': 'Checking docker version',
'zh': '检查 docker 版本'
},
'docker-compose-version': {
'en': 'Checking docker compose version',
'zh': '检查 docker compose 版本'
},
'keyboard-interrupt': {
'en': 'Installation cancelled',
"zh": '取消安装'
},
'docker-up-iptables-failed': {
'en': 'Iptables policy error, try to restart docker',
'zh': 'iptables 规则错误,尝试重启 docker'
},
'install-channel': {
'en': 'Installing: %s',
'zh': '安装通道:%s'
},
'preview-release': {
'en': 'Preview',
'zh': '预览版'
},
'lts-release': {
'en': 'LTS',
'zh': 'LTS 版'
},
'fail-to-docker-down': {
'en': 'Failed to stop container',
'zh': '停止 docker 容器失败'
},
'fail-to-remove-dir': {
'en': 'Failed to remove %s installation directory',
'zh': '删除 %s 安装目录失败'
},
'uninstall-finish': {
'en': '%s uninstall completed',
'zh': '%s 卸载完成'
},
'docker-down': {
'en': 'Stopping %s container',
'zh': '正在停止%s 容器'
},
'reset-tengine': {
'en': 'RESET TENGINE CONFIG',
'zh': '重置 tengine 配置',
},
'reset-postgres': {
'en': 'RESET DATABASE PASSWORD',
'zh': '重置数据库密码'
},
'fail-to-find-nginx': {
'en': 'Failed to find tengine config path',
'zh': '未找到 tengine 配置目录'
},
'nginx-backup-dir': {
'en': 'Tengine config backup directory',
'zh': 'tengine 配置备份目录'
},
'fail-to-backup-nginx': {
'en': 'Failed to backup tengine config',
'zh': '备份 tengine 目录失败'
},
'docker-restart': {
'en': 'Restart docker container',
'zh': '重启 docker 容器'
},
'docker-exec': {
'en': 'Executing docker command',
'zh': '执行 docker 命令'
},
'fail-to-recover-static': {
'en': 'Failed to recover tengine static config',
'zh': '恢复 tengine 静态站点资源失败'
},
'fail-to-find-env': {
'en': 'Failed to find .env file',
'zh': '未找到 .env 文件'
},
'fail-to-find-postgres-password': {
'en': 'Failed to find postgres password',
'zh': '未找到数据库密码'
},
'fail-to-reset-postgres-password': {
'en': 'Failed to reset postgres password',
'zh': '重置数据库密码失败'
},
'reset-postgres-password-finish': {
'en': 'Reset postgres password completed',
'zh': '重置数据库密码完成'
},
'reset-tengine-finish': {
'en': 'Reset tengine finish completed',
'zh': '重置 tengine 配置完成'
},
'if-remove-waf': {
'en': 'Do you want to uninstall %s, this operation will delete all data in the directory: %s',
'zh': '是否确认卸载%s,该操作会删除目录下所有数据:%s'
},
'restart-docker-finish': {
'en': 'Restart %s docker container completed',
'zh': '重启%s 容器完成'
},
'restart': {
'en': 'RESTART',
'zh': '重启'
},
'wait-mgt-health': {
'en': 'Wait for mgt healthy',
'zh': '等待 mgt 启动'
},
'if-remove-ipv6-scope': {
'en': '/etc/resolv.conf have ipv6 nameserver with scope. Do you want to remove these nameserver',
'zh': '/etc/resolv.conf 文件中存在 ipv6 地址包含区域 ID,是否删除包含区域 ID 的 ipv6 nameserver'
},
'input-target-version': {
'en': 'Input the %s version',
'zh': '请输入 %s 的版本'
},
'version-format-error': {
'en': 'version %s format error',
'zh': '版本 %s 格式错误'
},
'can-not-downgrade': {
'en': '%s can not downgrade %s to %s',
'zh': '%s 不支持从 %s 降级到 %s'
},
'get-version': {
'en': 'Getting %s latest version',
'zh': '正在获取%s 最新版本'
},
'get-version-from-mgt': {
'en': 'try to get version from mgt',
'zh': '尝试从 mgt 获取安装版本'
},
'skip-version-compare': {
'en': 'skip %s version compare',
'zh': '跳过%s 版本匹配'
},
'fail-to-get-version-from-mgt': {
'en': 'failed to get install version from mgt',
'zh': '从 mgt 获取安装版本失败'
},
'target-version': {
'en': 'target version: %s',
'zh': '目标版本:%s'
},
'fail-to-parse-assets': {
'en': 'failed to parse assets',
'zh': '解析补丁包失败'
},
'snap-docker-should-use-home-path': {
'en': 'Docker installed via snap can only be configured to set the installation directory under the user\'s home directory(%s)',
'zh': 'snap 安装的 docker 只能设置安装目录在用户的主目录(%s)下'
},
'fail-to-get-docker-path': {
'en': 'find docker binary path failed',
'zh': '获取 docker 二进制目录失败'
}
}
BOLD = 1
DIM = 1
BLINK = 5
REVERSE = 7
RED = 31
GREEN = 32
YELLOW = 33
BLUE = 34
CYAN = 36
INSTALL = False
DOMAIN = 'waf-ce.chaitin.cn'
REQUEST_CTX = None
LANG = 'zh'
PRODUCT = ''
DEBUG = False
SELF = True
def parse_assets(args):
global PRODUCT, SELF
if args.patch == '':
return True
assets = ''
with open(args.patch, 'r') as f:
for line in f.readlines():
assets += line
split_assets = assets.strip().split('.')
if len(split_assets) != 3:
return False
try:
assets_info = json.loads(base64.b64decode(split_assets[1].replace('_','/').replace('-','+') + '=' * ((4 - len(split_assets[1]) % 4) % 4)))
if args.en:
PRODUCT = assets_info['fullname_en']
else:
PRODUCT = assets_info['fullname']
if PRODUCT == '':
return False
if assets_info['self'] is None:
return False
if not assets_info['self']:
SELF = False
except Exception as e:
log.debug('parse assets failed: ' + str(e))
return False
return True
def init_global_config():
global REQUEST_CTX, LANG, DOMAIN, PRODUCT, DEBUG
REQUEST_CTX = ssl.create_default_context()
REQUEST_CTX.check_hostname = False
REQUEST_CTX.verify_mode = ssl.CERT_NONE
parser = argparse.ArgumentParser(
prog='installer-management',
description='installer-management',
allow_abbrev=False
)
parser.add_argument('--debug', action='store_true', help='install with debug log')
parser.add_argument("--lts", action='store_true', help='install lts version')
parser.add_argument('--image-clean', action='store_true', help='clean image when upgrade done')
parser.add_argument('--en', action='store_true', help='install international version')
parser.add_argument('--patch', default='', type=str, help='patch path')
args = parser.parse_args()
if args.en:
LANG = 'en'
DOMAIN = 'waf.chaitin.com'
PRODUCT = 'SafeLine WAF'
else:
PRODUCT = '雷池 WAF'
if args.debug:
DEBUG = True
if args.patch != '' and not os.path.exists(args.patch):
log.fatal('assets %s not exists' % args.patch)
if not parse_assets(args):
log.fatal(text('fail-to-parse-assets'))
return args
class log():
@staticmethod
def _log(c, l, s):
t = datetime.datetime.now().strftime('%H:%M:%S')
print('\r\033[0;%dm[%-5s %s]: %s\033[0m' % (c, l, t, s))
@staticmethod
def debug(s):
if DEBUG:
log._log(DIM, 'DEBUG', s)
@staticmethod
def info(s):
log._log(CYAN, 'INFO', s)
@staticmethod
def warning(s):
log._log(YELLOW, 'WARN', s)
@staticmethod
def error(s):
log._log(RED, 'ERROR', s)
@staticmethod
def fatal(s):
log._log(RED, 'ERROR', s)
sys.exit(1)
def text(label, var=()):
t = texts.get(label, {
'en': 'Unknown "%s" (%s)' % (label, var),
'zh': '未知变量 "%s" (%s)' % (label, var)
})
return t[LANG if LANG in t else 'en'] % var
def color(t, attrs=[], end=True):
t = '\x1B[%sm%s' % (';'.join([str(i) for i in attrs]), t)
if end:
t = t + '\x1B[m'
return t
GLOBAL_ARGS = init_global_config()
def banner():
t = r'''
______ ___ _____ _ ____ ____ _ ________
.' ____ \ .' ..] |_ _| (_) |_ _| |_ _| / \ |_ __ |
| (___ \_| ,--. _| |_ .---. | | __ _ .--. .---. \ \ /\ / / / _ \ | |_ \_|
_.____`. `'_\ : '-| |-' / /__\\ | | _ [ | [ `.-. | / /__\\ \ \/ \/ / / ___ \ | _|
| \____) | // | |, | | | \__., _| |__/ | | | | | | | | \__., \ /\ / _/ / \ \_ _| |_
\______.' \'-;__/ [___] '.__.' |________| [___] [___||__] '.__.' \/ \/ |____| |____| |_____|
'''.strip('\n')
print(color(t + '\n', [GREEN, BLINK]))
def get_url(url):
try:
response = urlopen(url, timeout=10, context=REQUEST_CTX)
content = response.read()
return content.decode('utf-8')
except Exception as e:
log.error('get url %s failed: %s' % (url, str(e)))
def ui_read(question, default):
while True:
if default is None:
sys.stdout.write('%s: ' % (
color(question, [GREEN]),
))
else:
sys.stdout.write('%s %s: ' % (
color(question, [GREEN]),
color('(%s %s)' % (text('default-value'), default), [YELLOW]),
))
r = input().strip()
if len(r) == 0:
if default is None or len(default) == 0:
continue
r = default
return r
def ui_choice(question, options):
while True:
s_options = '[ %s ]' % ' '.join(['%s.%s' % option for option in options])
s_choices = '(%s)' % '/'.join([option[0] for option in options])
sys.stdout.write('%s %s %s: ' % (color(question, [GREEN]), color(s_options, [YELLOW]), color(s_choices, [YELLOW])))
r = input().strip()
if r in [i[0] for i in options]:
return r
def humen_size(x):
if x >= 1024 * 1024 * 1024 * 1024:
return '%.02f TB' % (x / 1024 / 1024 / 1024 / 1024)
elif x >= 1024 * 1024 * 1024:
return '%.02f GB' % (x / 1024 / 1024 / 1024)
elif x >= 1024 * 1024:
return '%.02f MB' % (x / 1024 / 1024)
elif x >= 1024:
return '%.02f KB' % (x / 1024)
else:
return '%d Bytes'
def rand_subnet():
routes = []
try:
with open('/proc/net/route', 'r') as f:
next(f)
for line in f:
parts = line.split()
if len(parts) < 8:
continue
destination = int(parts[1], 0x10)
if destination == 0:
continue
mask = int(parts[7], 0x10)
routes.append((destination, mask))
except Exception as e:
log.error(text('fail-to-parse-route') + ' ' + str(e))
for i in range(256):
t = 192
t += 168 << 8
t += i << 16
for route in routes:
if t & route[1] == route[0]:
break
else:
return '%d.%d.%d' % (t & 0xFF, (t >> 8) & 0xFF, (t >> 16) & 0xFF)
return '172.22.222'
def free_space(path):
while not os.path.exists(path) and path != '/':
path = os.path.dirname(path)
try:
st = os.statvfs(path)
free_bytes = st.f_bavail * st.f_frsize
return free_bytes
except Exception as e:
log.error(text('get-space-failed', path) + ' ' + str(e))
return None
def free_memory():
t = filter(lambda x: 'MemAvailable' in x, open('/proc/meminfo', 'r').readlines())
return int(next(t).split()[1]) * 1024
def exec_command(*args,shell=False):
try:
proc = subprocess.run(args, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True,shell=shell)
subprocess_output(proc.stdout.strip())
return proc.returncode, proc.stdout, proc.stderr
except Exception as e:
return -1, '', str(e)
def exec_command_with_loading(*args, cwd=None, env=None):
try:
with subprocess.Popen(args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, env=env, cwd=cwd) as proc:
if not DEBUG:
loading = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]
iloading = 0
while proc.poll() is None:
sys.stderr.write('\r' + loading[iloading])
sys.stderr.flush()
iloading = (iloading + 1) % len(loading)
time.sleep(0.1)
sys.stderr.write('\r')
sys.stderr.flush()
else:
for line in iter(proc.stdout.readline, b''):
if line.strip() != '':
log.debug(" -->> "+line.strip())
if proc.poll() is not None and line == '':
break
return proc.returncode, proc.stdout.read(), proc.stderr.read()
except Exception as e:
return -1, '', str(e)
def subprocess_output(stdout):
if stdout != '':
log.debug(" -->> "+stdout)
else:
log.debug(" -->> subprocess empty output")
def start_docker():
return exec_command('systemctl enable docker && systemctl daemon-reload && systemctl restart docker',shell=True)
def check_port(port):
if not INSTALL:
return True
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('0.0.0.0', int(port)))
return True
except Exception as e:
log.debug("try listen mgt port failed: "+str(e))
return False
def install_docker():
log.info(text('install-docker'))
log.debug("downloading get-docker.sh")
if not save_file_from_url('https://'+DOMAIN+'/release/latest/get-docker.sh','get-docker.sh'):
raise Exception(text('fail-to-download-docker-installation'))
source = docker_source()
if source == '':
raise Exception(text('fail-to-connect-docker-source'))
log.debug(text('docker-source')+': '+source)
env = {
"DOWNLOAD_URL": source,
"https_proxy": os.environ.get('https_proxy',''),
}
log.debug("installing docker")
r = exec_command_with_loading('bash get-docker.sh',env=env)
if r[0] == 0:
p = start_docker()
subprocess_output(p[1].strip())
if p[0] != 0:
log.error("start docker failed: "+p[2].strip())
return p[0] == 0
else:
log.error("install docker error: "+r[2].strip())
return False
compose_command = ''
def precheck_docker_compose():
log.info(text("docker-compose-version"))
global compose_command
while True:
version_output = ''
proc = exec_command('docker', 'compose', 'version')
if proc[0] == 0:
help_proc = exec_command('docker', 'compose', 'up', '--help')
if help_proc[0] == 0 and '--detach' in help_proc[1]:
compose_command = 'docker compose -f docker-compose.yaml'
version_output = proc[1].strip()
else:
log.debug('docker compose can not find detach argument')
else:
compose_proc = exec_command('docker-compose', 'version')
if compose_proc[0] == 0:
help_proc = exec_command('docker-compose', 'up', '--help')
if help_proc[0] == 0 and '--detach' in help_proc[1]:
compose_command = 'docker-compose -f docker-compose.yaml'
version_output = compose_proc[1].strip()
else:
log.debug('docker-compose can not find detach argument')
else:
log.warning(text('docker-compose-not-installed', PRODUCT))
if version_output != '':
t = re.findall(r'^Docker Compose version v?(\d+)\.', version_output)
if len(t) == 0:
log.warning(text('docker-compose-not-installed', PRODUCT))
elif int(t[0]) < 2:
log.warning(text('docker-version-too-low', PRODUCT))
else:
return True
action = ui_choice(text('if-update-docker'), [
('y', text('yes')),
('n', text('no')),
])
if action.lower() == 'n':
return False
elif action.lower() == 'y':
if not install_docker():
log.warning(text('install-docker-failed', PRODUCT))
return False
def precheck_dns_scope():
resolve_file = '/etc/resolv.conf'
if not os.path.exists(resolve_file):
return True
have_scope = False
raw_lines = []
with open(resolve_file, 'r') as f:
for line in f.readlines():
strip_line = line.strip()
if not strip_line.startswith('nameserver') or '%' not in strip_line.lstrip('nameserver').strip():
raw_lines.append(line)
continue
have_scope = True
if not have_scope:
return True
action = ui_choice(text('if-remove-ipv6-scope'),[
('y', text('yes')),
('n', text('no')),
])
if action.lower() != 'y':
return False
shutil.copyfile(resolve_file, resolve_file+'.bak')
with open(resolve_file, 'w') as f:
for line in raw_lines:
f.write(line)
return True
def precheck():
if platform.machine() in ('x86_64', 'AMD64') and 'ssse3' not in open('/proc/cpuinfo', 'r').read().lower():
log.warning(text('ssse3-not-support'))
return False
if free_memory() < 1024 * 1024 * 1024:
log.warning(text('insufficient-memory'))
return False
log.info(text("docker-version"))
while True:
proc = exec_command('docker', '--version')
if proc[0] == 0:
t = re.findall(r'^Docker version (\d+)\.', proc[1])
if len(t) == 0:
log.warning(text('docker-not-installed', PRODUCT))
elif int(t[0]) < 20:
log.warning(text('docker-version-too-low', PRODUCT))
else:
break
else:
log.warning(text('docker-not-installed', PRODUCT))
action = ui_choice(text('if-install-docker'), [
('y', text('yes')),
('n', text('no')),
])
if action.lower() == 'n':
return False
elif action.lower() == 'y':
if not install_docker():
log.warning(text('install-docker-failed', PRODUCT))
return False
if not precheck_docker_compose():
return False
if not precheck_dns_scope():
return False
return True
def docker_pull(cwd):
log.info(text('docker-pull'))
try:
subprocess.check_call(compose_command+' pull', cwd=cwd, shell=True)
return True
except Exception as e:
log.warning("docker pull error: "+str(e))
return False
def docker_restart(container):
log.info(text('docker-restart')+": "+container)
try:
subprocess.check_call('docker restart '+container, shell=True)
return True
except Exception as e:
log.error("docker restart error: "+str(e))
return False
def docker_exec(container, command):
log.info(text('docker-exec')+": ("+container+") "+command)
try:
subprocess.check_call('docker exec '+container+' '+command, shell=True)
return True
except Exception as e:
log.error("docker exec error: "+str(e))
return False
def image_clean():
log.info(text('image-clean'))
proc = exec_command('docker image prune -f --filter="label=maintainer=SafeLine-CE"', shell=True)
if proc[0] != 0:
log.warning("remove docker image failed: "+proc[2])
def docker_up(cwd):
log.info(text('docker-up'))
while True:
p = exec_command_with_loading(compose_command+' up -d --remove-orphans', cwd=cwd)
if p[0] == 0:
return True
if 'iptables failed' in p[2]:
log.warning("docker up error: "+p[2])
while True:
action = ui_choice(text('docker-up-iptables-failed'),[
('y', text('yes')),
('n', text('no')),
])
if action.lower() == 'y':
start_docker()
break
elif action.lower() == 'n':
return False
else:
log.error("docker up error: "+p[2])
return False
def docker_down(cwd):
log.info(text('docker-down', PRODUCT))
try:
subprocess.check_call(compose_command+' down', cwd=cwd, shell=True)
return True
except Exception:
return False
def get_url_time(url):
now = datetime.datetime.now()
try:
urlopen(url, timeout=10, context=REQUEST_CTX)
except HTTPError as e:
log.debug("get url "+url+" status: "+str(e.status))
if e.status > 499:
return 999999
except Exception as e:
log.debug("get url "+url+" failed: "+str(e))
return 999999
return (datetime.datetime.now() - now).microseconds / 1000
def get_avg_delay(url):
log.debug("test url avg delay: "+url)
total_delay = 0
for i in range(3):
total_delay += get_url_time(url)
avg_delay = total_delay / 3
log.debug("url "+url+" avg delay: "+str(avg_delay))
return avg_delay
pull_failed_prefix = []
def image_source():
source = {
'https://registry-1.docker.io': 'chaitin',
"https://swr.cn-east-3.myhuaweicloud.com": 'swr.cn-east-3.myhuaweicloud.com/chaitin-safeline'
}
min_delay = -1
image_prefix = ''
for url, prefix in source.items():
if prefix in pull_failed_prefix:
continue
delay = get_avg_delay(url)
if delay > 0 and (min_delay < 0 or delay < min_delay):
min_delay = delay
image_prefix = prefix
log.debug("use image_prefix: "+image_prefix)
return image_prefix
def docker_source():
sources = [
"https://mirrors.aliyun.com/docker-ce/",
"https://mirrors.tencent.com/docker-ce/",
"https://download.docker.com"
]
min_delay = -1
source = ''
for v in sources:
delay = get_avg_delay(v)
if delay > 0 and (min_delay < 0 or delay < min_delay):
min_delay = delay
source = v
return source
def read_config(path,config):
with open(path, 'r') as f:
for line in f.readlines():
if line.strip() == '':
continue
try:
s = line.index('=')
if s > 0:
k = line[:s].strip()
v = line[s + 1:].strip()
config[k] = v
except ValueError:
continue
def write_config(path,config):
with open(path, 'w') as f:
for k in config:
f.write('%s=%s\n' % (k, config[k]))
def generate_config(path):
log.info(text('update-config'))
config = {
'SAFELINE_DIR': path,
'POSTGRES_PASSWORD': '',
'MGT_PORT': '',
'RELEASE': '',
'CHANNEL': '',
'REGION': '',
'IMAGE_PREFIX': '',
'IMAGE_TAG': '',
'SUBNET_PREFIX': '',
'ARCH_SUFFIX': ''
}
env_path = os.path.join(path,'.env')
if os.path.exists(env_path):
read_config(env_path,config)
if config['ARCH_SUFFIX'] == '':
if platform.machine() == 'aarch64':
config['ARCH_SUFFIX'] = '-arm'
if config['POSTGRES_PASSWORD'] == '':
config['POSTGRES_PASSWORD'] = ''.join([random.choice(string.ascii_letters + string.digits) for i in range(20)])
if config['SUBNET_PREFIX'] == '':
config['SUBNET_PREFIX'] = rand_subnet()
if config['RELEASE'] == '' and GLOBAL_ARGS.lts:
config['RELEASE'] = '-lts'
config['CHANNEL'] = '-lts'
if config['RELEASE'] == '-lts':
GLOBAL_ARGS.lts = True
default_try = False
if config['MGT_PORT'] == '9443':
default_try = True
while not config['MGT_PORT'].isnumeric() or int(config['MGT_PORT']) >= 65536 or int(config['MGT_PORT']) <= 0 or not check_port(config['MGT_PORT']):
if not default_try:
config['MGT_PORT'] = '9443'
default_try = True
else:
config['MGT_PORT'] = ui_read(text('input-mgt-port', PRODUCT),None)
if config['REGION'] == '' and GLOBAL_ARGS.en:
config['REGION'] = '-g'
if not config['POSTGRES_PASSWORD'].isalnum():
log.info(text('pg-pass-contains-invalid-char'))
raise Exception(text('pg-pass-contains-invalid-char'))
if config['IMAGE_PREFIX'] == '' or config['IMAGE_PREFIX'] in pull_failed_prefix:
config['IMAGE_PREFIX'] = image_source()
if config['IMAGE_PREFIX'] == '':
raise Exception(text('fail-to-connect-image-source'))
config['IMAGE_TAG'] = get_version(config['IMAGE_TAG'])
log.info(text('target-version', config['IMAGE_TAG']))
write_config(env_path, config)
return config
def show_address(mgt_port):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0]
log.info(text('go-to-panel', (PRODUCT, local_ip, mgt_port)))
log.info(text('go-to-panel', (PRODUCT, '0.0.0.0', mgt_port)))
def init_mgt():
while True:
p = exec_command('docker', 'inspect','--format=\'{{.State.Health.Status}}\'', 'safeline-mgt')
if p[0] == 0 and p[1].strip().replace("'",'') == 'healthy':
break
elif p[0] != 0:
log.debug("get safeline-mgt status error: "+str(p[2]))
log.info(text('wait-mgt-health'))
time.sleep(5)
log.info(text('reset-admin'))
proc = exec_command('docker exec safeline-mgt /app/mgt-cli reset-admin --once',shell=True)
if proc[0] != 0:
log.warning(proc[2])
elif proc[1].strip() != '':
log.info('\n'+proc[1].strip())
def check_install_path(safeline_path):
if not safeline_path.startswith('/'):
return False
proc = exec_command('which', 'docker')
if proc[0] != 0:
log.debug('get docker path failed: '+proc[2])
raise Exception(text('fail-to-get-docker-path'))
if not proc[1].startswith('/snap'):
return True
home_path = os.path.expanduser('~')
if not safeline_path.startswith(home_path):
log.warning(text('snap-docker-should-use-home-path', home_path))
return False
return True
def install():
global INSTALL
INSTALL = True
log.info(text('prepare-to-install', PRODUCT))
if not precheck():
log.error(text('precheck-failed', PRODUCT))
return
log.info(text('precheck-passed'))
default_path = '/data/safeline'
if not SELF:
default_path = '/data/waf'
while True:
safeline_path = ui_read(text('input-target-path', PRODUCT), default_path)
if not check_install_path(safeline_path):
log.warning(text('invalid-path', safeline_path))
continue
if os.path.exists(safeline_path):
log.warning(text('path-exists', safeline_path))
continue
if free_space(safeline_path) < 5 * 1024 * 1024 * 1024:
log.warning(text('insufficient-disk-capacity', (safeline_path, PRODUCT)))
continue
break
try:
os.makedirs(safeline_path)
except Exception as e:
log.error(text('fail-to-create-dir', safeline_path) + ' ' + str(e))
return
mgt_path = os.path.join(safeline_path,'resources','mgt')
try:
os.makedirs(mgt_path, exist_ok=True)
except Exception as e:
log.error(text('fail-to-create-dir', mgt_path) + ' ' + str(e))
return
if GLOBAL_ARGS.patch != '':
shutil.copyfile(GLOBAL_ARGS.patch, os.path.join(mgt_path, 'product.data'))
log.info(text('remain-disk-capacity', (safeline_path, humen_size(free_space(safeline_path)))))
log.info(text('download-compose'))
if not save_file_from_url('https://'+DOMAIN+'/release/latest/compose.yaml',os.path.join(safeline_path, 'docker-compose.yaml')):
log.error(text('fail-to-download-compose'))
return
rename_file(os.path.join(safeline_path, 'compose.yaml'),os.path.join(safeline_path, 'compose.yaml.bak'))
mgt_port = generate_config_and_run(safeline_path)
if mgt_port is None:
return
log.info(text('install-finish', PRODUCT))
finish(mgt_port)
def get_installed_dir():
safeline_path = ''
safeline_path_proc = exec_command('docker','inspect','--format','\'{{index .Config.Labels "com.docker.compose.project.working_dir"}}\'', 'safeline-mgt')
if safeline_path_proc[0] == 0:
safeline_path = safeline_path_proc[1].strip().replace("'",'')
else:
log.debug("get installed dir error: "+ safeline_path_proc[2])
log.debug("find safeline installed path: " + safeline_path)
if safeline_path == '' or not os.path.exists(safeline_path):
log.warning(text('fail-to-get-installed-dir'))
return ui_read(text('input-target-path', PRODUCT),None)
return safeline_path
def save_file_from_url(url, path):
log.debug('saving '+url+' to '+path)
data = get_url(url)
if data is None:
return False
with open(path, 'w') as f:
f.write(data)
return True
def rename_file(src, dst):
if os.path.exists(src):
os.rename(src, dst)
def remove_file(src):
if os.path.exists(src):
os.remove(src)
def generate_config_and_run(safeline_path):
env_file = os.path.join(safeline_path, '.env')
env_bak_file = os.path.join(safeline_path, '.env.bak')
if os.path.exists(env_file):
shutil.copyfile(env_file, env_bak_file)
try:
while True:
config = generate_config(safeline_path)
if docker_pull(safeline_path):
break
pull_failed_prefix.append(config['IMAGE_PREFIX'])
log.info(text('try-another-image-source'))
if not docker_up(safeline_path):
log.error(text('fail-to-up'))
rename_file(env_bak_file, env_file)
return None
except KeyboardInterrupt:
log.warning(text('keyboard-interrupt'))
rename_file(env_bak_file, env_file)
return None
except Exception as e:
log.error('start WAF failed: '+str(e))
rename_file(env_bak_file, env_file)
return None
remove_file(env_bak_file)
return config['MGT_PORT']
def upgrade():
safeline_path = get_installed_dir()
if not precheck_docker_compose() or not precheck_dns_scope():
log.error(text('precheck-failed', PRODUCT))
return
log.info(text('download-compose'))
rename_file(os.path.join(safeline_path, 'compose.yaml'), os.path.join(safeline_path, 'compose.yaml.bak'))
if not save_file_from_url('https://'+DOMAIN+'/release/latest/compose.yaml', os.path.join(safeline_path, 'docker-compose.yaml')):
log.error(text('fail-to-download-compose'))
return
mgt_port = generate_config_and_run(safeline_path)
if mgt_port is None:
return
if GLOBAL_ARGS.image_clean:
image_clean()
log.info(text('upgrade-finish', PRODUCT))
finish(mgt_port)
def finish(mgt_port):
init_mgt()
show_address(mgt_port)
def reset_tengine():
safeline_path = get_installed_dir()
resources_path = os.path.join(safeline_path, 'resources')
nginx_path = os.path.join(resources_path,'nginx')
if not os.path.exists(nginx_path):
log.error(text('fail-to-find-nginx'))
return
backup_path = os.path.join(resources_path, 'nginx.'+str(datetime.datetime.now().timestamp()))
log.info(text('nginx-backup-dir') +': '+ backup_path)
try:
shutil.move(nginx_path, backup_path)
except Exception as e:
log.error(text('fail-to-backup-nginx')+': '+str(e))
return
if docker_restart('safeline-tengine'):
docker_exec('safeline-mgt', 'gentenginewebsite')
if os.path.exists(os.path.join(backup_path, 'static')):
try:
shutil.copy(os.path.join(backup_path, 'static'), os.path.join(nginx_path, 'static'))
except Exception as e:
log.error(text('fail-to-recover-static')+': '+str(e))
return
log.info(text('reset-tengine-finish'))
def docker_restart_all(cwd):
if not docker_down(cwd):
log.error(text('fail-to-down'))
return False
if not docker_up(cwd):
log.error(text('fail-to-up'))
return False
return True
def reset_postgres():
safeline_path = get_installed_dir()
env_file = os.path.join(safeline_path, '.env')
if not os.path.exists(env_file):
log.error(text('fail-to-find-env'))
return
config = {}
read_config(env_file, config)
config['POSTGRES_PASSWORD'] = ''.join([random.choice(string.ascii_letters + string.digits) for i in range(20)])
write_config(env_file, config)
if not docker_exec('safeline-pg','psql -U safeline-ce -c "ALTER USER \\"safeline-ce\\" WITH PASSWORD \''+config['POSTGRES_PASSWORD']+'\';"'):
log.error(text('fail-to-reset-postgres-password'))
return
action = ui_choice(text('if-restart-docker', PRODUCT), [
('y', text('yes')),
('n', text('no')),
])
if action.lower() == 'y':
if not precheck_docker_compose():
log.error(text('precheck-failed', PRODUCT))
return
if not docker_restart_all(safeline_path):
return
log.info(text('reset-postgres-password-finish'))
def repair():
action = ui_choice(text('choice-action'),[
('1', text('reset-tengine')),
('2', text('reset-postgres')),
])
if action =='1':
reset_tengine()
elif action =='2':
reset_postgres()
def restart():
safeline_path = get_installed_dir()
if not precheck_docker_compose():
log.error(text('precheck-failed', PRODUCT))
return
if not docker_restart_all(safeline_path):
return
log.info(text('restart-docker-finish', PRODUCT))
def backup():
pass
def uninstall():
safeline_path = get_installed_dir()
action = ui_choice(text('if-remove-waf', (PRODUCT, safeline_path)),[
('y', text('yes')),
('n', text('no')),
])
if action == 'n':
return
if not precheck_docker_compose():
log.error(text('precheck-failed', PRODUCT))
return
if not docker_down(safeline_path):
log.error(text('fail-to-docker-down'))
return
image_clean()
try:
shutil.rmtree(safeline_path)
except Exception as e:
log.debug("remove dir failed: "+str(e))
log.error(text('fail-to-remove-dir', PRODUCT))
log.info(text('uninstall-finish', PRODUCT))
ACTION = ''
def get_version_from_input(old_version):
while True:
version = ui_read(text('input-target-version', ACTION),None)
if not check_version_format(version):
log.warning(text('version-format-error', version))
continue
if not compare_version(old_version, version):
log.warning(text('can-not-downgrade', (PRODUCT, old_version, version)))
continue
return version.lstrip('v')
def check_version_format(version):
if GLOBAL_ARGS.lts and not version.endswith('-lts'):
log.debug('lts version should end with -lts')
return False
split_version = version.lstrip('v').rstrip('-lts').split('.')
if len(split_version) != 3:
log.debug('split version length is not 3')
return False
try:
for v in split_version:
int(v)
except ValueError as e:
log.debug('check version %s format failed: %s' % (version, str(e)))
return False
return True
def compare_version(old_version, new_version):
if old_version == 'latest':
try:
log.info(text('get-version-from-mgt'))
old_version = get_version_from_mgt()
except Exception as e:
log.debug('get version from mgt failed: '+str(e))
log.warning(text('fail-to-get-version-from-mgt'))
log.warning(text('skip-version-compare', PRODUCT))
return True
elif old_version == '':
return True
if not check_version_format(old_version):
log.warning(text('version-format-error', old_version))
log.warning(text('skip-version-compare', PRODUCT))
return True
split_old_version = old_version.lstrip('v').rstrip('-lts').split('.')
split_new_version = new_version.lstrip('v').rstrip('-lts').split('.')
for index in range(len(split_old_version)):
int_old_version = int(split_old_version[index])
int_new_version = int(split_new_version[index])
if int_old_version > int_new_version:
return False
elif int_old_version < int_new_version:
return True
return True
def get_version_from_mgt():
proc = exec_command('docker exec safeline-mgt /app/mgt version',shell=True)
if proc[0] != 0:
raise Exception('stderr: ' + proc[2])
for line in proc[1].split('\n'):
strip_line = line.strip()
if not strip_line.startswith('version'):
continue
return strip_line.lstrip('version').strip()
raise Exception('mgt version not found')
TARGET_VERSION = ''
def get_version(old_version):
global TARGET_VERSION
if TARGET_VERSION != '':
return TARGET_VERSION
log.info(text('get-version', PRODUCT))
try:
data = get_url('https://'+DOMAIN+'/release/latest/version.json')
if data is None:
TARGET_VERSION = get_version_from_input(old_version)
return TARGET_VERSION
version = json.loads(data)
if GLOBAL_ARGS.lts:
latest_version = version['lts_version']
else:
latest_version = version['latest_version']
if not check_version_format(latest_version):
log.warning(text('version-format-error', latest_version))
TARGET_VERSION = get_version_from_input(old_version)
elif not compare_version(old_version, latest_version):
log.warning(text('can-not-downgrade', (old_version, latest_version)))
TARGET_VERSION = get_version_from_input(old_version)
else:
TARGET_VERSION = latest_version.lstrip('v')
return TARGET_VERSION
except Exception as e:
log.warning('get version failed: %s' % str(e))
TARGET_VERSION = get_version_from_input(old_version)
return TARGET_VERSION
def main():
if SELF:
banner()
log.info(text('hello1'))
log.info(text('hello2'))
print()
if GLOBAL_ARGS.lts:
log.info(text('install-channel', text('lts-release')))
if sys.version_info.major == 2 or (sys.version_info.major == 3 and sys.version_info.minor <= 5):
log.error(text('python-version-too-low'))
return
if not sys.stdin.isatty():
log.error(text('not-a-tty'))
return
if os.geteuid() != 0:
log.error(text('not-root'))
return
if platform.system() != 'Linux':
log.error(text('not-linux', (PRODUCT, platform.system())))
return
if platform.machine() not in ('aarch64', 'x86_64', 'AMD64'):
log.error(text('unsupported-arch', (PRODUCT, platform.machine())))
return
action = ui_choice(text('choice-action'), [
('1', text('install')),
('2', text('upgrade')),
('3', text('uninstall')),
('4', text('repair')),
('5', text('restart')),
# ('4', text('backup'))
])
global ACTION
if action == '1':
ACTION = text('install')
install()
elif action == '2':
ACTION = text('upgrade')
upgrade()
elif action == '3':
uninstall()
elif action == '4':
repair()
elif action == '5':
restart()
# elif action == '4':
# backup()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
log.warning(text('keyboard-interrupt'))
pass
except Exception as e:
log.error(e)
finally:
if SELF:
print(color(text('talking-group') + '\n', [GREEN]))
================================================
FILE: sdk/ingress-nginx/README.md
================================================
# ingress-nginx-safeline
[Ingress-nginx](https://kubernetes.github.io/ingress-nginx/) 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.
## Safeline Prepare
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.
1.Navigate to the configuration directory of the SafeLine detection engine:
```shell
cd /data/safeline/resources/detector/
```
2.Open the `detector.yml` file in a text editor. Modify the bind configuration from Unix socket to TCP by adding the following settings:
```yaml
bind_addr: 0.0.0.0
listen_port: 8000
```
These configuration values will override the default settings in the container, making the SafeLine engine listen on port 8000.
3.Next, map the container’s port 8000 to the host machine. First, navigate to the SafeLine installation directory:
```shell
cd /data/safeline
```
4.Open the compose.yaml file in a text editor and add the ports field to the detector container to expose port 8000:
```yaml
...
detect:
ports:
- 8000:8000
...
```
5.Save the changes and restart SafeLine with the following commands:
```shell
docker-compose down
docker-compose up -d
```
This will apply the changes and activate the new configuration.
## Plugin Usage
### Step 1: Install the plugin
way 1: Build your own ingress-nginx/controller image with the plugin installed.
```dockerfile
FROM registry.k8s.io/ingress-nginx/controller:v1.10.1
USER root
RUN apk add --no-cache make gcc unzip wget
# install luaroncks
RUN wget https://luarocks.org/releases/luarocks-3.11.0.tar.gz && \
tar zxpf luarocks-3.11.0.tar.gz && \
cd luarocks-3.11.0 && \
./configure && \
make && \
make install && \
cd .. && \
rm -rf luarocks-3.11.0 luarocks-3.11.0.tar.gz
RUN luarocks install ingress-nginx-safeline && \
ln -s /usr/local/share/lua/5.1/safeline /etc/nginx/lua/plugins/safeline
USER www-data
```
way 2: Use the chaitin ingress-nginx-controller image.
replace image ingress-nginx-controller with `docker.io/chaitin/ingress-nginx-controller:v1.10.1` in your deployment.
### Step 2: Configure the plugin
use a ConfigMap to configure the plugin
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: safeline
namespace: default
data:
host: "YOUR_DETECTOR_HOST"
port: "YOUR_DETECTOR_PORT"
```
### Step 3: Configure the ingress-controller
inject env `SAFELINE_HOST` and `SAFELINE_PORT` to the ingress-controller deployment
```yaml
...
env:
- name: SAFELINE_HOST
valueFrom:
configMapKeyRef:
name: safeline
key: host
- name: SAFELINE_PORT
valueFrom:
configMapKeyRef:
name: safeline
key: port
...
```
### Step 3: Enable the plugin
enable safeline plugin in configmap
```yaml
apiVersion: v1
data:
allow-snippet-annotations: "false"
plugins: "safeline"
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: default
```
### Step 4: Set externalTrafficPolicy to Local
by default, the ingress-nginx-controller service is of type LoadBalancer, which means the source IP of the request will be the IP of the LoadBalancer. If you want to get the real source IP, you can set the externalTrafficPolicy to Local.
### Step 5: Test the plugin
use a simple http sql injection test
```bash
curl -X POST http://localhost/ -d "select * from users where id=1 or 1=1"
```
you should get a 403 response.
```bash
{"code": 403, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "b53eb5b95796475699c52a019abb8e6a"}
```
================================================
FILE: sdk/ingress-nginx/ingress-nginx-safeline-1.0.2-1.rockspec
================================================
package = "ingress-nginx-safeline"
version = "1.0.2-1"
source = {
url = "git://github.com/xbingW/ingress-nginx-safeline.git"
}
description = {
summary = "Ingress-Nginx plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/xbingW/ingress-nginx-safeline",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang "
}
dependencies = {
"lua-resty-t1k"
}
build = {
type = "builtin",
modules = {
["safeline.main"] = "lib/safeline/main.lua"
}
}
================================================
FILE: sdk/ingress-nginx/ingress-nginx-safeline-1.0.3-1.rockspec
================================================
package = "ingress-nginx-safeline"
version = "1.0.3-1"
source = {
url = "git://github.com/xbingW/ingress-nginx-safeline.git"
}
description = {
summary = "Ingress-Nginx plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/xbingW/ingress-nginx-safeline",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang "
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["safeline.main"] = "lib/safeline/main.lua"
}
}
================================================
FILE: sdk/ingress-nginx/ingress-nginx-safeline-1.0.4-1.rockspec
================================================
package = "ingress-nginx-safeline"
version = "1.0.4-1"
source = {
url = "git://github.com/chaitin/ingress-nginx-safeline.git"
}
description = {
summary = "Ingress-Nginx plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/chaitin/ingress-nginx-safeline",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang "
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["safeline.main"] = "lib/safeline/main.lua"
}
}
================================================
FILE: sdk/ingress-nginx/lib/safeline/main.lua
================================================
local t1k = require "resty.t1k"
local t1k_constants = require "resty.t1k.constants"
local ngx = ngx
local fmt = string.format
local blocked_message = [[{"code": %s, "success":false, ]] ..
[["message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "%s"}]]
local _M = {}
local mode = os.getenv("SAFELINE_MODE")
local host = os.getenv("SAFELINE_HOST")
local port = os.getenv("SAFELINE_PORT")
local connect_timeout = os.getenv("SAFELINE_CONNECT_TIMEOUT")
local send_timeout = os.getenv("SAFELINE_SEND_TIMEOUT")
local read_timeout = os.getenv("SAFELINE_READ_TIMEOUT")
local req_body_size = os.getenv("SAFELINE_REQ_BODY_SIZE")
local keepalive_size = os.getenv("SAFELINE_KEEPALIVE_SIZE")
local keepalive_timeout = os.getenv("SAFELINE_KEEPALIVE_TIMEOUT")
local remote_addr = os.getenv("SAFELINE_REMOTE_ADDR")
local function get_conf()
local t = {
mode = mode or "block",
host = host,
port = port,
connect_timeout = connect_timeout or 1000,
send_timeout = send_timeout or 1000,
read_timeout = read_timeout or 1000,
req_body_size = req_body_size or 1024,
keepalive_size = keepalive_size or 256,
keepalive_timeout = keepalive_timeout or 60000,
remote_addr = remote_addr
}
return t
end
function _M.rewrite()
local t = get_conf()
if not t.host then
ngx.log(ngx.ERR, "safeline host is required")
return
end
local ok, err, result = t1k.do_access(t, false)
if not ok then
ngx.log(ngx.ERR, "failed to detector req: ", err)
return
end
if result then
if result.action == t1k_constants.ACTION_BLOCKED then
local msg = fmt(blocked_message, result.status, result.event_id)
ngx.log(ngx.ERR, "blocked by safeline waf: ",msg)
ngx.status = tonumber(result.status,10)
ngx.say(msg)
return ngx.exit(ngx.HTTP_OK)
end
end
end
return _M
================================================
FILE: sdk/kong/Readme.md
================================================
# Kong Safeline Plugin
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.
## Safeline Prepare
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.
1.Navigate to the configuration directory of the SafeLine detection engine:
```shell
cd /data/safeline/resources/detector/
```
2.Open the `detector.yml` file in a text editor. Modify the bind configuration from Unix socket to TCP by adding the following settings:
```yaml
bind_addr: 0.0.0.0
listen_port: 8000
```
These configuration values will override the default settings in the container, making the SafeLine engine listen on port 8000.
3.Next, map the container’s port 8000 to the host machine. First, navigate to the SafeLine installation directory:
```shell
cd /data/safeline
```
4.Open the compose.yaml file in a text editor and add the ports field to the detector container to expose port 8000:
```yaml
...
detect:
ports:
- 8000:8000
...
```
5.Save the changes and restart SafeLine with the following commands:
```shell
docker-compose down
docker-compose up -d
```
This will apply the changes and activate the new configuration.
## Plugin Installation
To install the plugin, run the following command in your Kong server:
```shell
$ luarocks install kong-safeline
```
## Plugin Configuration
You can add the plugin to your API by making the following request:
```shell
# if your detector is running on tcp port
$ curl -X POST http://kong:8001/services/{name}/plugins \
--data "name=safeline" \
--data "config.host=your_detector_host" \
--data "config.port=your_detector_port"
# if your detector is running on unix socket
$ curl -X POST http://kong:8001/services/{name}/plugins \
--data "name=safeline" \
--data "config.host=unix:/path/to/your/unix/socket"
```
## Test
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.
```shell
$ curl -X POST http://kong:8000?1=1%20and%202=2
# you will receive a 403 Forbidden response
{"code": 403, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "8b41a021ea9541c89bb88f3773b4da24"}
```
================================================
FILE: sdk/kong/kong/plugins/safeline/handler.lua
================================================
local t1k = require "resty.t1k"
local t1k_constants = require "resty.t1k.constants"
local fmt = string.format
local SafelineHandler = {
VERSION = "0.0.1",
PRIORITY = 1000
}
local blocked_message = [[{"code": %s, "success":false, ]] ..
[["message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "%s"}]]
local function get_conf(conf)
local t = {
mode = conf.mode,
host = conf.host,
port = conf.port,
connect_timeout = conf.connect_timeout,
send_timeout = conf.send_timeout,
read_timeout = conf.read_timeout,
req_body_size = conf.req_body_size,
keepalive_size = conf.keepalive_size,
keepalive_timeout = conf.keepalive_timeout,
remote_addr = conf.remote_addr
}
return t
end
function SafelineHandler:access(conf)
-- your custom code here
local t = get_conf(conf)
local ok, err, result = t1k.do_access(t, false)
if not ok then
kong.log.err("failed to detector req: ", err)
end
if result and result.status then
if result.action == t1k_constants.ACTION_BLOCKED then
local msg = fmt(blocked_message, result.status, result.event_id)
kong.log.debug("blocked by safeline: ",msg)
return kong.response.exit(tonumber(result.status), msg)
end
end
end
return SafelineHandler
================================================
FILE: sdk/kong/kong/plugins/safeline/schema.lua
================================================
local typedefs = require "kong.db.schema.typedefs"
return {
name = "kong-safeline",
fields = {{
consumer = typedefs.no_consumer
}, {
protocols = typedefs.protocols_http
}, {
config = {
type = "record",
fields = {{
host = {
type = "string",
required = true
}
}, {
port = {
type = "number",
required = false
}
}, {
mode = {
type = "string",
required = false,
default = "block",
one_of = {"monitor", "block", "off"}
}
}, {
connect_timeout = {
type = "number",
required = false,
default = 1000
}
}, {
send_timeout = {
type = "number",
required = false,
default = 1000
}
}, {
read_timeout = {
type = "number",
required = false,
default = 1000
}
}, {
req_body_size = {
type = "number",
required = false,
default = 1000
}
}, {
keepalive_size = {
type = "number",
required = false,
default = 1000
}
}, {
keepalive_timeout = {
type = "number",
required = false,
default = 1000
}
}, {
remote_addr = {
type = "string",
required = false
}
}}
}
}}
}
================================================
FILE: sdk/kong/kong-safeline-1.0.0-1.rockspec
================================================
package = "kong-safeline"
version = "1.0.0-1"
source = {
url = "git://github.com/xbingW/kong-safeline.git"
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/xbingW/kong-safeline",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang "
}
dependencies = {
"lua-resty-t1k"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}
================================================
FILE: sdk/kong/kong-safeline-1.0.1-1.rockspec
================================================
package = "kong-safeline"
version = "1.0.1-1"
source = {
url = "git://github.com/xbingW/kong-safeline.git"
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/xbingW/kong-safeline",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang "
}
dependencies = {
"lua-resty-t1k"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}
================================================
FILE: sdk/kong/kong-safeline-1.0.2-1.rockspec
================================================
package = "kong-safeline"
version = "1.0.2-1"
source = {
url = "git://github.com/xbingW/kong-safeline.git"
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/xbingW/kong-safeline",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang "
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}
================================================
FILE: sdk/kong/kong-safeline-1.0.3-1.rockspec
================================================
package = "kong-safeline"
version = "1.0.3-1"
source = {
url = "file://kong-safeline-1.0.3.tar.gz",
}
build = {
type = "script",
rockspec = {
build = {
"git clone https://github.com/chaitin/SafeLine.git",
"cp -r sdk/kong .",
"rm -rf SafeLine"
}
}
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/chaitin/SafeLine",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang "
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}
================================================
FILE: sdk/kong/kong-safeline-1.0.4-1.rockspec
================================================
package = "kong-safeline"
version = "1.0.4-1"
source = {
url = "git://github.com/chaitin/SafeLine.git",
}
build = {
type = "script",
rockspec = {
build = {
"git clone https://github.com/chaitin/SafeLine.git",
"cp -r sdk/kong .",
"rm -rf SafeLine"
}
}
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/chaitin/SafeLine",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang "
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}
================================================
FILE: sdk/kong/kong-safeline-1.0.5-1.rockspec
================================================
package = "kong-safeline"
version = "1.0.5-1"
source = {
url = "git://github.com/chaitin/SafeLine.git",
}
build = {
type = "script",
rockspec = {
build = {
"git clone https://github.com/chaitin/SafeLine.git",
"cp -r sdk/kong .",
"rm -rf SafeLine"
}
}
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/chaitin/SafeLine",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang "
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}
================================================
FILE: sdk/kong/kong-safeline-1.0.6-1.rockspec
================================================
package = "kong-safeline"
version = "1.0.6-1"
source = {
url = "git://github.com/xbingW/kong-safeline.git",
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/chaitin/SafeLine",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang "
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}
================================================
FILE: sdk/kong/kong-safeline-1.0.7-1.rockspec
================================================
package = "kong-safeline"
version = "1.0.7-1"
source = {
url = "git://github.com/chaitin/kong-safeline.git",
}
description = {
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
homepage = "https://github.com/chaitin/SafeLine",
license = "Apache License 2.0",
maintainer = "Xiaobing Wang "
}
dependencies = {
"lua-resty-t1k >= 1.1.5"
}
build = {
type = "builtin",
modules = {
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
}
}
================================================
FILE: sdk/lua-resty-t1k/.github/workflows/release.yml
================================================
name: Release
on:
push:
tags:
- "v*"
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Lua
uses: leafo/gh-actions-lua@v10
- name: Install Luarocks
uses: leafo/gh-actions-luarocks@v4
- name: Extract release tag
id: release_tag
run: |
# Extract the tag name from the ref
tag="${GITHUB_REF#refs/tags/}"
version_without_v="${tag#v}"
echo "version=${tag}" >> $GITHUB_ENV
echo "version_without_v=${version_without_v}" >> $GITHUB_ENV
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.version }}
release_name: ${{ env.version }}
draft: false
prerelease: false
- name: Upload to luarocks
env:
LUAROCKS_TOKEN: ${{ secrets.LUAROCKS_TOKEN }}
run: |
luarocks install dkjson
luarocks upload rockspec/lua-resty-t1k-${{ env.version_without_v }}-0.rockspec --api-key=${{ secrets.LUAROCKS_API_KEY }}
================================================
FILE: sdk/lua-resty-t1k/.github/workflows/test.yml
================================================
name: Test
on: [ push, pull_request ]
jobs:
luacheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: leafo/gh-actions-lua@v10
with:
luaVersion: "luajit-openresty"
- uses: leafo/gh-actions-luarocks@v4
- run: luarocks install luacheck
- run: luacheck lib
run_tests:
strategy:
matrix:
openresty_version:
- 1.17.8.2
- 1.19.9.1
- 1.21.4.3
- 1.25.3.1
services:
detector:
image: chaitin/safeline-detector:t1k-ci-1.6.0
runs-on: ubuntu-latest
container:
image: openresty/openresty:${{ matrix.openresty_version }}-alpine-fat
# --init runs tinit as PID 1 and prevents the 'WARNING: killing the child process' spam from the test suite
options: --init
steps:
- name: Install deps
run: |
apk add --no-cache bash bind-tools curl git git-lfs libarchive-tools perl perl-dev wget
ln -s /usr/bin/bsdtar /usr/bin/tar
- name: Install CPAN
run: curl -s -L http://xrl.us/cpanm > /bin/cpanm && chmod +x /bin/cpanm
- name: Cache
uses: actions/cache@v3
with:
path: |
~/.cpan
~/.cache
key: ${{ runner.os }}-${{ matrix.openresty_version }}-cache
- name: Install Test::Nginx
run: cpanm -q -n Test::Nginx
- uses: actions/checkout@v4
with:
lfs: true
- name: Run tests
run: |
curl -fs -X POST -H "Content-Type: application/octet-stream" --data-binary "@ci/bytecode" "http://detector:8001/update/policy"
env DETECTOR_IP=$(dig detector +short) prove -r t/
================================================
FILE: sdk/lua-resty-t1k/.gitignore
================================================
# IntelliJ project files
.idea
*.iml
out
gen
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# Test::Nginx files
t/servroot
# luarocks build files
*.src.rock
================================================
FILE: sdk/lua-resty-t1k/.luacheckrc
================================================
std = "ngx_lua"
redefined = false
max_line_length = 130
max_code_line_length = 130
max_string_line_length = 130
max_comment_line_length = 130
================================================
FILE: sdk/lua-resty-t1k/LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2023 Beijing Chaitin Technology Co., Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: sdk/lua-resty-t1k/README.md
================================================
# lua-resty-t1k
[](https://luarocks.org/modules/blaisewang/lua-resty-t1k)
[](https://github.com/chaitin/lua-resty-t1k/releases)
[](https://github.com/chaitin/lua-resty-t1k/blob/main/LICENSE)
## Name
Lua implementation of the T1K protocol for [Chaitin/SafeLine](https://github.com/chaitin/safeline) Web Application Firewall.
## Status
Production ready.
[](https://github.com/chaitin/lua-resty-t1k/actions)
## Installation
```bash
luarocks install lua-resty-t1k
```
If you are in Mainland China
```bash
luarocks install lua-resty-t1k --server https://luarocks.cn
```
## Synopsis
```lua
location / {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block", -- block or monitor or off, default off
host = "unix:/workdir/snserver.sock", -- required, SafeLine WAF detection service host, unix domain socket, IP, or domain is supported, string
port = 8000, -- required when the host is an IP or domain, SafeLine WAF detection service port, integer
connect_timeout = 1000, -- connect timeout, in milliseconds, integer, default 1s (1000ms)
send_timeout = 1000, -- send timeout, in milliseconds, integer, default 1s (1000ms)
read_timeout = 1000, -- read timeout, in milliseconds, integer, default 1s (1000ms)
req_body_size = 1024, -- request body size, in KB, integer, default 1MB (1024KB)
keepalive_size = 256, -- maximum concurrent idle connections to the SafeLine WAF detection service, integer, default 256
keepalive_timeout = 60000, -- idle connection timeout, in milliseconds, integer, default 60s (60000ms)
remote_addr = "http_x_forwarded_for: 1", -- remote address from ngx.var.VARIABLE, string, default from ngx.var.remote_addr
}
local ok, err, _ = t1k.do_access(t, true)
if not ok then
ngx.log(ngx.ERR, err)
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
}
```
## Lua Resty T1K vs. C T1K
[C T1K](https://t1k.chaitin.com/), as part of SafeLine's enterprise edition, is a deployment mode crafted in C language for enhanced performance.
It is compatible with all versions of Nginx and does not require deployment via OpenResty (lua_nginx_module).
| | Lua Resty T1K | C T1K |
|-----------------------|---------------|-------|
| Request Detection | ✅ | ✅ |
| Response Detection | ❌ | ✅ |
| Health Checks* | ❌ | ✅ |
| Cookie Protection | ❌ | ✅ |
| Bot Protection | ❌ | ✅ |
| Proxy-side Statistics | ❌ | ✅ |
* APISIX implements health check functionality for the `chaitin-waf` plugin. For more information, please see the [chaitin-waf documentation](https://apisix.apache.org/docs/apisix/next/plugins/chaitin-waf/).
================================================
FILE: sdk/lua-resty-t1k/ci/.dockerignore
================================================
/bytecode
================================================
FILE: sdk/lua-resty-t1k/ci/Dockerfile
================================================
# Usage:
# docker build -t chaitin/safeline-detector:t1k-ci-1.6.0 .
# docker push chaitin/safeline-detector:t1k-ci-1.6.0
FROM chaitin/safeline-detector:1.6.0
RUN sed -i "s/^# bind_addr/bind_addr/; s/^# listen_port/listen_port/; s/^bind_addr: unix/# bind_addr: unix/;" /detector/snserver.yml
================================================
FILE: sdk/lua-resty-t1k/lib/resty/t1k/buffer.lua
================================================
local _M = {
_VERSION = '1.0.0',
}
function _M:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
function _M:add(v)
self[#self + 1] = v
end
function _M:len()
local len = 0
for _, v in ipairs(self) do
len = len + #v
end
return len
end
return _M
================================================
FILE: sdk/lua-resty-t1k/lib/resty/t1k/constants.lua
================================================
local t = {}
t.ACTION_PASSED = "."
t.ACTION_BLOCKED = "?"
t.MODE_OFF = "off"
t.MODE_BLOCK = "block"
t.MODE_MONITOR = "monitor"
t.T1K_HEADER_SIZE = 5
t.TAG_HEAD = 0x01
t.TAG_BODY = 0x02
t.TAG_EXTRA = 0x03
t.TAG_VERSION = 0x20
t.TAG_EXTRA_HEADER = 0x23
t.TAG_EXTRA_BODY = 0x24
t.MASK_FIRST = 0x40
t.MASK_LAST = 0x80
t.NGX_HTTP_HEADER_PREFIX = "http_"
t.BLOCK_CONTENT_TYPE = "application/json"
t.BLOCK_CONTENT_FORMAT = [[
{"code": %s, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "%s"}]]
t.UNIX_SOCK_PREFIX = "unix:"
return t
================================================
FILE: sdk/lua-resty-t1k/lib/resty/t1k/file.lua
================================================
local buffer = require "resty.t1k.buffer"
local _M = {
_VERSION = '1.0.0'
}
local buffer_size = 2 ^ 13
function _M.read(p, size)
size = (not size or size < 0) and 0 or size
local f, err = io.open(p, "rb")
if not f or err then
return nil, err, nil
end
local left = size
local buf = buffer:new()
while left ~= 0 do
local block_size = math.min(left, buffer_size)
local block = f:read(block_size)
if not block then
break
end
buf:add(block)
left = math.max(left - block_size, 0)
end
f:close()
return true, nil, buf
end
return _M
================================================
FILE: sdk/lua-resty-t1k/lib/resty/t1k/filter.lua
================================================
local _M = {
_VERSION = '1.0.0'
}
local find = string.find
local sub = string.sub
local ngx = ngx
local function parse_extra_header(extra_header)
local t = {}
local idx = 1
while (idx <= #extra_header) do
local key, val
local _, to = find(extra_header, ":", idx)
if to == 0 then
break
else
key = sub(extra_header, idx, to - 1)
end
idx = to + 1
_, to = find(extra_header, "\n", idx)
if to == 0 then
break
else
val = sub(extra_header, idx, to - 1)
end
t[key] = val
idx = to + 1
end
return t
end
function _M.do_header_filter()
local extra_header = ngx.ctx.t1k_extra_header
if extra_header ~= nil then
local header_table = parse_extra_header(extra_header)
for k, v in pairs(header_table) do
if k ~= nil and v ~= nil then
ngx.header[k] = v
end
end
end
end
return _M
================================================
FILE: sdk/lua-resty-t1k/lib/resty/t1k/handler.lua
================================================
local consts = require "resty.t1k.constants"
local log = require "resty.t1k.log"
local fmt = string.format
local ngx = ngx
local log_fmt = log.fmt
local _M = {
_VERSION = '1.0.0'
}
function _M.handle(t)
local t_type = type(t)
if t_type ~= "table" then
local err = log_fmt("invalid result type: %s", t_type)
return nil, err
end
local action = t["action"]
if action == consts.ACTION_PASSED then
return true, nil
elseif action == consts.ACTION_BLOCKED then
ngx.status = t["status"] or ngx.HTTP_FORBIDDEN
ngx.header.content_type = consts.BLOCK_CONTENT_TYPE
ngx.say(fmt(consts.BLOCK_CONTENT_FORMAT, ngx.status, t["event_id"]))
return ngx.exit(ngx.status)
else
local err = log_fmt("unknown action from t1k server: %s", action)
return nil, err
end
end
return _M
================================================
FILE: sdk/lua-resty-t1k/lib/resty/t1k/log.lua
================================================
local _M = {
_VERSION = '1.0.0'
}
local fmt = string.format
local ERR = ngx.ERR
local WARN = ngx.WARN
local DEBUG = ngx.DEBUG
function _M.fmt(formatstring, ...)
return fmt("lua-resty-t1k: " .. formatstring, ...)
end
function _M.err_fmt(formatstring, ...)
return ERR, _M.fmt(formatstring, ...)
end
function _M.warn_fmt(formatstring, ...)
return WARN, _M.fmt(formatstring, ...)
end
function _M.debug_fmt(formatstring, ...)
return DEBUG, _M.fmt(formatstring, ...)
end
return _M
================================================
FILE: sdk/lua-resty-t1k/lib/resty/t1k/request.lua
================================================
local bit = require "bit"
local buffer = require "resty.t1k.buffer"
local consts = require "resty.t1k.constants"
local file = require "resty.t1k.file"
local log = require "resty.t1k.log"
local utils = require "resty.t1k.utils"
local uuid = require "resty.t1k.uuid"
local _M = {
_VERSION = '1.0.0',
}
local bor = bit.bor
local byte = string.byte
local char = string.char
local fmt = string.format
local sub = string.sub
local concat = table.concat
local ngx = ngx
local nlog = ngx.log
local ngx_req = ngx.req
local ngx_socket = ngx.socket
local ngx_var = ngx.var
local warn_fmt = log.warn_fmt
local debug_fmt = log.debug_fmt
local KEY_EXTRA_UUID = "UUID"
local KEY_EXTRA_LOCAL_ADDR = "LocalAddr"
local KEY_EXTRA_LOCAL_PORT = "LocalPort"
local KEY_EXTRA_REMOTE_ADDR = "RemoteAddr"
local KEY_EXTRA_REMOTE_PORT = "RemotePort"
local KEY_EXTRA_SCHEME = "Scheme"
local KEY_EXTRA_SERVER_NAME = "ServerName"
local KEY_EXTRA_PROXY_NAME = "ProxyName"
local KEY_EXTRA_REQ_BEGIN_TIME = "ReqBeginTime"
local KEY_EXTRA_HAS_RSP_IF_OK = "HasRspIfOK"
local KEY_EXTRA_HAS_RSP_IF_BLOCK = "HasRspIfBlock"
local TAG_HEAD_WITH_MASK_FIRST = bor(consts.TAG_HEAD, consts.MASK_FIRST)
local TAG_EXTRA_WITH_MASK_LAST = bor(consts.TAG_EXTRA, consts.MASK_LAST)
local T1K_PROTO = "Proto:2\n"
local T1K_PROTO_DATA = fmt("%s%s%s", char(consts.TAG_VERSION), utils.int_to_char_length(#T1K_PROTO), T1K_PROTO)
local function read_request_body(opt_req_body_size)
local ok, err
local req_body, req_body_size
local content_length = tonumber(ngx_var.http_content_length) or 0
local transfer_encoding = ngx_var.http_transfer_encoding
if content_length == 0 and not transfer_encoding then
return true, nil, nil
end
ngx_req.read_body()
req_body = ngx_req.get_body_data()
if req_body then
req_body_size = #req_body
if req_body_size > opt_req_body_size then
nlog(debug_fmt("request body is too long: %d bytes, cut to %d bytes", req_body_size, opt_req_body_size))
req_body = sub(req_body, 1, opt_req_body_size)
end
return true, nil, req_body
end
local path = ngx_req.get_body_file()
if not path then
return true, nil, nil
end
ok, err, req_body = file.read(path, opt_req_body_size)
if not ok then
err = fmt("failed to read temporary file %s: %s", path, err)
return ok, err, nil
end
return true, nil, req_body
end
local function get_remote_addr(remote_addr_var, remote_addr_idx)
local addr
if remote_addr_var then
addr = utils.get_indexed_element(ngx_var[remote_addr_var], remote_addr_idx)
end
return addr or ngx_var.remote_addr
end
local function parse_v(v)
if type(v) == "table" then
return concat(v, ", ")
end
return tostring(v)
end
local function build_header()
local http_version = ngx_req.http_version()
if http_version < 2.0 then
return true, nil, ngx_req.raw_header()
end
local headers, err = ngx_req.get_headers(0, true)
if err then
err = fmt("failed to call ngx_req.get_headers: %s", err)
return nil, err, nil
end
local buf = buffer:new()
buf:add(fmt("%s %s HTTP/%.1f\r\n", ngx_req.get_method(), ngx_var.request_uri, http_version))
for k, v in pairs(headers) do
buf:add(fmt("%s: %s\r\n", k, parse_v(v)))
end
buf:add("\r\n")
return true, nil, buf
end
local function build_body(opts)
local ok, err
local body
local req_body_size = opts.req_body_size * 1024
ok, err, body = read_request_body(req_body_size)
if not ok then
return ok, err, nil
end
return true, nil, body
end
local function build_extra(opts)
local err
local src_ip = get_remote_addr(opts.remote_addr_var, opts.remote_addr_idx)
if not src_ip then
err = fmt("failed to get remote_addr, var: %s, idx %d", opts.remote_addr_var, opts.remote_addr_idx)
return nil, err
end
local src_port = ngx_var.remote_port
if not src_port then
err = "failed to get ngx_var.remote_port"
return nil, err, nil
end
local local_ip = ngx_var.server_addr
if not local_ip then
err = "failed to get ngx_var.server_addr"
return nil, err, nil
end
local local_port = ngx_var.server_port
if not local_port then
err = "failed to get ngx_var.server_port"
return nil, err, nil
end
local extra = buffer:new({
KEY_EXTRA_UUID, ":", uuid.generate_v4(), "\n",
KEY_EXTRA_REMOTE_ADDR, ":", src_ip, "\n",
KEY_EXTRA_REMOTE_PORT, ":", src_port, "\n",
KEY_EXTRA_LOCAL_ADDR, ":", local_ip, "\n",
KEY_EXTRA_LOCAL_PORT, ":", local_port, "\n",
KEY_EXTRA_SCHEME, ":", ngx_var.scheme, "\n",
KEY_EXTRA_SERVER_NAME, ":", ngx_var.server_name, "\n",
KEY_EXTRA_PROXY_NAME, ":", ngx_var.hostname, "\n",
KEY_EXTRA_REQ_BEGIN_TIME, ":", fmt("%.0f", ngx_req.start_time() * 1000000), "\n",
KEY_EXTRA_HAS_RSP_IF_OK, ":n\n",
KEY_EXTRA_HAS_RSP_IF_BLOCK, ":n\n"
})
return true, nil, extra
end
local function do_send(sock, data)
local ok, err = sock:send(data)
if not ok then
return ok, err
end
return true, nil
end
local function receive_data(s, srv)
local t = {}
local ft = true
local finished
repeat
local err
local tag, length, packet, rsp_body
packet, err = s:receive(consts.T1K_HEADER_SIZE)
if err then
err = fmt("failed to receive info packet from t1k server %s: %s", srv, err)
return nil, err, nil
end
if not packet then
err = fmt("empty packet from t1k server %s", srv)
return nil, err, nil
end
if ft then
if not utils.is_mask_first(byte(packet, 1, 1)) then
err = fmt("first packet is not MASK_FIRST from t1k server %s", srv)
return nil, err, nil
end
ft = false
end
finished, tag, length = utils.packet_parser(packet)
if length > 0 then
rsp_body, err = s:receive(length)
if not rsp_body or #rsp_body ~= length then
err = fmt("failed to receive payload from t1k server %s: %s", srv, err)
return nil, err, nil
end
t[tag] = rsp_body
end
until (finished)
return true, nil, t
end
local function get_socket(opts)
local ok, err
local count, sock, server
sock, err = ngx_socket.tcp()
if not sock then
err = fmt("failed to create socket: %s", err)
return nil, err, nil, nil
end
sock:settimeouts(opts.connect_timeout, opts.send_timeout, opts.read_timeout)
if opts.uds then
server = opts.host
ok, err = sock:connect(opts.host)
else
server = fmt("%s:%d", opts.host, opts.port)
ok, err = sock:connect(opts.host, opts.port)
end
if not ok then
sock:close()
err = fmt("failed to connect to t1k server %s: %s", server, err)
return ok, err, nil, nil
end
nlog(debug_fmt("successfully connected to t1k server %s", server))
count, err = sock:getreusedtimes()
if not count then
nlog(warn_fmt("failed to get reused times from t1k server %s: %s", server, err))
end
if count == 0 then
ok, err = sock:setoption("keepalive", true)
if not ok then
nlog(warn_fmt("failed to set keepalive for t1k server %s: %s", server, err))
end
ok, err = sock:setoption("reuseaddr", true)
if not ok then
nlog(warn_fmt("failed to set reuseaddr for t1k server %s: %s", server, err))
end
ok, err = sock:setoption("tcp-nodelay", true)
if not ok then
nlog(warn_fmt("failed to set tcp-nodelay for t1k server %s: %s", server, err))
end
end
return true, nil, sock, server
end
local function do_socket(opts, header, body, extra)
local ok, err
local t, sock, server
ok, err, sock, server = get_socket(opts)
if not ok then
err = fmt("failed to get socket: %s", err)
return ok, err, nil
end
ok, err = do_send(sock, { char(TAG_HEAD_WITH_MASK_FIRST), utils.int_to_char_length(header:len()), header })
if not ok then
sock:close()
err = fmt("failed to send header data to t1k server %s: %s", server, err)
return ok, err, nil
end
if body ~= nil then
ok, err = do_send(sock, { char(consts.TAG_BODY), utils.int_to_char_length(body:len()), body })
if not ok then
sock:close()
err = fmt("failed to send body data to t1k server %s: %s", server, err)
return ok, err, nil
end
end
ok, err = do_send(sock, { T1K_PROTO_DATA, char(TAG_EXTRA_WITH_MASK_LAST), utils.int_to_char_length(extra:len()), extra })
if not ok then
sock:close()
err = fmt("failed to send extra data to t1k server %s: %s", server, err)
return ok, err, nil
end
ok, err, t = receive_data(sock, server)
if not ok then
return ok, err, nil
end
ok, err = sock:setkeepalive(opts.keepalive_timeout, opts.keepalive_size)
if not ok then
nlog(warn_fmt("failed to set keepalive: %s", err))
sock:close()
end
return true, nil, t
end
function _M.do_request(opts)
local ok, err
local header, body, extra, t
ok, err, header = build_header(opts)
if not ok then
return ok, err, nil
end
ok, err, body = build_body(opts)
if not ok then
return ok, err, nil
end
ok, err, extra = build_extra(opts)
if not ok then
return ok, err, nil
end
ok, err, t = do_socket(opts, header, body, extra)
if not ok then
return ok, err, nil
end
if opts.mode == consts.MODE_BLOCK then
local extra_header = t[consts.TAG_EXTRA_HEADER]
if extra_header then
ngx.ctx.t1k_extra_header = extra_header
end
end
local result = {
action = t[consts.TAG_HEAD],
status = t[consts.TAG_BODY],
event_id = utils.get_event_id(t[consts.TAG_EXTRA_BODY]),
}
return true, nil, result
end
return _M
================================================
FILE: sdk/lua-resty-t1k/lib/resty/t1k/utils.lua
================================================
local consts = require "resty.t1k.constants"
local _M = {
_VERSION = '1.0.0'
}
local band = bit.band
local bnot = bit.bnot
local lshift = bit.lshift
local rshift = bit.rshift
local abs = math.abs
local char = string.char
local ngx = ngx
local ngx_re = ngx.re
local re_match = ngx_re.match
local re_gmatch = ngx_re.gmatch
local re_gsub = ngx_re.gsub
local NOT_MASK_FIRST = bnot(consts.MASK_FIRST)
local NOT_MASK_LAST = bnot(consts.MASK_LAST)
function _M.int_to_char_length(x)
return char(band(x, 0xff)) .. char(band(rshift(x, 8), 0xff)) ..
char(band(rshift(x, 16), 0xff)) .. char(band(rshift(x, 24), 0xff))
end
function _M.char_to_int_length(l)
return l:byte(1, 1) + lshift(l:byte(2, 2), 8) + lshift(l:byte(3, 3), 16) + lshift(l:byte(4, 4), 24)
end
function _M.is_mask_first(b)
return band(b, consts.MASK_FIRST) == consts.MASK_FIRST
end
function _M.is_mask_last(b)
return band(b, consts.MASK_LAST) == consts.MASK_LAST
end
local function tag_parser(tag)
return band(band(tag, NOT_MASK_FIRST), NOT_MASK_LAST)
end
function _M.packet_parser(header)
if #header ~= consts.T1K_HEADER_SIZE then
return true, nil, 0
end
local fb = header:byte(1, 1)
local finished = _M.is_mask_last(fb)
local tag = tag_parser(fb)
local length = _M.char_to_int_length(header:sub(2, 5))
return finished, tag, length
end
function _M.starts_with(str, start)
return str:sub(1, #start) == start
end
function _M.to_var_idx(o)
local var = o
local idx
local _, p = o:find(":")
if p then
var = o:sub(1, p - 1)
idx = tonumber(o:sub(p + 1))
end
var = re_gsub(var:lower(), "-", "_")
if not _M.starts_with(var, consts.NGX_HTTP_HEADER_PREFIX) then
var = consts.NGX_HTTP_HEADER_PREFIX .. var
end
return var, idx
end
function _M.get_indexed_element(str, idx)
if not str or not idx or idx == 0 then
return str
end
local it, err = re_gmatch(str, [[([^,\s]+)]], "jo")
if err then
return nil
end
local t = {}
for m, e in it do
if e then
return nil
end
table.insert(t, m[1])
end
local len = #t
if len < abs(idx) then
return nil
end
return t[idx > 0 and idx or (len + idx + 1)]
end
function _M.get_event_id(str)
if not str then
return nil
end
local m, err = re_match(str, [[]], "jo")
if err then
return nil
end
if m then
return m[1]
end
return nil
end
return _M
================================================
FILE: sdk/lua-resty-t1k/lib/resty/t1k/uuid.lua
================================================
local bit = require "bit"
local ffi = require "ffi"
local log = require "resty.t1k.log"
local _M = {
_VERSION = '1.0.0'
}
local C = ffi.C
local N_BYTES = 32
local random = math.random
local nlog = ngx.log
local warn_fmt = log.warn_fmt
ffi.cdef [[
int RAND_bytes(unsigned char *buf, int num);
]]
local function _rand_bytes(buf, len)
return C.RAND_bytes(buf, len)
end
local function rand_bytes(len)
local buf = ffi.new("char[?]", len)
local ok, ret = pcall(_rand_bytes, buf, len)
if not ok or ret ~= 1 then
nlog(warn_fmt("call RAND_bytes failed: %s", ret))
return nil
end
return ffi.string(buf, len)
end
do
local band = bit.band
local bor = bit.bor
local tohex = bit.tohex
local fmt = string.format
local byte = string.byte
function _M.generate_v4()
local bytes = rand_bytes(N_BYTES)
-- fallback to math.random based method
if not bytes then
return (fmt('%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s',
tohex(random(0, 255), 2),
tohex(random(0, 255), 2),
tohex(random(0, 255), 2),
tohex(random(0, 255), 2),
tohex(random(0, 255), 2),
tohex(random(0, 255), 2),
tohex(bor(band(random(0, 255), 0x0F), 0x40), 2),
tohex(random(0, 255), 2),
tohex(bor(band(random(0, 255), 0x3F), 0x80), 2),
tohex(random(0, 255), 2),
tohex(random(0, 255), 2),
tohex(random(0, 255), 2),
tohex(random(0, 255), 2),
tohex(random(0, 255), 2),
tohex(random(0, 255), 2),
tohex(random(0, 255), 2)))
end
return fmt('%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s',
tohex(byte(bytes, 1, 2), 2),
tohex(byte(bytes, 3, 4), 2),
tohex(byte(bytes, 5, 6), 2),
tohex(byte(bytes, 7, 8), 2),
tohex(byte(bytes, 9, 10), 2),
tohex(byte(bytes, 11, 12), 2),
tohex(bor(band(byte(bytes, 13, 14), 0x0F), 0x40), 2),
tohex(byte(bytes, 15, 16), 2),
tohex(bor(band(byte(bytes, 17, 18), 0x3F), 0x80), 2),
tohex(byte(bytes, 19, 20), 2),
tohex(byte(bytes, 21, 22), 2),
tohex(byte(bytes, 23, 24), 2),
tohex(byte(bytes, 25, 26), 2),
tohex(byte(bytes, 27, 28), 2),
tohex(byte(bytes, 29, 30), 2),
tohex(byte(bytes, 31, 32), 2))
end
end
return setmetatable(_M, {
__call = _M.generate_v4
})
================================================
FILE: sdk/lua-resty-t1k/lib/resty/t1k.lua
================================================
local consts = require "resty.t1k.constants"
local filter = require "resty.t1k.filter"
local handler = require "resty.t1k.handler"
local log = require "resty.t1k.log"
local request = require "resty.t1k.request"
local utils = require "resty.t1k.utils"
local lower = string.lower
local ngx = ngx
local nlog = ngx.log
local log_fmt = log.fmt
local debug_fmt = log.debug_fmt
local _M = {
_VERSION = '1.0.0'
}
local DEFAULT_T1K_CONNECT_TIMEOUT = 1000 -- 1s
local DEFAULT_T1K_SEND_TIMEOUT = 1000 -- 1s
local DEFAULT_T1K_READ_TIMEOUT = 1000 -- 1s
local DEFAULT_T1K_REQ_BODY_SIZE = 1024 -- 1024 KB
local DEFAULT_T1K_KEEPALIVE_SIZE = 256
local DEFAULT_T1K_KEEPALIVE_TIMEOUT = 60 * 1000 -- 60s
function _M.do_access(t, handle)
local ok, err, result
local opts = {}
t = t or {}
if not t.mode then
return true, nil, nil
end
opts.mode = lower(t.mode)
if opts.mode == consts.MODE_OFF then
nlog(debug_fmt("t1k is not enabled"))
return true, nil, nil
end
if opts.mode ~= consts.MODE_OFF and opts.mode ~= consts.MODE_BLOCK and opts.mode ~= consts.MODE_MONITOR then
err = log_fmt("invalid t1k mode: %s", t.mode)
return nil, err, nil
end
if not t.host then
err = log_fmt("invalid t1k host: %s", t.host)
return nil, err, nil
end
opts.host = t.host
if utils.starts_with(opts.host, consts.UNIX_SOCK_PREFIX) then
opts.uds = true
else
if not tonumber(t.port) then
err = log_fmt("invalid t1k port: %s", t.port)
return nil, err, nil
end
opts.port = tonumber(t.port)
end
opts.connect_timeout = t.connect_timeout or DEFAULT_T1K_CONNECT_TIMEOUT
opts.send_timeout = t.send_timeout or DEFAULT_T1K_SEND_TIMEOUT
opts.read_timeout = t.read_timeout or DEFAULT_T1K_READ_TIMEOUT
opts.req_body_size = t.req_body_size or DEFAULT_T1K_REQ_BODY_SIZE
opts.keepalive_size = t.keepalive_size or DEFAULT_T1K_KEEPALIVE_SIZE
opts.keepalive_timeout = t.keepalive_timeout or DEFAULT_T1K_KEEPALIVE_TIMEOUT
if t.remote_addr then
local var, idx = utils.to_var_idx(t.remote_addr)
opts.remote_addr_var = var
opts.remote_addr_idx = idx
end
ok, err, result = request.do_request(opts)
if not ok then
return ok, err, result
end
if handle and opts.mode == consts.MODE_BLOCK then
ok, err = _M.do_handle(result)
end
return ok, err, result
end
function _M.do_handle(t)
local ok, err = handler.handle(t)
return ok, err
end
function _M.do_header_filter()
filter.do_header_filter()
end
return _M
================================================
FILE: sdk/lua-resty-t1k/mainspec/lua-resty-t1k-main-0-0.rockspec
================================================
package = "lua-resty-t1k-main"
version = "0-0"
source = {
url = "git://github.com/chaitin/lua-resty-t1k",
branch = "main",
}
description = {
summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall",
detailed = [[
Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.
]],
homepage = "https://github.com/chaitin/lua-resty-t1k",
license = "Apache License 2.0",
maintainer = "Xudong Wang "
}
build = {
type = "builtin",
modules = {
["resty.t1k"] = "lib/resty/t1k.lua",
["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua",
["resty.t1k.constants"] = "lib/resty/t1k/constants.lua",
["resty.t1k.file"] = "lib/resty/t1k/file.lua",
["resty.t1k.filter"] = "lib/resty/t1k/filter.lua",
["resty.t1k.handler"] = "lib/resty/t1k/handler.lua",
["resty.t1k.log"] = "lib/resty/t1k/log.lua",
["resty.t1k.request"] = "lib/resty/t1k/request.lua",
["resty.t1k.utils"] = "lib/resty/t1k/utils.lua",
["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua",
},
}
================================================
FILE: sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.0.0-0.rockspec
================================================
package = "lua-resty-t1k"
version = "1.0.0-0"
source = {
url = "git://github.com/chaitin/lua-resty-t1k",
tag = "v1.0.0"
}
description = {
summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall",
detailed = [[
Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.
]],
homepage = "https://github.com/chaitin/lua-resty-t1k",
license = "Apache License 2.0",
maintainer = "Xudong Wang "
}
build = {
type = "builtin",
modules = {
["resty.t1k"] = "lib/resty/t1k.lua",
["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua",
["resty.t1k.constants"] = "lib/resty/t1k/constants.lua",
["resty.t1k.file"] = "lib/resty/t1k/file.lua",
["resty.t1k.filter"] = "lib/resty/t1k/filter.lua",
["resty.t1k.handler"] = "lib/resty/t1k/handler.lua",
["resty.t1k.log"] = "lib/resty/t1k/log.lua",
["resty.t1k.request"] = "lib/resty/t1k/request.lua",
["resty.t1k.utils"] = "lib/resty/t1k/utils.lua",
["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua",
},
}
================================================
FILE: sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.0.1-0.rockspec
================================================
package = "lua-resty-t1k"
version = "1.0.1-0"
source = {
url = "git://github.com/chaitin/lua-resty-t1k",
tag = "v1.0.1"
}
description = {
summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall",
detailed = [[
Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.
]],
homepage = "https://github.com/chaitin/lua-resty-t1k",
license = "Apache License 2.0",
maintainer = "Xudong Wang "
}
build = {
type = "builtin",
modules = {
["resty.t1k"] = "lib/resty/t1k.lua",
["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua",
["resty.t1k.constants"] = "lib/resty/t1k/constants.lua",
["resty.t1k.file"] = "lib/resty/t1k/file.lua",
["resty.t1k.filter"] = "lib/resty/t1k/filter.lua",
["resty.t1k.handler"] = "lib/resty/t1k/handler.lua",
["resty.t1k.log"] = "lib/resty/t1k/log.lua",
["resty.t1k.request"] = "lib/resty/t1k/request.lua",
["resty.t1k.utils"] = "lib/resty/t1k/utils.lua",
["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua",
},
}
================================================
FILE: sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.0.2-0.rockspec
================================================
package = "lua-resty-t1k"
version = "1.0.2-0"
source = {
url = "git://github.com/chaitin/lua-resty-t1k",
tag = "v1.0.2"
}
description = {
summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall",
detailed = [[
Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.
]],
homepage = "https://github.com/chaitin/lua-resty-t1k",
license = "Apache License 2.0",
maintainer = "Xudong Wang "
}
build = {
type = "builtin",
modules = {
["resty.t1k"] = "lib/resty/t1k.lua",
["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua",
["resty.t1k.constants"] = "lib/resty/t1k/constants.lua",
["resty.t1k.file"] = "lib/resty/t1k/file.lua",
["resty.t1k.filter"] = "lib/resty/t1k/filter.lua",
["resty.t1k.handler"] = "lib/resty/t1k/handler.lua",
["resty.t1k.log"] = "lib/resty/t1k/log.lua",
["resty.t1k.request"] = "lib/resty/t1k/request.lua",
["resty.t1k.utils"] = "lib/resty/t1k/utils.lua",
["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua",
},
}
================================================
FILE: sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.0.3-0.rockspec
================================================
package = "lua-resty-t1k"
version = "1.0.3-0"
source = {
url = "git://github.com/chaitin/lua-resty-t1k",
tag = "v1.0.3"
}
description = {
summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall",
detailed = [[
Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.
]],
homepage = "https://github.com/chaitin/lua-resty-t1k",
license = "Apache License 2.0",
maintainer = "Xudong Wang "
}
build = {
type = "builtin",
modules = {
["resty.t1k"] = "lib/resty/t1k.lua",
["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua",
["resty.t1k.constants"] = "lib/resty/t1k/constants.lua",
["resty.t1k.file"] = "lib/resty/t1k/file.lua",
["resty.t1k.filter"] = "lib/resty/t1k/filter.lua",
["resty.t1k.handler"] = "lib/resty/t1k/handler.lua",
["resty.t1k.log"] = "lib/resty/t1k/log.lua",
["resty.t1k.request"] = "lib/resty/t1k/request.lua",
["resty.t1k.utils"] = "lib/resty/t1k/utils.lua",
["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua",
},
}
================================================
FILE: sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.0-0.rockspec
================================================
package = "lua-resty-t1k"
version = "1.1.0-0"
source = {
url = "git://github.com/chaitin/lua-resty-t1k",
tag = "v1.1.0"
}
description = {
summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall",
detailed = [[
Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.
]],
homepage = "https://github.com/chaitin/lua-resty-t1k",
license = "Apache License 2.0",
maintainer = "Xudong Wang "
}
build = {
type = "builtin",
modules = {
["resty.t1k"] = "lib/resty/t1k.lua",
["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua",
["resty.t1k.constants"] = "lib/resty/t1k/constants.lua",
["resty.t1k.file"] = "lib/resty/t1k/file.lua",
["resty.t1k.filter"] = "lib/resty/t1k/filter.lua",
["resty.t1k.handler"] = "lib/resty/t1k/handler.lua",
["resty.t1k.log"] = "lib/resty/t1k/log.lua",
["resty.t1k.request"] = "lib/resty/t1k/request.lua",
["resty.t1k.utils"] = "lib/resty/t1k/utils.lua",
["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua",
},
}
================================================
FILE: sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.1-0.rockspec
================================================
package = "lua-resty-t1k"
version = "1.1.1-0"
source = {
url = "git://github.com/chaitin/lua-resty-t1k",
tag = "v1.1.1"
}
description = {
summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall",
detailed = [[
Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.
]],
homepage = "https://github.com/chaitin/lua-resty-t1k",
license = "Apache License 2.0",
maintainer = "Xudong Wang "
}
build = {
type = "builtin",
modules = {
["resty.t1k"] = "lib/resty/t1k.lua",
["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua",
["resty.t1k.constants"] = "lib/resty/t1k/constants.lua",
["resty.t1k.file"] = "lib/resty/t1k/file.lua",
["resty.t1k.filter"] = "lib/resty/t1k/filter.lua",
["resty.t1k.handler"] = "lib/resty/t1k/handler.lua",
["resty.t1k.log"] = "lib/resty/t1k/log.lua",
["resty.t1k.request"] = "lib/resty/t1k/request.lua",
["resty.t1k.utils"] = "lib/resty/t1k/utils.lua",
["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua",
},
}
================================================
FILE: sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.2-0.rockspec
================================================
package = "lua-resty-t1k"
version = "1.1.2-0"
source = {
url = "git://github.com/chaitin/lua-resty-t1k",
tag = "v1.1.2"
}
description = {
summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall",
detailed = [[
Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.
]],
homepage = "https://github.com/chaitin/lua-resty-t1k",
license = "Apache License 2.0",
maintainer = "Xudong Wang "
}
build = {
type = "builtin",
modules = {
["resty.t1k"] = "lib/resty/t1k.lua",
["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua",
["resty.t1k.constants"] = "lib/resty/t1k/constants.lua",
["resty.t1k.file"] = "lib/resty/t1k/file.lua",
["resty.t1k.filter"] = "lib/resty/t1k/filter.lua",
["resty.t1k.handler"] = "lib/resty/t1k/handler.lua",
["resty.t1k.log"] = "lib/resty/t1k/log.lua",
["resty.t1k.request"] = "lib/resty/t1k/request.lua",
["resty.t1k.utils"] = "lib/resty/t1k/utils.lua",
["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua",
},
}
================================================
FILE: sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.3-0.rockspec
================================================
package = "lua-resty-t1k"
version = "1.1.3-0"
source = {
url = "git://github.com/chaitin/lua-resty-t1k",
tag = "v1.1.3"
}
description = {
summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall",
detailed = [[
Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.
]],
homepage = "https://github.com/chaitin/lua-resty-t1k",
license = "Apache License 2.0",
maintainer = "Xudong Wang "
}
build = {
type = "builtin",
modules = {
["resty.t1k"] = "lib/resty/t1k.lua",
["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua",
["resty.t1k.constants"] = "lib/resty/t1k/constants.lua",
["resty.t1k.file"] = "lib/resty/t1k/file.lua",
["resty.t1k.filter"] = "lib/resty/t1k/filter.lua",
["resty.t1k.handler"] = "lib/resty/t1k/handler.lua",
["resty.t1k.log"] = "lib/resty/t1k/log.lua",
["resty.t1k.request"] = "lib/resty/t1k/request.lua",
["resty.t1k.utils"] = "lib/resty/t1k/utils.lua",
["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua",
},
}
================================================
FILE: sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.4-0.rockspec
================================================
package = "lua-resty-t1k"
version = "1.1.4-0"
source = {
url = "git://github.com/chaitin/lua-resty-t1k",
tag = "v1.1.4"
}
description = {
summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall",
detailed = [[
Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.
]],
homepage = "https://github.com/chaitin/lua-resty-t1k",
license = "Apache License 2.0",
maintainer = "Xudong Wang "
}
build = {
type = "builtin",
modules = {
["resty.t1k"] = "lib/resty/t1k.lua",
["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua",
["resty.t1k.constants"] = "lib/resty/t1k/constants.lua",
["resty.t1k.file"] = "lib/resty/t1k/file.lua",
["resty.t1k.filter"] = "lib/resty/t1k/filter.lua",
["resty.t1k.handler"] = "lib/resty/t1k/handler.lua",
["resty.t1k.log"] = "lib/resty/t1k/log.lua",
["resty.t1k.request"] = "lib/resty/t1k/request.lua",
["resty.t1k.utils"] = "lib/resty/t1k/utils.lua",
["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua",
},
}
================================================
FILE: sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.5-0.rockspec
================================================
package = "lua-resty-t1k"
version = "1.1.5-0"
source = {
url = "git://github.com/chaitin/lua-resty-t1k",
tag = "v1.1.5"
}
description = {
summary = "Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall",
detailed = [[
Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.
]],
homepage = "https://github.com/chaitin/lua-resty-t1k",
license = "Apache License 2.0",
maintainer = "Xudong Wang "
}
build = {
type = "builtin",
modules = {
["resty.t1k"] = "lib/resty/t1k.lua",
["resty.t1k.buffer"] = "lib/resty/t1k/buffer.lua",
["resty.t1k.constants"] = "lib/resty/t1k/constants.lua",
["resty.t1k.file"] = "lib/resty/t1k/file.lua",
["resty.t1k.filter"] = "lib/resty/t1k/filter.lua",
["resty.t1k.handler"] = "lib/resty/t1k/handler.lua",
["resty.t1k.log"] = "lib/resty/t1k/log.lua",
["resty.t1k.request"] = "lib/resty/t1k/request.lua",
["resty.t1k.utils"] = "lib/resty/t1k/utils.lua",
["resty.t1k.uuid"] = "lib/resty/t1k/uuid.lua",
},
}
================================================
FILE: sdk/lua-resty-t1k/t/buffer.t
================================================
use Test::Nginx::Socket;
our $HttpConfig = <<'_EOC_';
lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;";
_EOC_
repeat_each(3);
plan tests => repeat_each() * (blocks() * 3);
run_tests();
__DATA__
=== TEST 1: buffer add
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local buffer = require "resty.t1k.buffer"
local b = buffer:new()
b:add("hello")
b:add(" ")
b:add("world")
b:add("!")
ngx.say(b[1], b[2], b[3], b[4])
ngx.say(b:len())
}
}
--- request
GET /t
--- response_body
hello world!
12
--- no_error_log
[error]
================================================
FILE: sdk/lua-resty-t1k/t/file.t
================================================
use Test::Nginx::Socket;
our $HttpConfig = <<'_EOC_';
lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;";
_EOC_
repeat_each(3);
plan tests => repeat_each() * (blocks() * 3);
run_tests();
__DATA__
=== TEST 1: read full file
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local file = require "resty.t1k.file"
local path = ngx.var.document_root .. "/foo.bar"
local ok, err, content = file.read(path, 2 ^ 15)
if not ok then
ngx.say(err)
end
ngx.print(table.concat(content))
}
}
--- user_files eval
[
["foo.bar" => "a" x (2 ** 14) ],
]
--- request
GET /t
--- response_body eval
"a" x (2 ** 14)
--- no_error_log
[error]
=== TEST 2: read partial file
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local file = require "resty.t1k.file"
local path = ngx.var.document_root .. "/foo.bar"
local ok, err, content = file.read(path, 2 ^ 13)
if not ok then
ngx.say(err)
end
ngx.print(table.concat(content))
}
}
--- user_files eval
[
["foo.bar" => "a" x (2 ** 14) ],
]
--- request
GET /t
--- response_body eval
"a" x (2 ** 13)
--- no_error_log
[error]
=== TEST 3: read negative bytes
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local file = require "resty.t1k.file"
local path = ngx.var.document_root .. "/foo.bar"
local ok, err, content = file.read(path, -1)
if not ok then
ngx.say(err)
end
ngx.print(table.concat(content))
}
}
--- user_files
>>> foo.bar
--- request
GET /t
--- response_body eval
""
--- no_error_log
[error]
=== TEST 4: read empty file
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local file = require "resty.t1k.file"
local path = ngx.var.document_root .. "/foo.bar"
local ok, err, content = file.read(path, 1)
if not ok then
ngx.say(err)
end
ngx.print(table.concat(content))
}
}
--- user_files
>>> foo.bar
--- request
GET /t
--- response_body eval
""
--- no_error_log
[error]
=== TEST 5: read non-existent file
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local file = require "resty.t1k.file"
local ok, err, buffer = file.read("/opt/non_existent_file", 0)
if not ok then
ngx.say(err)
end
}
}
--- request
GET /t
--- response_body
/opt/non_existent_file: No such file or directory
--- no_error_log
[error]
================================================
FILE: sdk/lua-resty-t1k/t/filter.t
================================================
use Test::Nginx::Socket;
our $HttpConfig = <<'_EOC_';
lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;";
_EOC_
repeat_each(3);
plan tests => repeat_each() * (blocks() * 5);
run_tests();
__DATA__
=== TEST 1: do_header_filter
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
ngx.ctx.t1k_extra_header = "k1:v1\nk2:v2\nk3:v3\n"
}
header_filter_by_lua_block {
local filter = require "resty.t1k.filter"
filter.do_header_filter()
}
content_by_lua_block {
ngx.say("hi")
}
}
--- request
GET /t
--- response_headers
k1: v1
k2: v2
k3: v3
--- no_error_log
[error]
================================================
FILE: sdk/lua-resty-t1k/t/handler.t
================================================
use Test::Nginx::Socket;
our $HttpConfig = <<'_EOC_';
lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;";
_EOC_
repeat_each(3);
plan tests => repeat_each() * (blocks() * 3);
run_tests();
__DATA__
=== TEST 1: handle passed action
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local handler = require "resty.t1k.handler"
local t = {
action = ".",
}
local ok, err = handler.handle(t)
if not ok then
ngx.log(ngx.ERR, err)
end
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t
--- response_body
passed
--- no_error_log
[error]
=== TEST 2: handle blocked action
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local handler = require "resty.t1k.handler"
local t = {
action = "?",
status = 405,
event_id = "c0c039a7c348486eaffd9e2f9846b66b",
}
local ok, err = handler.handle(t)
if not ok then
ngx.log(ngx.ERR, err)
end
}
header_filter_by_lua_block {
local filter = require "resty.t1k.filter"
filter.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t
--- response_body
{"code": 405, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "c0c039a7c348486eaffd9e2f9846b66b"}
--- error_code eval
"405"
--- no_error_log
[error]
--- no_error_log
[error]
=== TEST 3: handle unknown action
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local handler = require "resty.t1k.handler"
local t = {
action = "~"
}
local ok, err = handler.handle(t)
if not ok then
ngx.log(ngx.ERR, err)
end
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t
--- response_body
passed
--- error_log
lua-resty-t1k: unknown action from t1k server: ~
=== TEST 4: handle nil result
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local handler = require "resty.t1k.handler"
local ok, err = handler.handle(nil)
if not ok then
ngx.log(ngx.ERR, err)
end
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t
--- response_body
passed
--- error_log
lua-resty-t1k: invalid result type: nil
================================================
FILE: sdk/lua-resty-t1k/t/integration.t
================================================
use Test::Nginx::Socket 'no_plan';
our $MainConfig = <<'_EOC_';
env DETECTOR_IP;
_EOC_
our $HttpConfig = <<'_EOC_';
lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;";
lua_socket_log_errors off;
_EOC_
run_tests();
__DATA__
=== TEST 1: integration test blocked
--- main_config eval: $::MainConfig
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block",
host = os.getenv("DETECTOR_IP"),
port = 8000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
if t.mode ~= "block" then
ngx.log(ngx.DEBUG, "skip blocking")
return
end
ok, err = t1k.do_handle(result)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t/shell.php
--- response_headers
Content-Type: application/json
--- response_body_like eval
'^{"code": 403, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": ".*"}$'
--- error_code: 403
--- no_error_log
[error]
--- error_log eval
"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000"
--- log_level: debug
--- skip_eval
4: not exists($ENV{DETECTOR_IP})
=== TEST 2: integration test blocked internal handle
--- main_config eval: $::MainConfig
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block",
host = os.getenv("DETECTOR_IP"),
port = 8000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t, true)
if not ok then
ngx.log(ngx.ERR, err)
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t/shell.php
--- response_headers
Content-Type: application/json
--- response_body_like eval
'^{"code": 403, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": ".*"}$'
--- error_code: 403
--- no_error_log
[error]
--- error_log eval
"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000"
--- log_level: debug
--- skip_eval
4: not exists($ENV{DETECTOR_IP})
=== TEST 3: integration test blocked http2
--- main_config eval: $::MainConfig
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block",
host = os.getenv("DETECTOR_IP"),
port = 8000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
if t.mode ~= "block" then
ngx.log(ngx.DEBUG, "skip blocking")
return
end
ok, err = t1k.do_handle(result)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- http2
--- request
GET /t/shell.php
--- response_headers
Content-Type: application/json
--- response_body_like eval
'^{"code": 403, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": ".*"}$'
--- error_code: 403
--- no_error_log
[error]
--- error_log eval
"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000"
--- log_level: debug
--- skip_eval
4: not exists($ENV{DETECTOR_IP})
=== TEST 4: integration test blocked http2 internal handle
--- main_config eval: $::MainConfig
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block",
host = os.getenv("DETECTOR_IP"),
port = 8000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t, true)
if not ok then
ngx.log(ngx.ERR, err)
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- http2
--- request
GET /t/shell.php
--- response_headers
Content-Type: application/json
--- response_body_like eval
'^{"code": 403, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": ".*"}$'
--- error_code: 403
--- no_error_log
[error]
--- error_log eval
"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000"
--- log_level: debug
--- skip_eval
4: not exists($ENV{DETECTOR_IP})
=== TEST 5: integration test monitor
--- main_config eval: $::MainConfig
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "monitor",
host = os.getenv("DETECTOR_IP"),
port = 8000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
if t.mode ~= "block" then
ngx.log(ngx.DEBUG, "skip blocking")
return
end
ok, err = t1k.do_handle(result)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t/shell.php
--- response_body
passed
--- no_error_log
[error]
--- error_log eval
"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000"
--- log_level: debug
--- skip_eval
4: not exists($ENV{DETECTOR_IP})
=== TEST 6: integration test monitor internal handle
--- main_config eval: $::MainConfig
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "monitor",
host = os.getenv("DETECTOR_IP"),
port = 8000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t, true)
if not ok then
ngx.log(ngx.ERR, err)
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t/shell.php
--- response_body
passed
--- no_error_log
[error]
--- error_log eval
"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000"
--- log_level: debug
--- skip_eval
4: not exists($ENV{DETECTOR_IP})
=== TEST 7: integration test monitor http2
--- main_config eval: $::MainConfig
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "monitor",
host = os.getenv("DETECTOR_IP"),
port = 8000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
if t.mode ~= "block" then
ngx.log(ngx.DEBUG, "skip blocking")
return
end
ok, err = t1k.do_handle(result)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- http2
--- request
GET /t/shell.php
--- response_body
passed
--- no_error_log
[error]
--- error_log eval
["lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000", "skip blocking"]
--- log_level: debug
--- skip_eval
4: not exists($ENV{DETECTOR_IP})
=== TEST 8: integration test monitor http2 internal handle
--- main_config eval: $::MainConfig
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "monitor",
host = os.getenv("DETECTOR_IP"),
port = 8000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t, true)
if not ok then
ngx.log(ngx.ERR, err)
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- http2
--- request
GET /t/shell.php
--- response_body
passed
--- no_error_log
[error]
--- error_log eval
"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000"
--- log_level: debug
--- skip_eval
4: not exists($ENV{DETECTOR_IP})
=== TEST 9: integration test disabled
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "off",
}
local ok, err, result = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
if t.mode ~= "block" then
ngx.log(ngx.DEBUG, "skip blocking")
return
end
ok, err = t1k.do_handle(result)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t/shell.php
--- response_body
passed
--- no_error_log
[error]
--- error_log
lua-resty-t1k: t1k is not enabled
skip blocking
--- log_level: debug
=== TEST 10: integration test configuration priority
--- main_config eval: $::MainConfig
--- http_config eval: $::HttpConfig
--- config
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block",
host = os.getenv("DETECTOR_IP"),
port = 8000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
if t.mode ~= "block" then
ngx.log(ngx.DEBUG, "skip blocking")
return
end
ok, err = t1k.do_handle(result)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
location /pass {
access_by_lua_block {
}
content_by_lua_block {
ngx.say("passed")
}
}
location /block {
content_by_lua_block {
ngx.say("there must be a problem when you see this line")
}
}
--- request eval
["GET /pass/shell.php", "GET /block/shell.php"]
--- response_body_like eval
["passed", '^{"code": 403, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": ".*"}$']
--- error_code eval
[200, 403]
--- no_error_log
[error]
--- skip_eval
6: not exists($ENV{DETECTOR_IP})
=== TEST 11: integration test blocked extra headers
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
if t.mode ~= "block" then
ngx.log(ngx.DEBUG, "skip blocking")
return
end
ok, err = t1k.do_handle(result)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- tcp_listen: 18000
--- tcp_reply eval
"\x41\x01\x00\x00\x00?\x02\x03\x00\x00\x00405\x23\x12\x00\x00\x00k1:v1\x0ak2:v2\x0ak3:v3\x0a\xa4\x33\x00\x00\x00"
--- request
GET /t/shell.php
--- response_headers
k1: v1
k2: v2
k3: v3
--- response_headers
Content-Type: application/json
--- response_body
{"code": 405, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "c0c039a7c348486eaffd9e2f9846b66b"}
--- error_code eval
"405"
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000
--- log_level: debug
=== TEST 12: integration test blocked extra headers internal handle
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t, true)
if not ok then
ngx.log(ngx.ERR, err)
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- tcp_listen: 18000
--- tcp_reply eval
"\x41\x01\x00\x00\x00?\x02\x03\x00\x00\x00405\x23\x12\x00\x00\x00k1:v1\x0ak2:v2\x0ak3:v3\x0a\xa4\x33\x00\x00\x00"
--- request
GET /t/shell.php
--- response_headers
k1: v1
k2: v2
k3: v3
--- response_headers
Content-Type: application/json
--- response_body
{"code": 405, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "c0c039a7c348486eaffd9e2f9846b66b"}
--- error_code eval
"405"
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000
--- log_level: debug
=== TEST 13: integration test blocked extra headers with unix domain socket
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block",
host = "unix:t1k.sock",
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
if t.mode ~= "block" then
ngx.log(ngx.DEBUG, "skip blocking")
return
end
ok, err = t1k.do_handle(result)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- tcp_listen: t1k.sock
--- tcp_reply eval
"\x41\x01\x00\x00\x00?\x02\x03\x00\x00\x00405\x23\x12\x00\x00\x00k1:v1\x0ak2:v2\x0ak3:v3\x0a\xa4\x33\x00\x00\x00"
--- request
GET /t/shell.php
--- response_headers
k1: v1
k2: v2
k3: v3
--- response_headers
Content-Type: application/json
--- response_body
{"code": 405, "success":false, "message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "c0c039a7c348486eaffd9e2f9846b66b"}
--- error_code eval
"405"
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server unix:t1k.sock
--- log_level: debug
=== TEST 14: integration test passed extra headers
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
if t.mode ~= "block" then
ngx.log(ngx.DEBUG, "skip blocking")
return
end
ok, err = t1k.do_handle(result)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- tcp_listen: 18000
--- tcp_reply eval
"\x41\x01\x00\x00\x00.\xa3\x12\x00\x00\x00k1:v1\x0ak2:v2\x0ak3:v3\x0a"
--- request
GET /t
--- response_headers
k1: v1
k2: v2
k3: v3
--- response_body
passed
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000
--- log_level: debug
=== TEST 15: integration test passed extra headers internal handle
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t, true)
if not ok then
ngx.log(ngx.ERR, err)
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- tcp_listen: 18000
--- tcp_reply eval
"\x41\x01\x00\x00\x00.\xa3\x12\x00\x00\x00k1:v1\x0ak2:v2\x0ak3:v3\x0a"
--- request
GET /t
--- response_headers
k1: v1
k2: v2
k3: v3
--- response_body
passed
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000
--- log_level: debug
=== TEST 16: integration test passed extra headers with unix domain socket
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block",
host = "unix:t1k.sock",
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
if t.mode ~= "block" then
ngx.log(ngx.DEBUG, "skip blocking")
return
end
ok, err = t1k.do_handle(result)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- tcp_listen: t1k.sock
--- tcp_reply eval
"\x41\x01\x00\x00\x00.\xa3\x12\x00\x00\x00k1:v1\x0ak2:v2\x0ak3:v3\x0a"
--- request
GET /t
--- response_headers
k1: v1
k2: v2
k3: v3
--- response_body
passed
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server unix:t1k.sock
--- log_level: debug
=== TEST 17: integration test monitor extra headers
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "monitor",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
if t.mode ~= "block" then
ngx.log(ngx.DEBUG, "skip blocking")
return
end
ok, err = t1k.do_handle(result)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- tcp_listen: 18000
--- tcp_reply eval
"\x41\x01\x00\x00\x00?\x02\x03\x00\x00\x00405\x23\x12\x00\x00\x00k1:v1\x0ak2:v2\x0ak3:v3\x0a\xa4\x33\x00\x00\x00"
--- request
GET /t/shell.php
--- raw_response_headers_unlike eval
'.*k1: v1\r\n.*'
--- response_body
passed
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000
skip blocking
--- log_level: debug
=== TEST 18: integration test monitor extra headers internal handle
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "monitor",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t, true)
if not ok then
ngx.log(ngx.ERR, err)
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- tcp_listen: 18000
--- tcp_reply eval
"\x41\x01\x00\x00\x00?\x02\x03\x00\x00\x00405\x23\x12\x00\x00\x00k1:v1\x0ak2:v2\x0ak3:v3\x0a\xa4\x33\x00\x00\x00"
--- request
GET /t/shell.php
--- raw_response_headers_unlike eval
'.*k1: v1\r\n.*'
--- response_body
passed
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000
--- log_level: debug
=== TEST 19: integration test monitor extra headers with unix domain socket
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "monitor",
host = "unix:t1k.sock",
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
if t.mode ~= "block" then
ngx.log(ngx.DEBUG, "skip blocking")
return
end
ok, err = t1k.do_handle(result)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
header_filter_by_lua_block {
local t1k = require "resty.t1k"
t1k.do_header_filter()
}
content_by_lua_block {
ngx.say("passed")
}
}
--- tcp_listen: t1k.sock
--- tcp_reply eval
"\x41\x01\x00\x00\x00?\x02\x03\x00\x00\x00405\x23\x12\x00\x00\x00k1:v1\x0ak2:v2\x0ak3:v3\x0a\xa4\x33\x00\x00\x00"
--- request
GET /t/shell.php
--- raw_response_headers_unlike eval
'.*k1: v1\r\n.*'
--- response_body
passed
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server unix:t1k.sock
skip blocking
--- log_level: debug
================================================
FILE: sdk/lua-resty-t1k/t/log.t
================================================
use Test::Nginx::Socket;
our $HttpConfig = <<'_EOC_';
lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;";
_EOC_
repeat_each(3);
plan tests => repeat_each() * (blocks() * 2 + 2);
run_tests();
__DATA__
=== TEST 1: err_fmt
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local log = require "resty.t1k.log"
ngx.log(log.err_fmt("%s - %04d - %.4f", "test", 1, 1))
}
}
--- request
GET /t
[error]
--- error_log
lua-resty-t1k: test - 0001 - 1.0000
--- log_level: error
=== TEST 2: warn_fmt
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local log = require "resty.t1k.log"
ngx.log(log.warn_fmt("%s - %04d - %.4f", "test", 1, 1))
}
}
--- request
GET /t
--- no_error_log
[error]
--- error_log
lua-resty-t1k: test - 0001 - 1.0000
--- log_level: warn
=== TEST 3: debug_fmt
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local log = require "resty.t1k.log"
ngx.log(log.debug_fmt("%s - %04d - %.4f", "test", 1, 1))
}
}
--- request
GET /t
--- no_error_log
[error]
--- error_log
lua-resty-t1k: test - 0001 - 1.0000
--- log_level: debug
================================================
FILE: sdk/lua-resty-t1k/t/option.t
================================================
use Test::Nginx::Socket;
our $HttpConfig = <<'_EOC_';
lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;";
_EOC_
repeat_each(3);
plan tests => repeat_each() * (blocks() * 3 + 1);
run_tests();
__DATA__
=== TEST 1: do_access nil option
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local ok, err, _ = t1k.do_access(nil)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t/shell.php
--- response_body
passed
--- no_error_log
[debug]
--- log_level: debug
=== TEST 2: do_access disabled
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "off",
}
local ok, err, _ = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t/shell.php
--- response_body
passed
--- no_error_log
[error]
--- error_log
lua-resty-t1k: t1k is not enabled
--- log_level: debug
=== TEST 3: do_access invalid mode
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "invalid",
}
local ok, err, _ = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t/shell.php
--- response_body
passed
--- error_log
lua-resty-t1k: invalid t1k mode: invalid
=== TEST 4: do_access invalid host
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block"
}
local ok, err, _ = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t
--- response_body
passed
--- error_log
lua-resty-t1k: invalid t1k host: nil
--- log_level: debug
=== TEST 5: do_access invalid port
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local t1k = require "resty.t1k"
local t = {
mode = "block",
host = "127.0.0.1"
}
local ok, err, _ = t1k.do_access(t)
if not ok then
ngx.log(ngx.ERR, err)
return
end
}
content_by_lua_block {
ngx.say("passed")
}
}
--- request
GET /t
--- response_body
passed
--- error_log
lua-resty-t1k: invalid t1k port: nil
--- log_level: debug
================================================
FILE: sdk/lua-resty-t1k/t/request.t
================================================
use Test::Nginx::Socket;
our $HttpConfig = <<'_EOC_';
lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;";
_EOC_
repeat_each(3);
plan tests => repeat_each() * (blocks() * 3 + 14);
run_tests();
__DATA__
=== TEST 1: do_request blocked
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say(result["action"])
ngx.say(result["status"])
ngx.say(result["event_id"])
}
}
--- tcp_listen: 18000
--- tcp_reply eval
"\x41\x01\x00\x00\x00?\x02\x03\x00\x00\x00405\xa4\x33\x00\x00\x00"
--- request
GET /t/shell.php
--- response_body
?
405
c0c039a7c348486eaffd9e2f9846b66b
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000
--- log_level: debug
=== TEST 2: do_request blocked with unix domain socket
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "unix:t1k.sock",
uds = true,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say(result["action"])
ngx.say(result["status"])
ngx.say(result["event_id"])
}
}
--- tcp_listen: t1k.sock
--- tcp_reply eval
"\x41\x01\x00\x00\x00?\x02\x03\x00\x00\x00405\xa4\x33\x00\x00\x00"
--- request
GET /t/shell.php
--- response_body
?
405
c0c039a7c348486eaffd9e2f9846b66b
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server unix:t1k.sock
--- log_level: debug
=== TEST 3: do_request passed
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say(result["action"])
}
}
--- tcp_listen: 18000
--- tcp_reply eval
"\xc1\x01\x00\x00\x00."
--- request
GET /t
--- response_body
.
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000
--- log_level: debug
=== TEST 4: do_request passed with unix domain socket
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "unix:t1k.sock",
uds = true,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say(result["action"])
}
}
--- tcp_listen: t1k.sock
--- tcp_reply eval
"\xc1\x01\x00\x00\x00."
--- request
GET /t
--- response_body
.
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server unix:t1k.sock
--- log_level: debug
=== TEST 5: do_request trim request body
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 0.0625,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say(result["action"])
}
}
--- tcp_listen: 18000
--- tcp_reply eval
"\xc1\x01\x00\x00\x00."
--- request
GET /t
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
--- response_body
.
--- no_error_log
[error]
--- error_log
lua-resty-t1k: request body is too long: 123 bytes, cut to 64 bytes
lua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000
--- log_level: debug
=== TEST 6: do_request trim request body with unix domain socket
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "unix:t1k.sock",
uds = true,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 0.0625,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say(result["action"])
}
}
--- tcp_listen: t1k.sock
--- tcp_reply eval
"\xc1\x01\x00\x00\x00."
--- request
GET /t
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
--- response_body
.
--- no_error_log
[error]
--- error_log
lua-resty-t1k: request body is too long: 123 bytes, cut to 64 bytes
lua-resty-t1k: successfully connected to t1k server unix:t1k.sock
--- log_level: debug
=== TEST 7: do_request refuse connection
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 0.0625,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say("result: ", result)
}
}
--- request
GET /t
--- response_body
result: nil
--- error_log
failed to connect to t1k server 127.0.0.1:18000
--- log_level: debug
=== TEST 8: do_request refuse connection with unix domain socket
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "unix:t1k.sock",
uds = true,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 0.0625,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say("result: ", result)
}
}
--- request
GET /t
--- response_body
result: nil
--- error_log
failed to connect to t1k server unix:t1k.sock
--- log_level: debug
=== TEST 9: do_request timeout
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 100,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say("result: ", result)
}
}
--- tcp_listen: 18000
--- tcp_reply_delay: 200ms
--- tcp_reply eval
"\x41\x01\x00\x00\x00?\x02\x03\x00\x00\x00405\xa4\x33\x00\x00\x00"
--- request
GET /t/shell.php
--- response_body
result: nil
--- error_log
failed to receive info packet from t1k server 127.0.0.1:18000: timeout
--- log_level: debug
=== TEST 10: do_request timeout with unix domain socket
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "unix:t1k.sock",
uds = true,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 100,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say("result: ", result)
}
}
--- tcp_listen: t1k.sock
--- tcp_reply_delay: 200ms
--- tcp_reply eval
"\x41\x01\x00\x00\x00?\x02\x03\x00\x00\x00405\xa4\x33\x00\x00\x00"
--- request
GET /t/shell.php
--- response_body
result: nil
--- error_log
failed to receive info packet from t1k server unix:t1k.sock: timeout
--- log_level: debug
=== TEST 11: do_request invalid action
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 0.0625,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say("action: ", result["action"])
}
}
--- tcp_listen: 18000
--- tcp_reply eval
"\xc1\x01\x00\x00\x00~"
--- request
GET /t
--- response_body
action: ~
--- no_error_log
[error]
--- error_log
successfully connected to t1k server 127.0.0.1:18000
--- log_level: debug
=== TEST 12: do_request invalid action with unix domain socket
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "unix:t1k.sock",
uds = true,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 0.0625,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say("action: ", result["action"])
}
}
--- tcp_listen: t1k.sock
--- tcp_reply eval
"\xc1\x01\x00\x00\x00~"
--- request
GET /t
--- response_body
action: ~
--- no_error_log
[error]
--- error_log
successfully connected to t1k server unix:t1k.sock
--- log_level: debug
=== TEST 13: do_request remote address
--- http_config eval: $::HttpConfig
--- config
location /t {
access_by_lua_block {
local utils = require "resty.t1k.utils"
ngx.say("ngx.var.http_x_real_ip or ngx.var.remote_addr is ", utils.get_indexed_element(ngx.var.http_x_real_ip) or ngx.var.remote_addr)
ngx.say("ngx.var.http_x_forwarded_for: 2 or ngx.var.remote_addr is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 2) or ngx.var.remote_addr)
ngx.say("ngx.var.http_x_forwarded_for: -2 or ngx.var.remote_addr is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, -2) or ngx.var.remote_addr)
ngx.say("ngx.var.http_non_existent_header or ngx.var.remote_addr is ", utils.get_indexed_element(ngx.var.http_non_existent_header) or ngx.var.remote_addr)
}
}
--- request
GET /t
--- more_headers
X-Forwarded-For: 1.1.1.1, 2.2.2.2, 2001:db8:3333:4444:5555:6666:7777:8888, 3.3.3.3
X-Real-IP: 100.100.100.100
--- response_body
ngx.var.http_x_real_ip or ngx.var.remote_addr is 100.100.100.100
ngx.var.http_x_forwarded_for: 2 or ngx.var.remote_addr is 2.2.2.2
ngx.var.http_x_forwarded_for: -2 or ngx.var.remote_addr is 2001:db8:3333:4444:5555:6666:7777:8888
ngx.var.http_non_existent_header or ngx.var.remote_addr is 127.0.0.1
--- no_error_log
[error]
=== TEST 14: do_request http2
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "127.0.0.1",
port = 18000,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say(result["action"])
ngx.say(result["status"])
ngx.say(result["event_id"])
}
}
--- tcp_listen: 18000
--- tcp_reply eval
"\x41\x01\x00\x00\x00?\x02\x03\x00\x00\x00405\xa4\x33\x00\x00\x00"
--- http2
--- request
GET /t/shell.php
--- tcp_query eval
qr/.*HTTP\/2.0.*/
--- response_body
?
405
c0c039a7c348486eaffd9e2f9846b66b
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000
--- log_level: debug
=== TEST 15: do_request http2 with unix domain socket
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local request = require "resty.t1k.request"
local t = {
mode = "block",
host = "unix:t1k.sock",
uds = true,
connect_timeout = 1000,
send_timeout = 1000,
read_timeout = 1000,
req_body_size = 1024,
keepalive_size = 16,
keepalive_timeout = 10000,
}
local ok, err, result = request.do_request(t)
if not ok then
ngx.log(ngx.ERR, err)
end
ngx.say(result["action"])
ngx.say(result["status"])
ngx.say(result["event_id"])
}
}
--- tcp_listen: t1k.sock
--- tcp_reply eval
"\x41\x01\x00\x00\x00?\x02\x03\x00\x00\x00405\xa4\x33\x00\x00\x00"
--- http2
--- request
GET /t/shell.php
--- tcp_query eval
qr/.*HTTP\/2.0.*/
--- response_body
?
405
c0c039a7c348486eaffd9e2f9846b66b
--- no_error_log
[error]
--- error_log
lua-resty-t1k: successfully connected to t1k server unix:t1k.sock
--- log_level: debug
================================================
FILE: sdk/lua-resty-t1k/t/utils.t
================================================
use Test::Nginx::Socket;
our $HttpConfig = <<'_EOC_';
lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;";
_EOC_
repeat_each(3);
plan tests => repeat_each() * (blocks() * 3);
run_tests();
__DATA__
=== TEST 1: int_to_char_length
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local utils = require "resty.t1k.utils"
ngx.say("255 to char length: ", utils.int_to_char_length(255))
ngx.print("16777216 to char length: ", utils.int_to_char_length(16777216))
}
}
--- request
GET /t
--- response_body eval
"255 to char length: \x{ff}\x{00}\x{00}\x{00}
16777216 to char length: \x{00}\x{00}\x{00}\x{01}"
--- no_error_log
[error]
=== TEST 2: int_to_char_length
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local utils = require "resty.t1k.utils"
ngx.say("0xff 0x00 0x00 0x00 to int length: ", utils.char_to_int_length("\xff\x00\x00\x00"))
ngx.say("0x00 0x00 0x00 0x01 to int length: ", utils.char_to_int_length("\x00\x00\x00\x01"))
}
}
--- request
GET /t
--- response_body
0xff 0x00 0x00 0x00 to int length: 255
0x00 0x00 0x00 0x01 to int length: 16777216
--- no_error_log
[error]
=== TEST 3: is_mask_first
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local utils = require "resty.t1k.utils"
ngx.say("0x30 is MASK FIRST: ", utils.is_mask_first(0x30))
ngx.say("0x41 is MASK FIRST: ", utils.is_mask_first(0x41))
ngx.say("0xc0 is MASK FIRST: ", utils.is_mask_first(0xc0))
ngx.say("0xc1 is MASK FIRST: ", utils.is_mask_first(0xc1))
}
}
--- request
GET /t
--- response_body
0x30 is MASK FIRST: false
0x41 is MASK FIRST: true
0xc0 is MASK FIRST: true
0xc1 is MASK FIRST: true
--- no_error_log
[error]
=== TEST 4: is_mask_last
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local utils = require "resty.t1k.utils"
ngx.say("0x41 is MASK LAST: ", utils.is_mask_last(65))
ngx.say("0x80 is MASK LAST: ", utils.is_mask_last(128))
}
}
--- request
GET /t
--- response_body
0x41 is MASK LAST: false
0x80 is MASK LAST: true
--- no_error_log
[error]
=== TEST 5: packet_parser unfinished
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local utils = require "resty.t1k.utils"
local finished, tag, length = utils.packet_parser("\x41\x59\x00\x00\x00")
ngx.say("finished: ", finished)
ngx.say("tag == TAG_HEAD: ", tag == 1)
ngx.say("length: ", 89)
}
}
--- request
GET /t
--- response_body
finished: false
tag == TAG_HEAD: true
length: 89
--- no_error_log
[error]
=== TEST 6: packet_parser finished
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local utils = require "resty.t1k.utils"
local finished, tag, length = utils.packet_parser("\xa0\x08\x00\x00\x00")
ngx.say("finished: ", finished)
ngx.say("tag == TAG_VERSION: ", tag == 32)
ngx.say("length: ", 8)
}
}
--- request
GET /t
--- response_body
finished: true
tag == TAG_VERSION: true
length: 8
--- no_error_log
[error]
=== TEST 7: starts_with
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local utils = require "resty.t1k.utils"
ngx.say("http://www.baidu.com starts with \"http\": ", utils.starts_with("http://www.baidu.com", "http"))
ngx.say("http://www.baidu.com starts with \"https\": ", utils.starts_with("http://www.baidu.com", "https"))
}
}
--- request
GET /t
--- response_body
http://www.baidu.com starts with "http": true
http://www.baidu.com starts with "https": false
--- no_error_log
[error]
=== TEST 8: to_var_idx
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local utils = require "resty.t1k.utils"
local fmt = string.format
ngx.say(fmt("http_x_real_ip is %s and %s", utils.to_var_idx("http_x_real_ip")))
ngx.say(fmt("X_REAL_IP is %s and %s", utils.to_var_idx("X_REAL_IP")))
ngx.say(fmt("X-Forwarded-For: 1 is %s and %d", utils.to_var_idx("X-Forwarded-For: 1")))
ngx.say(fmt("X-Forwarded-For: -1 is %s and %d", utils.to_var_idx("X-Forwarded-For: -1")))
ngx.say(fmt("X-FORWARDED-FOR:100 is %s and %d", utils.to_var_idx("X-FORWARDED-FOR:-100")))
}
}
--- request
GET /t
--- response_body
http_x_real_ip is http_x_real_ip and nil
X_REAL_IP is http_x_real_ip and nil
X-Forwarded-For: 1 is http_x_forwarded_for and 1
X-Forwarded-For: -1 is http_x_forwarded_for and -1
X-FORWARDED-FOR:100 is http_x_forwarded_for and -100
--- no_error_log
[error]
=== TEST 9: get_indexed_element
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local utils = require "resty.t1k.utils"
ngx.say("X-Forwarded-For: 1 is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 1))
ngx.say("X-Forwarded-For: 2 is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 2))
ngx.say("X-Forwarded-For: 3 is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 3))
ngx.say("X-Forwarded-For: 4 is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 4))
ngx.say("X-Forwarded-For: 5 is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 5))
ngx.say("X-Forwarded-For: -1 is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, -1))
ngx.say("X-Forwarded-For: -2 is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, -2))
ngx.say("X-Forwarded-For: -3 is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, -3))
ngx.say("X-Forwarded-For: -4 is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, -4))
ngx.say("X-Forwarded-For: -5 is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, -5))
ngx.say("X-Forwarded-For: 0 is ", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 0))
ngx.say("X-Non-Existent-Header is ", utils.get_indexed_element(ngx.var.http_non_existent_header))
ngx.say("X-Real-IP is ", utils.get_indexed_element(ngx.var.http_x_real_ip))
ngx.say("X-Real-IP: 1 is ", utils.get_indexed_element(ngx.var.http_x_real_ip, 1))
ngx.say("X-Real-IP: 2 is ", utils.get_indexed_element(ngx.var.http_x_real_ip, 2))
ngx.say("X-Real-IP: -1 is ", utils.get_indexed_element(ngx.var.http_x_real_ip, -1))
ngx.say("X-Real-IP: -2 is ", utils.get_indexed_element(ngx.var.http_x_real_ip, -2))
}
}
--- request
GET /t
--- more_headers
X-Forwarded-For: 1.1.1.1, 2.2.2.2
X-Forwarded-For: 3.3.3.3, 4.4.4.4
X-Real-IP: 10.10.10.10
--- response_body
X-Forwarded-For: 1 is 1.1.1.1
X-Forwarded-For: 2 is 2.2.2.2
X-Forwarded-For: 3 is 3.3.3.3
X-Forwarded-For: 4 is 4.4.4.4
X-Forwarded-For: 5 is nil
X-Forwarded-For: -1 is 4.4.4.4
X-Forwarded-For: -2 is 3.3.3.3
X-Forwarded-For: -3 is 2.2.2.2
X-Forwarded-For: -4 is 1.1.1.1
X-Forwarded-For: -5 is nil
X-Forwarded-For: 0 is 1.1.1.1, 2.2.2.2, 3.3.3.3, 4.4.4.4
X-Non-Existent-Header is nil
X-Real-IP is 10.10.10.10
X-Real-IP: 1 is 10.10.10.10
X-Real-IP: 2 is nil
X-Real-IP: -1 is 10.10.10.10
X-Real-IP: -2 is nil
--- no_error_log
[error]
=== TEST 10: get_event_id
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local utils = require "resty.t1k.utils"
ngx.say(utils.get_event_id(""))
ngx.say(utils.get_event_id(""))
ngx.say(utils.get_event_id(""))
}
}
--- request
GET /t
--- response_body
0988987de04844c7a3ce6d27865c9513
8bae6adf33864c7f8bf715a9b7a65b2c
nil
--- no_error_log
[error]
================================================
FILE: sdk/lua-resty-t1k/t/uuid.t
================================================
use Test::Nginx::Socket;
our $HttpConfig = <<'_EOC_';
lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;";
_EOC_
repeat_each(3);
plan tests => repeat_each() * (blocks() * 3);
run_tests();
__DATA__
=== TEST 1: generate_v4
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local uuid = require "resty.t1k.uuid"
ngx.say(uuid.generate_v4())
}
}
--- request
GET /t
--- response_body_like
^[0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}$
--- no_error_log
[error]
================================================
FILE: version.json
================================================
{
"latest_version": "v9.2.7",
"rec_version": "v9.2.7",
"lts_version": "v9.1.0-lts"
}
================================================
FILE: yanshi/.gitignore
================================================
*.o
*.dot
*.output
/build
/src/parser.cc
/src/parser.hh
/src/lexer.cc
/src/lexer.hh
================================================
FILE: yanshi/Makefile
================================================
CPPFLAGS := -g3 -std=c++1y -Isrc -I. -DHAVE_READLINE
ifeq ($(build),release)
BUILD := release
CPPFLAGS += -Os
else
BUILD := build
CPPFLAGS += -fsanitize=undefined,address -DDEBUG
LDLIBS := -lasan -lubsan
endif
LDLIBS += -licuuc -lreadline
SRC := $(filter-out src/lexer.cc src/parser.cc, $(wildcard src/*.cc)) src/lexer.cc src/parser.cc
OBJ := $(addprefix $(BUILD)/,$(subst src/,,$(SRC:.cc=.o)))
UNITTEST_SRC := $(wildcard unittest/*.cc)
UNITTEST_EXE := $(subst unittest/,,$(UNITTEST_SRC:.cc=))
all: $(BUILD)/yanshi # unittest
unittest: $(addprefix $(BUILD)/unittest/,$(UNITTEST_EXE))
$(foreach x,$(addprefix $(BUILD)/unittest/,$(UNITTEST_EXE)),$x && ) :
sinclude $(OBJ:.o=.d)
# FIXME
$(BUILD)/repl.o: src/lexer.hh
$(BUILD) $(BUILD)/unittest:
mkdir -p $@
$(BUILD)/yanshi: $(OBJ)
$(LINK.cc) $^ $(LDLIBS) -o $@
$(BUILD)/%.o: src/%.cc | $(BUILD)
$(CXX) $(CPPFLAGS) -MM -MP -MT $@ -MF $(@:.o=.d) $<
$(COMPILE.cc) $< -o $@
$(BUILD)/unittest/%: unittest/%.cc $(wildcard unittest/*.hh) $(filter-out $(BUILD)/main.o,$(OBJ)) | $(BUILD)/unittest
$(CXX) $(CPPFLAGS) -MM -MP -MT $@ -MF $(@:.o=.d) $<
$(LINK.cc) $(filter-out %.hh,$^) $(LDLIBS) -o $@
src/lexer.cc src/lexer.hh: src/lexer.l
flex --header-file=src/lexer.hh -o src/lexer.cc $<
src/parser.cc src/parser.hh: src/parser.y src/common.hh src/location.hh src/option.hh src/syntax.hh
bison --defines=src/parser.hh -o src/parser.cc $<
$(BUILD)/loader.o: src/parser.hh
$(BUILD)/parser.o: src/lexer.hh
$(BUILD)/lexer.o: src/parser.hh
clean:
$(RM) -r build release
distclean: clean
$(RM) src/{lexer,parser}.{cc,hh}
.PHONY: all clean distclean
================================================
FILE: yanshi/README.md
================================================
# 偃师 (yanshi)
yanshi is a finite state automaton generator like Ragel. Use inline operators to embed C++ code in the recognition of a language. yanshi is enhanced with features to approximate context-free grammar:
- Approximation of substring grammar
- Approximation of recursive automaton to match expressions with recursion.
The motivation to create yanshi is that Ragel does not provide a mechanism to serialize its representation of finite state automata, making it difficult to post-process generated automata and obtain the substring grammar recognizer.
Later on, I found a simplified SQL grammar might contain more than 10000 states. It was not only slow to generate the automaton, making it hard to do trial-and-error experiments, but wasted memory to store the automaton. I introduced `CollapseExpr` to allow circular references.
`CallExpr` takes one step further, maintains a return address stack to imitate function calls. It can be seen as an augmented `CollapseExpr`, removing a lot of false positive cases.
## Name
From :
In ancient China, a curious account of automata is found in the Lie Zi text (列子), written in the 3rd century BC. Within it there is a description of a much earlier encounter between King Mu of Zhou (周穆王, 1023-957 BC) and a mechanical engineer known as Yan Shi (偃师), an 'artificer'.
## Build
* Debug: `make`
* Release: `make build=release`
## Getting Started
* Create file `a.ys`:
```
export foo = 'hello'
```
Run `yanshi -S a.ys -o /tmp/a.cc` to generate a C++ file from the yanshi source file `a.ys`.
+ `yanshi_foo_start`: the start state is 0. States are represented by natural numbers.
+ `yanshi_foo_is_final`: leave aside `ret_stack` and look at the last line, it checks whether `u` is one of the final states.
+ `yanshi_foo_transit`: leave aside `ret_stack`, `u` is the current state and `c` is the next input codepoint or label.
With the `-S` option, yanshi will generate a standalone C++ file.
```
% make -C /tmp a
make: Entering directory '/tmp'
g++ a.cc -o a
make: Leaving directory '/tmp'
% /tmp/a hello
0 h 1 e 2 l 3 l 4 o 5
len: 5
pref: 5
state: 5
final: true
% /tmp/a
hello0 h 1 e 2 l 3 l 4 o 5
len: 5
pref: 5
state: 5
final: true
```
States are yellow and interleaved with transition labels. Final states are bold yellow.
+ `len`: the length of input codepoints or labels
+ `pref`: the length of the longest prefix that does not enter the dead state
+ `state`: the state entered after consuming the input
+ `final`: whether the state is one of final states
* Interactive mode
The `-i` option enables interactive mode.
```
% yanshi -i a.ys
Testing foo
foo :: DefineStmt
.integer mode
Commands available from the prompt:
.automaton dump automaton
.assoc dump associated AST Expr for each state
.help display this help
.integer input is a list of non-negative integers, macros(#define) or '' quoted strings
.macro display defined macros
.string input is a string
.stmt change target DefineStmt to
.quit exit interactive mode
λ 104 101 108 108 111
0 104 1 101 2 108 3 108 4 111 5
export foo = 'hello':
λ .string
.string mode
λ hello
0 h 1 e 2 l 3 l 4 o 5
export foo = 'hello':
λ
```
* Regex-like syntax
```
export hello = [gh] 'e' l{2} 'o'
l = 'l'
```
`[gh]` is a bracket expression and `l{2}` denotes to matches `l` at least twice. This grammar matches `hello`, `gello`, `helllo`, ...
+ Union: `c = a | b`
+ Intersection: `c = a && b`
+ Difference: `c = a - b`
+ Concatenation: `c = a b`
+ Complement: `c = ~ a`
* Actions (embedded C++ code)
```
c++ {
#include
}
export hello = '喵' @ { puts("meow"); } {2}
```
I have not thought clearly on the implementation. The executing point may be counter-intuitive.
* Modules
```
# a.ys
import 'b.ys' as B # B::bar
import 'b.ys' # qux
export foo = B::bar | qux
bar = '4'
# b.ys
bar = '3'
qux = '5'
```
* Substring grammar
Specify the `--substring-grammar` option to generate code for substring grammar. That is, the generated code matches every substring of the grammar. The implementation creates a new start state and a new final state, connects the start state to the old start state, and old final states to the new final state.
* `EmbedExpr`, reference a nonterminal without modifiers
```
foo = bar
bar = [0-9]
```
The complete automaton of bar will be duplicated in each reference site. If the referenced automaton is large, `EmbedExpr` will significantly increase the number of states. `EmbedExpr` defines dependencies among states and no cyclic dependency is allowed.
* `CollapseExpr`, reference a nonterminal with the `!` modifier
```
export foo = 'pre' !bar 'post'
bar = [\u0300-\u034E]
quz = 'meow' !bar 'meow'
```
The final state of `'pre'` and the start state of `'post'` will be connected by a special directed arc. When exporting, an epsilon transition will be added from the tail of the arc to the start state of `bar`, others will be added from the final states of `bar` to the head of the arc. `CollapseExpr` behaves like function calls, however, the return address is not preserved (hence the name `CollapseExpr`) and the state may go to other call sites. In this example, the state may go to either `foo` or `quz` after traveling through `bar`, causing false positives.
* `CallExpr`, reference a nonterminal with the `&` modifier
```
export foo = 'pre' &bar 'post'
bar = '4'
```
This is a refinement of `CollapseExpr`. Suppose state `&B` is contained in `A`'s definition (`A` calls `B`). `&B` will be represented as an pseudo arc (`u -> v`), where `u` is the state before `&B` and `v` is the state after `&B`. If arcs of `u` do not collide with arcs of `B`, the transition function will push `v` to the return stack if current state set contains `u` and there is no other transition. Note `B` is disconnected from `A`, which is different from the `CollapseExpr` case. The machine will transit on automaton `B` greedily. If there is no transition, it will pop a return address(`v` in this case) and jumps to it.
## Contrib
### Vim
Syntax highlighting, and a syntax checking plugin for Synaptics
```zsh
ln -sr contrib/vim/compiler/yanshi.vim ~/.vim/compiler/
ln -sr contrib/vim/ftdetect/yanshi.vim ~/.vim/ftdetect/
ln -sr contrib/vim/ftplugin/yanshi.vim ~/.vim/ftplugin/
ln -sr contrib/vim/syntax/yanshi.vim ~/.vim/syntax/
ln -sr contrib/vim/syntax_checker/yanshi ~/.vim/syntax_checker/
```
### Zsh
Command line completion
```
# ~/.zshrc
fpath=(~/.zsh $fpath)
# ln -sr contrib/zsh/_yanshi ~/.zsh/
```
## Internals
```
src
common.{cc,hh}
main.{cc,hh}
syntax.{cc,hh}
loader.{cc,hh}
fsa.{cc,hh}
fsa_anno.{cc,hh}
compiler.{cc,hh}
parser.y
lexer.l
location.cc
```
* Lex `lexer.l`
* Parse and generate a syntax tree `parser.y`
* `loader.cc`
+ Get a list of definitions
+ Recursively load for each `import`
+ Resolve references and associate uses to definitions
+ Build a dependency graph from `EmbedExpr`
+ Compile automaton for each nonterminal in topological order. `CollapseExpr` and `CallExpr` are represented by special directed arcs.
+ Generate code for `export` nonterminals, resolving `CollapseExpr` and `CallExpr`
### Finite state automaton
Each node of the syntax tree corresponds to an automaton. The parent builds an automaton from its children according to the semantics. The automaton of the parent may contain states from the automaton of one of the children, or it is a state introduced by the parent.
`assoc[i]` records the associative nodes in the automaton tree (which part of the syntax tree has associations with this state) and positions (start state, final state or inner state) for state `i`. It serves three purposes:
* Check which action should be triggered
* Look for inner states (neither start nor final) in the implementation of substring grammar
* Check whether it is associated to a `CallExpr` or `CollapseExpr`
### `CollapseExpr`
================================================
FILE: yanshi/contrib/vim/compiler/yanshi.vim
================================================
"if exists('current_compiler')
" finish
"endif
let current_compiler = 'yanshi'
if exists(':CompilerSet') != 2
command -nargs=* CompilerSet setlocal
endif
CompilerSet errorformat=
\%E%f\ %l:%c-%*\\d\ error\ %m,
\%E%f\ %l-%*\\d:%c-%*\\d\ error\ %m,
\%W%f\ %l:%c-%*\\d\ warning\ %m,
\%W%f\ %l-%*\\d:%c-%*\\d\ warning\ %m,
\%C%.%#
CompilerSet makeprg=yanshi\ -d0\ -c\ $*\ %
================================================
FILE: yanshi/contrib/vim/ftdetect/yanshi.vim
================================================
au BufRead,BufNewFile *.yanshi setf yanshi
au BufRead,BufNewFile *.ys setf yanshi
================================================
FILE: yanshi/contrib/vim/ftplugin/yanshi.vim
================================================
if exists('b:did_ftplugin')
finish
endif
let b:did_ftplugin = 1
compiler yanshi
================================================
FILE: yanshi/contrib/vim/syntax/yanshi.vim
================================================
if exists('b:current_syntax')
finish
endif
syn cluster yanshiCommentGroup contains=yanshiTodo
syn include @yanshiCcode syntax/cpp.vim
syn keyword yanshiAction action
syn keyword yanshiMacro semicolon nosemicolon
syn keyword yanshiStorageClass export intact
syn keyword yanshiTodo contained TODO FIXME XXX
syn match yanshiCpp 'c++'
syn match yanshiActionOperator '[>$@%]'
syn match yanshiCall '\^\w\+\(::\w\+\)\?'
syn match yanshiCollapse '!\w\+\(::\w\+\)\?'
syn match yanshiHighOp '[+\*?]'
syn match yanshiIdent '\w\+\(::\w\+\)\?'
syn match yanshiCpp display "^c++\s*" skipwhite nextgroup=yanshiBrace
syn match yanshiImport display "^import\s*" contains=yanshiImported
syn match yanshiLowOp '[-&|]'
syn match yanshiSpecial display contained "\\\(x\x\x\|.\|$\)"
syn region yanshiBrace matchgroup=Delimiter start='{' end='}' fold contains=@yanshiCcode
syn region yanshiBracket start='\[' skip=+\\\\\|\\]+ end=']'
syn region yanshiComment start='/\*' end='\*/' keepend contains=@yanshiCommentGroup,@Spell
syn region yanshiImported display contained start="+" skip=+\\\\\|\\"+ end=+"+
syn region yanshiLineComment start='#\|//' skip='\\$' end='$' keepend contains=@yanshiCommentGroup,@Spell
syn region yanshiPreprocess start="#define" skip="\\$" end="$" keepend
syn region yanshiQQString start=+"+ skip=+\\.+ end=+"+ contains=yanshiSpecial
syn region yanshiQString start=+'+ skip=+\\.+ end=+'+
syn region yanshiDefineStmt start='^\w\+\s*[=:]' end='$' skipnl contains=@yanshiExpr,yanshiComment,yanshiLineComment,yanshiParen0
syn cluster yanshiExpr contains=yanshiActionOperator,yanshiBrace,yanshiBracket,yanshiCall,yanshiCollapse,yanshiIdent,yanshiHighOp,yanshiLowOp,yanshiQString,yanshiQQString,
sy region yanshiParen0 matchgroup=yanshiParen0 start='(' end=')' contains=@yanshiExpr,yanshiParen1
sy region yanshiParen1 matchgroup=yanshiParen1 start='(' end=')' contains=@yanshiExpr,yanshiParen2 contained
sy region yanshiParen2 matchgroup=yanshiParen2 start='(' end=')' contains=@yanshiExpr,yanshiParen3 contained
sy region yanshiParen3 matchgroup=yanshiParen3 start='(' end=')' contains=@yanshiExpr,yanshiParen4 contained
sy region yanshiParen4 matchgroup=yanshiParen4 start='(' end=')' contains=@yanshiExpr,yanshiParen5 contained
sy region yanshiParen5 matchgroup=yanshiParen5 start='(' end=')' contains=@yanshiExpr,yanshiParen0 contained
hi yanshiParen0 ctermfg=brown guifg=#3bb9ff
hi yanshiParen1 ctermfg=DarkBlue guifg=#f88017
hi yanshiParen2 ctermfg=darkgray guifg=#5efb6e
hi yanshiParen3 ctermfg=darkgreen guifg=#f62817
hi yanshiParen4 ctermfg=darkcyan guifg=#fdd017
hi yanshiParen5 ctermfg=darkmagenta guifg=#faafba
hi link yanshiIdent Identifier
"TODO color mismatch of {}
"hi link yanshiBrace Statement
"hi link yanshiDefineStmt Statement
hi def link yanshiCall Constant
hi def link yanshiCollapse Constant
hi def link yanshiAction Structure
hi def link yanshiActionOperator Type
hi def link yanshiBracket Function
hi def link yanshiCpp Structure
hi def link yanshiComment Comment
hi def link yanshiHighOp Operator
hi def link yanshiImport Include
hi def link yanshiImported String
hi def link yanshiLineComment Comment
hi def link yanshiLowOp Conditional
hi def link yanshiMacro Macro
hi def link yanshiPreprocess Macro
hi def link yanshiQQString String
hi def link yanshiQString String
hi def link yanshiSpecial SpecialChar
hi def link yanshiStorageClass StorageClass
hi def link yanshiTodo Todo
let b:current_syntax = 'yanshi'
================================================
FILE: yanshi/contrib/vim/syntax_checkers/yanshi/yanshi.vim
================================================
if exists('g:loaded_syntastic_yanshi_yanshi_checker')
finish
endif
let g:loaded_syntastic_yanshi_yanshi_checker = 1
let s:save_cpo = &cpo
set cpo&vim
fu! SyntaxCheckers_yanshi_yanshi_GetLocList() dict
let makeprg = self.makeprgBuild({ 'args': '-d0 -c' })
let errorformat =
\ '%C %.%#,'.
\ '%E%f %l:%c-%*\d error %m,'.
\ '%E%f %l-%*\d:%c-%*\d error %m,'.
\ '%W%f %l:%c-%*\d warning %m,'.
\ '%W%f %l-%*\d:%c-%*\d warning %m'
return SyntasticMake({
\ 'makeprg': makeprg,
\ 'errorformat': errorformat })
endf
call g:SyntasticRegistry.CreateAndRegisterChecker({
\ 'filetype': 'yanshi',
\ 'name': 'yanshi'})
let &cpo = s:save_cpo
unlet s:save_cpo
================================================
FILE: yanshi/contrib/zsh/_yanshi
================================================
#compdef yanshi
_arguments \
'(-b --bytes)'{-b,--bytes}'[make labels range over \[0,256), Unicode literals will be treated as UTF-8 bytes]' \
'(-c --check)'{-c,--check}'[check syntax & use/def]' \
'-C[generate C source code (default: C++)]' \
'(-d --debug)'{-d,--debug}'+[debug level]:level:(0 1 2 3 4 5)' \
'--dump-action[dump associated actions for each edge]' \
'--dump-assoc[dump associated AST Expr for each state]' \
'--dump-automaton[dump automata]' \
'--dump-embed[dump statistics of EmbedExpr]' \
'--dump-module[dump module use/def/...]' \
'--dump-tree[dump AST]' \
'(-G --graph)'{-G,--graph}'[output a Graphviz dot file]' \
'(-I --import)'{-I,--import}'=[add to search path for "import"]' \
'(-i --interactive)'{-i,--interactive}'[interactive mode]' \
'(-k --keep-inaccessible)'{-k,--keep-inaccessible}'[do not perform accessible/co-accessible]' \
'(-l --debug-output)'{-l,--debug-output}'=[filename for debug output]:file:_files' \
'--max-return-stack=[max length of return stack in C generator]:len:' \
'(-o --output)'{-o,--output}'=[.cc output filename]:file:_files' \
'(-O --output-header)'{-O,--output-header}'=[.hh output filename]:file:_files' \
'(-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]' \
'(-h --help)'{-h,--help}'[display this help]' \
'1:file:_files -g "*.{ys,yanshi}"'\
================================================
FILE: yanshi/src/common.cc
================================================
#include "common.hh"
#include "option.hh"
#include
#include
#include
#include
#include
#include
#include
#include
///// Error
static const char *ENAME[] = {
/* 0 */ "",
/* 1 */ "EPERM", "ENOENT", "ESRCH", "EINTR", "EIO", "ENXIO",
/* 7 */ "E2BIG", "ENOEXEC", "EBADF", "ECHILD",
/* 11 */ "EAGAIN/EWOULDBLOCK", "ENOMEM", "EACCES", "EFAULT",
/* 15 */ "ENOTBLK", "EBUSY", "EEXIST", "EXDEV", "ENODEV",
/* 20 */ "ENOTDIR", "EISDIR", "EINVAL", "ENFILE", "EMFILE",
/* 25 */ "ENOTTY", "ETXTBSY", "EFBIG", "ENOSPC", "ESPIPE",
/* 30 */ "EROFS", "EMLINK", "EPIPE", "EDOM", "ERANGE",
/* 35 */ "EDEADLK/EDEADLOCK", "ENAMETOOLONG", "ENOLCK", "ENOSYS",
/* 39 */ "ENOTEMPTY", "ELOOP", "", "ENOMSG", "EIDRM", "ECHRNG",
/* 45 */ "EL2NSYNC", "EL3HLT", "EL3RST", "ELNRNG", "EUNATCH",
/* 50 */ "ENOCSI", "EL2HLT", "EBADE", "EBADR", "EXFULL", "ENOANO",
/* 56 */ "EBADRQC", "EBADSLT", "", "EBFONT", "ENOSTR", "ENODATA",
/* 62 */ "ETIME", "ENOSR", "ENONET", "ENOPKG", "EREMOTE",
/* 67 */ "ENOLINK", "EADV", "ESRMNT", "ECOMM", "EPROTO",
/* 72 */ "EMULTIHOP", "EDOTDOT", "EBADMSG", "EOVERFLOW",
/* 76 */ "ENOTUNIQ", "EBADFD", "EREMCHG", "ELIBACC", "ELIBBAD",
/* 81 */ "ELIBSCN", "ELIBMAX", "ELIBEXEC", "EILSEQ", "ERESTART",
/* 86 */ "ESTRPIPE", "EUSERS", "ENOTSOCK", "EDESTADDRREQ",
/* 90 */ "EMSGSIZE", "EPROTOTYPE", "ENOPROTOOPT",
/* 93 */ "EPROTONOSUPPORT", "ESOCKTNOSUPPORT",
/* 95 */ "EOPNOTSUPP/ENOTSUP", "EPFNOSUPPORT", "EAFNOSUPPORT",
/* 98 */ "EADDRINUSE", "EADDRNOTAVAIL", "ENETDOWN", "ENETUNREACH",
/* 102 */ "ENETRESET", "ECONNABORTED", "ECONNRESET", "ENOBUFS",
/* 106 */ "EISCONN", "ENOTCONN", "ESHUTDOWN", "ETOOMANYREFS",
/* 110 */ "ETIMEDOUT", "ECONNREFUSED", "EHOSTDOWN", "EHOSTUNREACH",
/* 114 */ "EALREADY", "EINPROGRESS", "ESTALE", "EUCLEAN",
/* 118 */ "ENOTNAM", "ENAVAIL", "EISNAM", "EREMOTEIO", "EDQUOT",
/* 123 */ "ENOMEDIUM", "EMEDIUMTYPE", "ECANCELED", "ENOKEY",
/* 127 */ "EKEYEXPIRED", "EKEYREVOKED", "EKEYREJECTED",
/* 130 */ "EOWNERDEAD", "ENOTRECOVERABLE", "ERFKILL", "EHWPOISON"
};
#define MAX_ENAME 133
long action_label_base, action_label, call_label_base, call_label, collapse_label_base, collapse_label;
void output_error(bool use_err, const char *format, va_list ap)
{
char text[BUF_SIZE], msg[BUF_SIZE], buf[BUF_SIZE];
vsnprintf(msg, BUF_SIZE, format, ap);
if (use_err)
snprintf(text, BUF_SIZE, "[%s %s] ", 0 < errno && errno < MAX_ENAME ? ENAME[errno] : "?UNKNOWN?", strerror(errno));
else
strcpy(text, "");
snprintf(buf, BUF_SIZE, RED "%s%s\n", text, msg);
fputs(buf, stderr);
fputs(SGR0, stderr);
fflush(stderr);
}
void err_msg(const char *format, ...)
{
va_list ap;
va_start(ap, format);
int saved = errno;
output_error(errno > 0, format, ap);
errno = saved;
va_end(ap);
}
#define err_msg_g(...) ({err_msg(__VA_ARGS__); goto quit;})
void err_exit(int exitno, const char *format, ...)
{
va_list ap;
va_start(ap, format);
int saved = errno;
output_error(errno > 0, format, ap);
errno = saved;
va_end(ap);
void *bt[99];
char buf[1024];
int nptrs = backtrace(bt, LEN(buf));
int i = sprintf(buf, "addr2line -Cfipe %s", program_invocation_name), j = 0;
while (j < nptrs && i+30 < sizeof buf)
i += sprintf(buf+i, " %p", bt[j++]);
strcat(buf, ">&2");
fputs("\n", stderr);
system(buf);
//backtrace_symbols_fd(buf, nptrs, STDERR_FILENO);
exit(exitno);
}
long get_long(const char *arg)
{
char *end;
errno = 0;
long ret = strtol(arg, &end, 0);
if (errno)
err_exit(EX_USAGE, "get_long: %s", arg);
if (*end)
err_exit(EX_USAGE, "get_long: nonnumeric character");
return ret;
}
//// log
//
void log_generic(const char *prefix, const char *format, va_list ap)
{
char buf[BUF_SIZE];
timeval tv;
tm tm;
gettimeofday(&tv, NULL);
fputs(prefix, stdout);
if (localtime_r(&tv.tv_sec, &tm)) {
strftime(buf, sizeof buf, "%T.%%06u ", &tm);
printf(buf, tv.tv_usec);
}
vprintf(format, ap);
fputs(SGR0, stdout);
fflush(stdout);
}
void log_event(const char *format, ...)
{
va_list ap;
va_start(ap, format);
log_generic(CYAN, format, ap);
va_end(ap);
}
void log_action(const char *format, ...)
{
va_list ap;
va_start(ap, format);
log_generic(GREEN, format, ap);
va_end(ap);
}
void log_status(const char *format, ...)
{
va_list ap;
va_start(ap, format);
log_generic(YELLOW, format, ap);
va_end(ap);
}
void bold(long fd) { if (isatty(fd)) fputs("\x1b[1m", fd == STDOUT_FILENO ? stdout : stderr); }
void blue(long fd) { if (isatty(fd)) fputs(BLUE, fd == STDOUT_FILENO ? stdout : stderr); }
void cyan(long fd) { if (isatty(fd)) fputs(CYAN, fd == STDOUT_FILENO ? stdout : stderr); }
void green(long fd) { if (isatty(fd)) fputs(GREEN, fd == STDOUT_FILENO ? stdout : stderr); }
void magenta(long fd) { if (isatty(fd)) fputs(MAGENTA, fd == STDOUT_FILENO ? stdout : stderr); }
void red(long fd) { if (isatty(fd)) fputs(RED, fd == STDOUT_FILENO ? stdout : stderr); }
void sgr0(long fd) { if (isatty(fd)) fputs(SGR0, fd == STDOUT_FILENO ? stdout : stderr); }
void yellow(long fd) { if (isatty(fd)) fputs(YELLOW, fd == STDOUT_FILENO ? stdout : stderr); }
void normal_yellow(long fd) { if (isatty(fd)) fputs(NORMAL_YELLOW, fd == STDOUT_FILENO ? stdout : stderr); }
void indent(FILE* f, int d)
{
fprintf(f, "%*s", 2*d, "");
}
void DisjointIntervals::flip() {
long i = 0;
map to2;
for (auto &x: to) {
if (i < x.first)
to2.emplace(i, x.first);
i = x.second;
}
if (i < AB)
to2.emplace(i, AB);
to = move(to2);
}
void DisjointIntervals::print() {
for (auto& x: to)
printf("(%ld,%ld) ", x.first, x.second);
puts("");
}
================================================
FILE: yanshi/src/common.hh
================================================
#pragma once
#ifndef _GNU_SOURCE
# define _GNU_SOURCE
#endif
#include
#include