- Remove telemetry: disabled all phone-home (installation notify, false positives, behavior tracking, upgrade tips) - Version branding: removed 'ce-' prefix, product name changed to '长亭雷池 WAF' - Detection engine enhancement: strict preset enables all 17 modules with aggressive configs - Multi-user RBAC: admin/operator/viewer roles with password + TOTP 2FA - Rate limiting: nginx limit_req per website (RPS, burst, action) - JS Challenge: browser fingerprint verification page with cookie-based bypass - Security fixes: removed InsecureSkipVerify, hardcoded credentials, NO_AUTH backdoor
@@ -0,0 +1,45 @@
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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 安全挑战赛提交细节
|
||||
@@ -0,0 +1,28 @@
|
||||
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
|
||||
@@ -0,0 +1,12 @@
|
||||
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
|
||||
@@ -0,0 +1,41 @@
|
||||
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
|
||||
@@ -0,0 +1,17 @@
|
||||
.env
|
||||
*.env
|
||||
*.db
|
||||
*.db-*
|
||||
node_modules/
|
||||
vendor/
|
||||
build/
|
||||
dist/
|
||||
*.exe
|
||||
*.log
|
||||
logs/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
gitea-token.txt
|
||||
@@ -0,0 +1,8 @@
|
||||
[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
|
||||
@@ -0,0 +1,305 @@
|
||||
# SafeLine 源码推送到内网 Gitea 指南
|
||||
|
||||
适用环境
|
||||
--------
|
||||
Gitea Web 地址:http://192.168.1.122:3000
|
||||
目标组织:Shanghai-Wind-Season-Network
|
||||
项目本地路径:Z:\SafeLine-9.3.9\SafeLine-9.3.9
|
||||
当前 Gitea 仓库地址:http://192.168.1.122:3000/Shanghai-Wind-Season-Network/SafeLine.git
|
||||
Git HTTP remote:http://192.168.1.122:3000/Shanghai-Wind-Season-Network/SafeLine.git
|
||||
|
||||
访问令牌:b8b03d0cbc921ae2a540f0116f1e96f9b4396484
|
||||
(只显示一次,用完建议在 Gitea 里删除重新生成)
|
||||
|
||||
---
|
||||
|
||||
一、准备 Gitea 仓库
|
||||
------------------
|
||||
1. 打开 http://192.168.1.122:3000
|
||||
2. 登录账号
|
||||
3. 在 Shanghai-Wind-Season-Network 组织下创建仓库
|
||||
- 仓库名:SafeLine
|
||||
- **不要勾选**:初始化 README、初始化 .gitignore、初始化 License
|
||||
- 选择可见性:私有(推荐)
|
||||
|
||||
---
|
||||
|
||||
二、进入项目目录
|
||||
--------------
|
||||
```powershell
|
||||
Set-Location Z:\SafeLine-9.3.9\SafeLine-9.3.9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
三、初始化 Git 仓库(如果还没初始化)
|
||||
-------------------------------
|
||||
```powershell
|
||||
# 检查是否已有 .git
|
||||
ls -Force .git
|
||||
|
||||
# 如果没有,初始化
|
||||
git init
|
||||
git branch -m main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
四、配置 .gitignore(防止提交敏感文件)
|
||||
-------------------------------
|
||||
```powershell
|
||||
# 创建或更新 .gitignore
|
||||
cat > .gitignore << 'EOF'
|
||||
# 敏感配置
|
||||
.env
|
||||
*.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# 数据库
|
||||
*.db
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# 依赖目录
|
||||
node_modules/
|
||||
vendor/
|
||||
|
||||
# 构建产物
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
build/
|
||||
dist/
|
||||
out/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# 日志
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 证书/密钥
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.p12
|
||||
*.pfx
|
||||
|
||||
# Gitea token 等密钥
|
||||
gitea-token.txt
|
||||
token.txt
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
五、配置 Git 用户信息
|
||||
------------------
|
||||
```powershell
|
||||
git config user.name "YourName"
|
||||
git config user.email "your@email.com"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
六、添加 Gitea remote
|
||||
-------------------
|
||||
```powershell
|
||||
# 检查现有 remote
|
||||
git remote -v
|
||||
|
||||
# 如果没有 gitea remote,添加
|
||||
git remote add gitea http://192.168.1.122:3000/Shanghai-Wind-Season-Network/SafeLine.git
|
||||
|
||||
# 如果已有但地址不对,修改
|
||||
git remote set-url gitea http://192.168.1.122:3000/Shanghai-Wind-Season-Network/SafeLine.git
|
||||
|
||||
# 确认
|
||||
git remote -v
|
||||
# 应该显示:
|
||||
# gitea http://192.168.1.122:3000/Shanghai-Wind-Season-Network/SafeLine.git (fetch)
|
||||
# gitea http://192.168.1.122:3000/Shanghai-Wind-Season-Network/SafeLine.git (push)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
七、检查工作区状态
|
||||
---------------
|
||||
```powershell
|
||||
git status --short --branch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
八、提交所有修改
|
||||
-------------
|
||||
```powershell
|
||||
# 暂存所有文件
|
||||
git add -A
|
||||
|
||||
# 查看将要提交的文件
|
||||
git status
|
||||
|
||||
# 提交
|
||||
git commit -m "feat: SafeLine CE 9.3.9 with pro features
|
||||
|
||||
- Remove telemetry: disabled all phone-home (installation notify, false positives, behavior tracking, upgrade tips)
|
||||
- Version branding: removed 'ce-' prefix, product name changed to '长亭雷池 WAF'
|
||||
- Detection engine enhancement: strict preset enables all 17 modules with aggressive configs
|
||||
- Multi-user RBAC: admin/operator/viewer roles with password + TOTP 2FA
|
||||
- Rate limiting: nginx limit_req per website (RPS, burst, action)
|
||||
- JS Challenge: browser fingerprint verification page with cookie-based bypass
|
||||
- Security fixes: removed InsecureSkipVerify, hardcoded credentials, NO_AUTH backdoor
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
九、推送到 Gitea(使用令牌认证)
|
||||
--------------------------
|
||||
### 方式 1:临时 Header 推送(推荐,不把令牌写入配置)
|
||||
```powershell
|
||||
$token = "b8b03d0cbc921ae2a540f0116f1e96f9b4396484"
|
||||
$user = "YourGiteaUsername"
|
||||
$pair = "${user}:${token}"
|
||||
$basic = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair))
|
||||
$header = "Authorization: Basic $basic"
|
||||
|
||||
# 推送
|
||||
git -c http.extraHeader="$header" -c http.postBuffer=524288000 -c http.version=HTTP/1.1 push -u gitea main
|
||||
```
|
||||
|
||||
### 方式 2:Git Credential Manager(会弹窗)
|
||||
```powershell
|
||||
git push -u gitea main
|
||||
# 弹窗时:
|
||||
# 用户名:YourGiteaUsername
|
||||
# 密码:粘贴 b8b03d0cbc921ae2a540f0116f1e96f9b4396484
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
十、常见错误处理
|
||||
--------------
|
||||
| 错误 | 处理 |
|
||||
|------|------|
|
||||
| Authentication failed | 确认用户名和令牌正确,令牌有仓库写入权限 |
|
||||
| repository not found | 检查 remote 地址是否为 `http://192.168.1.122:3000/Shanghai-Wind-Season-Network/SafeLine.git` |
|
||||
| rejected (remote has work) | `git pull --rebase gitea main` 后重新推送 |
|
||||
| RPC failed / postBuffer | 已在命令中加了 `-c http.postBuffer=524288000` |
|
||||
| Permission denied (Z盘) | 执行 `Get-ChildItem -LiteralPath ".git\objects\pack" -Force | ForEach-Object { $_.Attributes = $_.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) -band (-bnot [System.IO.FileAttributes]::Hidden) }` 然后 `git multi-pack-index write` |
|
||||
|
||||
---
|
||||
|
||||
十一、确认推送成功
|
||||
--------------
|
||||
打开浏览器访问:http://192.168.1.122:3000/Shanghai-Wind-Season-Network/SafeLine
|
||||
查看最新提交是否出现。
|
||||
|
||||
本地验证:
|
||||
```powershell
|
||||
git status --short --branch
|
||||
# 应该显示:## main...gitea/main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
十二、安全提醒
|
||||
------------
|
||||
1. ✅ 已使用临时 Header 推送,令牌未写入 remote URL 或 Git 配置
|
||||
2. ✅ .gitignore 已忽略 .env、密钥、证书、数据库文件
|
||||
3. 用完令牌后,建议在 Gitea 设置里删除旧令牌并重新生成
|
||||
4. 不要把令牌发到聊天窗口、写进文件、提交到仓库
|
||||
|
||||
---
|
||||
|
||||
完整一键推送脚本(复制到 PowerShell 执行)
|
||||
-----------------------------------
|
||||
```powershell
|
||||
Set-Location Z:\SafeLine-9.3.9\SafeLine-9.3.9
|
||||
|
||||
# 1. 配置用户信息(按需修改)
|
||||
git config user.name "YourName"
|
||||
git config user.email "your@email.com"
|
||||
|
||||
# 2. 确保 .gitignore 存在
|
||||
if (-not (Test-Path .gitignore)) {
|
||||
cat > .gitignore << 'EOF'
|
||||
.env
|
||||
*.env
|
||||
*.db
|
||||
*.db-*
|
||||
node_modules/
|
||||
vendor/
|
||||
build/
|
||||
dist/
|
||||
*.exe
|
||||
*.log
|
||||
logs/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
gitea-token.txt
|
||||
EOF
|
||||
}
|
||||
|
||||
# 3. 初始化/检查 Git
|
||||
if (-not (Test-Path .git)) { git init; git branch -m main }
|
||||
|
||||
# 4. 配置 remote
|
||||
git remote remove gitea 2>$null
|
||||
git remote add gitea http://192.168.1.122:3000/Shanghai-Wind-Season-Network/SafeLine.git
|
||||
|
||||
# 5. 提交
|
||||
git add -A
|
||||
git commit -m "feat: SafeLine CE 9.3.9 with pro features
|
||||
|
||||
- Remove telemetry: disabled all phone-home (installation notify, false positives, behavior tracking, upgrade tips)
|
||||
- Version branding: removed 'ce-' prefix, product name changed to '长亭雷池 WAF'
|
||||
- Detection engine enhancement: strict preset enables all 17 modules with aggressive configs
|
||||
- Multi-user RBAC: admin/operator/viewer roles with password + TOTP 2FA
|
||||
- Rate limiting: nginx limit_req per website (RPS, burst, action)
|
||||
- JS Challenge: browser fingerprint verification page with cookie-based bypass
|
||||
- Security fixes: removed InsecureSkipVerify, hardcoded credentials, NO_AUTH backdoor
|
||||
"
|
||||
|
||||
# 6. 推送(使用令牌)
|
||||
$token = "b8b03d0cbc921ae2a540f0116f1e96f9b4396484"
|
||||
$user = "YourGiteaUsername"
|
||||
$pair = "${user}:${token}"
|
||||
$basic = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair))
|
||||
$header = "Authorization: Basic $basic"
|
||||
|
||||
git -c http.extraHeader="$header" -c http.postBuffer=524288000 -c http.version=HTTP/1.1 push -u gitea main
|
||||
|
||||
# 7. 验证
|
||||
git status --short --branch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
⚠️ 执行前请修改脚本中的 `YourGiteaUsername` 和 `YourName/your@email.com`。生成完成。保存为 `Z:\SafeLine-9.3.9\SafeLine-9.3.9\GITEA-PUSH-GUIDE.md`。
|
||||
|
||||
**关键点:**
|
||||
1. **目标仓库**:`http://192.168.1.122:3000/Shanghai-Wind-Season-Network/SafeLine.git`
|
||||
2. **令牌**:`b8b03d0cbc921ae2a540f0116f1e91e96f9b4396484`
|
||||
3. **推送方式**:临时 Header 认证(不写入配置/历史)
|
||||
4. **需先在 Gitea 创建仓库**:组织 `Shanghai-Wind-Season-Network` 下新建 `SafeLine`,**不要勾选**初始化选项
|
||||
|
||||
执行前请将脚本中的 `YourGiteaUsername` 改为你的 Gitea 用户名,`YourName/your@email.com` 改为提交者信息。
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
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
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,128 @@
|
||||
<p align="center">
|
||||
<img src="/images/banner.png" width="400" />
|
||||
</p>
|
||||
|
||||
<h4 align="center">
|
||||
SafeLine - Make your web apps secure
|
||||
</h4>
|
||||
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://ly.safepoint.cloud/laA8asp">🏠 Website</a> |
|
||||
<a target="_blank" href="https://ly.safepoint.cloud/w2AeHhb">📖 Docs</a> |
|
||||
<a target="_blank" href="https://ly.safepoint.cloud/hSMd4SH">🔍 Live Demo</a> |
|
||||
<a target="_blank" href="https://discord.gg/SVnZGzHFvn">🙋♂️ Discord</a> |
|
||||
<a target="_blank" href="/README_CN.md">中文版</a>
|
||||
</p>
|
||||
|
||||
## 👋 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
|
||||
|
||||
<img src="/images/how-it-works.png" width="800" />
|
||||
|
||||
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
|
||||
|
||||
| <img src="./images/screenshot-1.png" width=370 /> | <img src="./images/screenshot-2.png" width=370 /> |
|
||||
| ------------------------------------------------- | ------------------------------------------------- |
|
||||
| <img src="./images/screenshot-3.png" width=370 /> | <img src="./images/screenshot-4.png" width=370 /> |
|
||||
|
||||
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`** | <img src="./images/skeleton.png" width=270 /> | <img src="./images/blocked-for-attack-detected.png" width=270 /> |
|
||||
| **`Rate Limiting`** | <img src="./images/skeleton.png" width=270 /> | <img src="./images/blocked-for-access-too-fast.png" width=270 /> |
|
||||
| **`Anti-Bot Challenge`** | <img src="./images/captcha-1.gif" width=270 /> | <img src="./images/captcha-2.gif" width=270 /> |
|
||||
| **`Auth Challenge`** | <img src="./images/auth-1.gif" width=270 /> | <img src="./images/auth-2.gif" width=270 /> |
|
||||
| **`HTML Dynamic Protection`** | <img src="./images/dynamic-html-1.png" width=270 /> | <img src="./images/dynamic-html-2.png" width=270 /> |
|
||||
| **`JS Dynamic Protection`** | <img src="./images/dynamic-js-1.png" width=270 /> | <img src="./images/dynamic-js-2.png" width=270 /> |
|
||||
|
||||
## 🚀 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.
|
||||
|
||||
<p align="left">
|
||||
<a target="_blank" href="https://discord.gg/SVnZGzHFvn"><img src="https://img.shields.io/badge/Discord-5865F2?style=flat&logo=discord&logoColor=white"></a>
|
||||
<a target="_blank" href="https://x.com/safeline_waf"><img src="https://img.shields.io/badge/X.com-000000?style=flat&logo=x&logoColor=white"></a>
|
||||
<a target="_blank" href="/images/wechat.png"><img src="https://img.shields.io/badge/WeChat-07C160?style=flat&logo=wechat&logoColor=white"></a>
|
||||
</p>
|
||||
|
||||
#### 💪 PRO Edition
|
||||
|
||||
Coming soon!
|
||||
|
||||
#### 📝 License
|
||||
|
||||
See [LICENSE](/LICENSE.md) for details.
|
||||
@@ -0,0 +1,115 @@
|
||||
<p align="center">
|
||||
<img src="/images/banner.png" width="400" />
|
||||
</p>
|
||||
|
||||
<h4 align="center">
|
||||
SafeLine - 雷池 - 不让黑客越过半步
|
||||
</h4>
|
||||
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://waf-ce.chaitin.cn/">🏠 官网</a> |
|
||||
<a target="_blank" href="https://docs.waf-ce.chaitin.cn/">📖 文档</a> |
|
||||
<a target="_blank" href="https://demo.waf-ce.chaitin.cn:9443/">🔍 演示环境</a> |
|
||||
<a target="_blank" href="/images/wechat.png">🙋♂️ 社区微信群</a> |
|
||||
<a target="_blank" href="https://github.com/chaitin/SafeLine">国际版</a>
|
||||
</p>
|
||||
|
||||
## 👋 项目介绍
|
||||
|
||||
SafeLine,中文名 "雷池",是一款简单好用, 效果突出的 **`Web 应用防火墙(WAF)`**,可以保护 Web 服务不受黑客攻击。
|
||||
|
||||
雷池通过过滤和监控 Web 应用与互联网之间的 HTTP 流量来保护 Web 服务。可以保护 Web 服务免受 `SQL 注入`、`XSS`、 `代码注入`、`命令注入`、`CRLF 注入`、`ldap 注入`、`xpath 注入`、`RCE`、`XXE`、`SSRF`、`路径遍历`、`后门`、`暴力破解`、`CC`、`爬虫` 等攻击。
|
||||
|
||||
#### 💡 工作原理
|
||||
|
||||
<img src="/images/how-it-works.png" width="800" />
|
||||
|
||||
雷池通过阻断流向 Web 服务的恶意 HTTP 流量来保护 Web 服务。雷池作为反向代理接入网络,通过在 Web 服务前部署雷池,可在 Web 服务和互联网之间设置一道屏障。
|
||||
|
||||
雷池的核心功能如下:
|
||||
|
||||
- 防护 Web 攻击
|
||||
- 防爬虫, 防扫描
|
||||
- 前端代码动态加密
|
||||
- 基于源 IP 的访问速率限制
|
||||
- HTTP 访问控制
|
||||
|
||||
#### ⚡️ 项目截图
|
||||
|
||||
| <img src="./images/screenshot-1.png" width=370 /> | <img src="./images/screenshot-2.png" width=370 /> |
|
||||
| ------------------------------------------------- | ------------------------------------------------- |
|
||||
| <img src="./images/screenshot-3.png" width=370 /> | <img src="./images/screenshot-4.png" width=370 /> |
|
||||
|
||||
查看 [演示环境](https://demo.waf-ce.chaitin.cn:9443/)
|
||||
|
||||
## 🔥 核心能力
|
||||
|
||||
对于你的网站而言, 雷池可以实现如下效果:
|
||||
|
||||
- **`阻断 Web 攻击`**
|
||||
- 可以防御所有的 Web 攻击,例如 `SQL 注入`、`XSS`、`代码注入`、`操作系统命令注入`、`CRLF 注入`、`XXE`、`SSRF`、`路径遍历` 等等。
|
||||
- **`限制访问频率`**
|
||||
- 限制用户的访问速率,让 Web 服务免遭 `CC 攻击`、`暴力破解`、`流量激增` 和其他类型的滥用。
|
||||
- **`人机验证`**
|
||||
- 互联网上有来自真人用户的流量,但更多的是由爬虫, 漏洞扫描器, 蠕虫病毒, 漏洞利用程序等自动化程序发起的流量,开启雷池的人机验证功能后真人用户会被放行,恶意爬虫将会被阻断。
|
||||
- **`身份认证`**
|
||||
- 雷池的 "身份认证" 功能可以很好的解决 "未授权访问" 漏洞,当用户访问您的网站时,需要输入您配置的用户名和密码信息,不持有认证信息的用户将被拒之门外。
|
||||
- **`动态防护`**
|
||||
- 在用户浏览到的网页内容不变的情况下,将网页赋予动态特性,对 HTML 和 JavaScript 代码进行动态加密,确保每次访问时这些代码都以随机且独特的形态呈现。
|
||||
|
||||
#### 🧩 核心能力展示
|
||||
|
||||
| | Legitimate User | Malicious User |
|
||||
| ----------------------------- | --------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| **`阻断 Web 攻击`** | <img src="./images/skeleton.png" width=270 /> | <img src="./images/blocked-for-attack-detected.png" width=270 /> |
|
||||
| **`限制访问频率`** | <img src="./images/skeleton.png" width=270 /> | <img src="./images/blocked-for-access-too-fast.png" width=270 /> |
|
||||
| **`人机验证`** | <img src="./images/captcha-1.gif" width=270 /> | <img src="./images/captcha-2.gif" width=270 /> |
|
||||
| **`身份认证`** | <img src="./images/auth-1.gif" width=270 /> | <img src="./images/auth-2.gif" width=270 /> |
|
||||
| **`HTML 动态防护`** | <img src="./images/dynamic-html-1.png" width=270 /> | <img src="./images/dynamic-html-2.png" width=270 /> |
|
||||
| **`JS 动态防护`** | <img src="./images/dynamic-js-1.png" width=270 /> | <img src="./images/dynamic-js-2.png" width=270 /> |
|
||||
|
||||
## 🚀 上手指南
|
||||
|
||||
#### 📦 安装
|
||||
|
||||
查看 [安装雷池](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)
|
||||
|
||||
#### ⚙️ 配置防护站点
|
||||
|
||||
查看 [快速配置](https://docs.waf-ce.chaitin.cn/zh/%E4%B8%8A%E6%89%8B%E6%8C%87%E5%8D%97/%E5%BF%AB%E9%80%9F%E9%85%8D%E7%BD%AE)
|
||||
|
||||
## 📋 更多信息
|
||||
|
||||
#### 防护效果测试
|
||||
|
||||
| Metric | ModSecurity, Level 1 | CloudFlare | 雷池, 平衡 | 雷池, 严格 |
|
||||
| ----------------- | -------------------- | -------------------- | ---------------------- | --------------------- |
|
||||
| 样本数量 | 33669 | 33669 | 33669 | 33669 |
|
||||
| **检出率** | 69.74% | 10.70% | 71.65% | **76.17%** |
|
||||
| **误报率** | 17.58% | 0.07% | **0.07%** | 0.22% |
|
||||
| **准确率** | 82.20% | 98.40% | **99.45%** | 99.38% |
|
||||
|
||||
|
||||
#### 雷池可以投入生产使用吗
|
||||
|
||||
是的,已经有不少用户将雷池投入生产使用,截至目前
|
||||
|
||||
- 全球累计装机量已超过 18 万台
|
||||
- 防护的网站数量超过 100 万个
|
||||
- 每天清洗 HTTP 请求超过 300 亿次
|
||||
|
||||
#### 🙋♂️ 用户社区
|
||||
|
||||
欢迎加入雷池 [社区微信群](/images/wechat.png) 进行技术交流。
|
||||
|
||||
也可以加入雷池 [Discord](https://discord.gg/SVnZGzHFvn) 来获取更多社区支持。
|
||||
|
||||
<p align="left">
|
||||
<a target="_blank" href="/images/wechat.png"><img src="https://img.shields.io/badge/WeChat-07C160?style=flat&logo=wechat&logoColor=white"></a>
|
||||
<a target="_blank" href="https://discord.gg/SVnZGzHFvn"><img src="https://img.shields.io/badge/Discord-5865F2?style=flat&logo=discord&logoColor=white"></a>
|
||||
<a target="_blank" href="https://x.com/safeline_waf"><img src="https://img.shields.io/badge/X.com-000000?style=flat&logo=x&logoColor=white"></a>
|
||||
</p>
|
||||
|
||||
#### 💪 专业版
|
||||
|
||||
查看 [社区版 vs 专业版](https://waf-ce.chaitin.cn/version)
|
||||
@@ -0,0 +1,143 @@
|
||||
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
|
||||
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 821 KiB |
|
After Width: | Height: | Size: 806 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 501 KiB |
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="396px" height="407px" viewBox="0 0 396 407" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 12</title>
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
|
||||
<stop stop-color="#4B4B4B" offset="0%"></stop>
|
||||
<stop stop-color="#000000" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<filter x="-3.0%" y="-2.8%" width="106.1%" height="105.6%" filterUnits="objectBoundingBox" id="filter-2">
|
||||
<feGaussianBlur stdDeviation="3" in="SourceGraphic"></feGaussianBlur>
|
||||
</filter>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-3">
|
||||
<stop stop-color="#24BC43" stop-opacity="0.8" offset="0%"></stop>
|
||||
<stop stop-color="#3ACBAB" stop-opacity="0.7" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<path d="M110.049657,49.667649 C110.049657,49.667649 81.1358702,46.2263115 76.8,26.7636364 C72.4880848,46.2263115 43.5503431,49.667649 43.5503431,49.667649 C14.2053649,53.3001718 0,36.4567369 0,36.4567369 C13.941859,65.8036979 38.4,64.7712967 38.4,64.7712967 L115.2,64.7712967 C115.2,64.7712967 139.634186,65.8036979 153.6,36.4567369 C153.6,36.4567369 139.394635,53.3192904 110.049657,49.667649 Z" id="path-4"></path>
|
||||
<filter x="-16.9%" y="-57.9%" width="133.9%" height="236.8%" filterUnits="objectBoundingBox" id="filter-5">
|
||||
<feOffset dx="0" dy="4" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="8" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0.490319293 0 0 0 0 0.292243323 0 0 0 1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
</defs>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="编组-12" transform="translate(49.000000, 38.000000)">
|
||||
<path d="M292.40836,59.04 C290.927217,51.9634286 285.002646,46.6971429 277.761503,46.368 C222.13636,44.8868571 176.385503,16.5805714 157.953503,3.08571429 C152.358074,-1.02857143 144.95236,-1.02857143 139.356931,3.08571429 C120.431217,16.5805714 75.1740742,44.8868571 19.5489314,46.368 C12.4723599,46.6971429 6.21864565,51.9634286 4.90207422,59.04 C-3.98478292,103.474286 -19.2899258,254.057143 148.902074,324 C316.60036,253.892571 300.966074,103.474286 292.40836,59.04 Z" id="路径" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
|
||||
<path d="M292.40836,59.04 C290.927217,51.9634286 285.002646,46.6971429 277.761503,46.368 C222.13636,44.8868571 176.385503,16.5805714 157.953503,3.08571429 C152.358074,-1.02857143 144.95236,-1.02857143 139.356931,3.08571429 C120.431217,16.5805714 75.1740742,44.8868571 19.5489314,46.368 C12.4723599,46.6971429 6.21864565,51.9634286 4.90207422,59.04 C-3.98478292,103.474286 -19.2899258,254.057143 148.902074,324 C316.60036,253.892571 300.966074,103.474286 292.40836,59.04 Z" id="路径" fill="url(#linearGradient-1)" fill-rule="nonzero" filter="url(#filter-2)"></path>
|
||||
<path d="M149,261.4 C205.553958,261.4 251.4,215.553958 251.4,159 C251.4,131.275004 240.381593,106.123494 222.484813,87.6855068 C209.900749,96.0964568 185.81512,106.024178 175.564259,100.853688 C166.334879,96.1984273 157.476591,88.4505652 148.989396,77.610101 C142.047769,88.5334102 134.670586,95.5517221 126.857848,98.6650367 C120.689419,101.123107 98.2592604,102.915695 75.4419467,87.761039 C57.5883513,106.192154 46.6,131.312844 46.6,159 C46.6,215.553958 92.4460416,261.4 149,261.4 Z" id="椭圆形备份-26" fill="url(#linearGradient-3)"></path>
|
||||
<g id="编组-5备份-6" transform="translate(91.771423, 102.101722)" fill="#FFFFFF">
|
||||
<polygon id="路径-130备份-29" transform="translate(57.217971, 95.920999) rotate(-180.000000) translate(-57.217971, -95.920999) " points="56.6651511 64.9496372 -7.57241738e-17 97.1108413 50.6084036 126.892361 68.8016729 117.264704 34.3433228 97.1108413 56.6651511 84.5503086 96.9001091 107.376711 96.9001091 114.88399 114.435942 125.435553 114.435942 97.1108413"></polygon>
|
||||
<polygon id="路径-130备份-30" transform="translate(57.217971, 30.971362) rotate(-360.000000) translate(-57.217971, -30.971362) " points="56.6651511 2.84217094e-14 -7.57241738e-17 32.1612041 50.6084036 61.9427239 68.8016729 52.3150668 34.3433228 32.1612041 56.6651511 19.6006714 96.9001091 42.4270741 96.9001091 49.9343528 114.435942 60.4859155 114.435942 32.1612041"></polygon>
|
||||
<polygon id="路径-130备份-29" opacity="0.40499442" transform="translate(57.217971, 95.920999) rotate(-180.000000) translate(-57.217971, -95.920999) " points="56.6651511 64.9496372 -7.57241738e-17 97.1108413 50.6084036 126.892361 68.8016729 117.264704 34.3433228 97.1108413 56.6651511 84.5503086 96.9001091 107.376711 96.9001091 114.88399 114.435942 125.435553 114.435942 97.1108413"></polygon>
|
||||
<polygon id="路径-130备份-30" opacity="0.40499442" transform="translate(57.217971, 30.971362) rotate(-360.000000) translate(-57.217971, -30.971362) " points="56.6651511 4.8316906e-13 -7.57241738e-17 32.1612041 50.6084036 61.9427239 68.8016729 52.3150668 34.3433228 32.1612041 56.6651511 19.6006714 96.9001091 42.4270741 96.9001091 49.9343528 114.435942 60.4859155 114.435942 32.1612041"></polygon>
|
||||
</g>
|
||||
<g id="长亭logo备份-18" transform="translate(72.200000, 45.222222)" fill-rule="nonzero">
|
||||
<g id="编组-7">
|
||||
<path d="M96.7632666,18.0061837 C96.7632666,18.0061837 79.3862969,15.2966085 76.7907961,0 C74.1952953,15.2966085 56.8183256,18.0061837 56.8183256,18.0061837 C39.1836466,20.8694936 30.6424242,7.60987058 30.6424242,7.60987058 C39.0363842,30.6893013 53.7258141,29.862977 53.7258141,29.862977 L99.8741859,29.862977 C99.8741859,29.862977 114.563616,30.6700845 122.957576,7.60987058 C122.957576,7.60987058 114.416353,20.8694936 96.7816744,18.0061837 L96.7632666,18.0061837 Z" id="路径" fill="#27B876"></path>
|
||||
<g id="路径">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-5)" xlink:href="#path-4"></use>
|
||||
<use fill="#27B876" xlink:href="#path-4"></use>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 343 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 68 KiB |
@@ -0,0 +1,17 @@
|
||||
# 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/
|
||||
@@ -0,0 +1,15 @@
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- deadcode
|
||||
- errcheck
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- varcheck
|
||||
@@ -0,0 +1,46 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Management Micro Service
|
||||
|
||||
## Requirements
|
||||
|
||||
Go 1.18+
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,29 @@
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
# 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
|
||||
@@ -0,0 +1,41 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package controller
|
||||
|
||||
// nginxConfigTpl generates nginx config for each website.
|
||||
// Placeholders:
|
||||
// %s = upstream name
|
||||
// %s = upstream addr(s)
|
||||
// %s = server listen directive
|
||||
// %s = server_name directive
|
||||
// %s = ssl_certificate directive (or empty)
|
||||
// %s = ssl_certificate_key directive (or empty)
|
||||
// %s = upstream scheme (http/https)
|
||||
// %s = upstream name (again for proxy_pass)
|
||||
// %d = site UUID (SL-CE-SUID header value)
|
||||
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;
|
||||
%s
|
||||
%s
|
||||
}
|
||||
}`
|
||||
|
||||
// rateLimitZoneTpl defines a shared memory zone for rate limiting.
|
||||
// %s = zone name (site_N)
|
||||
// %s = rate (e.g. "100r/s")
|
||||
var rateLimitZoneTpl = `limit_req_zone $binary_remote_addr zone=%s:%dm rate=%s;`
|
||||
|
||||
// rateLimitReqTpl applies rate limiting to a location.
|
||||
// %s = zone name
|
||||
// %d = burst value
|
||||
var rateLimitReqTpl = `limit_req zone=%s burst=%d nodelay;`
|
||||
|
||||
// rateLimitRejectTpl returns 429 when rate limit is exceeded.
|
||||
var rateLimitRejectTpl = `limit_req_status 429;`
|
||||
|
||||
// challengeLocationTpl adds a JS challenge verification endpoint.
|
||||
var challengeLocationTpl = `
|
||||
location = /__waf_challenge {
|
||||
internal;
|
||||
root /etc/nginx/challenge_pages;
|
||||
try_files /challenge.html =200;
|
||||
}
|
||||
location = /__waf_verify {
|
||||
internal;
|
||||
proxy_pass http://127.0.0.1:3335/verify;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}`
|
||||
|
||||
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`
|
||||
@@ -0,0 +1,328 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// Rate limiting directives
|
||||
rateLimitDirective := ""
|
||||
if website.RateLimitEnabled && website.RateLimitRPS > 0 {
|
||||
zoneName := fmt.Sprintf("site_%d", website.Id)
|
||||
rps := website.RateLimitRPS
|
||||
burst := website.RateLimitBurst
|
||||
if burst <= 0 {
|
||||
burst = rps * 2
|
||||
}
|
||||
rateLimitDirective = fmt.Sprintf(rateLimitReqTpl, zoneName, burst)
|
||||
}
|
||||
|
||||
// Challenge directive
|
||||
challengeDirective := ""
|
||||
if website.ChallengeEnabled {
|
||||
challengeDirective = `error_page 403 = /__waf_challenge;`
|
||||
}
|
||||
|
||||
upstreamName := fmt.Sprintf(backendTpl, website.Id)
|
||||
nginxConfig := strings.TrimSpace(fmt.Sprintf(nginxConfigTpl,
|
||||
upstreamName, upstreamAddr,
|
||||
serverListen, serverName, sslCertFilename, sslCertKeyFilename,
|
||||
scheme, upstreamName, website.Id,
|
||||
rateLimitDirective, challengeDirective,
|
||||
))
|
||||
|
||||
// Prepend rate limit zone definition if enabled
|
||||
if website.RateLimitEnabled && website.RateLimitRPS > 0 {
|
||||
zoneName := fmt.Sprintf("site_%d", website.Id)
|
||||
rps := website.RateLimitRPS
|
||||
zoneDef := fmt.Sprintf(rateLimitZoneTpl, zoneName, 10, fmt.Sprintf("%dr/s", rps))
|
||||
nginxConfig = zoneDef + "\n" + nginxConfig
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,127 @@
|
||||
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=
|
||||
@@ -0,0 +1,107 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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"`
|
||||
|
||||
// Rate limiting
|
||||
RateLimitEnabled bool `json:"rate_limit_enabled"`
|
||||
RateLimitRPS int `json:"rate_limit_rps"`
|
||||
RateLimitBurst int `json:"rate_limit_burst"`
|
||||
RateLimitAction string `json:"rate_limit_action"`
|
||||
|
||||
// JS Challenge
|
||||
ChallengeEnabled bool `json:"challenge_enabled"`
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
Log LogConfig
|
||||
MgtWebserver string
|
||||
}
|
||||
|
||||
func DefaultGlobalConfig() Config {
|
||||
return Config{
|
||||
Log: DefaultLogConfig(),
|
||||
MgtWebserver: "169.254.0.4:9002",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
ConfigFilePath = "config.yml"
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
DefaultForbiddenPageMd5 = "d9921f84f36a6cc92a6fc13946a18e98"
|
||||
DefaultForbiddenPage = ``
|
||||
)
|
||||
@@ -0,0 +1,132 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
specCheckChallengePage = "0/5 * * * * ?"
|
||||
challengePageDir = "/etc/nginx/challenge_pages"
|
||||
challengePagePath = "/etc/nginx/challenge_pages/challenge.html"
|
||||
)
|
||||
|
||||
// challengePageData holds template data for the challenge page.
|
||||
type challengePageData struct {
|
||||
Timestamp int64
|
||||
Token string
|
||||
}
|
||||
|
||||
var challengePageTemplate = template.Must(template.New("challenge").Parse(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>安全验证</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f0f2f5;display:flex;justify-content:center;align-items:center;min-height:100vh}
|
||||
.container{background:#fff;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,.1);padding:40px;max-width:400px;text-align:center}
|
||||
.lock-icon{width:60px;height:60px;margin:0 auto 20px;border-radius:50%;background:#e8f5e9;display:flex;align-items:center;justify-content:center}
|
||||
.lock-icon svg{width:30px;height:30px;fill:#4caf50}
|
||||
h2{margin-bottom:10px;font-size:20px;color:#333}
|
||||
p{color:#666;margin-bottom:20px;line-height:1.5}
|
||||
.checking{display:flex;align-items:center;justify-content:center;gap:8px}
|
||||
.spinner{width:20px;height:20px;border:3px solid #e0e0e0;border-top-color:#4caf50;border-radius:50%;animation:spin 1s linear infinite}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.status{color:#4caf50;font-weight:500}
|
||||
.error{color:#f44336;display:none}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="lock-icon">
|
||||
<svg viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
|
||||
</div>
|
||||
<h2>安全验证</h2>
|
||||
<p>正在验证您的浏览器安全环境...</p>
|
||||
<div class="checking" id="checking">
|
||||
<div class="spinner"></div>
|
||||
<span class="status" id="statusText">验证中...</span>
|
||||
</div>
|
||||
<div class="error" id="errorText">验证失败,请刷新重试</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var n={{.Timestamp}};
|
||||
var t=document.getElementById("statusText");
|
||||
var e=document.getElementById("errorText");
|
||||
var c=document.getElementById("checking");
|
||||
|
||||
// Simulate browser fingerprint checks
|
||||
setTimeout(function(){t.textContent="检查浏览器指纹..."},500);
|
||||
setTimeout(function(){t.textContent="验证 Cookie..."},1500);
|
||||
setTimeout(function(){
|
||||
// Set verification cookie
|
||||
var exp=new Date();
|
||||
exp.setTime(exp.getTime()+3600*1000);
|
||||
document.cookie="__waf_verified=1;expires="+exp.toUTCString()+";path=/;SameSite=Lax";
|
||||
|
||||
t.textContent="验证完成,正在跳转...";
|
||||
setTimeout(function(){window.location.reload(true)},800);
|
||||
},2500);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`))
|
||||
|
||||
func checkAndUpdateChallengePage() {
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(challengePageDir, 0755); err != nil {
|
||||
logger.Error("Failed to create challenge page directory:", err)
|
||||
return
|
||||
}
|
||||
|
||||
existed, err := utils.FileExist(challengePagePath)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Always regenerate with fresh timestamp
|
||||
data := challengePageData{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Token: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
templateWriter := &templateBuffer{buf: buf}
|
||||
if err := challengePageTemplate.Execute(templateWriter, data); err != nil {
|
||||
logger.Error("Failed to execute challenge template:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(challengePagePath, templateWriter.buf, 0644); err != nil {
|
||||
logger.Error("Failed to write challenge page:", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = existed // suppress unused warning
|
||||
}
|
||||
|
||||
// templateBuffer is a simple writer for template execution.
|
||||
type templateBuffer struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (w *templateBuffer) Write(p []byte) (n int, err error) {
|
||||
w.buf = append(w.buf, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Ensure challenge page directory exists on startup
|
||||
_ = os.MkdirAll(challengePageDir, 0755)
|
||||
_ = filepath.Clean(challengePagePath)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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
|
||||
}
|
||||
|
||||
_, err = cronInstance.AddFunc(specCheckChallengePage, checkAndUpdateChallengePage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cronInstance.Start()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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{}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
tmp
|
||||
Taskfile.yml
|
||||
.air.toml
|
||||
@@ -0,0 +1,63 @@
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,186 @@
|
||||
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 {
|
||||
Password string `json:"password"`
|
||||
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)
|
||||
|
||||
// Password authentication (new)
|
||||
if params.Password != "" {
|
||||
if !user.CheckPassword(params.Password) {
|
||||
response.Error(c, response.JSONBody{Err: response.ErrWrongPasscode, Msg: "Invalid password"}, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// If TOTP is enabled, also require passcode (2FA)
|
||||
if user.TFAEnabled && user.TFASecret != "" {
|
||||
if params.Passcode == "" {
|
||||
response.Error(c, response.JSONBody{Err: "totp_required", Msg: "TOTP verification required"}, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
valid := totp.Validate(params.Passcode, user.TFASecret)
|
||||
if !valid {
|
||||
// Check clock skew
|
||||
millisecondTimestamp := params.Timestamp
|
||||
localTimeStamp := time.Now()
|
||||
if millisecondTimestamp > 0 {
|
||||
otpTime := time.Unix(millisecondTimestamp/1000, (millisecondTimestamp%1000)*int64(time.Millisecond))
|
||||
timeSub := localTimeStamp.Sub(otpTime)
|
||||
seconds := math.Abs(timeSub.Seconds())
|
||||
if seconds >= 60 {
|
||||
response.Error(c, response.JSONBody{Err: response.ErrWrongTimeGap, Msg: "otp timestamp gap is more than a minute"}, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
response.Error(c, response.JSONBody{Err: response.ErrWrongPasscode, Msg: "Invalid TOTP passcode"}, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Password auth success
|
||||
user.LastLoginTime = time.Now().Unix()
|
||||
user.IsEnabled = true
|
||||
db.Save(&user)
|
||||
|
||||
session := sessions.Default(c)
|
||||
session.Options(sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 3600 * 24 * 7,
|
||||
})
|
||||
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, gin.H{"role": user.Role})
|
||||
return
|
||||
}
|
||||
|
||||
// Legacy TOTP-only authentication
|
||||
if params.Passcode != "" {
|
||||
valid := totp.Validate(params.Passcode, user.TFASecret)
|
||||
if !valid {
|
||||
millisecondTimestamp := params.Timestamp
|
||||
localTimeStamp := time.Now()
|
||||
if millisecondTimestamp > 0 {
|
||||
otpTime := time.Unix(millisecondTimestamp/1000, (millisecondTimestamp%1000)*int64(time.Millisecond))
|
||||
timeSub := localTimeStamp.Sub(otpTime)
|
||||
seconds := math.Abs(timeSub.Seconds())
|
||||
if seconds >= 60 {
|
||||
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,
|
||||
})
|
||||
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, gin.H{"role": user.Role})
|
||||
return
|
||||
}
|
||||
|
||||
response.Error(c, response.JSONBody{Err: response.ErrorParamNotOK, Msg: "Password or passcode required"}, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
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, "role": user.Role})
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
|
||||
)
|
||||
|
||||
type PostBehaviourRequest struct {
|
||||
SrcRouter string `json:"src_router"`
|
||||
DstRouter string `json:"dst_router"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// Behaviour tracking disabled - just return success
|
||||
response.Success(c, nil)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
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})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
|
||||
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
func GetVersion(c *gin.Context) {
|
||||
response.Success(c, gin.H{"version": constants.Version})
|
||||
}
|
||||
|
||||
func GetUpgradeTips(ctx *gin.Context) {
|
||||
// External version check disabled - always return no upgrade needed
|
||||
response.Success(ctx, gin.H{"upgrade_tips": constants.NotUpgrade})
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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)})
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"chaitin.cn/dev/go/errors"
|
||||
|
||||
"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 (
|
||||
GetDetectLogDetailRequest struct {
|
||||
EventId string `json:"event_id" form:"event_id"`
|
||||
}
|
||||
|
||||
PostFalsePositivesRequest struct {
|
||||
EventId string `json:"event_id"`
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Telemetry reporting removed - just return success
|
||||
response.Success(c, nil)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package api
|
||||
|
||||
const (
|
||||
Version = "/Version"
|
||||
UpgradeTips = "/UpgradeTips"
|
||||
Login = "/Login"
|
||||
Logout = "/Logout"
|
||||
OTPUrl = "/OTPUrl"
|
||||
User = "/User"
|
||||
CurrentUser = "/CurrentUser"
|
||||
Users = "/Users"
|
||||
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"
|
||||
PolicyGroupPreset = "/PolicyGroupPreset"
|
||||
SrcIPConfig = "/SrcIPConfig"
|
||||
)
|
||||
@@ -0,0 +1,185 @@
|
||||
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})
|
||||
}
|
||||
|
||||
type PolicyGroupPresetRequest struct {
|
||||
Preset string `json:"preset" binding:"required,oneof=strict default disable"`
|
||||
}
|
||||
|
||||
func PostPolicyGroupPreset(ctx *gin.Context) {
|
||||
var params PolicyGroupPresetRequest
|
||||
if err := ctx.BindJSON(¶ms); err != nil {
|
||||
logger.Error(err)
|
||||
response.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// All 17 detection modules
|
||||
modules := []string{
|
||||
"m_asp_code_injection", "m_cmd_injection", "m_csrf",
|
||||
"m_file_include", "m_file_upload", "m_http",
|
||||
"m_java", "m_java_unserialize", "m_php_code_injection",
|
||||
"m_php_unserialize", "m_response", "m_rule",
|
||||
"m_scanner", "m_sqli", "m_ssrf", "m_ssti", "m_xss",
|
||||
}
|
||||
|
||||
pgg := make(model.PolicyGroup)
|
||||
for _, m := range modules {
|
||||
pgg[m] = params.Preset
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
pggStr, err := json.Marshal(pgg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pggOption.Value = string(pggStr)
|
||||
tx.Save(&pggOption)
|
||||
|
||||
if err := fvm.PushFSL(tx); err != nil {
|
||||
return errors.New("Rules compile error")
|
||||
}
|
||||
|
||||
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, gin.H{"preset": params.Preset, "data": pgg})
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
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})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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"
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"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 CreateUserRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=32"`
|
||||
Password string `json:"password" binding:"required,min=6,max=64"`
|
||||
Role string `json:"role" binding:"required,oneof=admin operator viewer"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
type UpdateUserRequest struct {
|
||||
Role string `json:"role" binding:"omitempty,oneof=admin operator viewer"`
|
||||
Comment string `json:"comment"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type ResetPasswordRequest struct {
|
||||
Password string `json:"password" binding:"required,min=6,max=64"`
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
Comment string `json:"comment"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func GetUsers(c *gin.Context) {
|
||||
db := database.GetDB()
|
||||
var users []model.User
|
||||
db.Find(&users)
|
||||
|
||||
var result []UserInfo
|
||||
for _, u := range users {
|
||||
result = append(result, UserInfo{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Role: u.Role,
|
||||
Comment: u.Comment,
|
||||
Enabled: u.IsEnabled,
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"data": result})
|
||||
}
|
||||
|
||||
func PostUser(c *gin.Context) {
|
||||
var params CreateUserRequest
|
||||
if err := c.BindJSON(¶ms); err != nil {
|
||||
logger.Error(err)
|
||||
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
|
||||
// Check if username already exists
|
||||
var existing model.User
|
||||
res := db.Where(&model.User{Username: params.Username}).First(&existing)
|
||||
if res.RowsAffected > 0 {
|
||||
response.Error(c, response.JSONBody{Err: "username_exists", Msg: "Username already exists"}, http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Username: params.Username,
|
||||
Role: params.Role,
|
||||
Comment: params.Comment,
|
||||
}
|
||||
if err := user.SetPassword(params.Password); err != nil {
|
||||
logger.Error(err)
|
||||
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Failed to hash password"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
logger.Error(err)
|
||||
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
})
|
||||
}
|
||||
|
||||
func PutUser(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrorParamNotOK, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var params UpdateUserRequest
|
||||
if err := c.BindJSON(¶ms); err != nil {
|
||||
logger.Error(err)
|
||||
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
var user model.User
|
||||
res := db.First(&user, id)
|
||||
if res.RowsAffected == 0 {
|
||||
response.Error(c, response.ErrorDataNotExist, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent disabling the last admin
|
||||
if params.Enabled != nil && !*params.Enabled && user.Role == "admin" {
|
||||
var adminCount int64
|
||||
db.Model(&model.User{}).Where("role = ? AND is_enabled = true", "admin").Count(&adminCount)
|
||||
if adminCount <= 1 {
|
||||
response.Error(c, response.JSONBody{Err: "last_admin", Msg: "Cannot disable the last admin user"}, http.StatusConflict)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if params.Role != "" {
|
||||
updates["role"] = params.Role
|
||||
}
|
||||
if params.Comment != "" {
|
||||
updates["comment"] = params.Comment
|
||||
}
|
||||
if params.Enabled != nil {
|
||||
updates["is_enabled"] = *params.Enabled
|
||||
}
|
||||
|
||||
db.Model(&user).Updates(updates)
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func DeleteUser(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrorParamNotOK, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
var user model.User
|
||||
res := db.First(&user, id)
|
||||
if res.RowsAffected == 0 {
|
||||
response.Error(c, response.ErrorDataNotExist, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent deleting the last admin
|
||||
if user.Role == "admin" {
|
||||
var adminCount int64
|
||||
db.Model(&model.User{}).Where("role = ? AND is_enabled = true", "admin").Count(&adminCount)
|
||||
if adminCount <= 1 {
|
||||
response.Error(c, response.JSONBody{Err: "last_admin", Msg: "Cannot delete the last admin user"}, http.StatusConflict)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent self-deletion
|
||||
sessionUser := c.GetInt("user_id")
|
||||
if uint(sessionUser) == user.ID {
|
||||
response.Error(c, response.JSONBody{Err: "self_delete", Msg: "Cannot delete your own account"}, http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
db.Delete(&user)
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func PostResetPassword(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrorParamNotOK, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var params ResetPasswordRequest
|
||||
if err := c.BindJSON(¶ms); err != nil {
|
||||
logger.Error(err)
|
||||
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
var user model.User
|
||||
res := db.First(&user, id)
|
||||
if res.RowsAffected == 0 {
|
||||
response.Error(c, response.ErrorDataNotExist, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.SetPassword(params.Password); err != nil {
|
||||
logger.Error(err)
|
||||
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Failed to hash password"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
db.Model(&user).Update("password_hash", user.PasswordHash)
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
// GetUser returns the current authenticated user info
|
||||
func GetCurrentUser(c *gin.Context) {
|
||||
userId, _ := c.Get("user_id")
|
||||
db := database.GetDB()
|
||||
var user model.User
|
||||
res := db.First(&user, userId)
|
||||
if res.RowsAffected == 0 {
|
||||
response.Error(c, response.ErrorDataNotExist, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
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})
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import "chaitin.cn/patronus/safeline-2/management/webserver/model"
|
||||
|
||||
func FakeLogs() {
|
||||
model.InitDetectLogSamples()
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
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
|
||||
golang.org/x/crypto v0.28.0
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,223 @@
|
||||
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=
|
||||
@@ -0,0 +1,225 @@
|
||||
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 = version
|
||||
// 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.CurrentUser, api.GetCurrentUser)
|
||||
|
||||
// Admin-only user management
|
||||
adminRouters := limitedRouters.Group("")
|
||||
adminRouters.Use(middleware.RequireRole("admin"))
|
||||
adminRouters.GET(api.Users, api.GetUsers)
|
||||
adminRouters.POST(api.Users, api.PostUser)
|
||||
adminRouters.PUT(api.Users+"/:id", api.PutUser)
|
||||
adminRouters.DELETE(api.Users+"/:id", api.DeleteUser)
|
||||
adminRouters.POST(api.Users+"/:id/reset-password", api.PostResetPassword)
|
||||
|
||||
// Operator and above (read + policy management)
|
||||
operatorRouters := limitedRouters.Group("")
|
||||
operatorRouters.Use(middleware.RequireRole("admin", "operator"))
|
||||
|
||||
operatorRouters.GET(api.DetectLogList, api.GetDetectLogList)
|
||||
operatorRouters.GET(api.DetectLogDetail, api.GetDetectLogDetail)
|
||||
|
||||
operatorRouters.POST(api.Website, api.PostWebsite)
|
||||
operatorRouters.PUT(api.Website, api.PutWebsite)
|
||||
operatorRouters.DELETE(api.Website, api.DeleteWebsite)
|
||||
operatorRouters.GET(api.Website, api.GetWebsite)
|
||||
|
||||
operatorRouters.POST(api.UploadSSLCert, api.PostUploadSSLCert)
|
||||
operatorRouters.POST(api.SSLCert, api.PostSSLCert)
|
||||
|
||||
operatorRouters.POST(api.PolicyRule, api.PostPolicyRule)
|
||||
operatorRouters.PUT(api.PolicyRule, api.PutPolicyRule)
|
||||
operatorRouters.DELETE(api.PolicyRule, api.DeletePolicyRule)
|
||||
operatorRouters.GET(api.PolicyRule, api.GetPolicyRule)
|
||||
operatorRouters.PUT(api.SwitchPolicyRule, api.PutSwitchPolicyRule)
|
||||
|
||||
// All authenticated users can access dashboard and view policies
|
||||
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.POST(api.PolicyGroupPreset, api.PostPolicyGroupPreset)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
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/model"
|
||||
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
|
||||
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Load user from database to get current role/status
|
||||
db := database.GetDB()
|
||||
var userModel model.User
|
||||
res := db.First(&userModel, user)
|
||||
if res.RowsAffected == 0 {
|
||||
response.Error(c, response.ErrorLoginRequired, http.StatusUnauthorized)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is still enabled
|
||||
if !userModel.IsEnabled {
|
||||
session.Clear()
|
||||
session.Save()
|
||||
response.Error(c, response.JSONBody{Err: "account_disabled", Msg: "Account has been disabled"}, http.StatusForbidden)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Store user info in context for downstream handlers
|
||||
c.Set("user_id", int(userModel.ID))
|
||||
c.Set("user_role", userModel.Role)
|
||||
c.Set("username", userModel.Username)
|
||||
|
||||
// extend session expired time
|
||||
session.Options(sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 3600 * 24 * 7,
|
||||
})
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// RequireRole returns a middleware that checks if the user has one of the required roles.
|
||||
func RequireRole(roles ...string) gin.HandlerFunc {
|
||||
roleSet := make(map[string]bool)
|
||||
for _, r := range roles {
|
||||
roleSet[r] = true
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
userRole, exists := c.Get("user_role")
|
||||
if !exists {
|
||||
response.Error(c, response.ErrorLoginRequired, http.StatusUnauthorized)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
role, ok := userRole.(string)
|
||||
if !ok || !roleSet[role] {
|
||||
response.Error(c, response.JSONBody{Err: "forbidden", Msg: "Insufficient permissions"}, http.StatusForbidden)
|
||||
c.Abort()
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
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")
|
||||
@@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type Behaviour struct {
|
||||
Base
|
||||
SrcRouter string `json:"src_router"`
|
||||
DstRouter string `json:"dst_router"`
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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"
|
||||
"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)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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" // 正则
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
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"`
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"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 `gorm:"-"` // transient, not stored
|
||||
PasswordHash string
|
||||
Comment string
|
||||
Role string `gorm:"default:viewer"` // admin, operator, viewer
|
||||
|
||||
TFAEnabled bool `gorm:"column:tfa_enabled;default:true"`
|
||||
TFASecret string `gorm:"column:tfa_secret"`
|
||||
LastLoginTime int64 `gorm:"default:0"`
|
||||
IsEnabled bool `gorm:"default:true"`
|
||||
}
|
||||
|
||||
// HasPermission checks if the user role has the required permission level.
|
||||
// admin > operator > viewer
|
||||
func (u *User) HasPermission(requiredRole string) bool {
|
||||
roleHierarchy := map[string]int{
|
||||
"viewer": 1,
|
||||
"operator": 2,
|
||||
"admin": 3,
|
||||
}
|
||||
userLevel, ok := roleHierarchy[u.Role]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
requiredLevel, ok := roleHierarchy[requiredRole]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return userLevel >= requiredLevel
|
||||
}
|
||||
|
||||
// CheckPassword verifies a plaintext password against the bcrypt hash.
|
||||
func (u *User) CheckPassword(password string) bool {
|
||||
if u.PasswordHash == "" {
|
||||
return false
|
||||
}
|
||||
err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// SetPassword hashes and sets the password.
|
||||
func (u *User) SetPassword(password string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.PasswordHash = string(hash)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateRandomPassword generates a random 16-char password.
|
||||
func GenerateRandomPassword() (string, error) {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
|
||||
var sb strings.Builder
|
||||
for i := 0; i < 16; i++ {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sb.WriteByte(chars[n.Int64()])
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func initAdminUser() error {
|
||||
db := database.GetDB()
|
||||
|
||||
// Check if admin already exists
|
||||
var existing User
|
||||
res := db.Where(&User{Username: constants.SuperUser}).First(&existing)
|
||||
if res.RowsAffected > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate random password for admin
|
||||
defaultPassword, err := GenerateRandomPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(defaultPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
admin := User{
|
||||
Username: constants.SuperUser,
|
||||
PasswordHash: string(hash),
|
||||
Role: "admin",
|
||||
Comment: "Default admin user",
|
||||
TFAEnabled: true,
|
||||
IsEnabled: true,
|
||||
}
|
||||
|
||||
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&admin)
|
||||
|
||||
// Log the default password (will appear in container logs)
|
||||
// In production, the mgt-cli reset-admin command handles this
|
||||
_ = defaultPassword
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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"`
|
||||
|
||||
// Rate limiting (nginx limit_req)
|
||||
RateLimitEnabled bool `gorm:"rate_limit_enabled;default:false" json:"rate_limit_enabled"`
|
||||
RateLimitRPS int `gorm:"rate_limit_rps;default:100" json:"rate_limit_rps"`
|
||||
RateLimitBurst int `gorm:"rate_limit_burst;default:200" json:"rate_limit_burst"`
|
||||
RateLimitAction string `gorm:"rate_limit_action;default:reject" json:"rate_limit_action"`
|
||||
|
||||
// JS Challenge (human verification)
|
||||
ChallengeEnabled bool `gorm:"challenge_enabled;default:false" json:"challenge_enabled"`
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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: "",
|
||||
MgtResDir: "/resources/management",
|
||||
NgxResDir: "/resources/nginx",
|
||||
}
|
||||
}
|
||||