Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d871e638b | ||
|
|
ad2aeb4cf9 | ||
|
|
8247d11dda | ||
|
|
63588b8f5e | ||
|
|
bf8d64c6b2 | ||
|
|
0b8b253efc | ||
|
|
24e2bac1bd | ||
|
|
8113dbb171 | ||
|
|
66c4e60255 | ||
|
|
eeae48affb | ||
|
|
75a5346e6f | ||
|
|
b6f694a376 | ||
|
|
9cab30cef1 | ||
|
|
8fd59844ea | ||
|
|
681e7f95f2 | ||
|
|
84c2db9ee3 | ||
|
|
c845231a1d | ||
|
|
91a26b8f93 | ||
|
|
24770cf5b4 | ||
|
|
ff4d6616fb | ||
|
|
a13d269c28 | ||
|
|
19db896a9a | ||
|
|
01c38c26d0 | ||
|
|
14fb45f648 | ||
|
|
f371d588a4 | ||
|
|
1317b3ed5a | ||
|
|
7fdf63310d | ||
|
|
1578ea6027 | ||
|
|
ec3dc96765 | ||
|
|
48e828cc03 | ||
|
|
78e71aae23 | ||
|
|
857e0c46ef | ||
|
|
8cee76ecbe | ||
|
|
6abcb6b69f | ||
|
|
075bd524a9 | ||
|
|
5a96c2d4d0 | ||
|
|
8de97ff483 | ||
|
|
b7449480b3 | ||
|
|
38f00302bf | ||
|
|
850fd440ff | ||
|
|
b243e2b51d | ||
|
|
34fc8fa8e5 | ||
|
|
6ceeae4d2b | ||
|
|
abd0427273 | ||
|
|
82f0c5d52e | ||
|
|
c7fa1efe5d | ||
|
|
be0571a67f | ||
|
|
a79d932801 | ||
|
|
8157ef050e | ||
|
|
04d1891891 | ||
|
|
1d7eeeba36 | ||
|
|
52f6e857df | ||
|
|
255fd4173d | ||
|
|
1a80a5ac07 | ||
|
|
6324dea166 | ||
|
|
695c438ec3 | ||
|
|
bb0b1187a0 | ||
|
|
ba84cc5380 | ||
|
|
e7f6a66083 | ||
|
|
9086cf52d4 | ||
|
|
b853673100 | ||
|
|
381f2cba28 | ||
|
|
37d37728ca | ||
|
|
a1f151eed6 | ||
|
|
0e88a09fcf | ||
|
|
644943fac1 | ||
|
|
41888dcff7 | ||
|
|
c0dfa51925 | ||
|
|
8d66f96228 | ||
|
|
b7a2e86c5e | ||
|
|
078ef2a3ce | ||
|
|
49deca7ce5 | ||
|
|
53a955b439 | ||
|
|
d4f31efdea | ||
|
|
c3f064fa4c | ||
|
|
233ac9547c | ||
|
|
5d73a333d0 | ||
|
|
e443cd47c0 | ||
|
|
2f61ebaf85 | ||
|
|
9fe9ff3ef1 | ||
|
|
a75123c86d | ||
|
|
599a6903a9 | ||
|
|
bf87488eff | ||
|
|
c2c3fe32fb | ||
|
|
a18cc16b33 | ||
|
|
3efdd3fff5 | ||
|
|
9d156c65ba | ||
|
|
8f575dc232 | ||
|
|
e44e88c136 | ||
|
|
3f8187ae6c | ||
|
|
de7307fbda | ||
|
|
2685813f4d | ||
|
|
78912d064e | ||
|
|
e1e801cec8 | ||
|
|
71b14ddcb0 | ||
|
|
f851033a7a | ||
|
|
82a2e4473a | ||
|
|
c6f5467000 | ||
|
|
2c7f0f94c5 | ||
|
|
e2672c93e2 | ||
|
|
4e033ed5f3 | ||
|
|
d060377caa | ||
|
|
07900ac11d | ||
|
|
cf62e2c07e | ||
|
|
e63799dd78 | ||
|
|
da209259cd | ||
|
|
3d9420095b | ||
|
|
86689954c0 | ||
|
|
b3238a462d | ||
|
|
b087450844 | ||
|
|
3c2ebf308b | ||
|
|
9476db4119 | ||
|
|
35acf92d5b | ||
|
|
b776a2a598 | ||
|
|
afc47d4faa | ||
|
|
3a20f0a8f5 | ||
|
|
325e169d7d | ||
|
|
2a549cae19 | ||
|
|
5917c2f184 | ||
|
|
1b642fd9c4 | ||
|
|
82a1cacd30 | ||
|
|
61302eb183 | ||
|
|
8a22751ade | ||
|
|
9c76cdd002 |
5
.github/ISSUE_TEMPLATE/feature-request.yaml
vendored
@@ -25,7 +25,4 @@ body:
|
||||
Background and the problem that frustrates you
|
||||
|
||||
validations:
|
||||
required: false
|
||||
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
|
||||
41
.github/workflows/slmcp-docker.yml
vendored
Normal file
@@ -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
|
||||
3
.gitignore
vendored
@@ -5,3 +5,6 @@
|
||||
*.tar.gz
|
||||
build.sh
|
||||
compose.yml
|
||||
__pycache__
|
||||
.cursor
|
||||
.vscode
|
||||
3
.gitmodules
vendored
@@ -3,3 +3,6 @@
|
||||
url = https://github.com/chaitin/blazehttp
|
||||
|
||||
|
||||
[submodule "sdk/traefik-safeline"]
|
||||
path = sdk/traefik-safeline
|
||||
url = https://github.com/chaitin/traefik-safeline
|
||||
|
||||
209
README.md
@@ -1,35 +1,34 @@
|
||||
|
||||
# SafeLine, make your web app secure default
|
||||
|
||||
<img src="/images/403.svg" align="right" width="200" />
|
||||
|
||||
SafeLine is a web security gateway to protect your websites from attacks and exploits.
|
||||
|
||||
It defenses for all of web attacks, such as sql injection, code injection, os command injection, CRLF injection, ldap injection, xpath injection, rce, xss, xxe, ssrf, path traversal, backdoor, bruteforce, http-flood, bot abused and so on.
|
||||
|
||||
<p align="left">
|
||||
<a target="_blank" href="https://waf.chaitin.com/">🏠Home</a> |
|
||||
<a target="_blank" href="https://docs.waf.chaitin.com/">📖Documentation</a> |
|
||||
<a target="_blank" href="https://demo.waf.chaitin.com:9443/dashboard">🔍Live Demo</a> |
|
||||
<a target="_blank" href="https://waf-ce.chaitin.cn/">中文版</a>
|
||||
<p align="center">
|
||||
<img src="/images/banner.png" width="400" />
|
||||
</p>
|
||||
|
||||
<p align="left">
|
||||
<a target="_blank" href="https://discord.gg/WBnHAsGgv7"><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-000000?style=flat&logo=x&logoColor=white"></a>
|
||||
<a target="_blank" href="https://t.me/safeline_waf"><img src="https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white"></a>
|
||||
<a target="_blank" href="/images/wechat-230825.png"><img src="https://img.shields.io/badge/WeChat-07C160?style=flat&logo=wechat&logoColor=white"></a>
|
||||
<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>
|
||||
|
||||
# Screenshots
|
||||
## 👋 INTRODUCTION
|
||||
|
||||
<img src="./images/safeline_en.png" width=600 />
|
||||
SafeLine is a self-hosted **`WAF(Web Application Firewall)`** to protect your web apps from attacks and exploits.
|
||||
|
||||
# How It Works
|
||||
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.
|
||||
|
||||
<img src="/images/safeline-as-proxy.png" align="right" width=400 />
|
||||
#### 💡 How It Works
|
||||
|
||||
SafeLine is developed based on nginx, it serves as a reverse proxy middleware to detect and cleans web attacks, its core capabilities include:
|
||||
<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
|
||||
@@ -37,107 +36,93 @@ SafeLine is developed based on nginx, it serves as a reverse proxy middleware to
|
||||
- IP-based rate limiting
|
||||
- Web Access Control List
|
||||
|
||||
# Installation
|
||||
#### ⚡️ Screenshots
|
||||
|
||||
**中国大陆用户安装国际版可能会导致无法连接云服务,请查看** [中文版安装文档](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)
|
||||
| <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 /> |
|
||||
|
||||
## Automatic Deploy
|
||||
Get [Live Demo](https://demo.waf.chaitin.com:9443/)
|
||||
|
||||
> 👍Recommended
|
||||
## 🔥 FEATURES
|
||||
|
||||
Use the following command to start the automated installation of SafeLine. (This process requires root privileges)
|
||||
List of the main features as follows:
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -fsSLk https://waf.chaitin.com/release/latest/setup.sh)"
|
||||
```
|
||||
- **`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.
|
||||
|
||||
After the command is executed, it means the installation is successfully. Please go to "Use Web UI" directly.
|
||||
#### 🧩 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% |
|
||||
|
||||
|
||||
## Mannually Deploy
|
||||
#### Is SafeLine Production-Ready?
|
||||
|
||||
to see [Documentation](https://docs.waf.chaitin.com/en/tutorials/install)
|
||||
Yes, SafeLine is production-ready.
|
||||
|
||||
# Usage
|
||||
- Over 180,000 installations worldwide
|
||||
- Protecting over 1,000,000 Websites
|
||||
- Handling over 30,000,000,000 HTTP Requests Daily
|
||||
|
||||
## Login
|
||||
#### 🙋♂️ Community
|
||||
|
||||
Open the web console page `https://<safeline-ip>:9443/` in the browser, then you will see below.
|
||||
Join our [Discord](https://discord.gg/SVnZGzHFvn) to get community support, the core team members are identified by the STAFF role in Discord.
|
||||
|
||||
<img width="400" src="/images/login.png">
|
||||
- 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.
|
||||
|
||||
Execute the following command to get administrator account
|
||||
Several contact options exist for our community, the primary one being Discord. These are in addition to GitHub issues for creating a new issue.
|
||||
|
||||
```bash
|
||||
docker exec safeline-mgt /app/mgt-cli reset-admin --once
|
||||
```
|
||||
|
||||
After the command is successfully executed, you will see the following content
|
||||
|
||||
> Please must remember this content
|
||||
|
||||
```text
|
||||
[SafeLine] Initial username:admin
|
||||
[SafeLine] Initial password:**********
|
||||
[SafeLine] Done
|
||||
```
|
||||
|
||||
Enter the password in the previous step and you will successfully logged into SafeLine.
|
||||
|
||||
## Protecting a website
|
||||
|
||||
Log into the SafeLine Web Admin Console, go to the "Site" -> "Website" page and click the "Add Site" button in the upper right corner.
|
||||
|
||||
<img src="/images/add-site-1.png" width=800>
|
||||
|
||||
In the next dialog box, enter the information to the original website.
|
||||
|
||||
- **Domain**: domain name of your original website, or hostname, or ip address, for example: `www.chaitin.com`
|
||||
- **Port**: port that SafeLine will listen, such as 80 or 443. (for `https` websites, please check the `SSL` option)
|
||||
- **Upstream**: real address of your original website, through which SafeLine will forward traffic to it
|
||||
|
||||
After completing the above settings, please resolve the domain name you just entered to the IP address of the server where SafeLine is located.
|
||||
|
||||
<img src="/images/add-site-2.png" width=400>
|
||||
|
||||
Then you can access the website protected by the SafeLine through the domain name like this.
|
||||
|
||||
<img src="/images/safeline-as-proxy-2.png" width=400>
|
||||
|
||||
## Try to attack your website
|
||||
|
||||
Now, your website is protected by SafeLine, let’s try tp attack it and see what happens.
|
||||
|
||||
If https://chaitin.com is a website protected by SafeLine, here are some test cases for common attacks:
|
||||
|
||||
- SQL Injection: `https://chaitin.com/?id=1+and+1=2+union+select+1`
|
||||
- XSS: `https://chaitin.com/?id=<img+src=x+onerror=alert()>`
|
||||
- Path Traversal: `https://chaitin.com/?id=../../../../etc/passwd`
|
||||
- Code Injection: `https://chaitin.com/?id=phpinfo();system('id')`
|
||||
- XXE: `https://chaitin.com/?id=<?xml+version="1.0"?><!DOCTYPE+foo+SYSTEM+"">`
|
||||
|
||||
Replace `chaitin.com` in the above cases with your website domain name and try to access it.
|
||||
|
||||
<img src="/images/blocked.png" width=400>
|
||||
|
||||
Check the web console of SafeLine to see the attack list
|
||||
|
||||
<img src="/images/log-list.png" width=800>
|
||||
|
||||
To view the specific details of the attack, click "detail"
|
||||
|
||||
<img src="/images/log-detail.png" width=600>
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://github.com/chaitin/safeline/stargazers">
|
||||
<img width="500" alt="Star History Chart" src="https://api.star-history.com/svg?repos=chaitin/safeline&type=Date">
|
||||
</a>
|
||||
|
||||
## Related Repo
|
||||
<p >
|
||||
<a href="https://github.com/chaitin/yanshi">Automaton Generator</a> |
|
||||
<a href="https://github.com/chaitin/safeline-open-platform">Lua Plugin</a> |
|
||||
<a href="https://github.com/chaitin/lua-resty-t1k">T1K Protocol</a> |
|
||||
<a href="https://github.com/chaitin/blazehttp">WAF Test Tool</a>
|
||||
<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.
|
||||
|
||||
115
README_CN.md
Normal file
@@ -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)
|
||||
73
compose.yaml
@@ -14,7 +14,7 @@ services:
|
||||
postgres:
|
||||
container_name: safeline-pg
|
||||
restart: always
|
||||
image: ${IMAGE_PREFIX}/safeline-postgres:15.2
|
||||
image: ${IMAGE_PREFIX}/safeline-postgres${ARCH_SUFFIX}:15.2
|
||||
volumes:
|
||||
- ${SAFELINE_DIR}/resources/postgres/data:/var/lib/postgresql/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
@@ -30,23 +30,24 @@ services:
|
||||
mgt:
|
||||
container_name: safeline-mgt
|
||||
restart: always
|
||||
image: ${IMAGE_PREFIX}/safeline-mgt-g:${IMAGE_TAG:?image tag required}
|
||||
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_READ_ONLY=true
|
||||
- MGT_NO_AUTH=true
|
||||
- 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"
|
||||
@@ -56,7 +57,7 @@ services:
|
||||
detect:
|
||||
container_name: safeline-detector
|
||||
restart: always
|
||||
image: ${IMAGE_PREFIX}/safeline-detector-g:${IMAGE_TAG}
|
||||
image: ${IMAGE_PREFIX}/safeline-detector${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}
|
||||
volumes:
|
||||
- ${SAFELINE_DIR}/resources/detector:/resources/detector
|
||||
- ${SAFELINE_DIR}/logs/detector:/logs/detector
|
||||
@@ -66,54 +67,40 @@ services:
|
||||
networks:
|
||||
safeline-ce:
|
||||
ipv4_address: ${SUBNET_PREFIX}.5
|
||||
mario:
|
||||
container_name: safeline-mario
|
||||
restart: always
|
||||
image: ${IMAGE_PREFIX}/safeline-mario-g:${IMAGE_TAG}
|
||||
volumes:
|
||||
- ${SAFELINE_DIR}/resources/mario:/resources/mario
|
||||
- ${SAFELINE_DIR}/logs/mario:/logs/mario
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
environment:
|
||||
- LOG_DIR=/logs/mario
|
||||
- GOGC=100
|
||||
- DATABASE_URL=postgres://safeline-ce:${POSTGRES_PASSWORD}@safeline-pg/safeline-ce
|
||||
logging:
|
||||
options:
|
||||
max-size: "100m"
|
||||
max-file: "5"
|
||||
networks:
|
||||
safeline-ce:
|
||||
ipv4_address: ${SUBNET_PREFIX}.6
|
||||
tengine:
|
||||
container_name: safeline-tengine
|
||||
restart: always
|
||||
image: ${IMAGE_PREFIX}/safeline-tengine-g:${IMAGE_TAG}
|
||||
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-g:${IMAGE_TAG}
|
||||
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"
|
||||
@@ -126,49 +113,31 @@ services:
|
||||
fvm:
|
||||
container_name: safeline-fvm
|
||||
restart: always
|
||||
image: ${IMAGE_PREFIX}/safeline-fvm-g:${IMAGE_TAG}
|
||||
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
|
||||
bridge:
|
||||
container_name: safeline-bridge
|
||||
restart: always
|
||||
image: ${IMAGE_PREFIX}/safeline-bridge-g:${IMAGE_TAG}
|
||||
command:
|
||||
- /app/bridge
|
||||
- serve
|
||||
- -n
|
||||
- unix
|
||||
- -a
|
||||
- /app/run/safeline.sock
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/run:/app/run
|
||||
logging:
|
||||
options:
|
||||
max-size: "100m"
|
||||
max-file: "5"
|
||||
networks:
|
||||
safeline-ce:
|
||||
ipv4_address: ${SUBNET_PREFIX}.9
|
||||
depends_on:
|
||||
- mgt
|
||||
chaos:
|
||||
container_name: safeline-chaos
|
||||
restart: always
|
||||
image: ${IMAGE_PREFIX}/safeline-chaos-g:${IMAGE_TAG}
|
||||
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
|
||||
ipv4_address: ${SUBNET_PREFIX}.10
|
||||
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 28 KiB |
BIN
images/auth-1.gif
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
images/auth-2.gif
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
images/banner.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
images/blocked-for-access-too-fast.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
images/blocked-for-attack-detected.png
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
images/captcha-1.gif
Normal file
|
After Width: | Height: | Size: 821 KiB |
BIN
images/captcha-2.gif
Normal file
|
After Width: | Height: | Size: 806 KiB |
BIN
images/dynamic-html-1.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
images/dynamic-html-2.png
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
images/dynamic-js-1.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
images/dynamic-js-2.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
images/how-it-works.png
Normal file
|
After Width: | Height: | Size: 501 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 46 KiB |
BIN
images/login.png
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 557 KiB |
BIN
images/screenshot-1.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
images/screenshot-2.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
images/screenshot-3.png
Normal file
|
After Width: | Height: | Size: 353 KiB |
BIN
images/screenshot-4.png
Normal file
|
After Width: | Height: | Size: 243 KiB |
BIN
images/skeleton.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
@@ -14,7 +14,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
)
|
||||
|
||||
28
mcp_server/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 go build -o mcp-server .
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
COPY --from=builder /app/mcp-server .
|
||||
COPY --from=builder /app/config.yaml .
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
EXPOSE 5678
|
||||
|
||||
CMD ["./mcp-server"]
|
||||
256
mcp_server/README.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# SafeLine MCP Server
|
||||
|
||||
SafeLine MCP Server is an implementation of the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) that provides complete management and control capabilities for SafeLine WAF.
|
||||
|
||||
[](docker-compose.yml)
|
||||
[](go.mod)
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Automated management and control of SafeLine WAF instances
|
||||
- WAF configuration and policy management through API
|
||||
- Building AI-based security protection tools and applications
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Install [Docker](https://www.docker.com/) (if running in container)
|
||||
2. Configure SafeLine API Token (obtained from SafeLine console)
|
||||
|
||||
## Features
|
||||
|
||||
- Complete MCP (Management Control Protocol) server implementation
|
||||
- Support for SafeLine WAF instance management and control
|
||||
- Flexible configuration system supporting file configuration and environment variables
|
||||
- Docker containerization support
|
||||
- Secure API communication
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Environment Variable | Description | Default Value | Required |
|
||||
|---------|------|--------|-----|
|
||||
| LISTEN_PORT | Service listening port | 5678 | No |
|
||||
| LISTEN_ADDRESS | Service listening address | 0.0.0.0 | No |
|
||||
| SAFELINE_SECRET | SSE server secret | - | No |
|
||||
| SAFELINE_ADDRESS | SafeLine API address | - | Yes |
|
||||
| SAFELINE_API_TOKEN | SafeLine API authentication token | - | Yes |
|
||||
|
||||
### Using Docker
|
||||
|
||||
#### Method 1: Using docker run
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name safeline-mcp \
|
||||
-p 5678:5678 \
|
||||
-e SAFELINE_API_TOKEN="your_api_token" \
|
||||
-e SAFELINE_ADDRESS="https://your.safeline.com" \
|
||||
-e LISTEN_PORT=5678 \
|
||||
-e LISTEN_ADDRESS="0.0.0.0" \
|
||||
chaitin/safeline-mcp:latest
|
||||
```
|
||||
|
||||
#### Method 2: Using docker-compose
|
||||
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone https://github.com/chaitin/safeline-mcp.git
|
||||
cd safeline-mcp
|
||||
|
||||
# 2. Edit docker-compose.yml to configure environment variables
|
||||
# Example docker-compose.yml:
|
||||
# version: '3'
|
||||
# services:
|
||||
# mcp:
|
||||
# image: chaitin/safeline-mcp:latest
|
||||
# container_name: safeline-mcp
|
||||
# ports:
|
||||
# - "5678:5678"
|
||||
# environment:
|
||||
# - SAFELINE_API_TOKEN=your_api_token
|
||||
# - SAFELINE_ADDRESS=https://your.safeline.com
|
||||
# - LISTEN_PORT=5678
|
||||
# - LISTEN_ADDRESS=0.0.0.0
|
||||
|
||||
# 3. Start service
|
||||
docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
#### Method 3: Using Go
|
||||
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone https://github.com/chaitin/SafeLine.git
|
||||
cd safeline-mcp
|
||||
|
||||
# 2. Install dependencies
|
||||
go mod download
|
||||
|
||||
# 3. Configure config.yaml
|
||||
cp config.yaml.example config.yaml
|
||||
# Edit config.yaml with necessary configurations
|
||||
|
||||
# 4. Run service
|
||||
go run main.go
|
||||
```
|
||||
|
||||
For more API details, please refer to the [API Documentation](https://demo.waf.chaitin.com:9443/swagger/index.html).
|
||||
|
||||
## Tools
|
||||
|
||||
### Application Management
|
||||
|
||||
- **create_application**
|
||||
|
||||
### Rule Management
|
||||
- **create_blacklist_rule**
|
||||
- **create_whitelist_rule**
|
||||
|
||||
### Analyze
|
||||
- **get_attack_events**
|
||||
|
||||
## Development Guide
|
||||
|
||||
The Go API in this project is currently under development, and APIs may change. If you have specific requirements, please submit an Issue for discussion.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
internal/
|
||||
├── api/ # API implementation
|
||||
│ ├── app/ # Application-related APIs
|
||||
│ │ └── create_application.go
|
||||
│ └── rule/ # Rule-related APIs
|
||||
│ └── create_rule.go
|
||||
└── tools/ # MCP tool implementation
|
||||
├── app/ # Application-related tools
|
||||
│ └── create_application.go
|
||||
└── rule/ # Rule-related tools
|
||||
└── create_rule.go
|
||||
```
|
||||
|
||||
### Adding New Tools
|
||||
|
||||
1. **Create Tool File**
|
||||
- Create corresponding directory and file under `internal/tools`
|
||||
- File name should match tool name
|
||||
- Use separate file for each tool
|
||||
- Example: `internal/tools/app/create_application.go`
|
||||
|
||||
2. **Tool Implementation Template**
|
||||
```go
|
||||
package app
|
||||
|
||||
type ToolName struct{}
|
||||
|
||||
type ToolParams struct {
|
||||
// Parameter definitions
|
||||
Param1 string `json:"param1" desc:"parameter description" required:"true"`
|
||||
Param2 int `json:"param2" desc:"parameter description" required:"false"`
|
||||
}
|
||||
|
||||
type ToolResult struct {
|
||||
Field1 string `json:"field1"`
|
||||
}
|
||||
|
||||
func (t *ToolName) Name() string {
|
||||
return "tool_name"
|
||||
}
|
||||
|
||||
func (t *ToolName) Description() string {
|
||||
return "tool description"
|
||||
}
|
||||
|
||||
func (t *ToolName) Validate(params ToolParams) error {
|
||||
// Parameter validation logic
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ToolName) Execute(ctx context.Context, params ToolParams) (result ToolResult, err error) {
|
||||
// Tool execution logic
|
||||
return result, nil
|
||||
}
|
||||
```
|
||||
|
||||
3. **[Optional]Create API Implementation**
|
||||
|
||||
If you need to use some APIs that have not been implemented yet, you need to create corresponding files in the api directory for implementation
|
||||
- Create same directory structure under `internal/api`
|
||||
- File name should match tool func
|
||||
- Example: `internal/api/app/create_application.go`
|
||||
|
||||
**API Implementation Template**
|
||||
```go
|
||||
package app
|
||||
|
||||
type RequestType struct {
|
||||
// Request parameter definitions
|
||||
Param1 string `json:"param1"`
|
||||
Param2 int `json:"param2"`
|
||||
}
|
||||
|
||||
func APIName(ctx context.Context, req *RequestType) (ResultType, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("request is required")
|
||||
}
|
||||
|
||||
var resp api.Response[ResultType]
|
||||
err := api.Service().Post(ctx, "/api/path", req, &resp)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to execute")
|
||||
}
|
||||
|
||||
if resp.Err != nil {
|
||||
return nil, errors.New(resp.Msg)
|
||||
}
|
||||
|
||||
return resp.Data, nil
|
||||
}
|
||||
```
|
||||
4. **Tool Registration (init.go)**
|
||||
|
||||
The tool registration file `internal/tools/init.go` is used to centrally manage all tool registrations
|
||||
- Register all tools uniformly in the `init()` function
|
||||
- Use the `AppendTool()` method for registration
|
||||
- Example:
|
||||
```go
|
||||
// Register create application tool
|
||||
AppendTool(&app.CreateApp{})
|
||||
|
||||
// Register create blacklist rule tool
|
||||
AppendTool(&rule.CreateBlacklistRule{})
|
||||
```
|
||||
|
||||
### Development Standards
|
||||
|
||||
1. **Naming Conventions**
|
||||
- Use lowercase letters and underscores for tool names
|
||||
- File names should match tool names
|
||||
|
||||
2. **Directory Organization**
|
||||
- Divide directories by functional modules (e.g., app, rule, etc.)
|
||||
- Maintain consistent structure between tools and api directories
|
||||
- Keep related functionality in the same directory
|
||||
|
||||
3. **Code Standards**
|
||||
- Follow Go standard code conventions
|
||||
- Add necessary parameter validation
|
||||
- Use unified error handling approach
|
||||
- Add appropriate logging
|
||||
|
||||
4. **Documentation Requirements**
|
||||
- Provide clear functional description in tool Description
|
||||
- Add detailed description for parameters
|
||||
- Update API toolkit documentation in README
|
||||
|
||||
### Example
|
||||
|
||||
Refer to the implementation of the `create_application` tool:
|
||||
- Tool implementation: `internal/tools/app/create_application.go`
|
||||
- API implementation: `internal/api/app/create_application.go`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
27
mcp_server/config.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
# Server Configuration
|
||||
server:
|
||||
name: "SafeLine MCP Server"
|
||||
version: "1.0.0"
|
||||
# Can be overridden by environment variable LISTEN_PORT
|
||||
port: 5678
|
||||
# Can be overridden by environment variable LISTEN_ADDRESS
|
||||
host: "0.0.0.0"
|
||||
# Can be overridden by environment variable SAFELINE_SECRET
|
||||
secret: "" # Secret for SSE server
|
||||
# Logger Configuration
|
||||
logger:
|
||||
level: "info" # Log level: debug, info, warn, error
|
||||
file_path: "" # Log file path
|
||||
console: true # Whether to output to console
|
||||
caller: false # Whether to record caller information
|
||||
development: true # Whether to use development mode
|
||||
|
||||
# API Configuration
|
||||
api:
|
||||
# Can be overridden by environment variable SAFELINE_ADDRESS
|
||||
base_url: "" # API service address
|
||||
# Can be overridden by environment variable SAFELINE_API_TOKEN
|
||||
token: "" # Authentication token
|
||||
timeout: 30 # Timeout in seconds
|
||||
debug: false # Whether to enable debug mode
|
||||
insecure_skip_verify: true # Whether to skip certificate verification
|
||||
15
mcp_server/docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mcp_server:
|
||||
image: chaitin/safeline-mcp:latest
|
||||
container_name: mcp_server
|
||||
restart: always
|
||||
ports:
|
||||
- "5678:5678"
|
||||
environment:
|
||||
- SAFELINE_SECRET=your_secret_key # optional, if you want to use secret key to authenticate
|
||||
- SAFELINE_ADDRESS=https://your_safeline_ip:9443 # required, your SafeLine WAF address
|
||||
- SAFELINE_API_TOKEN=your_safeline_api_token # required, your SafeLine WAF api token
|
||||
- LISTEN_PORT=5678 # optional, default is 5678
|
||||
- LISTEN_ADDRESS=0.0.0.0 # optional, default is 0.0.0.0
|
||||
16
mcp_server/go.mod
Normal file
@@ -0,0 +1,16 @@
|
||||
module github.com/chaitin/SafeLine/mcp_server
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/mark3labs/mcp-go v0.18.0
|
||||
go.uber.org/zap v1.27.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mcuadros/go-defaults v1.2.0
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
)
|
||||
28
mcp_server/go.sum
Normal file
@@ -0,0 +1,28 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mark3labs/mcp-go v0.18.0 h1:YuhgIVjNlTG2ZOwmrkORWyPTp0dz1opPEqvsPtySXao=
|
||||
github.com/mark3labs/mcp-go v0.18.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
|
||||
github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc=
|
||||
github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
47
mcp_server/internal/api/analyze/get_event_list.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package analyze
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api"
|
||||
)
|
||||
|
||||
type GetEventListRequest struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
IP string `json:"ip"`
|
||||
Start int64 `json:"start"`
|
||||
End int64 `json:"end"`
|
||||
}
|
||||
|
||||
type GetEventListResponse struct {
|
||||
Nodes []Event `json:"nodes"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
ID uint `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Protocol int `json:"protocol"`
|
||||
Host string `json:"host"`
|
||||
DstPort uint64 `json:"dst_port"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
StartAt int64 `json:"start_at"`
|
||||
EndAt int64 `json:"end_at"`
|
||||
DenyCount int64 `json:"deny_count"`
|
||||
PassCount int64 `json:"pass_count"`
|
||||
Finished bool `json:"finished"`
|
||||
Country string `json:"country"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
func GetEventList(ctx context.Context, req *GetEventListRequest) (*GetEventListResponse, error) {
|
||||
var resp api.Response[GetEventListResponse]
|
||||
err := api.Service().Get(ctx, fmt.Sprintf("/api/open/events?page=%d&page_size=%d&ip=%s&start=%d&end=%d", req.Page, req.PageSize, req.IP, req.Start, req.End), &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.Data, nil
|
||||
}
|
||||
34
mcp_server/internal/api/app/create_application.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
)
|
||||
|
||||
type CreateAppRequest struct {
|
||||
ServerNames []string `json:"server_names"`
|
||||
Ports []string `json:"ports"`
|
||||
Upstreams []string `json:"upstreams"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// CreateApp Create new website or app
|
||||
func CreateApp(ctx context.Context, req *CreateAppRequest) (int64, error) {
|
||||
if req == nil {
|
||||
return 0, errors.New("request is required")
|
||||
}
|
||||
|
||||
var resp api.Response[int64]
|
||||
err := api.Service().Post(ctx, "/api/open/site", req, &resp)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to create app")
|
||||
}
|
||||
|
||||
if resp.Err != nil {
|
||||
return 0, errors.New(resp.Msg)
|
||||
}
|
||||
|
||||
return resp.Data, nil
|
||||
}
|
||||
157
mcp_server/internal/api/client.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
// Client API client
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
headers map[string]string
|
||||
}
|
||||
|
||||
// ClientOption Client configuration options
|
||||
type ClientOption func(*Client)
|
||||
|
||||
// WithTimeout Set timeout duration
|
||||
func WithTimeout(timeout time.Duration) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.httpClient.Timeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeader Set request header
|
||||
func WithHeader(key, value string) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.headers[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// WithBaseURL Set base URL
|
||||
func WithBaseURL(baseURL string) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.baseURL = baseURL
|
||||
}
|
||||
}
|
||||
|
||||
// WithInsecureSkipVerify Set whether to skip certificate verification
|
||||
func WithInsecureSkipVerify(skip bool) ClientOption {
|
||||
return func(c *Client) {
|
||||
if transport, ok := c.httpClient.Transport.(*http.Transport); ok {
|
||||
if transport.TLSClientConfig == nil {
|
||||
transport.TLSClientConfig = &tls.Config{}
|
||||
}
|
||||
transport.TLSClientConfig.InsecureSkipVerify = skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient Create new API client
|
||||
func NewClient(opts ...ClientOption) *Client {
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{},
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: transport,
|
||||
},
|
||||
headers: make(map[string]string),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Request Send request
|
||||
func (c *Client) Request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
|
||||
reqURL := fmt.Sprintf("%s%s", c.baseURL, path)
|
||||
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshal request body failed")
|
||||
}
|
||||
bodyReader = bytes.NewReader(bodyBytes)
|
||||
}
|
||||
logger.With("url", reqURL).Debug("request url")
|
||||
req, err := http.NewRequestWithContext(ctx, method, reqURL, bodyReader)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create request failed")
|
||||
}
|
||||
|
||||
// Set common headers
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
for k, v := range c.headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "send request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read response body
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read response body failed")
|
||||
}
|
||||
|
||||
// Check status code
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return errors.New(fmt.Sprintf("request failed with status %d: %s", resp.StatusCode, string(respBody)))
|
||||
}
|
||||
|
||||
// Parse response
|
||||
if result != nil {
|
||||
if err := json.Unmarshal(respBody, result); err == nil {
|
||||
return nil
|
||||
}
|
||||
var respData map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &respData); err != nil {
|
||||
return errors.Wrap(err, "unmarshal response failed")
|
||||
}
|
||||
if respData["err"] != nil || respData["msg"] != nil {
|
||||
return errors.New(respData["msg"].(string))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get Send GET request
|
||||
func (c *Client) Get(ctx context.Context, path string, result interface{}) error {
|
||||
return c.Request(ctx, http.MethodGet, path, nil, result)
|
||||
}
|
||||
|
||||
// Post Send POST request
|
||||
func (c *Client) Post(ctx context.Context, path string, body interface{}, result interface{}) error {
|
||||
return c.Request(ctx, http.MethodPost, path, body, result)
|
||||
}
|
||||
|
||||
// Put Send PUT request
|
||||
func (c *Client) Put(ctx context.Context, path string, body interface{}, result interface{}) error {
|
||||
return c.Request(ctx, http.MethodPut, path, body, result)
|
||||
}
|
||||
|
||||
// Delete Send DELETE request
|
||||
func (c *Client) Delete(ctx context.Context, path string, result interface{}) error {
|
||||
return c.Request(ctx, http.MethodDelete, path, nil, result)
|
||||
}
|
||||
11
mcp_server/internal/api/response.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package api
|
||||
|
||||
// Response Common API response structure
|
||||
type Response[T any] struct {
|
||||
// Response data
|
||||
Data T `json:"data"`
|
||||
// Error message
|
||||
Err any `json:"err"`
|
||||
// Prompt message
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
35
mcp_server/internal/api/rule/create_rule.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
)
|
||||
|
||||
type CreateRuleRequest struct {
|
||||
Name string `json:"name"`
|
||||
IP []string `json:"ip"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
Pattern [][]api.Pattern `json:"pattern"`
|
||||
Action int `json:"action"`
|
||||
}
|
||||
|
||||
// CreateRule Create new rule
|
||||
func CreateRule(ctx context.Context, req *CreateRuleRequest) (int64, error) {
|
||||
if req == nil {
|
||||
return 0, errors.New("request is required")
|
||||
}
|
||||
|
||||
var resp api.Response[int64]
|
||||
err := api.Service().Post(ctx, "/api/open/policy", req, &resp)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to create policy rule")
|
||||
}
|
||||
|
||||
if resp.Err != nil {
|
||||
return 0, errors.New(resp.Msg)
|
||||
}
|
||||
|
||||
return resp.Data, nil
|
||||
}
|
||||
100
mcp_server/internal/api/service.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/config"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
// APIClient API client implementation
|
||||
type APIClient struct {
|
||||
client *Client
|
||||
config *config.APIConfig
|
||||
}
|
||||
|
||||
var (
|
||||
instance *APIClient
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// Init Initialize API service
|
||||
func Init(cfg *config.APIConfig) error {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
instance, err = newAPIClient(cfg)
|
||||
if err != nil {
|
||||
logger.With("error", err).Error("failed to initialize API service")
|
||||
return
|
||||
}
|
||||
logger.Info("API service initialized successfully")
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Service Get API service instance
|
||||
func Service() *APIClient {
|
||||
if instance == nil {
|
||||
logger.Error("API service not initialized")
|
||||
panic("API service not initialized")
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
// newAPIClient Create new API client
|
||||
func newAPIClient(config *config.APIConfig) (*APIClient, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is required")
|
||||
}
|
||||
|
||||
if config.BaseURL == "" {
|
||||
return nil, errors.New("base_url is required")
|
||||
}
|
||||
|
||||
timeout := 30
|
||||
if config.Timeout > 0 {
|
||||
timeout = config.Timeout
|
||||
}
|
||||
|
||||
opts := []ClientOption{
|
||||
WithBaseURL(config.BaseURL),
|
||||
WithTimeout(time.Duration(timeout) * time.Second),
|
||||
WithHeader("User-Agent", "SafeLine-MCP/1.0"),
|
||||
WithInsecureSkipVerify(config.InsecureSkipVerify),
|
||||
}
|
||||
|
||||
// If token is configured, add authentication header
|
||||
if config.Token != "" {
|
||||
opts = append(opts, WithHeader("X-SLCE-API-TOKEN", config.Token))
|
||||
}
|
||||
|
||||
client := NewClient(opts...)
|
||||
|
||||
return &APIClient{
|
||||
client: client,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Post Send POST request
|
||||
func (c *APIClient) Post(ctx context.Context, path string, body interface{}, result interface{}) error {
|
||||
return c.client.Request(ctx, "POST", path, body, result)
|
||||
}
|
||||
|
||||
// Get Send GET request
|
||||
func (c *APIClient) Get(ctx context.Context, path string, result interface{}) error {
|
||||
return c.client.Request(ctx, "GET", path, nil, result)
|
||||
}
|
||||
|
||||
// Put Send PUT request
|
||||
func (c *APIClient) Put(ctx context.Context, path string, body interface{}, result interface{}) error {
|
||||
return c.client.Request(ctx, "PUT", path, body, result)
|
||||
}
|
||||
|
||||
// Delete Send DELETE request
|
||||
func (c *APIClient) Delete(ctx context.Context, path string, result interface{}) error {
|
||||
return c.client.Request(ctx, "DELETE", path, nil, result)
|
||||
}
|
||||
50
mcp_server/internal/api/types.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package api
|
||||
|
||||
type PolicyRuleAction int
|
||||
|
||||
const (
|
||||
PolicyRuleActionAllow PolicyRuleAction = iota
|
||||
PolicyRuleActionDeny
|
||||
PolicyRuleActionMax
|
||||
)
|
||||
|
||||
type Key = string
|
||||
|
||||
const (
|
||||
KeySrcIP Key = "src_ip"
|
||||
KeyURI Key = "uri"
|
||||
KeyURINoQuery Key = "uri_no_query"
|
||||
KeyHost Key = "host"
|
||||
KeyMethod Key = "method"
|
||||
KeyReqHeader Key = "req_header"
|
||||
KeyReqBody Key = "req_body"
|
||||
KeyGetParam Key = "get_param"
|
||||
KeyPostParam Key = "post_param"
|
||||
)
|
||||
|
||||
type Op = string
|
||||
|
||||
const (
|
||||
OpEq Op = "eq" // equal
|
||||
OpNotEq Op = "not_eq" // not equal
|
||||
OpMatch Op = "match" // match
|
||||
OpCIDR Op = "cidr" // cidr
|
||||
OpHas Op = "has" // has
|
||||
OpNotHas Op = "not_has" // not has
|
||||
OpPrefix Op = "prefix" // prefix
|
||||
OpRe Op = "re" // regex
|
||||
OpIn Op = "in" // in
|
||||
OpNotIn Op = "not_in" // not in
|
||||
OpNotCIDR Op = "not_cidr" // not cidr
|
||||
OpExist Op = "exist" // exist
|
||||
OpNotExist Op = "not_exist" // not exist
|
||||
OpGeoEq Op = "geo_eq" // geo equal
|
||||
OpGeoNotEq Op = "geo_not_eq" // geo not equal
|
||||
)
|
||||
|
||||
type Pattern struct {
|
||||
K Key `json:"k"`
|
||||
Op Op `json:"op"`
|
||||
V []string `json:"v"`
|
||||
SubK string `json:"sub_k"`
|
||||
}
|
||||
118
mcp_server/internal/config/config.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config Global configuration structure
|
||||
type Config struct {
|
||||
Server *ServerConfig `yaml:"server"`
|
||||
Logger *LoggerConfig `yaml:"logger"`
|
||||
API *APIConfig `yaml:"api"`
|
||||
}
|
||||
|
||||
// APIConfig API configuration
|
||||
type APIConfig struct {
|
||||
// API base URL
|
||||
BaseURL string `yaml:"base_url"`
|
||||
// API token
|
||||
Token string `yaml:"token"`
|
||||
// API timeout
|
||||
Timeout int `yaml:"timeout"`
|
||||
// API debug mode
|
||||
Debug bool `yaml:"debug"`
|
||||
// API insecure skip verify
|
||||
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
|
||||
}
|
||||
|
||||
// ServerConfig Server configuration
|
||||
type ServerConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Version string `yaml:"version"`
|
||||
Port int `yaml:"port"`
|
||||
Host string `yaml:"host"`
|
||||
Secret string `yaml:"secret"`
|
||||
}
|
||||
|
||||
// LoggerConfig Logger configuration
|
||||
type LoggerConfig struct {
|
||||
Level string `yaml:"level"`
|
||||
FilePath string `yaml:"file_path"`
|
||||
Console bool `yaml:"console"`
|
||||
Caller bool `yaml:"caller"`
|
||||
Development bool `yaml:"development"`
|
||||
}
|
||||
|
||||
var config *Config
|
||||
|
||||
// getEnvString Get string value from environment variable, return default value if not exists
|
||||
func getEnvString(key, defaultValue string) string {
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// getEnvInt Get integer value from environment variable, return default value if not exists or cannot be parsed
|
||||
func getEnvInt(key string, defaultValue int) int {
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
if intValue, err := strconv.Atoi(value); err == nil {
|
||||
return intValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Load Load configuration file
|
||||
func Load(path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read config file failed")
|
||||
}
|
||||
|
||||
config = &Config{}
|
||||
if err := yaml.Unmarshal(data, config); err != nil {
|
||||
return errors.Wrap(err, "unmarshal config failed")
|
||||
}
|
||||
|
||||
// Override configuration from environment variables
|
||||
if config.Server != nil {
|
||||
config.Server.Host = getEnvString("LISTEN_ADDRESS", config.Server.Host)
|
||||
config.Server.Port = getEnvInt("LISTEN_PORT", config.Server.Port)
|
||||
}
|
||||
|
||||
if config.API != nil {
|
||||
config.API.BaseURL = getEnvString("SAFELINE_ADDRESS", config.API.BaseURL)
|
||||
config.API.Token = getEnvString("SAFELINE_API_TOKEN", config.API.Token)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetServer Get server configuration
|
||||
func GetServer() *ServerConfig {
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
return config.Server
|
||||
}
|
||||
|
||||
// GetLogger Get logger configuration
|
||||
func GetLogger() *LoggerConfig {
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
return config.Logger
|
||||
}
|
||||
|
||||
// GetAPI Get API configuration
|
||||
func GetAPI() *APIConfig {
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
return config.API
|
||||
}
|
||||
45
mcp_server/internal/tools/analyze/get_atttack_events.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package analyze
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api/analyze"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
type GetAttackEventsParams struct {
|
||||
IP string `json:"ip" desc:"ip" required:"false"`
|
||||
Page int `json:"page" desc:"page" required:"false" default:"1"`
|
||||
PageSize int `json:"page_size" desc:"page size" required:"false" default:"10"`
|
||||
Start int64 `json:"start" desc:"start unix timestamp in milliseconds" required:"false"`
|
||||
End int64 `json:"end" desc:"end unix timestamp in milliseconds" required:"false"`
|
||||
}
|
||||
|
||||
type GetAttackEvents struct{}
|
||||
|
||||
func (t *GetAttackEvents) Name() string {
|
||||
return "get_attack_events"
|
||||
}
|
||||
|
||||
func (t *GetAttackEvents) Description() string {
|
||||
return "get attack events"
|
||||
}
|
||||
|
||||
func (t *GetAttackEvents) Validate(params GetAttackEventsParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *GetAttackEvents) Execute(ctx context.Context, params GetAttackEventsParams) (analyze.GetEventListResponse, error) {
|
||||
resp, err := analyze.GetEventList(ctx, &analyze.GetEventListRequest{
|
||||
IP: params.IP,
|
||||
PageSize: params.PageSize,
|
||||
Page: params.Page,
|
||||
Start: params.Start,
|
||||
End: params.End,
|
||||
})
|
||||
if err != nil {
|
||||
return analyze.GetEventListResponse{}, err
|
||||
}
|
||||
logger.With("total", resp.Total).Info("get attack events")
|
||||
return *resp, nil
|
||||
}
|
||||
41
mcp_server/internal/tools/app/create_application.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api/app"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
type CreateApp struct{}
|
||||
|
||||
type CreateAppParams struct {
|
||||
ServerNames []string `json:"server_names" desc:"domain list" required:"true"`
|
||||
Ports []string `json:"ports" desc:"port list" required:"true"`
|
||||
Upstreams []string `json:"upstreams" desc:"upstream list" required:"true"`
|
||||
}
|
||||
|
||||
func (t *CreateApp) Name() string {
|
||||
return "create_http_application"
|
||||
}
|
||||
|
||||
func (t *CreateApp) Description() string {
|
||||
return "create a new website or app"
|
||||
}
|
||||
|
||||
func (t *CreateApp) Validate(params CreateAppParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *CreateApp) Execute(ctx context.Context, params CreateAppParams) (int64, error) {
|
||||
id, err := app.CreateApp(ctx, &app.CreateAppRequest{
|
||||
ServerNames: params.ServerNames,
|
||||
Ports: params.Ports,
|
||||
Upstreams: params.Upstreams,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
logger.Info("create app success", logger.Int64("id", id))
|
||||
return id, nil
|
||||
}
|
||||
46
mcp_server/internal/tools/example.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
type CalculateSum struct{}
|
||||
|
||||
func (t *CalculateSum) Name() string {
|
||||
return "calculate_sum"
|
||||
}
|
||||
|
||||
func (t *CalculateSum) Description() string {
|
||||
return "Add two numbers together"
|
||||
}
|
||||
|
||||
type MyToolInput struct {
|
||||
A int `json:"a" desc:"number a" required:"true"`
|
||||
B int `json:"b" desc:"number b" required:"true"`
|
||||
}
|
||||
|
||||
type MyToolOutput struct {
|
||||
C int `json:"c"`
|
||||
}
|
||||
|
||||
func (t *CalculateSum) Validate(params MyToolInput) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *CalculateSum) Execute(ctx context.Context, params MyToolInput) (MyToolOutput, error) {
|
||||
logger.With("a", params.A).
|
||||
With("b", params.B).
|
||||
Debug("Executing calculation")
|
||||
|
||||
result := MyToolOutput{
|
||||
C: params.A + params.B,
|
||||
}
|
||||
|
||||
logger.With("result", result.C).
|
||||
Debug("Calculation completed")
|
||||
|
||||
return result, errors.New("test error")
|
||||
}
|
||||
19
mcp_server/internal/tools/init.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/tools/analyze"
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/tools/app"
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/tools/rule"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// app
|
||||
AppendTool(&app.CreateApp{})
|
||||
|
||||
// rule
|
||||
AppendTool(&rule.CreateBlacklistRule{})
|
||||
AppendTool(&rule.CreateWhitelistRule{})
|
||||
|
||||
// analyze
|
||||
AppendTool(&analyze.GetAttackEvents{})
|
||||
}
|
||||
66
mcp_server/internal/tools/rule/create_blacklist_rule.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api"
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api/rule"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
type CreateBlacklistRule struct{}
|
||||
|
||||
type CreateBlacklistRuleParams struct {
|
||||
Name string `json:"name" desc:"name" required:"true"`
|
||||
IP []string `json:"ip" desc:"ip" required:"false"`
|
||||
URINoQuery []string `json:"uri_no_query" desc:"uri_no_query" required:"false"`
|
||||
}
|
||||
|
||||
func (t *CreateBlacklistRule) Name() string {
|
||||
return "create_blacklist_rule"
|
||||
}
|
||||
|
||||
func (t *CreateBlacklistRule) Description() string {
|
||||
return "create a new blacklist rule"
|
||||
}
|
||||
|
||||
func (t *CreateBlacklistRule) Validate(params CreateBlacklistRuleParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *CreateBlacklistRule) Execute(ctx context.Context, params CreateBlacklistRuleParams) (int64, error) {
|
||||
var pattern [][]api.Pattern
|
||||
if len(params.IP) > 0 {
|
||||
pattern = append(pattern, []api.Pattern{
|
||||
{
|
||||
K: api.KeySrcIP,
|
||||
Op: api.OpEq,
|
||||
V: params.IP,
|
||||
SubK: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(params.URINoQuery) > 0 {
|
||||
pattern = append(pattern, []api.Pattern{
|
||||
{
|
||||
K: api.KeyURINoQuery,
|
||||
Op: api.OpEq,
|
||||
V: params.URINoQuery,
|
||||
SubK: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
id, err := rule.CreateRule(ctx, &rule.CreateRuleRequest{
|
||||
Name: params.Name,
|
||||
IP: params.IP,
|
||||
IsEnabled: true,
|
||||
Action: int(api.PolicyRuleActionDeny),
|
||||
Pattern: pattern,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
logger.With("id", id).Info("create blacklist rule success")
|
||||
return id, nil
|
||||
}
|
||||
66
mcp_server/internal/tools/rule/create_whitelist_rule.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api"
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api/rule"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
type CreateWhitelistRule struct{}
|
||||
|
||||
type CreateWhitelistRuleParams struct {
|
||||
Name string `json:"name" desc:"name" required:"true"`
|
||||
IP []string `json:"ip" desc:"ip" required:"false"`
|
||||
URINoQuery []string `json:"uri_no_query" desc:"uri_no_query" required:"false"`
|
||||
}
|
||||
|
||||
func (t *CreateWhitelistRule) Name() string {
|
||||
return "create_whitelist_rule"
|
||||
}
|
||||
|
||||
func (t *CreateWhitelistRule) Description() string {
|
||||
return "create a new whitelist rule"
|
||||
}
|
||||
|
||||
func (t *CreateWhitelistRule) Validate(params CreateWhitelistRuleParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *CreateWhitelistRule) Execute(ctx context.Context, params CreateWhitelistRuleParams) (int64, error) {
|
||||
var pattern [][]api.Pattern
|
||||
if len(params.IP) > 0 {
|
||||
pattern = append(pattern, []api.Pattern{
|
||||
{
|
||||
K: api.KeySrcIP,
|
||||
Op: api.OpEq,
|
||||
V: params.IP,
|
||||
SubK: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(params.URINoQuery) > 0 {
|
||||
pattern = append(pattern, []api.Pattern{
|
||||
{
|
||||
K: api.KeyURINoQuery,
|
||||
Op: api.OpEq,
|
||||
V: params.URINoQuery,
|
||||
SubK: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
id, err := rule.CreateRule(ctx, &rule.CreateRuleRequest{
|
||||
Name: params.Name,
|
||||
IP: params.IP,
|
||||
IsEnabled: true,
|
||||
Action: int(api.PolicyRuleActionAllow),
|
||||
Pattern: pattern,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
logger.With("id", id).Info("create whitelist rule success")
|
||||
return id, nil
|
||||
}
|
||||
41
mcp_server/internal/tools/tool.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/mcp"
|
||||
)
|
||||
|
||||
// By deferring the concretization of generic types to the Register method,
|
||||
// we avoid type inference issues.
|
||||
|
||||
// Each Tool is wrapped in a toolWrapper that knows its concrete type,
|
||||
// allowing correct passing of generic parameters during registration.
|
||||
type ToolWrapper interface {
|
||||
Register(s *mcp.MCPServer) error
|
||||
}
|
||||
|
||||
var (
|
||||
tools = []ToolWrapper{}
|
||||
)
|
||||
|
||||
func AppendTool[T any, R any](tool ...mcp.Tool[T, R]) {
|
||||
for _, t := range tool {
|
||||
tools = append(tools, &toolWrapper[T, R]{tool: t})
|
||||
}
|
||||
}
|
||||
|
||||
func Tools() []ToolWrapper {
|
||||
return tools
|
||||
}
|
||||
|
||||
type toolWrapper[T any, R any] struct {
|
||||
tool mcp.Tool[T, R]
|
||||
}
|
||||
|
||||
func (w *toolWrapper[T, R]) Register(s *mcp.MCPServer) error {
|
||||
logger.Info("Registering tool",
|
||||
logger.String("name", w.tool.Name()),
|
||||
logger.String("description", w.tool.Description()),
|
||||
)
|
||||
return mcp.RegisterTool(s, w.tool)
|
||||
}
|
||||
62
mcp_server/main.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api"
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/config"
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/tools"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/mcp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "config.yaml", "path to config file")
|
||||
flag.Parse()
|
||||
|
||||
if err := config.Load(*configPath); err != nil {
|
||||
panic(fmt.Errorf("failed to load config: %v", err))
|
||||
}
|
||||
|
||||
logConfig := config.GetLogger()
|
||||
if err := logger.Init(&logger.Config{
|
||||
Level: logConfig.Level,
|
||||
FilePath: logConfig.FilePath,
|
||||
Console: logConfig.Console,
|
||||
Caller: logConfig.Caller,
|
||||
Development: logConfig.Development,
|
||||
}); err != nil {
|
||||
panic(fmt.Errorf("failed to init logger: %v", err))
|
||||
}
|
||||
|
||||
logger.With("base_url", config.GetAPI().BaseURL).Info("Initializing API service...")
|
||||
if err := api.Init(config.GetAPI()); err != nil {
|
||||
panic(fmt.Errorf("failed to init API service: %v", err))
|
||||
}
|
||||
|
||||
logger.Info("Starting MCP Server...")
|
||||
serverConfig := config.GetServer()
|
||||
s := mcp.NewMCPServer(
|
||||
serverConfig.Name,
|
||||
serverConfig.Version,
|
||||
serverConfig.Secret,
|
||||
)
|
||||
|
||||
logger.Info("Registering tools...")
|
||||
for _, tool := range tools.Tools() {
|
||||
if err := tool.Register(s); err != nil {
|
||||
logger.With("error", err).
|
||||
Error("Failed to register tool")
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", serverConfig.Host, serverConfig.Port)
|
||||
logger.With("addr", addr).Info("Starting server")
|
||||
if err := s.Start(addr); err != nil {
|
||||
logger.With("error", err).
|
||||
Error("Server failed to start")
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
72
mcp_server/pkg/config/config.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config Global configuration structure
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Logger LoggerConfig `yaml:"logger"`
|
||||
}
|
||||
|
||||
// ServerConfig Server configuration
|
||||
type ServerConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Version string `yaml:"version"`
|
||||
Port int `yaml:"port"`
|
||||
Host string `yaml:"host"`
|
||||
Secret string `yaml:"secret"`
|
||||
}
|
||||
|
||||
// LoggerConfig Logger configuration
|
||||
type LoggerConfig struct {
|
||||
Level string `yaml:"level"`
|
||||
FilePath string `yaml:"file_path"`
|
||||
Console bool `yaml:"console"`
|
||||
Caller bool `yaml:"caller"`
|
||||
Development bool `yaml:"development"`
|
||||
}
|
||||
|
||||
var (
|
||||
globalConfig *Config
|
||||
)
|
||||
|
||||
// Load Load configuration from file
|
||||
func Load(filename string) error {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := &Config{}
|
||||
if err := yaml.Unmarshal(data, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
globalConfig = config
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get Get global configuration
|
||||
func Get() *Config {
|
||||
return globalConfig
|
||||
}
|
||||
|
||||
// GetServer Get server configuration
|
||||
func GetServer() ServerConfig {
|
||||
if globalConfig == nil {
|
||||
return ServerConfig{}
|
||||
}
|
||||
return globalConfig.Server
|
||||
}
|
||||
|
||||
// GetLogger Get logger configuration
|
||||
func GetLogger() LoggerConfig {
|
||||
if globalConfig == nil {
|
||||
return LoggerConfig{}
|
||||
}
|
||||
return globalConfig.Logger
|
||||
}
|
||||
136
mcp_server/pkg/errors/errors.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
// Common errors
|
||||
ErrInternal = New("internal error")
|
||||
ErrInvalidParam = New("invalid parameter")
|
||||
ErrNotFound = New("resource not found")
|
||||
ErrUnauthorized = New("unauthorized")
|
||||
ErrForbidden = New("forbidden")
|
||||
ErrTimeout = New("timeout")
|
||||
)
|
||||
|
||||
// Error Custom error structure
|
||||
type Error struct {
|
||||
err error
|
||||
stack []string
|
||||
msg string
|
||||
location string
|
||||
}
|
||||
|
||||
// Error Implement error interface
|
||||
func (e *Error) Error() string {
|
||||
if e.msg != "" {
|
||||
return fmt.Sprintf("%s: %v (at %s)", e.msg, e.err, e.location)
|
||||
}
|
||||
return fmt.Sprintf("%v (at %s)", e.err, e.location)
|
||||
}
|
||||
|
||||
// Unwrap Return original error
|
||||
func (e *Error) Unwrap() error {
|
||||
if e.err == nil {
|
||||
return nil
|
||||
}
|
||||
if wrapped, ok := e.err.(*Error); ok {
|
||||
return wrapped.Unwrap()
|
||||
}
|
||||
return e.err
|
||||
}
|
||||
|
||||
// Stack Return error stack
|
||||
func (e *Error) Stack() []string {
|
||||
return e.stack
|
||||
}
|
||||
|
||||
// Location Return error location
|
||||
func (e *Error) Location() string {
|
||||
return e.location
|
||||
}
|
||||
|
||||
// getCallerLocation Get caller location
|
||||
func getCallerLocation(skip int) string {
|
||||
_, file, line, ok := runtime.Caller(skip)
|
||||
if !ok {
|
||||
return "unknown"
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
|
||||
// WrapL Wrap error and print log
|
||||
func WrapL(err error, msg string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get stack trace information
|
||||
var stack []string
|
||||
for i := 1; i < 32; i++ {
|
||||
pc, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
break
|
||||
}
|
||||
name := fn.Name()
|
||||
if strings.Contains(name, "runtime.") {
|
||||
break
|
||||
}
|
||||
stack = append(stack, fmt.Sprintf("%s:%d", file, line))
|
||||
}
|
||||
|
||||
wrappedErr := &Error{
|
||||
err: err,
|
||||
stack: stack,
|
||||
msg: msg,
|
||||
location: getCallerLocation(2),
|
||||
}
|
||||
|
||||
// Print error information and stack using logger
|
||||
logger.With("error", err).
|
||||
With("location", wrappedErr.location).
|
||||
With("stack", strings.Join(stack, "\n")).
|
||||
Error(msg)
|
||||
|
||||
return wrappedErr
|
||||
}
|
||||
|
||||
// Is Check error type
|
||||
func Is(err, target error) bool {
|
||||
return errors.Is(err, target)
|
||||
}
|
||||
|
||||
// As Type assertion
|
||||
func As(err error, target interface{}) bool {
|
||||
return errors.As(err, target)
|
||||
}
|
||||
|
||||
// Wrap Wrap error without printing log
|
||||
func Wrap(err error, msg string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &Error{
|
||||
err: err,
|
||||
msg: msg,
|
||||
location: getCallerLocation(2),
|
||||
}
|
||||
}
|
||||
|
||||
// New Create new error
|
||||
func New(text string) error {
|
||||
return &Error{
|
||||
err: errors.New(text),
|
||||
location: getCallerLocation(2),
|
||||
}
|
||||
}
|
||||
49
mcp_server/pkg/logger/field.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// Field 日志字段
|
||||
type Field = zapcore.Field
|
||||
|
||||
// String 创建字符串字段
|
||||
func String(key string, val string) Field {
|
||||
return zap.String(key, val)
|
||||
}
|
||||
|
||||
// Int 创建整数字段
|
||||
func Int(key string, val int) Field {
|
||||
return zap.Int(key, val)
|
||||
}
|
||||
|
||||
// Int64 创建 int64 字段
|
||||
func Int64(key string, val int64) Field {
|
||||
return zap.Int64(key, val)
|
||||
}
|
||||
|
||||
// Float64 创建浮点数字段
|
||||
func Float64(key string, val float64) Field {
|
||||
return zap.Float64(key, val)
|
||||
}
|
||||
|
||||
// Bool 创建布尔字段
|
||||
func Bool(key string, val bool) Field {
|
||||
return zap.Bool(key, val)
|
||||
}
|
||||
|
||||
// Err 创建错误字段
|
||||
func Err(err error) Field {
|
||||
return zap.Error(err)
|
||||
}
|
||||
|
||||
// Any 创建任意类型字段
|
||||
func Any(key string, val interface{}) Field {
|
||||
return zap.Any(key, val)
|
||||
}
|
||||
|
||||
// Duration 创建时间段字段
|
||||
func Duration(key string, val float64) Field {
|
||||
return zap.Float64(key, val)
|
||||
}
|
||||
205
mcp_server/pkg/logger/logger.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// Logger 封装 zap.Logger
|
||||
type Logger struct {
|
||||
zl *zap.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
defaultLogger *Logger
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// Config 日志配置
|
||||
type Config struct {
|
||||
// 日志级别
|
||||
Level string `json:"level" yaml:"level"`
|
||||
// 日志文件路径
|
||||
FilePath string `json:"file_path" yaml:"file_path"`
|
||||
// 是否输出到控制台
|
||||
Console bool `json:"console" yaml:"console"`
|
||||
// 是否记录调用者信息
|
||||
Caller bool `json:"caller" yaml:"caller"`
|
||||
// 是否使用开发模式(更详细的日志)
|
||||
Development bool `json:"development" yaml:"development"`
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
var defaultConfig = Config{
|
||||
Level: "info",
|
||||
FilePath: "logs/mcp.log",
|
||||
Console: true,
|
||||
Caller: true,
|
||||
Development: false,
|
||||
}
|
||||
|
||||
// Init 初始化日志系统
|
||||
func Init(cfg *Config) error {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
if cfg == nil {
|
||||
cfg = &defaultConfig
|
||||
}
|
||||
|
||||
// 确保日志目录存在
|
||||
if cfg.FilePath != "" {
|
||||
dir := filepath.Dir(cfg.FilePath)
|
||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 配置编码器
|
||||
encoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.Format("2006-01-02 15:04:05.000"))
|
||||
},
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
|
||||
// 设置日志级别
|
||||
var level zapcore.Level
|
||||
if err = level.UnmarshalText([]byte(cfg.Level)); err != nil {
|
||||
level = zapcore.InfoLevel
|
||||
}
|
||||
|
||||
// 创建Core
|
||||
var cores []zapcore.Core
|
||||
|
||||
// 文件输出
|
||||
if cfg.FilePath != "" {
|
||||
fileWriter, err := os.OpenFile(cfg.FilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cores = append(cores, zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(encoderConfig),
|
||||
zapcore.AddSync(fileWriter),
|
||||
level,
|
||||
))
|
||||
}
|
||||
|
||||
// 控制台输出
|
||||
if cfg.Console {
|
||||
cores = append(cores, zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(encoderConfig),
|
||||
zapcore.AddSync(os.Stdout),
|
||||
level,
|
||||
))
|
||||
}
|
||||
|
||||
// 创建Logger
|
||||
core := zapcore.NewTee(cores...)
|
||||
zl := zap.New(core)
|
||||
|
||||
// 是否记录调用者信息
|
||||
if cfg.Caller {
|
||||
zl = zl.WithOptions(zap.AddCaller())
|
||||
}
|
||||
|
||||
// 是否使用开发模式
|
||||
if cfg.Development {
|
||||
zl = zl.WithOptions(zap.Development())
|
||||
}
|
||||
|
||||
defaultLogger = &Logger{zl: zl}
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetLogger 获取日志实例
|
||||
func GetLogger() *Logger {
|
||||
if defaultLogger == nil {
|
||||
Init(nil)
|
||||
}
|
||||
return defaultLogger
|
||||
}
|
||||
|
||||
// With 创建带有字段的新Logger
|
||||
func (l *Logger) With(key string, value interface{}) *Logger {
|
||||
return &Logger{zl: l.zl.With(Any(key, value))}
|
||||
}
|
||||
|
||||
// Debug level
|
||||
func (l *Logger) Debug(msg string, fields ...Field) {
|
||||
l.zl.Debug(msg, fields...)
|
||||
}
|
||||
|
||||
// Info level
|
||||
func (l *Logger) Info(msg string, fields ...Field) {
|
||||
l.zl.Info(msg, fields...)
|
||||
}
|
||||
|
||||
// Warn level
|
||||
func (l *Logger) Warn(msg string, fields ...Field) {
|
||||
l.zl.Warn(msg, fields...)
|
||||
}
|
||||
|
||||
// Error level
|
||||
func (l *Logger) Error(msg string, fields ...Field) {
|
||||
l.zl.Error(msg, fields...)
|
||||
}
|
||||
|
||||
// Fatal level
|
||||
func (l *Logger) Fatal(msg string, fields ...Field) {
|
||||
l.zl.Fatal(msg, fields...)
|
||||
}
|
||||
|
||||
// 全局函数
|
||||
|
||||
// With 创建带有字段的新Logger
|
||||
func With(key string, value interface{}) *Logger {
|
||||
l := GetLogger()
|
||||
// 为链式调用创建新的logger实例,并添加caller skip
|
||||
return &Logger{zl: l.zl.WithOptions(zap.AddCallerSkip(1)).With(Any(key, value))}
|
||||
}
|
||||
|
||||
// Debug level
|
||||
func Debug(msg string, fields ...Field) {
|
||||
l := GetLogger()
|
||||
l.zl.WithOptions(zap.AddCallerSkip(1)).Debug(msg, fields...)
|
||||
}
|
||||
|
||||
// Info level
|
||||
func Info(msg string, fields ...Field) {
|
||||
l := GetLogger()
|
||||
l.zl.WithOptions(zap.AddCallerSkip(1)).Info(msg, fields...)
|
||||
}
|
||||
|
||||
// Warn level
|
||||
func Warn(msg string, fields ...Field) {
|
||||
l := GetLogger()
|
||||
l.zl.WithOptions(zap.AddCallerSkip(1)).Warn(msg, fields...)
|
||||
}
|
||||
|
||||
// Error level
|
||||
func Error(msg string, fields ...Field) {
|
||||
l := GetLogger()
|
||||
l.zl.WithOptions(zap.AddCallerSkip(1)).Error(msg, fields...)
|
||||
}
|
||||
|
||||
// Fatal level
|
||||
func Fatal(msg string, fields ...Field) {
|
||||
l := GetLogger()
|
||||
l.zl.WithOptions(zap.AddCallerSkip(1)).Fatal(msg, fields...)
|
||||
}
|
||||
148
mcp_server/pkg/mcp/mcp.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/mcuadros/go-defaults"
|
||||
)
|
||||
|
||||
type Tool[T any, R any] interface {
|
||||
Name() string
|
||||
|
||||
Description() string
|
||||
|
||||
Execute(ctx context.Context, params T) (R, error)
|
||||
|
||||
Validate(params T) error
|
||||
}
|
||||
|
||||
type SSEServer struct {
|
||||
sse *server.SSEServer
|
||||
secret string
|
||||
}
|
||||
|
||||
func (s *SSEServer) Start(addr string) error {
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: s,
|
||||
}
|
||||
return srv.ListenAndServe()
|
||||
}
|
||||
|
||||
func (s *SSEServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if s.secret == "" {
|
||||
s.sse.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
messagePath := s.sse.CompleteMessagePath()
|
||||
if messagePath != "" && r.URL.Path == messagePath {
|
||||
secret := r.Header.Get("Secret")
|
||||
if secret != s.secret {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
s.sse.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
type MCPServer struct {
|
||||
server *server.MCPServer
|
||||
sse *SSEServer
|
||||
}
|
||||
|
||||
func NewMCPServer(name, version string, secret string) *MCPServer {
|
||||
s := server.NewMCPServer(
|
||||
name,
|
||||
version,
|
||||
server.WithLogging(),
|
||||
)
|
||||
return &MCPServer{
|
||||
server: s,
|
||||
sse: &SSEServer{sse: server.NewSSEServer(s), secret: secret},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MCPServer) Start(addr string) error {
|
||||
return s.sse.Start(addr)
|
||||
}
|
||||
|
||||
func handleToolCall[T any, R any](ctx context.Context, request mcp.CallToolRequest, tool Tool[T, R]) (result *mcp.CallToolResult, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
var raw []byte
|
||||
raw, err = json.Marshal(request.Params.Arguments)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "marshal arguments failed")
|
||||
}
|
||||
var params T
|
||||
defaults.SetDefaults(¶ms)
|
||||
if err = json.Unmarshal(raw, ¶ms); err != nil {
|
||||
return nil, errors.Wrap(err, "unmarshal parameters failed")
|
||||
}
|
||||
|
||||
if err = tool.Validate(params); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var execResult R
|
||||
execResult, err = tool.Execute(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := any(execResult)
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return mcp.NewToolResultText(v), nil
|
||||
case []byte:
|
||||
return mcp.NewToolResultText(string(v)), nil
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return mcp.NewToolResultText(json.Number(fmt.Sprint(v)).String()), nil
|
||||
case bool:
|
||||
return mcp.NewToolResultText(strconv.FormatBool(v)), nil
|
||||
default:
|
||||
bytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid result type")
|
||||
}
|
||||
return mcp.NewToolResultText(string(bytes)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterTool[T any, R any](s *MCPServer, tool Tool[T, R]) error {
|
||||
var v T
|
||||
opts, err := SchemaToOptions(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts = append(opts, mcp.WithDescription(tool.Description()))
|
||||
t := mcp.NewTool(tool.Name(),
|
||||
opts...,
|
||||
)
|
||||
|
||||
s.server.AddTool(t, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
result, err := handleToolCall(ctx, request, tool)
|
||||
if err != nil {
|
||||
logger.With("error", err).Error("handle tool call failed")
|
||||
if wrapped, ok := err.(*errors.Error); ok {
|
||||
return nil, wrapped.Unwrap()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
169
mcp_server/pkg/mcp/schema.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// SchemaToOptions Convert struct to MCP ToolOption list
|
||||
func SchemaToOptions(schema any) ([]mcp.ToolOption, error) {
|
||||
t := reflect.TypeOf(schema)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
var options []mcp.ToolOption
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
jsonTag := field.Tag.Get("json")
|
||||
if jsonTag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
desc := field.Tag.Get("desc")
|
||||
required := field.Tag.Get("required") == "true"
|
||||
enumTag := field.Tag.Get("enum")
|
||||
defaultTag := field.Tag.Get("default")
|
||||
minTag := field.Tag.Get("min")
|
||||
maxTag := field.Tag.Get("max")
|
||||
opts := []mcp.PropertyOption{}
|
||||
|
||||
if desc != "" {
|
||||
opts = append(opts, mcp.Description(desc))
|
||||
}
|
||||
if required {
|
||||
opts = append(opts, mcp.Required())
|
||||
}
|
||||
if enumTag != "" && field.Type.Kind() == reflect.String {
|
||||
enumValues := strings.Split(enumTag, ",")
|
||||
opts = append(opts, mcp.Enum(enumValues...))
|
||||
}
|
||||
|
||||
switch field.Type.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:
|
||||
if defaultTag != "" {
|
||||
if defaultValue, err := strconv.Atoi(defaultTag); err == nil {
|
||||
opts = append(opts, mcp.DefaultNumber(float64(defaultValue)))
|
||||
}
|
||||
}
|
||||
if minTag != "" {
|
||||
if minValue, err := strconv.Atoi(minTag); err == nil {
|
||||
opts = append(opts, mcp.Min(float64(minValue)))
|
||||
}
|
||||
}
|
||||
if maxTag != "" {
|
||||
if maxValue, err := strconv.Atoi(maxTag); err == nil {
|
||||
opts = append(opts, mcp.Max(float64(maxValue)))
|
||||
}
|
||||
}
|
||||
options = append(options, mcp.WithNumber(jsonTag, opts...))
|
||||
case reflect.Bool:
|
||||
if defaultTag != "" {
|
||||
if defaultValue, err := strconv.ParseBool(defaultTag); err == nil {
|
||||
opts = append(opts, mcp.DefaultBool(defaultValue))
|
||||
}
|
||||
}
|
||||
options = append(options, mcp.WithBoolean(jsonTag, opts...))
|
||||
case reflect.String:
|
||||
if defaultTag != "" {
|
||||
opts = append(opts, mcp.DefaultString(defaultTag))
|
||||
}
|
||||
options = append(options, mcp.WithString(jsonTag, opts...))
|
||||
case reflect.Struct:
|
||||
subSchema := reflect.New(field.Type).Interface()
|
||||
subOptions, err := SchemaToOptions(subSchema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a temporary Tool to get JSON Schema of sub-struct
|
||||
tempTool := mcp.NewTool("temp", subOptions...)
|
||||
tempJSON, _ := tempTool.MarshalJSON()
|
||||
var tempMap map[string]any
|
||||
if err := json.Unmarshal(tempJSON, &tempMap); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract properties from temporary Tool
|
||||
if inputSchema, ok := tempMap["inputSchema"].(map[string]any); ok {
|
||||
if properties, ok := inputSchema["properties"].(map[string]any); ok {
|
||||
// Check if there are required fields
|
||||
if required, ok := inputSchema["required"].([]any); ok {
|
||||
// Add required field information to corresponding properties
|
||||
for _, req := range required {
|
||||
if reqStr, ok := req.(string); ok {
|
||||
if prop, ok := properties[reqStr].(map[string]any); ok {
|
||||
prop["required"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
opts = append(opts, mcp.Properties(properties))
|
||||
}
|
||||
}
|
||||
options = append(options, mcp.WithObject(jsonTag, opts...))
|
||||
|
||||
case reflect.Slice:
|
||||
elemType := field.Type.Elem()
|
||||
var items map[string]any
|
||||
|
||||
switch elemType.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:
|
||||
items = map[string]any{
|
||||
"type": "number",
|
||||
}
|
||||
case reflect.Bool:
|
||||
items = map[string]any{
|
||||
"type": "boolean",
|
||||
}
|
||||
case reflect.String:
|
||||
items = map[string]any{
|
||||
"type": "string",
|
||||
}
|
||||
case reflect.Struct:
|
||||
subSchema := reflect.New(elemType).Interface()
|
||||
subOptions, err := SchemaToOptions(subSchema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a temporary Tool to get JSON Schema of sub-struct
|
||||
tempTool := mcp.NewTool("temp", subOptions...)
|
||||
tempJSON, _ := tempTool.MarshalJSON()
|
||||
var tempMap map[string]any
|
||||
if err := json.Unmarshal(tempJSON, &tempMap); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract properties from temporary Tool
|
||||
if inputSchema, ok := tempMap["inputSchema"].(map[string]any); ok {
|
||||
if properties, ok := inputSchema["properties"].(map[string]any); ok {
|
||||
// Check if there are required fields
|
||||
if required, ok := inputSchema["required"].([]any); ok {
|
||||
// Add required field information to corresponding properties
|
||||
for _, req := range required {
|
||||
if reqStr, ok := req.(string); ok {
|
||||
if prop, ok := properties[reqStr].(map[string]any); ok {
|
||||
prop["required"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
items = map[string]any{
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
opts = append(opts, mcp.Items(items))
|
||||
options = append(options, mcp.WithArray(jsonTag, opts...))
|
||||
}
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
227
mcp_server/pkg/mcp/schema_test.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func TestSchemaToOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args any
|
||||
want mcp.Tool
|
||||
}{
|
||||
{
|
||||
name: "test number",
|
||||
args: struct {
|
||||
A int `json:"a" desc:"number a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test number",
|
||||
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test number int64",
|
||||
args: struct {
|
||||
A int64 `json:"a" desc:"number a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test number int64",
|
||||
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test number float64",
|
||||
args: struct {
|
||||
A float64 `json:"a" desc:"number a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test number float64",
|
||||
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test number default",
|
||||
args: struct {
|
||||
A int `json:"a" desc:"number a" required:"true" default:"10"`
|
||||
}{},
|
||||
want: mcp.NewTool("test number default",
|
||||
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a"), mcp.DefaultNumber(10)),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test number min max",
|
||||
args: struct {
|
||||
A int `json:"a" desc:"number a" required:"true" min:"10" max:"20"`
|
||||
}{},
|
||||
want: mcp.NewTool("test number min max",
|
||||
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a"), mcp.Min(10), mcp.Max(20)),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test number optional",
|
||||
args: struct {
|
||||
A int `json:"a" desc:"number a"`
|
||||
}{},
|
||||
want: mcp.NewTool("test number optional",
|
||||
mcp.WithNumber("a", mcp.Description("number a")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test boolean",
|
||||
args: struct {
|
||||
A bool `json:"a" desc:"boolean a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test boolean",
|
||||
mcp.WithBoolean("a", mcp.Required(), mcp.Description("boolean a")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test string",
|
||||
args: struct {
|
||||
A string `json:"a" desc:"string a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test string",
|
||||
mcp.WithString("a", mcp.Required(), mcp.Description("string a")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test string default",
|
||||
args: struct {
|
||||
A string `json:"a" desc:"string a" required:"true" default:"hello"`
|
||||
}{},
|
||||
want: mcp.NewTool("test string default",
|
||||
mcp.WithString("a", mcp.Required(), mcp.Description("string a"), mcp.DefaultString("hello")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test string enum",
|
||||
args: struct {
|
||||
A string `json:"a" desc:"string a" required:"true" enum:"1,2,3"`
|
||||
}{},
|
||||
want: mcp.NewTool("test string enum",
|
||||
mcp.WithString("a", mcp.Required(), mcp.Description("string a"), mcp.Enum("1", "2", "3")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test object",
|
||||
args: struct {
|
||||
A struct {
|
||||
B int `json:"b" desc:"number b" required:"true"`
|
||||
} `json:"a" desc:"object a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test object",
|
||||
mcp.WithObject("a", mcp.Required(), mcp.Description("object a"),
|
||||
mcp.Properties(map[string]any{
|
||||
"b": map[string]any{
|
||||
"type": "number",
|
||||
"description": "number b",
|
||||
"required": true,
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test object optional",
|
||||
args: struct {
|
||||
A struct {
|
||||
B int `json:"b" desc:"number b" required:"true"`
|
||||
C int `json:"c" desc:"number c"`
|
||||
} `json:"a" desc:"object a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test object optional",
|
||||
mcp.WithObject("a", mcp.Required(), mcp.Description("object a"),
|
||||
mcp.Properties(map[string]any{
|
||||
"b": map[string]any{
|
||||
"type": "number",
|
||||
"description": "number b",
|
||||
"required": true,
|
||||
},
|
||||
"c": map[string]any{
|
||||
"type": "number",
|
||||
"description": "number c",
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test nested object",
|
||||
args: struct {
|
||||
A struct {
|
||||
B struct {
|
||||
C int `json:"c" desc:"number c" required:"true"`
|
||||
} `json:"b" desc:"object b" required:"true"`
|
||||
} `json:"a" desc:"object a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test nested object",
|
||||
mcp.WithObject("a", mcp.Required(), mcp.Description("object a"),
|
||||
mcp.Properties(map[string]any{
|
||||
"b": map[string]any{
|
||||
"type": "object",
|
||||
"description": "object b",
|
||||
"required": true,
|
||||
"properties": map[string]any{
|
||||
"c": map[string]any{
|
||||
"type": "number",
|
||||
"description": "number c",
|
||||
"required": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test array",
|
||||
args: struct {
|
||||
A []int `json:"a" desc:"array a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test array",
|
||||
mcp.WithArray("a", mcp.Required(), mcp.Description("array a"),
|
||||
mcp.Items(map[string]any{
|
||||
"type": "number",
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test array of object",
|
||||
args: struct {
|
||||
A []struct {
|
||||
B int `json:"b" desc:"number b" required:"true"`
|
||||
} `json:"a" desc:"array of object a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test array of object",
|
||||
mcp.WithArray("a", mcp.Required(), mcp.Description("array of object a"),
|
||||
mcp.Items(map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"b": map[string]any{
|
||||
"type": "number",
|
||||
"description": "number b",
|
||||
"required": true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := SchemaToOptions(tt.args)
|
||||
if err != nil {
|
||||
t.Errorf("SchemaToOptions() error = %v", err)
|
||||
return
|
||||
}
|
||||
s1, _ := mcp.NewTool(tt.name, got...).MarshalJSON()
|
||||
s2, _ := tt.want.MarshalJSON()
|
||||
if !reflect.DeepEqual(s1, s2) {
|
||||
t.Errorf("\n got %v\n want %v", string(s1), string(s2))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
1567
scripts/manage.py
Normal file
@@ -2,7 +2,42 @@
|
||||
|
||||
[Ingress-nginx](https://kubernetes.github.io/ingress-nginx/) plugin for Chaitin SafeLine Web Application Firewall (WAF). This plugin is used to protect your API from malicious requests. It can be used to block requests that contain malicious content in the request body, query parameters, headers, or URI.
|
||||
|
||||
## Usage
|
||||
## Safeline Prepare
|
||||
The detection engine of the SafeLine provides services by default via Unix socket. We need to modify it to use TCP, so it can be called by the t1k plugin.
|
||||
|
||||
1.Navigate to the configuration directory of the SafeLine detection engine:
|
||||
```shell
|
||||
cd /data/safeline/resources/detector/
|
||||
```
|
||||
2.Open the `detector.yml` file in a text editor. Modify the bind configuration from Unix socket to TCP by adding the following settings:
|
||||
```yaml
|
||||
bind_addr: 0.0.0.0
|
||||
listen_port: 8000
|
||||
```
|
||||
These configuration values will override the default settings in the container, making the SafeLine engine listen on port 8000.
|
||||
|
||||
3.Next, map the container’s port 8000 to the host machine. First, navigate to the SafeLine installation directory:
|
||||
```shell
|
||||
cd /data/safeline
|
||||
```
|
||||
|
||||
4.Open the compose.yaml file in a text editor and add the ports field to the detector container to expose port 8000:
|
||||
```yaml
|
||||
...
|
||||
detect:
|
||||
ports:
|
||||
- 8000:8000
|
||||
...
|
||||
```
|
||||
|
||||
5.Save the changes and restart SafeLine with the following commands:
|
||||
```shell
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
```
|
||||
This will apply the changes and activate the new configuration.
|
||||
|
||||
## Plugin Usage
|
||||
|
||||
### Step 1: Install the plugin
|
||||
|
||||
@@ -67,7 +102,7 @@ env:
|
||||
configMapKeyRef:
|
||||
name: safeline
|
||||
key: port
|
||||
...
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
|
||||
20
sdk/ingress-nginx/ingress-nginx-safeline-1.0.4-1.rockspec
Normal file
@@ -0,0 +1,20 @@
|
||||
package = "ingress-nginx-safeline"
|
||||
version = "1.0.4-1"
|
||||
source = {
|
||||
url = "git://github.com/chaitin/ingress-nginx-safeline.git"
|
||||
}
|
||||
description = {
|
||||
summary = "Ingress-Nginx plugin for Chaitin SafeLine Web Application Firewall",
|
||||
homepage = "https://github.com/chaitin/ingress-nginx-safeline",
|
||||
license = "Apache License 2.0",
|
||||
maintainer = "Xiaobing Wang <xiaobing.wang@chaitin.com>"
|
||||
}
|
||||
dependencies = {
|
||||
"lua-resty-t1k >= 1.1.5"
|
||||
}
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
["safeline.main"] = "lib/safeline/main.lua"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,49 @@
|
||||
# Kong Safeline Plugin
|
||||
Kong plugin for Chaitin SafeLine Web Application Firewall (WAF). This plugin is used to protect your API from malicious requests. It can be used to block requests that contain malicious content in the request body, query parameters, headers, or URI.
|
||||
|
||||
## Installation
|
||||
## Safeline Prepare
|
||||
The detection engine of the SafeLine provides services by default via Unix socket. We need to modify it to use TCP, so it can be called by the t1k plugin.
|
||||
|
||||
1.Navigate to the configuration directory of the SafeLine detection engine:
|
||||
```shell
|
||||
cd /data/safeline/resources/detector/
|
||||
```
|
||||
2.Open the `detector.yml` file in a text editor. Modify the bind configuration from Unix socket to TCP by adding the following settings:
|
||||
```yaml
|
||||
bind_addr: 0.0.0.0
|
||||
listen_port: 8000
|
||||
```
|
||||
These configuration values will override the default settings in the container, making the SafeLine engine listen on port 8000.
|
||||
|
||||
3.Next, map the container’s port 8000 to the host machine. First, navigate to the SafeLine installation directory:
|
||||
```shell
|
||||
cd /data/safeline
|
||||
```
|
||||
|
||||
4.Open the compose.yaml file in a text editor and add the ports field to the detector container to expose port 8000:
|
||||
```yaml
|
||||
...
|
||||
detect:
|
||||
ports:
|
||||
- 8000:8000
|
||||
...
|
||||
```
|
||||
|
||||
5.Save the changes and restart SafeLine with the following commands:
|
||||
```shell
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
```
|
||||
This will apply the changes and activate the new configuration.
|
||||
|
||||
## Plugin Installation
|
||||
To install the plugin, run the following command in your Kong server:
|
||||
|
||||
```shell
|
||||
$ luarocks install kong-safeline
|
||||
```
|
||||
|
||||
## Configuration
|
||||
## Plugin Configuration
|
||||
You can add the plugin to your API by making the following request:
|
||||
|
||||
```shell
|
||||
|
||||
31
sdk/kong/kong-safeline-1.0.3-1.rockspec
Normal file
@@ -0,0 +1,31 @@
|
||||
package = "kong-safeline"
|
||||
version = "1.0.3-1"
|
||||
source = {
|
||||
url = "file://kong-safeline-1.0.3.tar.gz",
|
||||
}
|
||||
build = {
|
||||
type = "script",
|
||||
rockspec = {
|
||||
build = {
|
||||
"git clone https://github.com/chaitin/SafeLine.git",
|
||||
"cp -r sdk/kong .",
|
||||
"rm -rf SafeLine"
|
||||
}
|
||||
}
|
||||
}
|
||||
description = {
|
||||
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
|
||||
homepage = "https://github.com/chaitin/SafeLine",
|
||||
license = "Apache License 2.0",
|
||||
maintainer = "Xiaobing Wang <xiaobing.wang@chaitin.com>"
|
||||
}
|
||||
dependencies = {
|
||||
"lua-resty-t1k >= 1.1.5"
|
||||
}
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
|
||||
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
|
||||
}
|
||||
}
|
||||
31
sdk/kong/kong-safeline-1.0.4-1.rockspec
Normal file
@@ -0,0 +1,31 @@
|
||||
package = "kong-safeline"
|
||||
version = "1.0.4-1"
|
||||
source = {
|
||||
url = "git://github.com/chaitin/SafeLine.git",
|
||||
}
|
||||
build = {
|
||||
type = "script",
|
||||
rockspec = {
|
||||
build = {
|
||||
"git clone https://github.com/chaitin/SafeLine.git",
|
||||
"cp -r sdk/kong .",
|
||||
"rm -rf SafeLine"
|
||||
}
|
||||
}
|
||||
}
|
||||
description = {
|
||||
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
|
||||
homepage = "https://github.com/chaitin/SafeLine",
|
||||
license = "Apache License 2.0",
|
||||
maintainer = "Xiaobing Wang <xiaobing.wang@chaitin.com>"
|
||||
}
|
||||
dependencies = {
|
||||
"lua-resty-t1k >= 1.1.5"
|
||||
}
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
|
||||
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
|
||||
}
|
||||
}
|
||||
31
sdk/kong/kong-safeline-1.0.5-1.rockspec
Normal file
@@ -0,0 +1,31 @@
|
||||
package = "kong-safeline"
|
||||
version = "1.0.5-1"
|
||||
source = {
|
||||
url = "git://github.com/chaitin/SafeLine.git",
|
||||
}
|
||||
build = {
|
||||
type = "script",
|
||||
rockspec = {
|
||||
build = {
|
||||
"git clone https://github.com/chaitin/SafeLine.git",
|
||||
"cp -r sdk/kong .",
|
||||
"rm -rf SafeLine"
|
||||
}
|
||||
}
|
||||
}
|
||||
description = {
|
||||
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
|
||||
homepage = "https://github.com/chaitin/SafeLine",
|
||||
license = "Apache License 2.0",
|
||||
maintainer = "Xiaobing Wang <xiaobing.wang@chaitin.com>"
|
||||
}
|
||||
dependencies = {
|
||||
"lua-resty-t1k >= 1.1.5"
|
||||
}
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
|
||||
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
|
||||
}
|
||||
}
|
||||
21
sdk/kong/kong-safeline-1.0.6-1.rockspec
Normal file
@@ -0,0 +1,21 @@
|
||||
package = "kong-safeline"
|
||||
version = "1.0.6-1"
|
||||
source = {
|
||||
url = "git://github.com/xbingW/kong-safeline.git",
|
||||
}
|
||||
description = {
|
||||
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
|
||||
homepage = "https://github.com/chaitin/SafeLine",
|
||||
license = "Apache License 2.0",
|
||||
maintainer = "Xiaobing Wang <xiaobing.wang@chaitin.com>"
|
||||
}
|
||||
dependencies = {
|
||||
"lua-resty-t1k >= 1.1.5"
|
||||
}
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
|
||||
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
|
||||
}
|
||||
}
|
||||
21
sdk/kong/kong-safeline-1.0.7-1.rockspec
Normal file
@@ -0,0 +1,21 @@
|
||||
package = "kong-safeline"
|
||||
version = "1.0.7-1"
|
||||
source = {
|
||||
url = "git://github.com/chaitin/kong-safeline.git",
|
||||
}
|
||||
description = {
|
||||
summary = "Kong plugin for Chaitin SafeLine Web Application Firewall",
|
||||
homepage = "https://github.com/chaitin/SafeLine",
|
||||
license = "Apache License 2.0",
|
||||
maintainer = "Xiaobing Wang <xiaobing.wang@chaitin.com>"
|
||||
}
|
||||
dependencies = {
|
||||
"lua-resty-t1k >= 1.1.5"
|
||||
}
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
["kong.plugins.safeline.handler"] = "kong/plugins/safeline/handler.lua",
|
||||
["kong.plugins.safeline.schema"] = "kong/plugins/safeline/schema.lua"
|
||||
}
|
||||
}
|
||||
@@ -34,11 +34,11 @@ function SafelineHandler:access(conf)
|
||||
if not ok then
|
||||
kong.log.err("failed to detector req: ", err)
|
||||
end
|
||||
if result then
|
||||
if result and result.status then
|
||||
if result.action == t1k_constants.ACTION_BLOCKED then
|
||||
local msg = fmt(blocked_message, result.status, result.event_id)
|
||||
kong.log.debug("blocked by safeline: ",msg)
|
||||
return kong.response.exit(result.status, msg)
|
||||
return kong.response.exit(tonumber(result.status), msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
1
sdk/traefik-safeline
Submodule
@@ -1,9 +0,0 @@
|
||||
displayName: Chaitin Safeline WAF
|
||||
type: middleware
|
||||
|
||||
import: github.com/xbingW/traefik-safeline
|
||||
|
||||
summary: 'Traefik plugin to proxy requests to safeline waf.t serves as a reverse proxy access to protect your website from network attacks that including OWASP attacks, zero-day attacks, web crawlers, vulnerability scanning, vulnerability exploit, http flood and so on.'
|
||||
|
||||
testData:
|
||||
addr: safeline-detector:8000
|
||||
@@ -1,62 +0,0 @@
|
||||
# Traefik Plugin Safeline
|
||||
|
||||
This plugin is a middleware for Traefik that can be used to detect and block malicious requests which base on the [Safeline](https://waf.chaitin.com/) engine.
|
||||
|
||||
## Usage
|
||||
|
||||
For a plugin to be active for a given Traefik instance, it must be declared in the static configuration.
|
||||
|
||||
Plugins are parsed and loaded exclusively during startup, which allows Traefik to check the integrity of the code and catch errors early on.
|
||||
If an error occurs during loading, the plugin is disabled.
|
||||
|
||||
For security reasons, it is not possible to start a new plugin or modify an existing one while Traefik is running.
|
||||
|
||||
Once loaded, middleware plugins behave exactly like statically compiled middlewares.
|
||||
Their instantiation and behavior are driven by the dynamic configuration.
|
||||
|
||||
Plugin dependencies must be [vendored](https://golang.org/ref/mod#vendoring) for each plugin.
|
||||
Vendored packages should be included in the plugin's GitHub repository. ([Go modules](https://blog.golang.org/using-go-modules) are not supported.)
|
||||
|
||||
### Configuration
|
||||
|
||||
For each plugin, the Traefik static configuration must define the module name (as is usual for Go packages).
|
||||
|
||||
The following declaration (given here in YAML) defines a plugin:
|
||||
|
||||
```yaml
|
||||
# Static configuration
|
||||
|
||||
experimental:
|
||||
plugins:
|
||||
safeline:
|
||||
moduleName: github.com/xbingW/traefik-safeline
|
||||
version: v1.0.0
|
||||
```
|
||||
|
||||
Here is an example of a file provider dynamic configuration (given here in YAML), where the interesting part is the `http.middlewares` section:
|
||||
|
||||
```yaml
|
||||
# Dynamic configuration
|
||||
|
||||
http:
|
||||
routers:
|
||||
my-router:
|
||||
rule: host(`demo.localhost`)
|
||||
service: service-foo
|
||||
entryPoints:
|
||||
- web
|
||||
middlewares:
|
||||
- chaitin
|
||||
|
||||
services:
|
||||
service-foo:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: http://127.0.0.1:5000
|
||||
|
||||
middlewares:
|
||||
chaitin:
|
||||
plugin:
|
||||
safeline:
|
||||
addr: safeline-detector.safeline:8000
|
||||
```
|
||||
@@ -1,5 +0,0 @@
|
||||
module github.com/xbingW/traefik-safeline
|
||||
|
||||
go 1.17
|
||||
|
||||
require github.com/xbingW/t1k v1.2.1
|
||||
@@ -1,75 +0,0 @@
|
||||
package traefik_safeline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/xbingW/t1k"
|
||||
)
|
||||
|
||||
// Package example a example plugin.
|
||||
|
||||
// Config the plugin configuration.
|
||||
type Config struct {
|
||||
// Addr is the address for the detector
|
||||
Addr string `yaml:"addr"`
|
||||
// Get ip from header, if not set, get ip from remote addr
|
||||
IpHeader string `yaml:"ipHeader"`
|
||||
// When ip_header has multiple ip, use this to get the ip
|
||||
//
|
||||
//for example, X-Forwarded-For: ip1, ip2, ip3
|
||||
// when ip_last_index is 0, the client ip is ip3
|
||||
// when ip_last_index is 1, the client ip is ip2
|
||||
// when ip_last_index is 2, the client ip is ip1
|
||||
IPRightIndex uint `yaml:"ipRightIndex"`
|
||||
}
|
||||
|
||||
// CreateConfig creates the default plugin configuration.
|
||||
func CreateConfig() *Config {
|
||||
return &Config{
|
||||
Addr: "",
|
||||
IpHeader: "",
|
||||
IPRightIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Safeline a plugin.
|
||||
type Safeline struct {
|
||||
next http.Handler
|
||||
name string
|
||||
config *Config
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// New created a new plugin.
|
||||
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
|
||||
return &Safeline{
|
||||
next: next,
|
||||
name: name,
|
||||
config: config,
|
||||
logger: log.New(os.Stdout, "safeline", log.LstdFlags),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Safeline) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
d := t1k.NewDetector(t1k.Config{
|
||||
Addr: s.config.Addr,
|
||||
IpHeader: s.config.IpHeader,
|
||||
IPRightIndex: s.config.IPRightIndex,
|
||||
})
|
||||
resp, err := d.DetectorRequest(req)
|
||||
if err != nil {
|
||||
s.logger.Printf("Failed to detect request: %v", err)
|
||||
}
|
||||
if resp != nil && !resp.Allowed() {
|
||||
rw.WriteHeader(resp.StatusCode())
|
||||
if err := json.NewEncoder(rw).Encode(resp.BlockMessage()); err != nil {
|
||||
s.logger.Printf("Failed to encode block message: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
s.next.ServeHTTP(rw, req)
|
||||
}
|
||||
4
sdk/traefik/vendor/github.com/xbingW/t1k/README.md
generated
vendored
@@ -1,4 +0,0 @@
|
||||
# t1k-go
|
||||
|
||||
## Getting started
|
||||
|
||||
173
sdk/traefik/vendor/github.com/xbingW/t1k/detector.go
generated
vendored
@@ -1,173 +0,0 @@
|
||||
package t1k
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/xbingW/t1k/pkg/datetime"
|
||||
"github.com/xbingW/t1k/pkg/rand"
|
||||
"github.com/xbingW/t1k/pkg/t1k"
|
||||
)
|
||||
|
||||
type Detector struct {
|
||||
cfg Config
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Addr string `json:"addr"`
|
||||
// Get ip from header, if not set, get ip from remote addr
|
||||
IpHeader string `json:"ip_header"`
|
||||
// When ip_header has multiple ip, use this to get ip from right
|
||||
//
|
||||
//for example, X-Forwarded-For: ip1, ip2, ip3
|
||||
// when ip_last_index is 0, the client ip is ip3
|
||||
// when ip_last_index is 1, the client ip is ip2
|
||||
// when ip_last_index is 2, the client ip is ip1
|
||||
IPRightIndex uint `json:"ip_right_index"`
|
||||
}
|
||||
|
||||
func NewDetector(cfg Config) *Detector {
|
||||
return &Detector{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Detector) GetConn() (net.Conn, error) {
|
||||
_, _, err := net.SplitHostPort(d.cfg.Addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("detector add %s is invalid because %v", d.cfg.Addr, err)
|
||||
}
|
||||
return net.Dial("tcp", d.cfg.Addr)
|
||||
}
|
||||
|
||||
func (d *Detector) DetectorRequestStr(req string) (*t1k.DetectorResponse, error) {
|
||||
httpReq, err := http.ReadRequest(bufio.NewReader(strings.NewReader(req)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read request failed: %v", err)
|
||||
}
|
||||
return d.DetectorRequest(httpReq)
|
||||
}
|
||||
|
||||
func (d *Detector) DetectorRequest(req *http.Request) (*t1k.DetectorResponse, error) {
|
||||
extra, err := d.GenerateExtra(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate extra failed: %v", err)
|
||||
}
|
||||
dc := t1k.NewHttpDetector(req, extra)
|
||||
conn, err := d.GetConn()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get conn failed: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
return dc.DetectRequest(conn)
|
||||
}
|
||||
|
||||
func (d *Detector) DetectorResponseStr(req string, resp string) (*t1k.DetectorResponse, error) {
|
||||
httpReq, err := http.ReadRequest(bufio.NewReader(strings.NewReader(req)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read request failed: %v", err)
|
||||
}
|
||||
httpResp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(resp)), httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response failed: %v", err)
|
||||
}
|
||||
extra, err := d.GenerateExtra(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate extra failed: %v", err)
|
||||
}
|
||||
conn, err := d.GetConn()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get conn failed: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
return t1k.NewHttpDetector(httpReq, extra).SetResponse(httpResp).DetectResponse(conn)
|
||||
}
|
||||
|
||||
func (d *Detector) DetectorResponse(req *http.Request, resp *http.Response) (*t1k.DetectorResponse, error) {
|
||||
extra, err := d.GenerateExtra(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate extra failed: %v", err)
|
||||
}
|
||||
conn, err := d.GetConn()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get conn failed: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
return t1k.NewHttpDetector(req, extra).SetResponse(resp).DetectResponse(conn)
|
||||
}
|
||||
|
||||
func (d *Detector) GenerateExtra(req *http.Request) (*t1k.HttpExtra, error) {
|
||||
clientHost, err := d.getClientIP(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverHost, serverPort := req.Host, "80"
|
||||
if hasPort(req.Host) {
|
||||
serverHost, serverPort, err = net.SplitHostPort(req.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &t1k.HttpExtra{
|
||||
UpstreamAddr: "",
|
||||
RemoteAddr: clientHost,
|
||||
RemotePort: d.getClientPort(req),
|
||||
LocalAddr: serverHost,
|
||||
LocalPort: serverPort,
|
||||
ServerName: "",
|
||||
Schema: req.URL.Scheme,
|
||||
ProxyName: "",
|
||||
UUID: rand.String(32),
|
||||
HasRspIfOK: "y",
|
||||
HasRspIfBlock: "n",
|
||||
ReqBeginTime: strconv.FormatInt(datetime.Now(), 10),
|
||||
ReqEndTime: "",
|
||||
RspBeginTime: strconv.FormatInt(datetime.Now(), 10),
|
||||
RepEndTime: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Detector) getClientIP(req *http.Request) (string, error) {
|
||||
if d.cfg.IpHeader != "" {
|
||||
ips := req.Header.Get(d.cfg.IpHeader)
|
||||
if ips != "" {
|
||||
ipList := reverseStrSlice(strings.Split(ips, ","))
|
||||
if len(ipList) > int(d.cfg.IPRightIndex) {
|
||||
return strings.TrimSpace(ipList[d.cfg.IPRightIndex]), nil
|
||||
}
|
||||
return ipList[0], nil
|
||||
}
|
||||
}
|
||||
if !hasPort(req.RemoteAddr) {
|
||||
return req.RemoteAddr, nil
|
||||
}
|
||||
clientHost, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return clientHost, nil
|
||||
}
|
||||
|
||||
func (d *Detector) getClientPort(req *http.Request) string {
|
||||
_, clientPort, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return clientPort
|
||||
}
|
||||
|
||||
// has port check if host has port
|
||||
func hasPort(host string) bool {
|
||||
return strings.LastIndex(host, ":") > strings.LastIndex(host, "]")
|
||||
}
|
||||
|
||||
func reverseStrSlice(arr []string) []string {
|
||||
if len(arr) == 0 {
|
||||
return arr
|
||||
}
|
||||
return append(reverseStrSlice(arr[1:]), arr[0])
|
||||
}
|
||||
7
sdk/traefik/vendor/github.com/xbingW/t1k/pkg/datetime/datetime.go
generated
vendored
@@ -1,7 +0,0 @@
|
||||
package datetime
|
||||
|
||||
import "time"
|
||||
|
||||
func Now() int64 {
|
||||
return time.Now().UnixNano() / 1e3
|
||||
}
|
||||
15
sdk/traefik/vendor/github.com/xbingW/t1k/pkg/rand/str.go
generated
vendored
@@ -1,15 +0,0 @@
|
||||
package rand
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
func String(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
163
sdk/traefik/vendor/github.com/xbingW/t1k/pkg/t1k/detector.go
generated
vendored
@@ -1,163 +0,0 @@
|
||||
package t1k
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ResultFlag string
|
||||
|
||||
const (
|
||||
ResultFlagAllowed ResultFlag = "."
|
||||
ResultFlagBlocked ResultFlag = "?"
|
||||
)
|
||||
|
||||
func (d ResultFlag) Byte() byte {
|
||||
return d[0]
|
||||
}
|
||||
|
||||
type DetectorResponse struct {
|
||||
Head byte
|
||||
Body []byte
|
||||
Delay []byte
|
||||
ExtraHeader []byte
|
||||
ExtraBody []byte
|
||||
Context []byte
|
||||
Cookie []byte
|
||||
WebLog []byte
|
||||
BotQuery []byte
|
||||
BotBody []byte
|
||||
Forward []byte
|
||||
}
|
||||
|
||||
func (r *DetectorResponse) Allowed() bool {
|
||||
return r.Head == ResultFlagAllowed.Byte()
|
||||
}
|
||||
|
||||
func (r *DetectorResponse) StatusCode() int {
|
||||
str := string(r.Body)
|
||||
if str == "" {
|
||||
return http.StatusForbidden
|
||||
}
|
||||
code, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
log.Printf("t1k convert status code failed: %v", err)
|
||||
return http.StatusForbidden
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
func (r *DetectorResponse) BlockMessage() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"status": r.StatusCode(),
|
||||
"success": false,
|
||||
"message": "blocked by Chaitin SafeLine Web Application Firewall",
|
||||
"event_id": r.EventID(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DetectorResponse) EventID() string {
|
||||
extra := string(r.ExtraBody)
|
||||
if extra == "" {
|
||||
return ""
|
||||
}
|
||||
// <!-- event_id: e1impksyjq0gl92le6odi0fnobi270cj -->
|
||||
re, err := regexp.Compile(`<\!--\s*event_id:\s*([a-zA-Z0-9]+)\s*-->\s*`)
|
||||
if err != nil {
|
||||
log.Printf("t1k compile regexp failed: %v", err)
|
||||
return ""
|
||||
}
|
||||
matches := re.FindStringSubmatch(extra)
|
||||
if len(matches) < 2 {
|
||||
log.Printf("t1k regexp not match event id: %s", extra)
|
||||
return ""
|
||||
}
|
||||
return matches[1]
|
||||
}
|
||||
|
||||
type HttpDetector struct {
|
||||
extra *HttpExtra
|
||||
req Request
|
||||
resp Response
|
||||
}
|
||||
|
||||
func NewHttpDetector(req *http.Request, extra *HttpExtra) *HttpDetector {
|
||||
return &HttpDetector{
|
||||
req: NewHttpRequest(req, extra),
|
||||
extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *HttpDetector) SetResponse(resp *http.Response) *HttpDetector {
|
||||
d.resp = NewHttpResponse(d.req, resp, d.extra)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *HttpDetector) DetectRequest(socket io.ReadWriter) (*DetectorResponse, error) {
|
||||
raw, err := d.req.Serialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = socket.Write(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.ReadResponse(socket)
|
||||
}
|
||||
|
||||
func (d *HttpDetector) DetectResponse(socket io.ReadWriter) (*DetectorResponse, error) {
|
||||
raw, err := d.resp.Serialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = socket.Write(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.ReadResponse(socket)
|
||||
}
|
||||
|
||||
func (d *HttpDetector) ReadResponse(r io.Reader) (*DetectorResponse, error) {
|
||||
res := &DetectorResponse{}
|
||||
for {
|
||||
p, err := ReadPacket(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch p.Tag().Strip() {
|
||||
case TAG_HEADER:
|
||||
if len(p.PayLoad()) != 1 {
|
||||
return nil, errors.New("len(T1K_HEADER) != 1")
|
||||
}
|
||||
res.Head = p.PayLoad()[0]
|
||||
case TAG_DELAY:
|
||||
res.Delay = p.PayLoad()
|
||||
case TAG_BODY:
|
||||
res.Body = p.PayLoad()
|
||||
case TAG_EXTRA_HEADER:
|
||||
res.ExtraHeader = p.PayLoad()
|
||||
case TAG_EXTRA_BODY:
|
||||
res.ExtraBody = p.PayLoad()
|
||||
case TAG_CONTEXT:
|
||||
res.Context = p.PayLoad()
|
||||
case TAG_COOKIE:
|
||||
res.Cookie = p.PayLoad()
|
||||
case TAG_WEB_LOG:
|
||||
res.WebLog = p.PayLoad()
|
||||
case TAG_BOT_QUERY:
|
||||
res.BotQuery = p.PayLoad()
|
||||
case TAG_BOT_BODY:
|
||||
res.BotBody = p.PayLoad()
|
||||
case TAG_FORWARD:
|
||||
res.Forward = p.PayLoad()
|
||||
}
|
||||
if p.Last() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
75
sdk/traefik/vendor/github.com/xbingW/t1k/pkg/t1k/extra.go
generated
vendored
@@ -1,75 +0,0 @@
|
||||
package t1k
|
||||
|
||||
import "fmt"
|
||||
|
||||
type HttpExtra struct {
|
||||
UpstreamAddr string
|
||||
RemoteAddr string
|
||||
RemotePort string
|
||||
LocalAddr string
|
||||
LocalPort string
|
||||
ServerName string
|
||||
Schema string
|
||||
ProxyName string
|
||||
UUID string
|
||||
HasRspIfOK string
|
||||
HasRspIfBlock string
|
||||
ReqBeginTime string
|
||||
ReqEndTime string
|
||||
RspBeginTime string
|
||||
RepEndTime string
|
||||
}
|
||||
|
||||
func (h *HttpExtra) ReqSerialize() ([]byte, error) {
|
||||
format := "UpstreamAddr:%s\n" +
|
||||
"RemotePort:%s\n" +
|
||||
"LocalPort:%s\n" +
|
||||
"RemoteAddr:%s\n" +
|
||||
"LocalAddr:%s\n" +
|
||||
"ServerName:%s\n" +
|
||||
"Schema:%s\n" +
|
||||
"ProxyName:%s\n" +
|
||||
"UUID:%s\n" +
|
||||
"HasRspIfOK:%s\n" +
|
||||
"HasRspIfBlock:%s\n" +
|
||||
"ReqBeginTime:%s\n" +
|
||||
"ReqEndTime:%s\n"
|
||||
return []byte(fmt.Sprintf(
|
||||
format,
|
||||
h.UpstreamAddr,
|
||||
h.RemotePort,
|
||||
h.LocalPort,
|
||||
h.RemoteAddr,
|
||||
h.LocalAddr,
|
||||
h.ServerName,
|
||||
h.Schema,
|
||||
h.ProxyName,
|
||||
h.UUID,
|
||||
h.HasRspIfOK,
|
||||
h.HasRspIfBlock,
|
||||
h.ReqBeginTime,
|
||||
h.ReqEndTime,
|
||||
)), nil
|
||||
}
|
||||
|
||||
func (h *HttpExtra) RspSerialize() ([]byte, error) {
|
||||
format := "Scheme:%s\n" +
|
||||
"ProxyName:%s\n" +
|
||||
"RemoteAddr:%s\n" +
|
||||
"RemotePort:%s\n" +
|
||||
"LocalAddr:%s\n" +
|
||||
"LocalPort:%s\n" +
|
||||
"UUID:%s\n" +
|
||||
"RspBeginTime:%s\n"
|
||||
return []byte(fmt.Sprintf(
|
||||
format,
|
||||
h.Schema,
|
||||
h.ProxyName,
|
||||
h.RemoteAddr,
|
||||
h.RemotePort,
|
||||
h.LocalAddr,
|
||||
h.LocalPort,
|
||||
h.UUID,
|
||||
h.RspBeginTime,
|
||||
)), nil
|
||||
}
|
||||
69
sdk/traefik/vendor/github.com/xbingW/t1k/pkg/t1k/packet.go
generated
vendored
@@ -1,69 +0,0 @@
|
||||
package t1k
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Packet interface {
|
||||
Serialize() []byte
|
||||
Last() bool
|
||||
Tag() Tag
|
||||
PayLoad() []byte
|
||||
}
|
||||
|
||||
type HttpPacket struct {
|
||||
tag Tag
|
||||
payload []byte
|
||||
}
|
||||
|
||||
func (p *HttpPacket) Last() bool {
|
||||
return p.tag.Last()
|
||||
}
|
||||
|
||||
func (p *HttpPacket) Tag() Tag {
|
||||
return p.tag
|
||||
}
|
||||
|
||||
func (p *HttpPacket) PayLoad() []byte {
|
||||
return p.payload
|
||||
}
|
||||
|
||||
func NewHttpPacket(tag Tag, payload []byte) Packet {
|
||||
return &HttpPacket{
|
||||
tag: tag,
|
||||
payload: payload,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *HttpPacket) SizeBytes() []byte {
|
||||
sizeBytes := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(sizeBytes, uint32(len(p.payload)))
|
||||
return sizeBytes
|
||||
}
|
||||
|
||||
func (p *HttpPacket) Serialize() []byte {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte(byte(p.tag))
|
||||
buf.Write(p.SizeBytes())
|
||||
buf.Write(p.payload)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func ReadPacket(r io.Reader) (Packet, error) {
|
||||
tag := make([]byte, 1)
|
||||
if _, err := io.ReadFull(r, tag); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sizeBytes := make([]byte, 4)
|
||||
if _, err := io.ReadFull(r, sizeBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size := binary.LittleEndian.Uint32(sizeBytes)
|
||||
payload := make([]byte, size)
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewHttpPacket(Tag(tag[0]), payload), nil
|
||||
}
|
||||
122
sdk/traefik/vendor/github.com/xbingW/t1k/pkg/t1k/req.go
generated
vendored
@@ -1,122 +0,0 @@
|
||||
package t1k
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Request interface {
|
||||
Header() ([]byte, error)
|
||||
Body() ([]byte, error)
|
||||
Extra() ([]byte, error)
|
||||
Serialize() ([]byte, error)
|
||||
}
|
||||
|
||||
type HttpRequest struct {
|
||||
extra *HttpExtra
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
func NewHttpRequest(req *http.Request, extra *HttpExtra) *HttpRequest {
|
||||
return &HttpRequest{
|
||||
req: req,
|
||||
extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
func NewHttpRequestRead(req string) *HttpRequest {
|
||||
httpReq, _ := http.ReadRequest(bufio.NewReader(strings.NewReader(req)))
|
||||
return &HttpRequest{
|
||||
req: httpReq,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *HttpRequest) Header() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
proto := r.req.Proto
|
||||
startLine := fmt.Sprintf("%s %s %s\r\n", r.req.Method, r.req.URL.RequestURI(), proto)
|
||||
_, err := buf.Write([]byte(startLine))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = buf.Write([]byte(fmt.Sprintf("Host: %s\r\n", r.req.Host)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = r.req.Header.Write(&buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = buf.Write([]byte("\r\n"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (r *HttpRequest) Body() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
_, err := buf.ReadFrom(r.req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.req.Body = io.NopCloser(bytes.NewReader(buf.Bytes()))
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (r *HttpRequest) Extra() ([]byte, error) {
|
||||
return r.extra.ReqSerialize()
|
||||
}
|
||||
|
||||
func (r *HttpRequest) Version() []byte {
|
||||
return []byte("Proto:3\n")
|
||||
}
|
||||
|
||||
func (r *HttpRequest) Serialize() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
{
|
||||
raw, err := r.Header()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packet := NewHttpPacket(TAG_HEADER|MASK_FIRST, raw)
|
||||
_, err = buf.Write(packet.Serialize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
{
|
||||
raw, err := r.Body()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packet := NewHttpPacket(TAG_BODY, raw)
|
||||
_, err = buf.Write(packet.Serialize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
{
|
||||
raw, err := r.Extra()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packet := NewHttpPacket(TAG_EXTRA, raw)
|
||||
_, err = buf.Write(packet.Serialize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
{
|
||||
packet := NewHttpPacket(TAG_VERSION|MASK_LAST, r.Version())
|
||||
_, err := buf.Write(packet.Serialize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
127
sdk/traefik/vendor/github.com/xbingW/t1k/pkg/t1k/res.go
generated
vendored
@@ -1,127 +0,0 @@
|
||||
package t1k
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Response interface {
|
||||
RequestHeader() ([]byte, error)
|
||||
RspHeader() ([]byte, error)
|
||||
Body() ([]byte, error)
|
||||
Extra() ([]byte, error)
|
||||
Serialize() ([]byte, error)
|
||||
}
|
||||
|
||||
type HttpResponse struct {
|
||||
req Request
|
||||
extra *HttpExtra
|
||||
rsp *http.Response
|
||||
}
|
||||
|
||||
func NewHttpResponse(req Request, rsp *http.Response, extra *HttpExtra) *HttpResponse {
|
||||
return &HttpResponse{
|
||||
req: req,
|
||||
rsp: rsp,
|
||||
extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *HttpResponse) RequestHeader() ([]byte, error) {
|
||||
return r.req.Header()
|
||||
}
|
||||
|
||||
func (r *HttpResponse) RspHeader() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
statusLine := fmt.Sprintf("HTTP/1.1 %s\n", r.rsp.Status)
|
||||
_, err := buf.Write([]byte(statusLine))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = r.rsp.Header.Write(&buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = buf.Write([]byte("\r\n"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (r *HttpResponse) Body() ([]byte, error) {
|
||||
data, err := io.ReadAll(r.rsp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.rsp.Body = io.NopCloser(bytes.NewReader(data))
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (r *HttpResponse) Extra() ([]byte, error) {
|
||||
return r.extra.RspSerialize()
|
||||
}
|
||||
|
||||
func (r *HttpResponse) Version() []byte {
|
||||
return []byte("Proto:3\n")
|
||||
|
||||
}
|
||||
|
||||
func (r *HttpResponse) Serialize() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
{
|
||||
header, err := r.RequestHeader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packet := NewHttpPacket(TAG_HEADER|MASK_FIRST, header)
|
||||
_, err = buf.Write(packet.Serialize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
{
|
||||
header, err := r.RspHeader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packet := NewHttpPacket(TAG_RSP_HEADER, header)
|
||||
_, err = buf.Write(packet.Serialize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
{
|
||||
body, err := r.Body()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packet := NewHttpPacket(TAG_RSP_BODY, body)
|
||||
_, err = buf.Write(packet.Serialize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
{
|
||||
extra, err := r.Extra()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packet := NewHttpPacket(TAG_RSP_EXTRA, extra)
|
||||
_, err = buf.Write(packet.Serialize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
{
|
||||
version := r.Version()
|
||||
packet := NewHttpPacket(TAG_VERSION|MASK_LAST, version)
|
||||
_, err := buf.Write(packet.Serialize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
45
sdk/traefik/vendor/github.com/xbingW/t1k/pkg/t1k/tag.go
generated
vendored
@@ -1,45 +0,0 @@
|
||||
package t1k
|
||||
|
||||
type Tag byte
|
||||
|
||||
const (
|
||||
TAG_HEADER Tag = 0x01
|
||||
TAG_BODY Tag = 0x02
|
||||
TAG_EXTRA Tag = 0x03
|
||||
TAG_RSP_HEADER Tag = 0x11
|
||||
TAG_RSP_BODY Tag = 0x12
|
||||
TAG_RSP_EXTRA Tag = 0x13
|
||||
TAG_VERSION Tag = 0x20
|
||||
TAG_ALOG Tag = 0x21
|
||||
TAG_STAT Tag = 0x22
|
||||
TAG_EXTRA_HEADER Tag = 0x23
|
||||
TAG_EXTRA_BODY Tag = 0x24
|
||||
TAG_CONTEXT Tag = 0x25
|
||||
TAG_COOKIE Tag = 0x26
|
||||
TAG_WEB_LOG Tag = 0x27
|
||||
TAG_USER_DATA Tag = 0x28
|
||||
TAG_BOT_QUERY Tag = 0x29
|
||||
TAG_DELAY Tag = 0x2b
|
||||
TAG_FORWARD Tag = 0x2c
|
||||
TAG_BOT_BODY Tag = 0x30
|
||||
|
||||
MASK_TAG Tag = 0x3f
|
||||
MASK_FIRST Tag = 0x40
|
||||
MASK_LAST Tag = 0x80
|
||||
)
|
||||
|
||||
func (t Tag) Last() bool {
|
||||
return t&MASK_LAST != 0
|
||||
}
|
||||
|
||||
func (t Tag) First() bool {
|
||||
return t&MASK_FIRST != 0
|
||||
}
|
||||
|
||||
func (t Tag) Strip() Tag {
|
||||
return t & MASK_TAG
|
||||
}
|
||||
|
||||
func (t Tag) Byte() byte {
|
||||
return byte(t)
|
||||
}
|
||||
6
sdk/traefik/vendor/modules.txt
vendored
@@ -1,6 +0,0 @@
|
||||
# github.com/xbingW/t1k v1.2.1
|
||||
## explicit; go 1.17
|
||||
github.com/xbingW/t1k
|
||||
github.com/xbingW/t1k/pkg/datetime
|
||||
github.com/xbingW/t1k/pkg/rand
|
||||
github.com/xbingW/t1k/pkg/t1k
|
||||
5
version.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"latest_version": "v9.2.7",
|
||||
"rec_version": "v9.2.7",
|
||||
"lts_version": "v9.1.0-lts"
|
||||
}
|
||||