feat: new version of website

This commit is contained in:
delong.wang
2023-08-02 13:56:10 +08:00
parent 7c8e4f2585
commit 45987efeea
102 changed files with 18519 additions and 0 deletions

3
website/.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
build
.docusaurus

20
website/.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

4
website/.npmrc Normal file
View File

@@ -0,0 +1,4 @@
strict-ssl=false
save-prefix=""
engine-strict=true
registry="https://registry.npmmirror.com"

7
website/Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM node:20-alpine
COPY . /app
WORKDIR /app
RUN npm ci
RUN npm run build
CMD npm run serve -- --port 80 --host 0.0.0.0

21
website/README.md Normal file
View File

@@ -0,0 +1,21 @@
# 雷池社区官网
使用 [Docusaurus 2](https://docusaurus.io/) 开发。包含两部分内容:页面和文档。
- 页面,在 src 目录下,手写 js
- 文档,在 docs 目录下,手写 markdown
### 开发
```sh
# 开发
npm start
# 支持 搜索功能的预览
npm run serve -- --build --host 0.0.0.0
```
### 部署
```
docker build -t website .
```

3
website/babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};

View File

@@ -0,0 +1,57 @@
---
title: "雷池简介"
group:
title: "上手指南"
order: 1
order: 1
---
# 雷池简介
## 什么是 WAF
WAF 是 Web Application Firewall 的缩写,也被称为 Web 应用防火墙。区别于传统防火墙WAF 工作在应用层,对基于 HTTP/HTTPS 协议的 Web 系统有着更好的防护效果,使其免于受到黑客的攻击。
## 什么是雷池
雷池是长亭科技耗时近 10 年倾情打造的 WAF核心检测能力由智能语义分析算法驱动。
Slogan不让黑客越雷池半步。
## 为什么是雷池
#### 便捷性
采用容器化部署一条命令即可完成安装0 成本上手
安全配置开箱即用,无需人工维护,可实现安全躺平式管理
#### 安全性
首创业内领先的智能语义分析算法,精准检测、低误报、难绕过
语义分析算法无规则,面对未知特征的 0day 攻击不再手足无措
#### 高性能
无规则引擎,线性安全检测算法,平均请求检测延迟在 1 毫秒级别
并发能力强,单核轻松检测 2000+ TPS只要硬件足够强可支撑的流量规模无上限
#### 高可用
流量处理引擎基于 Nginx 开发,性能与稳定性均可得到保障
内置完善的健康检查机制,服务可用性高达 99.99%
## WAF 部署架构
下图是一个简单的网站流量拓扑,外部用户发出请求,经过网络最终传递到网站服务器。
此时,若外部用户中存在恶意用户,那么由恶意用户发出的攻击请求也会经过网络最终传递到网站服务器。
![](/images/docs/guide_introduction/website_without_safeline.png)
社区版雷池以反向代理方式接入,优先于网站服务器接收流量,对流量中的攻击行为进行检测和清洗,将清洗过后的流量转发给网站服务器。
通过以上行为,最终确保外部攻击流量无法触达网站服务器。
![](/images/docs/guide_introduction/website_with_safeline.png)

View File

@@ -0,0 +1,100 @@
---
title: "安装雷池"
group: "上手指南"
order: 2
---
# 安装雷池
## 配置需求
- 操作系统Linux
- 指令架构x86_64
- 软件依赖Docker 20.10.6 版本以上
- 软件依赖Docker Compose 2.0.0 版本以上
- 最小化环境1 核 CPU / 1 GB 内存 / 5 GB 磁盘
可以逐行执行以下命令来确认服务器配置
```shell
uname -m # 查看指令架构
docker version # 查看 Docker 版本
docker compose version # 查看 Docker Compose 版本
docker-compose version # 同上(兼容老版本 Docker Compose
cat /proc/cpuinfo # 查看 CPU 信息
cat /proc/meminfo # 查看内存信息
df -h # 查看磁盘信息
```
有三种安装方式供选择
- [在线安装](#在线安装) : 推荐安装方式
- [离线安装](#离线安装) : 服务器无法连接 Docker Hub 时选择
- [一键安装](#使用牧云助手安装) : 最简单的安装方式
## 在线安装
***如果服务器可以访问互联网环境,推荐使用该方式***
执行以下命令,即可开始安装
```
bash -c "$(curl -fsSLk https://waf-ce.chaitin.cn/release/latest/setup.sh)"
```
> 如果连接 Docker Hub 网络不稳,导致镜像下载失败,可以采用 [离线安装](#离线安装) 方式
经过以上步骤,你的雷池已经安装好了,下一步请参考 [登录雷池](/docs/上手指南/guide_login)
## 离线安装
如果你的服务器无法连接互联网环境,或连接 Docker Hub 网络不稳,可以使用镜像包安装方式
> 这里忽略 Docker 安装的过程
首先,下载 [雷池社区版镜像包](http://demo.waf-ce.chaitin.cn/image.tar.gz) 并传输到需要安装雷池的服务器上,执行以下命令加载镜像
```
cat image.tar.gz | gzip -d | docker load
```
执行以下命令创建并进入雷池安装目录
```
mkdir -p safeline # 创建 safeline 目录
cd safeline # 进入 safeline 目录
```
下载 [编排脚本](https://waf-ce.chaitin.cn/release/latest/compose.yaml) 并传输到 safeline 目录中
执行以下命令,生成雷池运行所需的相关环境变量
```
echo "SAFELINE_DIR=$(pwd)" >> .env
echo "IMAGE_TAG=latest" >> .env
echo "MGT_PORT=9443" >> .env
echo "POSTGRES_PASSWORD=$(LC_ALL=C tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" >> .env
echo "REDIS_PASSWORD=$(LC_ALL=C tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" >> .env
echo "SUBNET_PREFIX=169.254.0" >> .env
```
执行以下命令启动雷池
```
docker compose up -d
```
经过以上步骤,你的雷池已经安装好了,下一步请参考 [登录雷池](/docs/上手指南/guide_login)
## 使用牧云助手安装
也可以使用 [牧云主机管理助手](https://collie.chaitin.cn/) 进行一键安装
![](/images/docs/guide_install/collie_apps.png)
参考视频教程 [用 “白嫖的云主机” 一键安装 “开源的Web防火墙”](https://www.bilibili.com/video/BV1sh4y1t7Pk/)
## 常见安装问题
请参考 [安装问题](/docs/常见问题排查/faq_install)

View File

@@ -0,0 +1,11 @@
---
title: "登录雷池"
group: "上手指南"
order: 3
---
# 登录雷池
浏览器打开后台管理页面 `https://<waf-ip>:9443`。根据界面提示,使用 **支持 TOTP 的认证软件** 扫描二维码,然后输入动态口令登录:
![login.gif](https://waf-ce.chaitin.cn/images/gif/login.gif)

View File

@@ -0,0 +1,26 @@
---
title: "配置防护站点"
group: "上手指南"
order: 4
---
# 配置防护站点
![config_site.gif](https://waf-ce.chaitin.cn/images/gif/config_site.gif)
💡 TIPS: 添加后,执行 `curl -H "Host: <域名>" http://<WAF IP>:<端口>` 应能获取到业务网站的响应。
## 将网站流量切到雷池
- 若网站通过域名访问,则可将域名的 DNS 解析指向雷池所在设备
![DNS.png](/images/docs/DNS.png)
- 若网站前有 nginx 、负载均衡等代理设备,则可将雷池部署在代理设备和业务服务器之间,然后将代理设备的 upstream 指向雷池
![DNS.png](/images/docs/LoadBlance.png)
## 如何配置https
![safeline_https_website.gif](/images/docs/safeline_https_website.gif)
## 测试防护效果
[测试防护效果](/docs/上手指南/guide_test)

View File

@@ -0,0 +1,94 @@
---
title: "测试防护效果"
group: "上手指南"
order: 5
---
# 测试防护效果
## 确认网站可以正常访问
根据雷池 WAF 配置的网站参数访问你的网站
打开浏览器访问 `http://<IP或域名>:<端口>/`
> 网站协议默认是 http勾选 ssl 则为 https
> 主机名可以是雷池的 IP也可以是网站的域名确保域名已经解析到雷池
> 端口是你在雷池页面中配置的网站端口
若网站访问不正常,请参考 [网站无法访问](/docs/02-常见问题排查/03-faq_access.md)
## 尝试手动模拟攻击
打开浏览器,访问以下地址即可模拟出对应的攻击:
- 模拟 SQL 注入,请访问 `http://<IP或域名>:<端口>/?id=1%20AND%201=1`
- 模拟 XSS请访问 `http://<IP或域名>:<端口>/?html=<script>alert(1)</script>`
通过浏览器,你将会看到雷池已经发现并阻断了攻击请求。
若请求没有被阻断,请参考 [防护不生效](/docs/02-常见问题排查/05-faq_other.md)
## 自动化测试防护效果
两条请求当然无法完整的测试雷池的防护效果,可以使用 blazehttp 自动化工具进行批量测试
#### 下载测试工具
- [Windows 版本](https://waf-ce.chaitin.cn/blazehttp/blazehttp_windows.exe)
- [Mac 版本(x64)](https://waf-ce.chaitin.cn/blazehttp/blazehttp_mac_x64)
- [Mac 版本(M1)](https://waf-ce.chaitin.cn/blazehttp/blazehttp_mac_m1)
- [Linux 版本(x64)](https://waf-ce.chaitin.cn/blazehttp/blazehttp_linux_x64)
- [Linux 版本(ARM)](https://waf-ce.chaitin.cn/blazehttp/blazehttp_linux_arm64)
- [源码仓库](https://github.com/chaitin/blazehttp)
#### 准备测试样本
- [测试样本](https://waf-ce.chaitin.cn/blazehttp/testcases.zip)
下载请求样本后解压到 `testcases` 目录
#### 开始测试
1. 将测试工具 `blazehttp` 和测试样本 `testcases` 放在同一个目录下
2. 进入对应的目录
3. 使用以下请求开始测试
```
./blazehttp -t http://<IP或域名>:<端口> -g './testcases/**/*.http'
```
#### 测试效果展示
```
# 测试请求
./blazehttp -t http://192.168.0.1:8080 -g './testcases/**/*.http'
sending 100% |██████████████████████████████████████████| (18/18, 86 it/s)
Total http file: 18, success: 18 failed: 0
Stat http response code
Status code: 403 hit: 16
Status code: 200 hit: 2
Stat http request tag
tag: sqli hit: 1
tag: black hit: 16
tag: file_include hit: 1
tag: file_upload hit: 1
tag: java_unserialize hit: 1
tag: php_unserialize hit: 1
tag: cmdi hit: 1
tag: ssrf hit: 1
tag: xslti hit: 1
tag: xss hit: 1
tag: xxe hit: 1
tag: asp_code hit: 1
tag: white hit: 2
tag: ognl hit: 1
tag: shellshock hit: 1
tag: ssti hit: 1
tag: directory_traversal hit: 1
tag: php_code hit: 1
```

View File

@@ -0,0 +1,56 @@
---
title: "升级雷池"
group: "上手指南"
order: 6
---
# 升级雷池
**注意**: 升级雷池时服务会重启,流量会中断一小段时间,根据业务情况选择合适的时间来执行升级操作。
## 在线升级
执行以下命令即可进行升级。
```
bash -c "$(curl -fsSLk https://waf-ce.chaitin.cn/release/latest/upgrade.sh)"
```
[可选] 升级成功后, 可以执行以下命令删除旧版本 Docke 镜像, 以释放磁盘空间
```
docker rmi $(docker images | grep "safeline" | grep "none" | awk '{print $3}')
```
> 有部分环境的默认 SafeLine 安装路径是在 `/data/safeline-ce`,安装之后可能会发现需要重新绑定 OTP、配置丢失等情况可以修改 .env 的 `SAFELINE_DIR` 变量,指向 `/data/safeline-ce`
## 离线镜像
适用于 docker hub 拉取镜像失败的场景,手动更新镜像,注意还是要执行 `upgrade.sh` 来处理 `.env` 的更新,否则有可能会因为缺少参数而启动失败。
下载 [雷池社区版镜像包](http://demo.waf-ce.chaitin.cn/image.tar.gz) 并传输到需要安装雷池的服务器上,执行以下命令加载镜像
```
docker load -i image.tar.gz
```
**重要**, 进入安装雷池的目录
执行以下命令修改配置文件
```
sed -i "s/IMAGE_TAG=.*/IMAGE_TAG=latest/g" ".env"
grep "SAFELINE_DIR" ".env" > /dev/null || echo "SAFELINE_DIR=$(pwd)" >> ".env"
grep "SUBNET_PREFIX" ".env" > /dev/null || echo "SUBNET_PREFIX=169.254.0" >> ".env"
grep "REDIS_PASSWORD" ".env" > /dev/null || echo "REDIS_PASSWORD=$(LC_ALL=C tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" >> ".env"
```
执行以下命令替换 Docker 容器
```
docker compose down
docker compose up -d
```
OK, 你已经完成了升级

View File

@@ -0,0 +1,64 @@
---
title: "安装问题"
group:
title: "常见问题排查"
order: 2
order: 1
---
# 安装问题
## 支不支持Mac or Windows
不支持由于雷池所依赖的部分docker特性在Mac or Windows上并不生效所以雷池在Mac or windows并不能正常工作
## 我能把雷池和业务服务部署到同一台机器中吗?
不建议,如放在一起,在流量不变的情况下,机器负载将高于分开部署,增大了资源耗尽的可能性
## docker compose 还是 docker-compose
`docker compose`(带空格)是 V2 版本Go 写的。`docker-compose` 是 V1 版本Python 写的,已经不维护了。
我们推荐使用 V2 版本的 `docker compose`V1 可能会有兼容性等问题。
[docker/compose](https://github.com/docker/compose/) 中提到:
> For a smooth transition from legacy docker-compose 1.xx, please consider installing [compose-switch](https://github.com/docker/compose-switch) to translate `docker-compose ...` commands into Compose V2's `docker compose ....` . Also check V2's `--compatibility` flag.
其他参考:[https://stackoverflow.com/questions/66514436/difference-between-docker-compose-and-docker-compose](https://stackoverflow.com/a/66516826)
## 镜像下载缓慢甚至连接超时
这个是因为 docker hub 默认使用位于美西节点拉取镜像,可以自行配置国内镜像加速源
## ERROR: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
如描述,你需要启动 docker daemon 才能执行相关的命令。尝试 `systemctl start docker`
As shown, you shall start docker first. Try `systemctl start docker`.
## docker not found, unable to deploy
如描述,你需要安装 `docker`。尝试 `curl -fLsS https://get.docker.com/ | sh` 或者 [Install Docker Engine](https://docs.docker.com/engine/install/)
## docker compose v2 not found, unable to deploy
如描述,你需要安装 `docker compose v2`。尝试 `[Install Docker Compose](https://docs.docker.com/compose/install/)`
## safeline-tengine 出现 Address already in use
`docker logs -f safeline-tengine` 容器日志中看到 `Address already in use` 信息。
端口冲突,根据报错信息中的端口号,排查是哪个服务占用了,手动处理冲突。
## safeline-postgres 出现 Operation not permitted
`docker logs -f safeline-postgres` 容器日志中看到 `Operation not permitted` 报错
可能是您的 docker 版本过低,升级 docker 到最新版本尝试一下。
## 如何自定义 SafeLine 安装路径?
基于最新的 `compose.yaml`,你可以手动修改 `.env` 文件的 `SAFELINE_DIR` 变量。
## 如何修改 SafeLine 后台管理的默认端口?本机 `:9443` 已经被别的服务占用了
基于最新的 `compose.yaml`,你可以手动添加 `MGT_PORT` 变量到 `.env` 文件。

View File

@@ -0,0 +1,44 @@
---
title: "登录问题"
group: "常见问题排查"
order: 2
---
# 登录问题
> TOTP (Time-based One-Time Password algorithm) 将密钥与当前时间进行组合,通过哈希算法产生一次性密码,已被采纳为 RFC 6238被用于许多双因素身份验证系统。
## 动态口令错误
#### 时间不准
雷池社区版动态口令认证采用了 TOTP 算法TOTP 与时间强相关,如果相关设备的时间不准,可能会导致动态口令计算错误。
1. 检查手机时间是否准确(或其他 TOTP 扫码设备)
2. 检查雷池服务器时间是否准确
#### 动态口令可能已失效
TOTP 动态口令只有 30 秒的有效期,如果认证失败,请在动态口令刷新后重新尝试。
## 重新绑定动态口令
登录服务器,打开终端,执行以下命令即可重置动态口令
```
docker exec safeline-mgt-api resetadmin
```
命令执行完成后打开雷池页面重新绑定即可。
**注意重置动态口令后要尽快完成绑定,别被其他人捷足先登了**
## 多人使用
如果想多人使用雷池社区版,只需要以下 3 步:
1. 重置动态口令(参考 [重置认证](#重置认证)
2. 进入登录页面,这时会自动跳转到 TOTP 绑定页面,保存 “绑定二维码”(注意,非 “认证二维码”)
3. 将 “绑定二维码” 分享给其他人进行绑定,绑定后即可登录(注:“绑定二维码” 无绑定次数限制,无时效限制)
**注意保存的 “绑定二维码” 千万别泄漏,任何人得到以后都可以绑定并登录**

View File

@@ -0,0 +1,26 @@
---
title: "网站无法访问"
group: "常见问题排查"
order: 3
---
# 网站无法访问
为了方便讨论问题, 我们假设:
在没有 SafeLine 的时候,假设小明的域名 `xiaoming.com` 通过 DNS 解析到自己主机 `192.168.1.111`,上面在 `:8888` 端口监听了自己的服务(网站/博客/靶场)等等。
小明通过 `http://xiaoming.com:8888` 或者 `192.168.1.111:8888` 来访问自己的服务。
## 如果返回 502
![DNS.png](/images/docs/tengine_502.png)
如果出现类似相应,请检查上游服务器地址是否配置正确或者雷池是否能够访问能访问到上游服务器
## 请求返回缓慢
1. 首先检查服务器负载情况
2. 检查雷池服务器与上游服务器的网络状况,检查命令`curl -H "Host: <SafeLine-IP>" -vv -o /dev/null -s -w 'time_namelookup: %{time_namelookup}\ntime_connect: %{time_connect}\ntime_starttransfer: %{time_starttransfer}\ntime_total: %{time_total}\n' http://xiaoming.com:8888` <br/>
如果 `time_namelookup` 时间过大,请检查 dns server 配置
如果 `time_connect` 时间过大,请检查与上游服务器之间的网络状态
如果 `time_starttransfer` 时间过大,请检查上游服务器状态,是否出现资源过载情况

View File

@@ -0,0 +1,77 @@
---
title: "配置问题"
group: "常见问题排查"
order: 10
---
# 配置问题
## 站点配置问题
在没有 SafeLine 的时候,假设小明的域名 `xiaoming.com` 通过 DNS 解析到自己主机 `192.168.1.111`,上面在 `:8888` 端口监听了自己的服务(网站/博客/靶场)等等。
小明通过 `http://xiaoming.com:8888` 或者 `192.168.1.111:8888` 来访问自己的服务。
### 我该如何配置?/ 域名填什么?/ 端口怎么写?/ 上游服务器是什么?
目前社区版 SafeLine 支持的是反向代理的方式接入站点,也就是类似于一台 nginx 服务。这时候小明需要做的就是让流量先抵达 SafeLine然后经过 SafeLine 检测之后,再转发给自己原先的业务。
小明只需要按照如下方式创建站点即可:
- `xiaoming.com` 填入页面的「域名」
- `:7777` 填入「端口」;或者别的任意非 `:8888``:9443`(被 SafeLine 后台管理页面占用)端口
- `http://192.168.1.111:8888` 填入「上游服务器」
创建之后,就可以通过 `http://xiaoming.com:7777` 或者 `192.168.1.111:7777` 访问自己的服务了,这时候请求到 `http://xiaoming.com:7777` 的流量都会被 SafeLine 检测。经过 SafeLine 过滤后,安全的流量会被透传到原先的 `:8888` 业务服务器(即上游服务器)。
**注:直接访问 `http://xiaoming.com:8888` 的流量,仍然不会被 SafeLine 检测,因为流量并没有经过 SafeLine而是绕过 SafeLine 直接打到了上游服务器上**
**如果按照如上配置,还是无法成功访问到我的上游服务器,接着往下看,尝试逐项进行问题排查。**
## 配置完成之后,还是没有成功访问到上游服务器
下面例子都还按照上面小明的环境情况介绍。
#### 1. netstat/ss/lsof 查看端口占用情况
先确认下 `0.0.0.0:7777` 端口是否有服务在监听。SafeLine 使用 Tengine 来作为代理服务,所以正常来说,应该有一个 nginx 进程监听在 `:7777` 端口。如果没有的话,可能是 SafeLine 的问题,请通过社群或者 Github issue 提交反馈。
如果有的话,继续往下排查。
#### 2. 是否是被非 SafeLine 的 nginx 监听
基于第一步,已经能确认 `:7777` 是被某个 nginx 进程监听了,但是并不能确认是被 SafeLine 自己的 nginx 监听。排查是否自己原先有 nginx conf 中配置了 server 监听 `:7777`。如果有的话,手动解决冲突。要么修改自己原先的 nginx conf要么修改 SafeLine 的站点配置。
也可以直接通过 `docker logs -f safeline-tengine` 确认 SafeLine 是否有 nginx 报错说端口冲突。
*常见的情况就是自己原先有一个服务监听在 `:80`SafeLine 上配置了站点也监听 `:80` 端口,就产生了冲突。*
如果没有的话,继续往下排查。
#### 3. 是否被防火墙拦截
有操作系统本身的防火墙,还有可能是云服务商的防火墙。根据实际情况逐项排查,配置开放端口的 TCP 访问。
出现如下情况,可能就是被中间某防火墙拦截了:
1.`192.168.1.111` 上 curl -vv `127.0.0.1:7777` 能访问到业务,有 HTTP 返回码。
2. 在本机 curl -vv `192.168.1.111:7777` 不通,没有 HTTP 响应;`telnet 192.168.1.111 7777` 返回 `Unable to connect to remote host: Connection refused`
#### 4. SafeLine 是否能访问到上游服务器
小明的情况是 SafeLine 和业务在同一台机器,一般不会有不同机器之间的网络问题,但是也建议在 SafeLine 部署的机器上测试一下。如果是两台机器的情况下,需要考虑是否互相之间能正常通信。
直接 `curl -H "Host: <SafeLine-IP>" -vv http://xiaoming.com:8888` 测一下是否能访问到。如果不行,需要自行排查为什么 SafeLine 的机器没法访问到。
注:这里需要 -H 指定 Host `Host: <SafeLine-IP>` 进行连通性测试。收到比较多的反馈,在 WAF 上直接配置上游服务器为 HTTPS 的域名,比如 `https://xiaoming.com`。实际场景是希望先测试 WAF 能力正常后再把域名解析切到 WAF 进行上线。这种本地测试的场景,需要修改本机 host`xiaoming.com` 解析到 `SafeLine-IP`,否则可能会无法成功代理。因为 SafeLine 向上游服务器转发时,代理请求中的 Host 使用的是原始 HTTP 请求中的 Host此时需要自行判断上游业务服务器能够正确处理该代理请求例如上游业务服务器在 Host 没有匹配自己的站点名称时,是否能够处理)
#### 5. 其他情况
如果执行了 1-4
1. 确认有 nginx 进程监听了 SafeLine 机器的 `0.0.0.0:7777` 端口
2. 确认 SafeLine tengine 无端口冲突报错
3. 确认主机和云服务商的防火墙都没有限制 `:7777` 端口的 TCP 访问
4. 确认在 SafeLine 上能访问到「上游服务器」
问题还是没有解决,可能是 SafeLine 产品的问题,请通过社群或者 Github issue 提交反馈。

View File

@@ -0,0 +1,246 @@
---
title: "其他问题"
group: "常见问题排查"
order: 10
---
# 其他问题
## 内网用户,如何使用在线的威胁情报 IP 呢?需要加白哪个域名?
威胁情报的云服务部署在百川云平台,域名是 https://challenge.rivers.chaitin.cn/ ,雷池部署在内网的师傅可以加白一下,就可以正常同步情报数据了。
## 源 IP 显示不正确
雷池默认会通过 Socket 连接获取请求者的源 IP如果请求在到达雷池之前还经过了其他代理设备反代、LB、CDN、AD 等),这种情况会影响雷池获取正确的源 IP 信息。
通常,代理设备都会将真实源 IP 通过 HTTP Header 的方式传递给下一跳设备。如下方的 HTTP 请求,在 `X-Forwarded-For``X-Real-IP` 两个 Header 中都包含了源 IP
```
GET /path HTTP/1.1
Host: waf-ce.chaitin.cn
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
X-Forwarded-For: 110.123.66.233, 10.10.3.15
X-Real-IP: 110.123.66.233
```
> `X-Forwarded-For` 是链式结构,若请求经过了多级代理,这里将会按顺序记录每一跳的客户端 IP。
如果请求中没有包含存在源 IP 的相关 Header可以通过修改前方代理设备的配置来解决。例如Nginx 可以增加如下配置来传递 `X-Real-IP` 给后方设备:
```
location /xxx {
proxy_pass http://xxx.xxx;
...
proxy_set_header X-Real-IP $remote_addr;
...
}
```
遇到这种情况,打开雷池控制台的 “通用配置” 页面,将选项 “源 IP 获取方式” 的内容修改为 “从 HTTP Header 中获取”,并在对应的输入框中填入 `X-Real-IP 即可`
![get_source_ip.png](/images/docs/get_source_ip.png)
## 如何清理数据库中的统计信息和检测日志
**_注意该操作会清除所有日志信息且不可恢复_**
```shell
docker exec -it safeline-mgt-api cleanlogs
```
## 如何记录所有访问雷池的请求
默认情况下雷池是并不会保存请求记录的,如果需要保存请求记录,可以修改安装路径下的**resources/nginx/nginx.conf**
![config_access_log.png](/images/docs/config_access_log.png)
如图所示,去掉文件第 99 行的注释,删除第 100 行的内容,保存后运行命令检查配置文件
```shell
docker exec safeline-tengine nginx -t
```
检查应显示
```shell
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```
最后应用配置文件
```shell
docker exec safeline-tengine nginx -s reload
```
配置生效后,访问日志将会保存至安装路径下的**logs/nginx**
**_注意该操作会加快对硬盘的消耗请定时清理访问日志_**
## 如果配置完成后,测试时返回 400 Request Header Or Cookie Too Large
请麻烦检查是否形成了环路,即:雷池将请求转发给上游服务器后,上游服务器又将请求转发回雷池。
## 如何将雷池的日志导出到 XXX
雷池社区版自发布以来经常有用户询问如何将拦截日志通过 syslog 转发至目标地址,接下来我们将尝试使用 fluentd 来实现这个需求。
首先,我们编写 fluent.conf我们将读取 mgt_detect_log_basic 中的数据,并通过配置 syslog 转发出去。下面是 input 部分match 部分可以参考参考文档中的 syslog 部分。
```
<source>
@type sql
host safeline-postgres // 默认数据库地址如果在compose.yml中该过请使用改后值
port 5432
database safeline-ce // 数据库名
adapter postgresql
username safeline-ce // 默认用户名如果在compose.yml中该过请使用改后值
password POSTGRES_PASSWORD // 数据库密码,见安装目录下.env
select_interval 60s # optional
select_limit 500 # optional
state_file /var/run/fluentd/sql_state
<table>
table mgt_detect_log_basic
update_column timestamp
time_column timestamp # optional
</table>
# detects all tables instead of <table> sections
#all_tables
</source>
```
之后,来编写我们的 fluentd 的 Dockerfile
```
FROM fluent/fluentd:v1.16-1
# Use root account to use apk
USER root
# below RUN includes plugin as examples elasticsearch is not required
# you may customize including plugins as you wish
RUN apk add --no-cache --update --virtual .build-deps \
sudo build-base ruby-dev \
&& apk add libpq-dev \
&& sudo gem install pg --no-document \
&& sudo gem install fluent-plugin-remote_syslog \
&& sudo gem sources --clear-all \
&& apk del .build-deps libpq-dev \
&& rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem
COPY fluent.conf /fluentd/etc/fluent.conf
USER fluent
```
最后,编译完成后,我们将容器跑起来,参考命令
```sh
echo "" > ./sql-state
docker run -d --restart=always --name safeline-fluentd \
--net safeline-ce -v ./sql-state:/var/run/fluentd/sql_state \
safeline-flunetd:latest
```
参考文档
[SQL input plugin for Fluentd event collector](https://github.com/fluent/fluent-plugin-sql)
[fluent-plugin-remote_syslog](https://github.com/fluent-plugins-nursery/fluent-plugin-remote_syslog)
## 如何开启监听 ipv6
雷池默认不开启 ipv6, 如果需要开启 ipv6需手动修改安装路径下的**resources/nginx/sites-enabled/** 文件夹下对应域名的配置文件
如需同时监听 ipv4 与 ipv6
```shell
server {
listen [::]:80;
listen 0.0.0.0:80;
server_name example.com;
}
```
如只需监听 ipv6
```shell
server {
listen [::]:80;
server_name example.com;
}
```
**_注意页面上编辑当前站点会覆盖配置_**
修改完成后运行命令检查配置文件
```shell
docker exec safeline-tengine nginx -t
```
检查应显示
```shell
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```
最后应用配置文件
```shell
docker exec safeline-tengine nginx -s reload
```
## 有多个防护站点监听在同一个端口上,匹配顺序是怎么样的
如果域名处填写的分别为 ip 与域名,那么当使用进行 ip 请求时,则将会命中第一个配置的站点
![server_index02.png](/images/docs/server_index02.png)
以上图为例,如果用户使用 ip 访问,命中 example.com
如果域名处填写的分别为域名与泛域名,除非准确命中域名,则会命中泛域名,不论泛域名第几个配置
![server_index01.png](/images/docs/server_index01.png)
以上图为例,如果用户使用 a.example.com 访问,命中 a.example.com。 如果用户使用 b.example.com,命中\*.example.com
## 自定义站点 nginx conf
雷池每次修改站点或者重启服务时,都会重新生成 **resources/nginx/sites-enabled/** 下的 nginx conf 文件。因为没法“智能”合并用户自定义的配置和自动生成的配置。但是也还是有方式能持久化地添加一些 nginx conf不会被覆盖。
每个 `IF_backend_XXX` 的 location 中都有 `include proxy_params;` 这一行配置,且 `resources/nginx/proxy_params` 这个文件不会被修改站点、重启服务等动作覆盖。2.1.0 版本之后支持 `include custom_params/backend_XXX;` 可以自定义站点级的 nginx location 配置。
```shell
server {
location ^~ / {
proxy_pass http://backend_1;
include proxy_params;
include custom_params/IF_backend_1;
# ...
}
}
```
所以只需要根据需求修改对应的文件就可以了。比如在 `resources/nginx/proxy_params` 里面增加如下配置,即可支持 `X-Forwarded-Proto`
```shell
proxy_set_header X-Forwarded-Proto $scheme;
```
修改完成后运行命令检查配置文件
```shell
docker exec safeline-tengine nginx -t
```
检查应显示
```shell
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```
最后应用配置文件
```shell
docker exec safeline-tengine nginx -s reload
```

View File

@@ -0,0 +1,35 @@
---
title: "智能语义分析技术"
group: "关于雷池"
order: 2
---
# 智能语义分析技术
## 传统规则防护,在当下为什么失灵?
当下Web 应用防火墙大多采用规则匹配方式来识别和阻断攻击流量,但由于 Web 攻击成本低、方式复杂多样、高危漏洞不定期爆发等原因,管理者们在安全运维工作中不得不持续调整防护规则,以保障业务的可用性和安全性。尽管如此,每天依然面临着不少的误报和漏报,影响正常业务运转甚至导致 Web 服务失陷。
究其原因,是由于基于规则匹配的攻击识别方法存在先天不足导致的。在乔姆斯基文法体系中,编写匹配规则的正则文法属于 3 型文法,而用于构造攻击 Payload 的程序语言属于 2 型文法,如下图所示:
![Untitled](/images/docs/Untitled10.png)
从文法表达能力比较3 型文法包含在 2 型文法之内,基于正则的规则描述无法完全覆盖基于程序语言的攻击 Payload这也是基于规则匹配识别攻击的 WAF 防护效果低于预期的根本原因。
## 雷池的解决之道:算法的革新重构 WAF
长亭科技自成立起便深入探索 Web 安全防护的新思路,创新性提出以 “智能语义分析算法” 解决 Web 攻击识别问题,给 WAF 内置 “智能大脑”,使其具备自主识别攻击行为的能力,同时结合机器学习建模,不断增强和完善 “大脑” 的分析能力,不依赖传统的规则库即可满足 Web 应用日常安全防护需求。
雷池通过对 Web 请求和返回内容进行智能分析,使 WAF 具备智能判断攻击威胁的能力。智能语义分析算法由词法分析、语法分析、语义分析和威胁模型匹配 4 个步骤组成。
![Untitled](/images/docs/Untitled11.png)
雷池内置涵盖常用编程语言的编译器,通过对 HTTP 的载荷内容进行深度解码后,按照其语言类型匹配相应语法编译器,进而匹配威胁模型得到威胁评级,阻断或允许访问请求。
与规则匹配型威胁检测方式相比,智能语义分析技术具有准确率高、误报率低的特点。以 SQL 注入检测为例:
![Untitled](/images/docs/Untitled12.png)
![Untitled](/images/docs/Untitled13.png)
作为全球范围内第一款以智能语义分析算法为核心引擎能力打造的下一代 WAF雷池展现出了更多让安全产品 “更聪明” 的可能。除了形成了质变的检测引擎的精准程度,它可以通过插件形式灵活扩展、实现瑞士军刀般的功能增加,可以变形适配、安装部署进各种网络环境,可以跟机器学习等前沿技术更好的融合、增强流量分析的能力等。

View File

@@ -0,0 +1,30 @@
---
title: "人机验证"
group:
title: "关于雷池"
order: 3
order: 3
---
# 人机验证
自从雷池社区版发布以来,我们一直密切关注用户对于爬虫和扫描器的反馈和防护需求。 因此在2.0版本中,我们致力于探索与此相关的功能,以满足用户的期望和保护网站的安全。
![challenge.png](/images/docs/challenge.png)
### 加入人机验证之后的请求处理流程
![flow.png](/images/docs/flow.png)
### 人机验证如何配置
首先,点击位于左边栏的人机验证。之后,点击 **添加人机验证**
![add_challenge.png](/images/docs/add_challenge.png)
在这里我们可以配置是否开启交互式校验以及规则的名称以及规则的触发条件。
### 人机验证触发规则
1. 规则内的条件之间是并且的关系,即需要全部命中,才会触发
2. 规则与规则之间是或的关系,则有一个命中,便会触发
### 交互与非交互的区别
如果选择开启交互,那么用户需要点击页面中间的勾选框开始验证,如果选择非交互,那么将自动开始验证
![manual.png](/images/docs/manual.png)

View File

@@ -0,0 +1,91 @@
---
title: "关于我们"
group: "关于雷池"
order: 9
---
# 关于我们
## 关于长亭
雷池是长亭科技耗时近 10 年倾情打造的 Web 应用防护产品,核心检测能力由智能语义分析算法驱动。
北京长亭未来科技有限公司是国际顶尖的网络信息安全公司之一,创始人团队 5 人均为清华博士,并引入阿里云安全核心人才团队。全球首发基于智能语义分析的下一代 Web 应用防火墙产品目前公司已形成以攻安全评估系统、防下一代Web应用防火墙、知安全分析与管理平台、查主机安全管理平台、抓伪装欺骗系统为核心的新一代安全防护体系并提供优质的安全测试及咨询服务为企业级客户带来智能的全新安全防护思路。
长亭专注为企业级用户提供专业的网络信息安全解决方案。2016 年即发布基于人工智能语义分析的下一代 Web 应用防火墙,颠覆了传统依赖规则防护的工作原理,为企业用户带来智能、简单、省心的安全产品及服务。
长亭雷池坚持以技术为导向,产品与服务所涉及到的算法与核心技术均领先国际行业前沿标准,不仅颠覆了繁琐耗时的传统工作原理,更将产品性能提升至领先水准,为企业用户带来更快、更精准、更智能的安全防护。
## 荣誉 & 资质
### 2021 年
- 入选 IDC《中国硬件Web应用防火墙WAF市场份额》前四
- 重磅发布《实战攻防-企业红蓝对抗实践指南》
- 荣获 2021 网信自主创新优秀产品 “补天奖”
- 荣膺 CNNVD 2020 年度优秀技术支撑单位
- 10 项虚拟机漏洞获 Oracle 官方致谢
- 入选安全牛第八版中国网络安全行业全景图
- 入选 CCSIP 2021 中国网络安全产业全景图
- 入选嘶吼 2021 网络安全产业链图谱
### 2020 年
- 2020 年金融科技产品创新突出贡献奖
- 2020 年网络安全创新能力100强
- 入选数世咨询《蜜罐诱捕能力指南》
- 入选数说安全中国网络安全市场全景图
- Network Products Guide IT World Award
- 2020 年中国人工智能商业落地价值潜力 100 强
- 2020 Application Security and Testing 铜奖
- 入选安全牛《2020 中国网络安全企业 100 强报告》
- 发现并命名幽灵猫(Ghostcat)漏洞
- 联合发布《国家区块链漏洞库-区块链漏洞定级细则》
### 2019 年
- 入选 Forrester《Now TechWeb Application Firewalls, Q4 2019》报告
- 2019 年关键信息基础设施“盘古奖”
- 荣获《金融电子化》“2019 年度金融科技产品创新突出贡献奖”
- 中国网络安全与信息产业 “金智奖” 2019 年度优秀单位
### 2018 年
- 通过国家保密局涉密信息系统产品认证
- 通信网络安全服务能力一级资质认证
- 中国 IT 思想力奖-金融科技产品创新奖
- TSRC 2017 最佳客户端洞主 & 年度最佳合作伙伴
- 获 Info Security Products Guide 全球卓越奖
- 入选《CIO Advisor》亚太地区 25 家最热门人工智能公司
- 入围 Gartner 2018《Web 应用防火墙魔力象限报告亚太版》
- 2018 年度金融科技优秀产品创新奖
- “2018 年度金牌服务机构” 安全服务奖
### 2017 年
- OWASP 认证雷池SafeLine下一代 Web 应用防火墙
- 通过国家测评中心/信息安全服务资质测评单位
- Gartner 魔力象限报告提名
- 再次登上 Black Hat USA 演讲
- 《财富》杂志评选中国创新百强 “人工智能和机器人” 领域全国第一
- 受邀出席世界互联网大会网络安全闭门会
- 阿里巴巴年度优秀生态合作伙伴
- 入选 Cyber Defense Magazine 全球网络安全领导者 Top 25
### 2016 年
- ISO9001 国际质量体系认证
- ISO27001 国际质量体系认证
- 长亭雷池 Web 应用防火墙(增强级)销售许可证
- 国家信息安全漏洞库CNNVD二级技术支撑单位资质
- 中国年度最佳产品奖、IT 行业最具影响力企业奖
- 年度特殊贡献奖
- GeekPwn 三周年特别贡献奖
### 2015 年
- 中国国家高新技术企业称号
- 中关村高新技术企业称号
- “最具价值安全问题” 荣誉认证
- 首次登上 Black Hat USA 演讲

View File

@@ -0,0 +1,226 @@
---
title: "版本更新记录"
group: "关于雷池"
order: 3
---
# 版本更新记录
[版本升级方法](https://waf-ce.chaitin.cn/docs/01-上手指南/06-guide_upgrade)
## [2.3.2] - 2023-07-24
#### 修复
- 修复了攻击事件 - 原始日志中,请求报文没有格式化的问题
- 优化了一些已知问题
## [2.3.1] - 2023-07-20
#### 新增
- 检测日志升级为**攻击事件** ,自动聚合同一攻击 IP 短时间内的所有攻击日志,方便管理员进行监控和处置
- 日志支持按时间([#102](https://github.com/chaitin/safeline/issues/102))、动作筛选
#### 修复
- 修复添加/编辑站点时,上传证书处未翻译中文的问题
- 修复数据统计中,“访问来源地区” 小概率出现的部分地区显示不正确的问题
## [2.2.0] - 2023-07-14
#### 新增
- IP 组中新增长亭社区恶意 IP 情报,内容来自社区版共享的攻击 IP每日自动更新
#### 优化
- 升级核心检测引擎,修复一些绕过和误报
- 管理界面增加浏览器版本检查,如果版本过旧,会提示升级浏览器
- 优化一些界面的 UI 交互细节
- 修复一些中英文翻译的问题
## [2.1.2] - 2023-07-07
- 修复了日志详情中防护策略模块没有翻译的问题
## [2.1.1] - 2023-07-06
- 修复了防护策略模块没有翻译的问题
## [2.1.0] - 2023-07-06
#### 新增
- 添加/编辑站点时,自动检查端口占用情况,避免保存后配置不生效
- 支持自定义站点的 nginx conf详情可见[官网文档](https://waf-ce.chaitin.cn/docs/常见问题排查/faq_other#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%AB%99%E7%82%B9-nginx-conf)
- [站点列表支持按域名、端口或访问量进行排序](https://github.com/chaitin/safeline/issues/14)
- [绑定 TOTP 密钥时,支持直接复制密钥;登录时的 6 位动态密码输入框,支持粘贴](https://github.com/chaitin/safeline/issues/30)
#### 优化
- [黑白名单和人机验证列表中,可以鼠标悬浮查看 “复合条件” 的具体内容](https://github.com/chaitin/safeline/issues/120)
- 修复人机验证列表中,“源 IP 不属于 IP 组” 的规则IP 组名称显示成了 id 的问题
- [优化人机验证启用/禁用交互](https://github.com/chaitin/safeline/issues/130)
- [优化描述文字](https://github.com/chaitin/safeline/issues/122)
- 优化一些界面 UI 交互、提示文字
- 修复一些已知问题
## [2.0.1] - 2023-06-30
- 调整了人机验证的策略,降低误拦的情况
- 修复了人机验证启动/禁用规则不会自动刷新状态的问题
- 修复其他一些已知问题
## [2.0.0] - 2023-06-29
#### 新增
- 人机验证
- 支持嵌入式部署,可以通过 [t1k 协议](https://github.com/chaitin/lua-resty-t1k) 直接把流量转发到雷池进行检测
- 把日志详情中的源 IP 加入到 IP 组时,支持调整 IP 为任意 IP 或网段
#### 优化
- 日志列表按照时间和 ID 排序,防止出现小范围的时间乱序
- 转发流量时,自动设置请求头 X-Forwarded-Proto适配更多代理场景
- 优化界面 UI修复其他一些已知问题
## [1.10.0] - 2023-06-21
#### 新增
- 防护站点新增 “运行模式”,可以一键将站点设为 观察 或 维护 模式了
#### 优化
- 修复了站点列表没有分页器的问题
- 修复了窗口水平滚动时导航栏会错位的问题
- 修复了黑白名单配置 “不属于 IP 组” 的条件时,列表显示组 ID而未显示组名称的问题
- 优化了界面的 UI 与交互
## [1.9.0] - 2023-06-16
#### 新增
- 界面 UI 改造,信息层级更清晰
- 黑白名单支持添加 源 IP - 不属于 IP 组 的条件
#### 优化
- 检测日志的路由进一步完善
- 数据统计页面更加紧凑,现在可以在 1920*1080 的屏幕上完全显示了
- 黑白名单的展示优化
- 修复 通用配置-防护模块配置 中,“批量配置为” 按钮有时候不生效的问题
- 修复一些已知问题
## [1.8.2] - 2023-06-12
- 修复了「30 天访问情况」和「30 天拦截情况」显示相同数据的问题
## [1.8.1] - 2023-06-09
- 修复了「全部请求」和「仅拦截」数据一样的问题
## [1.8.0] - 2023-06-09
#### 新增
- 数据统计页面增加访问来源地区、流量统计,更好把控网站运营情况
![](/images/docs/about_changelog/map.png)
#### 优化
- 更新语义引擎版本,优化了一大批检测逻辑,降低误报
- 优化了部分操作提示信息:
- IP 组正在使用时,无法被删除的提示
- 未创建 IP 组时,在黑白名单中无法选择属于 IP 组的提示
- 添加站点时,域名格式错误的提示
## [1.7.1] - 2023-06-05
#### 修复
- 部分情况下无法打开日志详情页面的问题
- 部分情况下页面查询数量只有 10 条的问题
## [1.7.0] - 2023-06-01
#### 新增
- 新增 “IP 组” 功能,可以快速配置大量 IP 的黑/白名单了
- 防护策略增加 “仅观察” 配置
- 防护策略增加 “批量配置为” 按钮,可以快速切换所有模块的防护策略
#### 优化
- 自定义规则列表增加翻页
- 优化规则生效顺序,现在会优先执行完所有白名单,再执行黑名单
## [1.6.0] - 2023-05-25
#### 新增
- 自定义规则支持匹配 Header 和 Body
- 检测日志支持按域名搜索
- 支持命令行清理检测日志和统计信息
## [1.5.1] - 2023-05-18
- 修复了自定义规则切换白名单之后,无法创建/编辑的问题
## [1.5.0] - 2023-05-18
#### 新增
- 支持 i18n
- 数据统计新增 “今日请求错误情况”
- 检测日志的筛选条件现在会显示在 URL方便保存
#### 优化
- 修复自定义规则的编辑表单,有时候会丢失编辑中数据的问题
- 修复 Safari 浏览器上的一些显示问题
- 修复 Payload 中存在非 Unicode 编码时,检测日志会入库失败的问题(不影响拦截)
- 修复新增的 “HTTP 请求走私” 攻击类型会被错误地展示成 “未知” 攻击类型的问题
## [1.4.0] - 2023-05-12
#### 新增
- 自定义规则支持匹配域名
- 支持在一条自定义规则内,设置多个匹配条件
- 站点列表新增 “今日访问 / 拦截量”
#### 优化
- 优化交互和提示文案、修复已知问题
## [1.3.0] - 2023-05-05
#### 新增
- 支持按照源 IP、攻击类型、URL 筛选检测日志
#### 修复
- 修复 dashboard 在部分低版本浏览器下的兼容问题
- 修复按源 IP 添加自定义规则时,添加不了 /8 和更大的网段的问题
## [1.2.0] - 2023-04-27
#### 新增
- 新增了数据统计页面,可以直观的看到流量大小
- 支持配置源 IP 提取方式,解决了源 IP 获取不对的问题
- 支持自定义检测策略,可以动态调整检测引擎
## [1.1.0] - 2023-04-20
#### 新增
- 支持根据 IP 和 URL 特征配置黑白名单
- 默认开启高防模式
#### 优化
- 支持在日志详情中展示响应报文
- 服务器时间不准导致 TOTP 无法登录时增加了提示语
- 修复了上游服务器填 HTTPS 时端口解析不正确的问题
- 优化了 SSL 上传逻辑,体验更好
## [1.0.0] - 2023-04-13
- 站点配置
## [0.9.0] - 2023-03-20
- OTP 登录
- 攻击检测日志
- 默认防护策略

View File

@@ -0,0 +1,176 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require("prism-react-renderer/themes/github");
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
const cnzz = `<script type="text/javascript" src="https://v1.cnzz.com/z_stat.php?id=1281262430&web_id=1281262430"/>`;
/** @type {import('@docusaurus/types').Config} */
const config = {
title: "长亭雷池 WAF 社区版",
tagline: "Dinosaurs are cool",
favicon: "images/favicon.ico",
plugins: [require.resolve("@cmfcmf/docusaurus-search-local")],
// Set the production url of your site here
url: "https://waf-ce.chaitin.cn/",
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: "/",
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: "chaitin", // Usually your GitHub org/user name.
projectName: "safeline-ce-site", // Usually your repo name.
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: "zh-Hans",
locales: ["zh-Hans"],
},
presets: [
[
"classic",
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: require.resolve("./sidebars.js"),
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl: "https://github.com/chaitin/safeline/tree/main/website/docs",
},
blog: {
showReadingTime: true,
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl: "https://github.com/chaitin/safeline/tree/main/website/blog",
},
theme: {
customCss: require.resolve("./src/css/custom.css"),
},
}),
],
],
themes: [
[
// @ts-ignore
require.resolve("@easyops-cn/docusaurus-search-local"),
/** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */
// @ts-ignore
({
// ... Your options.
// `hashed` is recommended as long-term-cache of index file is possible.
hashed: true,
// For Docs using Chinese, The `language` is recommended to set to:
// ```
language: ["zh"],
// ```
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
// Replace with your project's social card
image: "images/safeline.png",
navbar: {
title: "",
logo: { alt: "Logo", src: "images/logo.png" },
items: [
{
to: "/docs/上手指南/guide_introduction",
label: "技术文档",
position: "right",
},
{ to: "/detection", label: "效果对比", position: "right" },
{
to: "https://www.bilibili.com/medialist/detail/ml2342694989",
label: "教学视频",
position: "right",
},
{
to: "https://demo.waf-ce.chaitin.cn:9443/dashboard",
label: "演示环境",
position: "right",
},
{
href: "https://github.com/chaitin/safeline",
label: "GitHub",
position: "right",
},
],
},
footer: {
style: "dark",
links: [
{
title: " ",
items: [
{
label: "北京长亭科技有限公司",
to: "https://www.chaitin.cn/zh/",
},
{
label: "长亭 B 站主页",
to: "https://space.bilibili.com/521870525",
},
],
},
{
title: " ",
items: [
{
label: "CT Stack 安全社区",
href: "https://stack.chaitin.cn/",
},
{
label: "长亭合作伙伴论坛",
href: "https://bbs.chaitin.cn/",
},
],
},
{
title: " ",
items: [
{
label: "长亭百川云平台",
to: "https://rivers.chaitin.cn/",
},
{
label: "关于我们",
href: "https://waf-ce.chaitin.cn/docs/关于雷池/about_chaitin",
},
],
},
{
title: " ",
items: [
{
label: "长亭 GitHub 主页",
to: "https://github.com/chaitin",
},
],
},
],
copyright: `${
process.env.NODE_ENV === "production" ? cnzz : ""
} <b>Copyright</b> © ${new Date().getFullYear()}
北京长亭未来科技有限公司京 ICP 备 19035216 号京公网安备 11010802020947 号`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
}),
};
module.exports = config;

13622
website/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

55
website/package.json Normal file
View File

@@ -0,0 +1,55 @@
{
"name": "homepage",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "2.4.1",
"@docusaurus/preset-classic": "2.4.1",
"@easyops-cn/docusaurus-search-local": "0.35.0",
"@emotion/react": "11.11.1",
"@emotion/styled": "11.11.0",
"@mdx-js/react": "1.6.22",
"@mui/icons-material": "5.14.1",
"@mui/lab": "5.0.0-alpha.137",
"@mui/material": "5.14.2",
"clsx": "2.0.0",
"countup.js": "2.7.0",
"prism-react-renderer": "^1.3.5",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-responsive-carousel": "3.2.23"
},
"devDependencies": {
"@cmfcmf/docusaurus-search-local": "1.1.0",
"@docusaurus/module-type-aliases": "2.4.1",
"@tsconfig/docusaurus": "2.0.0",
"typescript": "5.1.6"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"engines": {
"node": ">=18"
}
}

33
website/sidebars.js Normal file
View File

@@ -0,0 +1,33 @@
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
*/
};
module.exports = sidebars;

View File

@@ -0,0 +1,42 @@
export { submitSampleSet, getSampleSet, getSampleSetResult, getSampleDetail };
const BASE_API = "/api/poc/";
function submitSampleSet(data: {
pocs: Array<{ content: string; tag: string }>;
record_it: boolean;
}) {
return fetch(BASE_API + "list", {
method: "POST",
headers: { "Content-Type": "application/json" },
mode: "cors",
// credentials: "include",
body: JSON.stringify(data),
}).then((res) => res.json());
}
function getSampleSet(id: string) {
return fetch(BASE_API + "list?id=" + id).then((res) => res.json());
}
function getSampleDetail(id: string) {
return fetch(BASE_API + "detail?id=" + id).then((res) => res.json());
}
async function getSampleSetResult(id: string, timeout: number = 60) {
const startAt = new Date().getTime();
const isTimeout = () => new Date().getTime() - startAt > timeout * 1000;
const maxRetry = 20;
for (let i = 0; i < maxRetry; i++) {
const res = await fetch(BASE_API + "results?id=" + id).then((res) =>
res.json()
);
if (res.code == 0 && res.data.data) {
return { data: res.data.data, timeout: false };
}
if (isTimeout()) break;
await new Promise((r) => setTimeout(r, 2000));
}
return { data: [], timeout: true };
}

5
website/src/api/home.ts Normal file
View File

@@ -0,0 +1,5 @@
export { getSetupCount };
function getSetupCount() {
return fetch("/api/count").then((res) => res.json());
}

View File

@@ -0,0 +1,101 @@
import React from 'react'
import { Grid, Box, Typography } from "@mui/material";
const FEATURE_LIST = [
{
title: "便捷",
content: (
<>
<Typography variant="body1" sx={{ mb: "10px" }}>
0
</Typography>
<Typography variant="body1">
</Typography>
</>
),
},
{
title: "安全",
content: (
<>
<Typography variant="body1" sx={{ mb: "10px" }}>
</Typography>
<Typography variant="body1">
0day
</Typography>
</>
),
},
{
title: "高性能",
content: (
<>
<Typography variant="body1" sx={{ mb: "10px" }}>
线 1
</Typography>
<Typography variant="body1">
2000+
TPS
</Typography>
</>
),
},
{
title: "高可用",
content: (
<>
<Typography variant="body1" sx={{ mb: "10px" }}>
Nginx
</Typography>
<Typography variant="body1">
99.99%
</Typography>
</>
),
},
];
const Features = () => {
return (
<Box
sx={{
borderRadius: "12px",
backgroundColor: "#fff",
color: "#000",
boxShadow: "0 12px 25px -12px rgba(93,99,112, 0.2)",
px: 5,
pb: 5,
}}
>
<Grid container spacing={5}>
{FEATURE_LIST.map((feature) => (
<Grid item xs={12} sm={6} key={feature.title}>
<Typography
variant="h5"
sx={{ fontWeight: 500, fontSize: "18px", display: "flex" }}
>
<img
src="/images/feature.svg"
alt="feature"
width={40}
height={40}
/>
<Box component="span" sx={{ mt: "5px" }}>
{feature.title}
</Box>
</Typography>
<Box
sx={{ color: "rgba(0,0,0,.7)", whiteSpace: "pre-line", mt: 3 }}
>
{feature.content}
</Box>
</Grid>
))}
</Grid>
</Box>
);
};
export default Features;

View File

@@ -0,0 +1,27 @@
import React, { type FC, useEffect } from "react";
import { Box, type SxProps } from "@mui/material";
interface IconProps {
type: string;
sx?: SxProps;
[propName: string]: any;
}
const Icon: FC<IconProps> = ({ type, sx, ...restProps }) => {
useEffect(() => {
require("../../static/fonts/iconfont");
}, []);
return (
// @ts-ignore
<Box
component="svg"
sx={{ width: "1em", height: "1em", fill: "currentColor", ...sx }}
aria-hidden="true"
{...restProps}
>
<use xlinkHref={`#${type}`} />
</Box>
);
};
export default Icon;

View File

@@ -0,0 +1,57 @@
import React, { type FC, useRef, useEffect } from 'react'
import { Alert as MAlert, type AlertColor } from '@mui/material'
interface AlertProps {
duration?: number
onClose?(key: React.Key): void
noticeKey: React.Key
content?: React.ReactNode
severity: AlertColor
}
const Alert: FC<AlertProps> = (props) => {
const { duration, severity, content, noticeKey, onClose } = props
const closeTimer = useRef<number | null>(null)
const startCloseTimer = () => {
if (duration) {
closeTimer.current = window.setTimeout(() => {
close()
}, duration * 1000)
}
}
const clearCloseTimer = () => {
if (closeTimer.current) {
clearTimeout(closeTimer.current)
closeTimer.current = null
}
}
const close = (e?: React.MouseEvent<HTMLAnchorElement>) => {
if (e) {
e.stopPropagation()
}
clearCloseTimer()
if (onClose) {
onClose(noticeKey)
}
}
useEffect(() => {
startCloseTimer()
return () => {
clearCloseTimer()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<MAlert severity={severity} sx={{ mb: '10px' }}>
{content}
</MAlert>
)
}
export default Alert

View File

@@ -0,0 +1,97 @@
import React, { useState, forwardRef, useImperativeHandle } from "react";
import { Snackbar, Box, type AlertColor } from "@mui/material";
import { render, unmount } from "@site/src/components/utils";
import Alert from "./Alert";
export interface Notice {
key?: React.Key;
content?: React.ReactNode;
severity: AlertColor;
onClose?: () => void;
}
interface MessageProps {}
let seed = 0;
const now = Date.now();
function getUuid() {
const id = seed;
seed += 1;
return `ctMessage_${now}_${id}`;
}
// eslint-disable-next-line react/display-name
const Message = forwardRef<any, MessageProps>((props, ref) => {
const [notices, setNotices] = useState<Notice[]>([]);
const add = (notice: Notice) => {
const key = notice.key ?? getUuid();
setNotices((state) => {
state.push({ ...notice, key });
return [...state];
});
};
const remove = (key: React.Key) => {
setNotices((state) => state.filter((s) => s.key !== key));
};
useImperativeHandle(ref, () => ({
add,
remove,
}));
return (
<Snackbar open anchorOrigin={{ vertical: "top", horizontal: "center" }}>
<Box>
{notices.map((item) => {
const alertProps = {
...item,
noticeKey: item.key!,
onClose: (noticeKey: React.Key) => {
remove(noticeKey);
},
};
return <Alert {...alertProps} key={item.key} />;
})}
</Box>
</Snackbar>
);
});
// @ts-ignore
Message.newInstance = (properties: MessageProps, callback) => {
const { ...props } = properties || {};
const div = document?.createElement("div");
document.body.appendChild(div);
let called = false;
function ref(notification: any) {
if (called) {
return;
}
called = true;
callback({
notice(noticeProps: MessageProps) {
notification.add(noticeProps);
},
removeNotice(key: React.Key) {
notification.remove(key);
},
component: notification,
destroy() {
unmount(div);
if (div.parentNode) {
div.parentNode.removeChild(div);
}
},
});
}
setTimeout(() => {
render(<Message {...props} ref={ref} />, div);
});
};
export default Message;

View File

@@ -0,0 +1,37 @@
import type React from "react";
import { type AlertColor } from "@mui/material";
import Notification from "./Message";
type MessageStaticFunctions = Record<
AlertColor,
(context: React.ReactNode, duration?: number) => void
>;
const Message = {} as MessageStaticFunctions;
let notification: any = null;
if (typeof window !== "undefined") {
// @ts-ignore
Notification.newInstance({}, (n: any) => {
notification = n;
});
const commonOpen =
(type: AlertColor) =>
(content: React.ReactNode, duration: number = 3) => {
notification.notice({
duration,
severity: type,
content,
});
};
(["success", "warning", "info", "error"] as const).forEach((type) => {
Message[type] = commonOpen(type);
});
}
export default Message;

View File

@@ -0,0 +1,40 @@
import React, { type FC } from "react";
import ErrorIcon from "@mui/icons-material/Error";
import { Box } from "@mui/material";
import Modal, { type ModalProps } from "./Modal";
export interface ConfirmDialogProps extends ModalProps {
content?: React.ReactNode;
width?: string;
}
const ConfirmDialog: FC<ConfirmDialogProps> = (props) => {
const { title = "提示", content, width = "480px", ...rest } = props;
return (
<Modal
title={
<Box
sx={{
display: "flex",
alignItems: "center",
lineHeight: "22px",
color: "text.main",
fontWeight: 500,
}}
>
<ErrorIcon sx={{ color: "#FFBF00", mr: "16px", fontSize: "24px" }} />
{title}
</Box>
}
closable={false}
{...rest}
sx={{ width }}
>
<Box sx={{ color: "text.main", pl: "40px" }}>{content}</Box>
</Modal>
);
};
export default ConfirmDialog;

View File

@@ -0,0 +1,130 @@
import React, { type FC, useState } from 'react'
import CloseIcon from '@mui/icons-material/Close'
import { LoadingButton } from '@mui/lab'
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton } from '@mui/material'
export interface ModalProps {
open?: boolean
title?: React.ReactNode
children?: React.ReactNode
footer?: false | React.ReactNode
okText?: React.ReactNode
cancelText?: React.ReactNode
showCancel?: boolean
okColor?: 'primary' | 'error'
cancelColor?: 'primary' | 'error'
closable?: boolean
onOk?(): void
onClose?(): void
onCancel?(): void
sx?: any
}
const Modal: FC<ModalProps> = (props) => {
const {
open = false,
title,
children,
footer,
okText = '确认',
okColor = 'primary',
cancelColor = 'primary',
showCancel = true,
cancelText = '取消',
onOk,
onClose,
onCancel,
closable = true,
sx = {},
} = props
const [loading, setLoading] = useState(false)
const onConfirm = async () => {
setLoading(true)
try {
await onOk?.()
} catch (error) {}
setLoading(false)
}
return (
<Dialog
open={open}
onClose={onClose || onCancel}
PaperProps={{
elevation: 0,
}}
sx={{
'.MuiDialog-paper': {
borderRadius: '12px',
...sx,
},
}}
>
{(title || closable) && (
<DialogTitle
sx={{
fontWeight: 600,
fontSize: '16px',
p: '32px',
pb: '16px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{title}
{closable && (
<IconButton
sx={{
color: 'text.auxiliary',
}}
onClick={onClose || onCancel}
>
<CloseIcon sx={{ fontSize: '20px' }} />
</IconButton>
)}
</DialogTitle>
)}
<DialogContent sx={{ px: '32px' }}>{children}</DialogContent>
{footer === false && null}
{footer === undefined && (
<DialogActions
sx={{
p: '0 32px 32px',
'.MuiButtonBase-root': {
px: '30px',
},
}}
>
{showCancel && (
<Button color={cancelColor} onClick={onCancel}>
{cancelText}
</Button>
)}
<LoadingButton loading={loading} variant='contained' color={okColor} onClick={onConfirm}>
{okText}
</LoadingButton>
</DialogActions>
)}
{footer && (
<DialogActions
sx={{
p: '4px 24px 24px',
'.MuiButtonBase-root': {
p: '8px 30px',
},
}}
>
{footer}
</DialogActions>
)}
</Dialog>
)
}
export default Modal

View File

@@ -0,0 +1,32 @@
import { render as reactRender } from "@site/src/components/utils";
import ConfirmDialog, { type ConfirmDialogProps } from "./ConfirmDialog";
export default function confirm(config: ConfirmDialogProps) {
const container = document.createDocumentFragment();
const { onCancel: propCancel, onOk: propOk } = config;
const onCancel = async () => {
await propCancel?.();
close();
};
const onOk = async () => {
await propOk?.();
close();
};
let currentConfig = { ...config, open: true, onCancel, onOk } as any;
function render(props: ConfirmDialogProps) {
setTimeout(() => {
reactRender(<ConfirmDialog {...props} />, container);
});
}
function close() {
currentConfig = {
...currentConfig,
open: false,
};
render(currentConfig);
}
render(currentConfig);
}

View File

@@ -0,0 +1,12 @@
import { type ConfirmDialogProps } from './ConfirmDialog'
import OriginModal from './Modal'
import confirm from './confrim'
type ModalStaticFunctions = Record<'confirm', (config: ConfirmDialogProps) => void>
type ModalType = typeof OriginModal
const Modal = OriginModal as ModalType & ModalStaticFunctions
Modal.confirm = confirm
export default Modal

View File

@@ -0,0 +1,332 @@
import React, { useEffect, useMemo } from "react";
import { zhCN } from "@mui/material/locale";
import { createTheme } from "@mui/material/styles";
import { ThemeProvider as MUIThemeProvider } from "@mui/material";
interface Props {
children?: React.ReactNode;
}
export default ThemeProvider;
function ThemeProvider({ children }: Props) {
const theme = useMemo(() => {
return themes();
}, []);
useEffect(() => {
const bodyStyle = document.body.style;
// @ts-ignore
bodyStyle.backgroundColor = theme.palette.background.paper0;
bodyStyle.color = theme.palette.text.primary;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [theme]);
return <MUIThemeProvider theme={theme}>{children}</MUIThemeProvider>;
}
function themes() {
// const color: Color = colors[mode] as Color;
const themeOptions = {
palette: themePalette(light),
shadows: shadows(light),
breakpoints: {
values: {
xs: 0,
sm: 680,
md: 900,
lg: 1200,
xl: 1536,
},
},
mixins: {
toolbar: {
minHeight: "48px",
padding: "16px",
"@media (min-width: 600px)": {
minHeight: "48px",
},
},
},
typography: {
fontFamily: "inherit",
body1: {
fontSize: "14px",
},
},
};
const themes = createTheme(themeOptions as any, zhCN);
themes.components = componentStyleOverrides(light);
return themes;
}
function themePalette(color: Color) {
return {
mode: "light",
common: { black: "#000", white: "#fff" },
primary: color.primary,
secondary: color.secondary,
info: color.info,
success: color.success,
warning: color.warning,
error: color.error,
neutral: color.neutral,
divider: color.divider,
text: color.text,
background: color.background,
shadowColor: color.shadowColor,
charts: color.charts,
action: {
selectedOpacity: 0.1,
},
};
}
function shadows(color: Color) {
return [
`0px 12px 24px -4px ${color.shadowColor},0px 0px 2px 0px ${color.shadowColor}`,
`0px 12px 24px -4px ${color.shadowColor},0px 0px 2px 0px ${color.shadowColor}`,
`0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.12),0px 1px 5px 0px rgba(0,0,0,0.12)`,
`0px 3px 3px -2px rgba(0,0,0,0.2),0px 3px 4px 0px rgba(0,0,0,0.12),0px 1px 8px 0px rgba(0,0,0,0.12)`,
`0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.12),0px 1px 10px 0px rgba(0,0,0,0.12)`,
`0px 3px 5px -1px rgba(0,0,0,0.2),0px 5px 8px 0px rgba(0,0,0,0.12),0px 1px 14px 0px rgba(0,0,0,0.12)`,
`0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.12),0px 1px 18px 0px rgba(0,0,0,0.12)`,
`0px 4px 5px -2px rgba(0,0,0,0.2),0px 7px 10px 1px rgba(0,0,0,0.12),0px 2px 16px 1px rgba(0,0,0,0.12)`,
`0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.12),0px 3px 14px 2px rgba(0,0,0,0.12)`,
`0px 5px 6px -3px rgba(0,0,0,0.2),0px 9px 12px 1px rgba(0,0,0,0.12),0px 3px 16px 2px rgba(0,0,0,0.12)`,
`0px 6px 6px -3px rgba(0,0,0,0.2),0px 10px 14px 1px rgba(0,0,0,0.12),0px 4px 18px 3px rgba(0,0,0,0.12)`,
`0px 6px 7px -4px rgba(0,0,0,0.2),0px 11px 15px 1px rgba(0,0,0,0.12),0px 4px 20px 3px rgba(0,0,0,0.12)`,
`0px 7px 8px -4px rgba(0,0,0,0.2),0px 12px 17px 2px rgba(0,0,0,0.12),0px 5px 22px 4px rgba(0,0,0,0.12)`,
`0px 7px 8px -4px rgba(0,0,0,0.2),0px 13px 19px 2px rgba(0,0,0,0.12),0px 5px 24px 4px rgba(0,0,0,0.12)`,
`0px 7px 9px -4px rgba(0,0,0,0.2),0px 14px 21px 2px rgba(0,0,0,0.12),0px 5px 26px 4px rgba(0,0,0,0.12)`,
`0px 8px 9px -5px rgba(0,0,0,0.2),0px 15px 22px 2px rgba(0,0,0,0.12),0px 6px 28px 5px rgba(0,0,0,0.12)`,
`0px 8px 10px -5px rgba(0,0,0,0.2),0px 16px 24px 2px rgba(0,0,0,0.12),0px 6px 30px 5px rgba(0,0,0,0.12)`,
`0px 8px 11px -5px rgba(0,0,0,0.2),0px 17px 26px 2px rgba(0,0,0,0.12),0px 6px 32px 5px rgba(0,0,0,0.12)`,
`0px 9px 11px -5px rgba(0,0,0,0.2),0px 18px 28px 2px rgba(0,0,0,0.12),0px 7px 34px 6px rgba(0,0,0,0.12)`,
`0px 9px 12px -6px rgba(0,0,0,0.2),0px 19px 29px 2px rgba(0,0,0,0.12),0px 7px 36px 6px rgba(0,0,0,0.12)`,
`0px 10px 13px -6px rgba(0,0,0,0.2),0px 20px 31px 3px rgba(0,0,0,0.12),0px 8px 38px 7px rgba(0,0,0,0.12)`,
`0px 10px 13px -6px rgba(0,0,0,0.2),0px 21px 33px 3px rgba(0,0,0,0.12),0px 8px 40px 7px rgba(0,0,0,0.12)`,
`0px 10px 14px -6px rgba(0,0,0,0.2),0px 22px 35px 3px rgba(0,0,0,0.12),0px 8px 42px 7px rgba(0,0,0,0.12)`,
`0px 11px 14px -7px rgba(0,0,0,0.2),0px 23px 36px 3px rgba(0,0,0,0.12),0px 9px 44px 8px rgba(0,0,0,0.12)`,
`0px 11px 15px -7px rgba(0,0,0,0.2),0px 24px 38px 3px rgba(0,0,0,0.12),0px 9px 46px 8px rgba(0,0,0,0.12)`,
];
}
const light = {
primary: {
main: "#0fc6c2",
contrastText: "#fff",
},
secondary: {
lighter: "#D6E4FF",
light: "#84A9FF",
main: "#3366FF",
dark: "#1939B7",
darker: "#091A7A",
contrastText: "#fff",
},
info: {
lighter: "#D0F2FF",
light: "#74CAFF",
main: "#1890FF",
dark: "#0C53B7",
darker: "#04297A",
contrastText: "#fff",
},
success: {
lighter: "#E9FCD4",
light: "#AAF27F",
main: "#02BFA5",
mainShadow: "#02BFA5",
dark: "#229A16",
darker: "#08660D",
contrastText: "rgba(0,0,0,0.7)",
},
warning: {
lighter: "#FFF7CD",
light: "#FFE16A",
main: "#FFBF00",
dark: "#B78103",
darker: "#7A4F01",
contrastText: "rgba(0,0,0,0.7)",
},
neutral: {
main: "#F4F6F8",
contrastText: "rgba(0, 0, 0, 0.60)",
},
error: {
lighter: "#FFE7D9",
light: "#FFA48D",
main: "#FF1844",
dark: "#B72136",
darker: "#7A0C2E",
contrastText: "#fff",
},
divider: "#E3E8EF",
text: {
primary: "#000",
secondary: "rgba(0,0,0,0.7)",
auxiliary: "rgba(0,0,0,0.5)",
slave: "rgba(0,0,0,0.05)",
disabled: "rgba(0,0,0,0.15)",
inversePrimary: "#fff",
inverseAuxiliary: "rgba(255,255,255,0.5)",
inverseDisabled: "rgba(255,255,255,0.15)",
},
background: {
paper0: "#fff",
paper: "#fff",
paper2: "#f6f8fa",
default: "#fff",
chip: "#F4F6F8",
circle: "#E6E8EC",
},
shadowColor: "rgba(145,158,171,0.2)",
table: {
head: {
backgroundColor: "#F4F6F8",
color: "rgba(0,0,0,0.7)",
},
row: {
hoverColor: "#F9FAFB",
},
cell: {
borderColor: "#F3F4F5",
},
},
charts: {
color: ["#673AB7", "#02BFA5"],
},
};
type Color = typeof light;
function componentStyleOverrides(color: Color) {
return {
MuiButton: {
styleOverrides: {
root: ({ ownerState, theme }: any) => {
return {
boxShadow: "none",
"&:hover": {
boxShadow: "none",
...(ownerState.color === "neutral" && {
color: color.primary.main,
fontWeight: 700,
}),
},
};
},
},
},
MuiFormControl: {
styleOverrides: {
root: {
".MuiFormLabel-asterisk": {
color: color.error.main,
},
},
},
},
MuiTableRow: {
styleOverrides: {
root: {
"&:hover": {
".MuiTableCell-root": {
// backgroundColor: color.table.row.hoverColor,
},
},
},
},
},
MuiTableBody: {
styleOverrides: {
root: {
".MuiTableRow-root:hover": {
".MuiTableCell-root": {
backgroundColor: color.table.row.hoverColor,
},
},
},
},
},
MuiTableCell: {
styleOverrides: {
root: {
background: color.background.paper,
lineHeight: 1.5,
fontSize: "14px",
paddingTop: "24px",
paddingBottom: "24px",
borderColor: color.table.cell.borderColor,
paddingLeft: 0,
"&:first-of-type": {
paddingLeft: "32px",
},
},
head: {
backgroundColor: color.table.head.backgroundColor,
color: color.table.head.color,
fontSize: "12px",
height: "24px",
paddingTop: 0,
paddingBottom: 0,
},
},
},
MuiMenu: {
defaultProps: {
elevation: 0,
},
},
MuiPaper: {
defaultProps: {
elevation: 1,
},
styleOverrides: {
root: ({ ownerState }: any) => {
return {
...(ownerState.elevation === 0 && {
backgroundColor: color.background.paper0,
}),
...(ownerState.elevation === 2 && {
backgroundColor: color.background.paper2,
}),
backgroundImage: "none",
};
},
},
},
MuiChip: {
styleOverrides: {
root: {
borderRadius: "4px",
},
},
},
MuiAppBar: {
defaultProps: {
elevation: 1,
},
},
MuiOutlinedInput: {
styleOverrides: {
root: {
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: color.primary.main,
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: color.divider,
},
},
},
},
};
}

View File

@@ -0,0 +1,26 @@
import React from "react";
import { Typography, SxProps } from "@mui/material";
interface TitleProps {
title: string;
sx?: SxProps;
}
const Title: React.FC<TitleProps> = ({ title, sx }) => {
return (
<Typography
variant="h4"
sx={{ fontSize: "32px", fontWeight: 500, position: "relative", ...sx }}
>
<img
src="/images/class.png"
width={56}
height={56}
style={{ position: "absolute", top: -20, left: -24 }}
/>
{title}
</Typography>
);
};
export default Title;

View File

@@ -0,0 +1,101 @@
import React from 'react'
import { useState, useEffect } from "react";
import Message from "@site/src/components/Message";
import Modal from "@site/src/components/Modal";
import { Box, TextField, Typography, Button } from "@mui/material";
function Consultation() {
const [text, setText] = useState("");
const [wrongPhoneNumber, setWrongPhoneNumber] = useState(false);
const [consultOpen, setConsultOpen] = useState(false);
const consultHandler = () => {
const valid = /^1[3-9]\d{9}$/.test(text);
setWrongPhoneNumber(!valid);
if (!valid) {
Message.error("手机号格式不正确");
return;
}
fetch("https://leads.chaitin.net/api/trial", {
// fetch('http://116.62.230.26:8999/api/trial', { // 测试用地址
method: "POST",
mode: "cors",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
phone: text,
platform_source: "product-official-site",
product_source: "safeline-ce",
source_detail: "来自雷池社区版官网",
}),
})
.then((d) => d.json())
.then((d) => {
if (d.code == 0) {
Message.success("提交成功");
} else {
Message.error("提交失败");
}
setConsultOpen(false);
});
};
const textHandler = (v: string) => {
setText(v);
};
useEffect(() => {
if (!consultOpen) {
setText("");
}
}, [consultOpen]);
return (
<>
<Button
variant="outlined"
sx={{
width: 200,
my: 2,
"&:hover": {
borderColor: "primary.main",
backgroundColor: "primary.main",
color: "#fff",
},
}}
onClick={() => setConsultOpen(true)}
>
</Button>
<Modal
open={consultOpen}
onCancel={() => setConsultOpen(false)}
title="咨询企业版"
okText="提交"
onOk={consultHandler}
sx={{ width: 600 }}
>
<Box sx={{}}>
<div style={{ margin: "10px 0 15px" }}>
<TextField
error={wrongPhoneNumber}
fullWidth
size="small"
label="手机号"
helperText={
wrongPhoneNumber ? "手机号格式不正确" : "请输入您的手机号"
}
variant="outlined"
value={text}
onChange={(e) => textHandler(e.target.value)}
/>
</div>
<Typography>
2
</Typography>
</Box>
</Modal>
</>
);
}
export default Consultation;

View File

@@ -0,0 +1,312 @@
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Box,
Collapse,
ListItem,
ListItemIcon,
ListItemText,
alpha,
Tooltip,
} from "@mui/material";
import React, { useState } from "react";
import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore";
import Icon from "@site/src/components/Icon";
const Support = () => {
return (
<Icon type="icon-duihao" color="success.main" sx={{ margin: "auto" }} />
);
};
const NotSupport = () => {
return <Icon type="icon-chahao" color="error.main" sx={{ margin: "auto" }} />;
};
const Unlimited = () => {
return <Box color="success.main"></Box>;
};
const FunctionTable = () => {
const [open, setOpen] = useState<string[]>([
"基本功能",
"高级防护能力",
"部署形态",
"流量接入方式",
]);
const cells = [
{
title: "基本功能",
data: [
{
name: "可视化 DashBoard",
experience: <Support />,
basics: <Support />,
},
{
name: "自定义黑白名单",
experience: <Support />,
basics: <Support />,
},
{
name: "自定义防护策略",
experience: <Support />,
basics: <Support />,
},
{
name: "可防护站点数量",
experience: <Unlimited />,
basics: <Unlimited />,
},
{
name: "可支撑流量大小",
experience: <Unlimited />,
basics: <Unlimited />,
},
],
},
{
title: "高级防护能力",
data: [
{
name: "智能语义分析引擎",
tip: "",
experience: <Support />,
basics: <Support />,
},
{
name: "智能业务建模",
experience: <NotSupport />,
basics: <Support />,
},
{
name: "动态防护",
experience: <NotSupport />,
basics: <Support />,
},
{
name: "API 防护",
experience: <NotSupport />,
basics: <Support />,
},
{
name: "Bot 管理",
experience: <NotSupport />,
basics: <Support />,
},
{
name: "拟态防护",
experience: <NotSupport />,
basics: <Support />,
},
],
},
{
title: "部署形态",
data: [
{
name: "软件形态",
experience: <Support />,
basics: <Support />,
},
{
name: "硬件形态",
experience: <NotSupport />,
basics: <Support />,
},
{
name: "分布式集群形态",
experience: <NotSupport />,
basics: <Support />,
},
{
name: "云原生 K8S 集群形态",
experience: <NotSupport />,
basics: <Support />,
},
],
},
{
title: "流量接入方式",
data: [
{
name: "反向代理接入",
experience: <Support />,
basics: <Support />,
},
{
name: "SDK 接入",
experience: <NotSupport />,
basics: <Support />,
},
{
name: "透明代理接入",
experience: <NotSupport />,
basics: <Support />,
},
{
name: "透明桥接接入",
experience: <NotSupport />,
basics: <Support />,
},
{
name: "旁路镜像接入",
experience: <NotSupport />,
basics: <Support />,
},
],
},
];
const handleClick = (id: string) => {
if (open?.includes(id)) {
const udpateOpen = [...open];
udpateOpen.splice(open?.indexOf(id), 1);
setOpen([...udpateOpen]);
} else {
setOpen((open) => [...open, id]);
}
};
return (
<>
<TableContainer>
<Table
sx={{
".MuiTableCell-root": {
border: "none",
backgroundColor: "transparent",
px: "12px",
},
}}
style={{ marginTop: "48px" }}
>
<TableHead>
<TableRow>
<TableCell sx={{ width: "33%" }} />
<TableCell align="center" sx={{ width: "33%", fontSize: "16px" }}>
<Box
sx={(theme) => ({
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
height: "40px",
borderRadius: "4px",
color: theme.palette.primary.main,
backgroundColor: alpha(theme.palette.primary.main, 0.2),
})}
>
</Box>
</TableCell>
<TableCell align="left" sx={{ width: "33%", fontSize: "16px" }}>
<Box
sx={(theme) => ({
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
color: theme.palette.primary.main,
backgroundColor: alpha(theme.palette.primary.main, 0.1),
height: "40px",
borderRadius: "4px",
})}
>
</Box>
</TableCell>
</TableRow>
</TableHead>
</Table>
</TableContainer>
{cells?.map((data) => (
<React.Fragment key={`sub-table-${data.title}`}>
<ListItem
onClick={() => handleClick(data?.title)}
sx={{
backgroundColor: "#EFF1F8",
borderRadius: "4px",
cursor: "pointer",
pl: "20px",
}}
>
<ListItemText
primary={data.title}
sx={{ ".MuiTypography-root": { fontSize: "16px" } }}
/>
<ListItemIcon sx={{ color: "#000" }}>
{open?.includes(data?.title) ? <ExpandLess /> : <ExpandMore />}
</ListItemIcon>
</ListItem>
<Collapse
in={open?.includes(data?.title)}
timeout="auto"
unmountOnExit
sx={{ width: "100%" }}
>
<TableContainer>
<Table
sx={{
".MuiTableRow-root": {
"&:last-of-type": {
".MuiTableCell-root": {
borderBottom: "none",
},
},
},
".MuiTableCell-root": {
pl: "20px !important",
pr: "8px !important",
py: "12px !important",
backgroundColor: "transparent !important",
color: "#000",
borderRight: "1px solid",
borderColor: "rgba(0,0,0,.04)",
"&:last-of-type": {
borderRight: "none",
},
},
}}
>
<TableBody
sx={{
backgroundColor: alpha("#EFF1F8", 0.6),
}}
>
{data.data.map((item) => (
<TableRow key={item.name}>
<TableCell sx={{ width: "33%" }}>
<Box sx={{ display: "flex", alignItems: "center" }}>
{item.name}
<Tooltip title={item.tip} arrow placement="right">
<Box component="span" sx={{ ml: 1 }}>
<Icon type="icon-bangzhu1" />
</Box>
</Tooltip>
</Box>
</TableCell>
<TableCell sx={{ width: "33%" }} align="center">
{item.experience}
</TableCell>
<TableCell sx={{ width: "33%" }} align="center">
{item.basics}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Collapse>
</React.Fragment>
))}
</>
);
};
export default FunctionTable;

View File

@@ -0,0 +1,156 @@
import React from "react";
import { Typography, Box, Button, alpha } from "@mui/material";
import Title from "@site/src/components/Title";
import FunctionTable from "./FunctionTable";
import Consultation from "./Consultation";
const FREE_FUNCTION = [
"智能语义分析检测",
"反向代理接入",
"自定义黑白名单",
"精细化引擎调节",
"可视化安全分析",
];
const ENTERPRISE_FUNCTION = [
"智能语义分析检测",
"串行、旁路均可接入",
"集群式可扩展部署",
"CC 攻击防护",
"业务 API 智能建模",
"Bot 管理,恶意 Bot 防护",
"专业技术支持服务",
"漏洞应急服务",
];
const Version = () => {
return (
<>
<Box
sx={{
display: "flex",
justifyContent: "center",
width: "100%",
flexWrap: "wrap",
}}
style={{ marginTop: "48px" }}
>
<Box
sx={{
width: { xs: "100%", sm: 306 },
flexShrink: 0,
height: { xs: "auto", sm: 440 },
px: 3,
py: 2,
mb: { xs: 2, sm: 0 },
mr: { xs: 0, sm: "20px" },
border: "1px solid #eee",
borderRadius: "12px",
display: "flex",
flexDirection: "column",
alignItems: "center",
backgroundImage: "linear-gradient(to top,#C3CFE2,#EFF1F8)",
"&:hover": {
boxShadow:
"0 36px 70px -10px rgba(61,64,76,.15), 0 18px 20px -10px rgba(61,64,76,.05)",
},
// color: "#fff",
}}
>
<Typography variant="h5" sx={{ fontWeight: 500, fontSize: "18px" }}>
</Typography>
<Button
variant="contained"
// component={}
target="_blank"
sx={{
width: 200,
my: 2,
}}
href="https://stack.chaitin.com/tool/detail?id=717"
>
使
</Button>
<Box>
{FREE_FUNCTION.map((f) => (
<Box
key={f}
sx={{
py: 1,
position: "relative",
pl: 2,
"&:before": {
content: "' '",
position: "absolute",
left: 0,
top: "16px",
width: 6,
height: 6,
borderRadius: "50%",
backgroundColor: "success.main",
},
}}
>
{f}
</Box>
))}
</Box>
</Box>
<Box
sx={(theme) => ({
width: { xs: "100%", sm: 306 },
flexShrink: 0,
height: { xs: "auto", sm: 440 },
px: 3,
py: 2,
border: "1px solid",
borderColor: alpha(theme.palette.primary.main, 0.5),
borderRadius: "12px",
display: "flex",
flexDirection: "column",
alignItems: "center",
"&:hover": {
boxShadow:
"0 36px 70px -10px rgba(61,64,76,.15), 0 18px 20px -10px rgba(61,64,76,.05)",
},
})}
>
<Typography variant="h5" sx={{ fontWeight: 500, fontSize: "18px" }}>
</Typography>
<Consultation />
<Box>
{ENTERPRISE_FUNCTION.map((f) => (
<Box
key={f}
sx={{
py: 1,
position: "relative",
pl: 2,
"&:before": {
content: "' '",
position: "absolute",
left: 0,
top: "16px",
width: 6,
height: 6,
borderRadius: "50%",
backgroundColor: "success.main",
},
}}
>
{f}
</Box>
))}
</Box>
</Box>
</Box>
<Title title="详细参数" sx={{ mt: "120px !important" }} />
<FunctionTable />
</>
);
};
export default Version;

View File

@@ -0,0 +1,67 @@
import React from "react";
import TableContainer from "@mui/material/TableContainer";
import Box from "@mui/material/Box";
import Table from "@mui/material/Table";
import TableRow from "@mui/material/TableRow";
import TableHead from "@mui/material/TableHead";
import TableCell from "@mui/material/TableCell";
import TableBody from "@mui/material/TableBody";
import Paper from "@mui/material/Paper";
import Title from "@site/src/components/Title";
import type { ResultRowsType } from "./types";
import { Typography } from "@mui/material";
export default Result;
function Result({ rows }: { rows: ResultRowsType }) {
return (
<Box sx={{ mt: 4 }}>
<Title title="测试结果" sx={{ fontSize: "16px", marginBottom: "16px" }} />
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }}>
<TableHead>
<TableRow>
<TableCell>WAF </TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, index) => (
<TableRow key={index}>
<TableCell>{appendLink(row.engine)}</TableCell>
<TableCell>{row.version}</TableCell>
<TableCell>{row.detectionRate}</TableCell>
<TableCell>{row.failedRate}</TableCell>
<TableCell>{row.accuracy}</TableCell>
<TableCell>{row.cost}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
);
}
function appendLink(engine: string) {
if (engine == "ModSecurity")
return (
<a href="https://github.com/SpiderLabs/ModSecurity" target="_blank">
<Typography sx={{ color: "primary.main" }}>{engine}</Typography>
</a>
);
if (engine == "TADK")
return (
<a
target="_blank"
href="https://networkbuilders.intel.com/university/course/traffic-analytics-development-kit-tadk"
>
<Typography sx={{ color: "primary.main" }}>{engine}</Typography>
</a>
);
return engine;
}

View File

@@ -0,0 +1,25 @@
import React from 'react'
import Typography from "@mui/material/Typography";
function SampleCount({
total,
normal,
attack,
}: {
total: number;
normal: number;
attack: number;
}) {
return (
<div style={{ display: "flex" }}>
<Typography> {total} HTTP </Typography>
<Typography sx={{ color: "success.main" }}>
{normal}
</Typography>
<Typography sx={{ color: "error.main" }}> {attack} </Typography>
</div>
);
}
export default SampleCount;

View File

@@ -0,0 +1,155 @@
import React from 'react'
import { useState } from "react";
import {
Box,
Button,
Table,
TableHead,
TableRow,
TableBody,
Typography,
Dialog,
DialogActions,
DialogContent,
} from "@mui/material";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import TableCell from "@mui/material/TableCell";
import Title from "@site/src/components/Title";
import SampleCount from "./SampleCount";
import SamplesForm from "./SamplesForm";
import Message from "@site/src/components/Message";
import { getSampleDetail } from "@site/src/api/detection";
import { sizeLength } from "@site/src/components/utils";
import type { RecordSamplesType } from "./types";
export default SampleList;
interface SampleListProps {
value: RecordSamplesType;
onSetIdChange: (id: string) => void;
}
function SampleList({ value, onSetIdChange }: SampleListProps) {
const [open, setOpen] = useState(false);
const [detail, setDetail] = useState("");
const handleDetail = (id: string) => async () => {
const res = await getSampleDetail(id);
if (res.code != 0) {
Message.error(res.msg || "获取详情失败");
return;
}
const text = document.createElement("textarea");
text.innerHTML = res.data.content;
const highlighted = hljs.highlight(text.value, {
language: "http",
});
setDetail(highlighted.value);
setOpen(true);
};
const handleClose = () => {
setDetail("");
setOpen(false);
};
return (
<>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
marginBottom: "18px",
alignItems: "center",
}}
>
<Title title="测试样本" sx={{ fontSize: "16px" }} />
<SamplesForm onSetIdChange={onSetIdChange} />
</Box>
<Accordion
sx={{ borderRadius: "4px" }}
className="detection-samples-accordion"
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<SampleCount
total={value.length}
normal={value.filter((i) => !i.isAttack).length}
attack={value.filter((i) => i.isAttack).length}
/>
</AccordionSummary>
<AccordionDetails>
<Table>
<TableHead>
<TableRow>
<TableCell width={150}></TableCell>
<TableCell width={150}></TableCell>
<TableCell></TableCell>
<TableCell width={100}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{value.map((row, index) => {
const text = document.createElement("textarea");
text.innerHTML = row.summary;
return (
<TableRow
key={index}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell>
{row.isAttack ? (
<Typography sx={{ color: "error.main" }}>
</Typography>
) : (
<Typography sx={{ color: "success.main" }}>
</Typography>
)}
</TableCell>
<TableCell>{sizeLength(row.size)}</TableCell>
<TableCell>
<Typography
noWrap
sx={{
width: "600px",
fontFamily:
"ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace",
}}
>
{text.value}
</Typography>
</TableCell>
<TableCell>
<Button onClick={handleDetail(row.id)}></Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</AccordionDetails>
</Accordion>
<Dialog open={open} onClose={handleClose}>
<DialogContent sx={{ marginBottom: 0 }}>
<Box
component="code"
style={{ whiteSpace: "pre-line", wordBreak: "break-all" }}
dangerouslySetInnerHTML={{ __html: detail }}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}></Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,281 @@
import React, { useEffect, useState } from "react";
import {
Box,
Button,
RadioGroup,
StepLabel,
TextField,
FormControlLabel,
Radio,
Typography,
} from "@mui/material";
import {
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
} from "@mui/material";
import Checkbox from "@mui/material/Checkbox";
import Stepper from "@mui/material/Stepper";
import Step from "@mui/material/Step";
import SampleCount from "./SampleCount";
import {
Table,
TableHead,
TableRow,
TableCell,
TableBody,
} from "@mui/material";
import { sampleLength, sampleSummary } from "@site/src/components/utils";
export default SampleSteps;
interface SampleStepsProps {
onDetect: (value: {
sample: string;
publish: boolean;
isAttack: boolean;
}) => void;
}
function SampleSteps({ onDetect }: SampleStepsProps) {
const [activeStep, setActiveStep] = useState(0);
const [completed, setCompleted] = useState([false, false, false]);
const [sampleText, setSampleText] = useState("");
const [sampleIsAttack, setSampleIsAttack] = useState(false);
const [sampleTextError, setSampleTextError] = useState("");
const [checked, setChecked] = useState(true);
const [count, setCount] = useState({ total: 0, normal: 0, attack: 0 });
const nextStep = () => {
setActiveStep(activeStep + 1);
completed[activeStep] = true;
setCompleted([...completed]);
};
const handleCommit = () => {
if (sampleText == "") {
setSampleTextError("样本内容不能为空");
return;
}
nextStep();
};
const handleMark = (v: Array<{ isAttack: boolean }>) => {
const isAttack = v[0].isAttack;
setSampleIsAttack(isAttack);
setCount({ total: 1, normal: isAttack ? 0 : 1, attack: isAttack ? 1 : 0 });
nextStep();
};
const handleDetect = () => {
onDetect({
sample: sampleText,
publish: checked,
isAttack: sampleIsAttack,
});
};
const handleStep = (n: number) => {
if (completed[n]) setActiveStep(n);
};
return (
<Box>
<Stepper nonLinear activeStep={activeStep} sx={{ marginBottom: 2 }}>
<Step completed={completed[0]} onClick={() => handleStep(0)}>
<StepLabel></StepLabel>
</Step>
<Step completed={completed[1]} onClick={() => handleStep(1)}>
<StepLabel></StepLabel>
</Step>
<Step completed={completed[2]} onClick={() => handleStep(2)}>
<StepLabel></StepLabel>
</Step>
</Stepper>
{activeStep == 0 && (
<Box sx={{ p: 1 }}>
<TextField
multiline
fullWidth
minRows={4}
error={sampleTextError != ""}
helperText={sampleTextError}
value={sampleText}
onChange={(e) => setSampleText(e.target.value)}
placeholder={`GET /path/api HTTP/1.1
Host: example.com`}
sx={{ mb: 2, overflow: "auto", maxHeight: "65vh" }}
/>
<Button fullWidth variant="contained" onClick={handleCommit}>
</Button>
</Box>
)}
{activeStep == 1 && (
<SampleMarkable onChange={handleMark} samples={[sampleText]} />
)}
{activeStep == 2 && (
<Box>
<SampleCount
total={count.total}
normal={count.normal}
attack={count.attack}
/>
<FormControlLabel
sx={{ mt: 1 }}
control={
<Checkbox
checked={checked}
onChange={(e: any) => setChecked(e.target.checked)}
/>
}
label="提交到公开样本集"
/>
<Button
sx={{ mt: 2 }}
fullWidth
variant="contained"
onClick={handleDetect}
>
</Button>
</Box>
)}
</Box>
);
}
function SampleMarkable({
samples,
onChange,
}: {
samples: Array<string>;
onChange: (value: Array<{ sample: string; isAttack: boolean }>) => void;
}) {
const [sampleDetail, setSampleDetail] = useState("");
const [code, setCode] = useState("");
const [rows, setRows] = useState(() => {
const rows: Array<{
isAttack: boolean;
summary: string;
size: string;
raw: string;
}> = [];
samples.forEach((i) => {
rows.push({
isAttack: false,
summary: sampleSummary(i),
raw: i,
size: sampleLength(i),
});
});
return rows;
});
useEffect(() => {
const highlighted = hljs.highlight(sampleDetail, { language: "http" });
setCode(highlighted.value);
}, [sampleDetail]);
const handle = () => {
onChange(rows.map((i) => ({ sample: i.raw, isAttack: i.isAttack })));
};
const handleType = (n: number) => (v: string) => {
rows[n].isAttack = v == "attack";
setRows([...rows]);
};
const handleSampleDetail = (text: string) => () => {
setSampleDetail(text);
};
const handleClose = () => {
setSampleDetail("");
};
return (
<Box>
<Table sx={{ mb: 2 }}>
<TableHead>
<TableRow>
<TableCell width={350}></TableCell>
<TableCell width={150}></TableCell>
<TableCell></TableCell>
<TableCell width={100}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, index) => (
<TableRow
key={index}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell>
<RadioGroup
value={row.isAttack ? "attack" : "normal"}
onChange={(e) => handleType(index)(e.target.value)}
sx={{ flexDirection: "row" }}
>
<FormControlLabel
value="attack"
control={<Radio sx={{ color: "divider" }} />}
label="攻击样本"
/>
<FormControlLabel
value="normal"
control={<Radio sx={{ color: "divider" }} />}
label="普通样本"
/>
</RadioGroup>
</TableCell>
<TableCell>{row.size}</TableCell>
<TableCell>
<Typography
sx={{
width: "220px",
fontFamily:
"ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace",
}}
>
{row.summary}
</Typography>
</TableCell>
<TableCell>
<Button onClick={handleSampleDetail(row.raw)}></Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Button onClick={handle} fullWidth variant="contained">
</Button>
<Dialog open={sampleDetail != ""} onClick={handleClose}>
{/* <DialogTitle>样本详情</DialogTitle> */}
<DialogContent>
<DialogContentText>
<Box
component="code"
style={{ whiteSpace: "pre-line", wordBreak: "break-all" }}
dangerouslySetInnerHTML={{ __html: code }}
/>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}></Button>
</DialogActions>
</Dialog>
</Box>
);
}

View File

@@ -0,0 +1,120 @@
import React from "react";
import { useState } from "react";
import Message from "@site/src/components/Message";
import { submitSampleSet, getSampleSetResult } from "@site/src/api/detection";
import CircularProgress from "@mui/material/CircularProgress";
import CloseIcon from "@mui/icons-material/Close";
import IconButton from "@mui/material/IconButton";
import SampleSteps from "./SampleSteps";
import Button from "@mui/material/Button";
import Modal from "@mui/material/Modal";
import Box from "@mui/material/Box";
import Title from "@site/src/components/Title";
export default SamplesForm;
function SamplesForm({
onSetIdChange,
}: {
onSetIdChange: (id: string) => void;
}) {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const testHandler = () => {
setOpen(true);
};
const closeHandler = () => {
setOpen(false);
};
const getSetId = async (content: string, publish: boolean, tag: string) => {
const res = await submitSampleSet({
pocs: [{ content, tag }],
record_it: publish,
});
if (res.code != 0) throw res.msg;
if (res.data.total != 1) throw "样本数量错误";
return res.data.id;
};
const submit = async ({
sample,
publish,
isAttack,
}: {
sample: string;
publish: boolean;
isAttack: boolean;
}) => {
setLoading(true);
try {
const setId = await getSetId(
sample,
publish,
isAttack ? "black" : "white"
);
onSetIdChange(setId);
} catch (e) {
Message.error(("解析失败: " + e) as string);
}
setOpen(false);
setLoading(false);
};
return (
<>
<Button variant="contained" onClick={testHandler}>
</Button>
<Modal open={open} onClose={closeHandler}>
<Box
sx={{
position: "absolute" as "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 750,
bgcolor: "background.paper",
boxShadow: 24,
borderRadius: "6px",
p: 3,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
{loading && (
<Box
sx={{
position: "absolute",
top: "0",
left: "0",
right: "0",
bottom: "0",
display: "flex",
justifyContent: "center",
alignItems: "center",
background: "rgba(0,0,0,0.3)",
}}
>
<CircularProgress />
</Box>
)}
<Title title="测试我的样本" sx={{ fontSize: "18px", mb: 2 }} />
<IconButton onClick={closeHandler}>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
<Box>
<SampleSteps onDetect={submit} />
</Box>
</Box>
</Modal>
</>
);
}

View File

@@ -0,0 +1,107 @@
const BuiltinAttackSamples = [
`
POST /doUpload.action HTTP/1.1
Host: localhost:8080
Content-Length: 1000000000
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXd004BVJN9pBYBL2
------WebKitFormBoundaryXd004BVJN9pBYBL2
Content-Disposition: form-data; name="upload"; filename="ok"
<%eval request("sb")%>
------WebKitFormBoundaryXd004BVJN9pBYBL2--
`,
`GET /fe/Channel/25545911?tabNum=cat%20%2Fetc%2Fhosts HTTP/1.1
Host: monster
User-Agent: Chrome`,
`GET /scripts/%2e%2e/%2e/Windows/System32/cmd.exe?/c+dir+c HTTP/1.1
Host: a.cn
User-Agent: Chrome
Cookie: 2333;`,
`GET /?s=/../../../etc/login\0.img HTTP/1.1
Host: monster
User-Agent: Chrome`,
`POST / HTTP/1.1
Host: monster
User-Agent: Chrome
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4JGjXRl94NnI4Og7
------WebKitFormBoundary4JGjXRl94NnI4Og7
Content-Disposition: form-data; name="file"; filename="hack.asp%00.png"
Content-Type: application/octet-stream
<%@codepage=65000%><%response.codepage=65001:eval(request("key"))%>
------WebKitFormBoundary4JGjXRl94NnI4Og7--"`,
`GET /?s=%ac%ed%00%05%73%72%00%1a%63%6f%6d%2e%63%74%2e%61%72%61%6c%65%69%69%2e%74%65%73%74%2e%50%65%72%73%6f%6e%00%00%00%00%00%00%00%01%02%00%08%49%00%03%61%61%61%43%00%03%63%63%63%42%00%03%64%64%64%5a%00%03%65%65%65%4a%00%03%66%66%66%46%00%03%67%67%67%44%00%03%68%68%68%4c%00%03%62%62%62%74%00%12%4c%6a%61%76%61%2f%6c%61%6e%67%2f%53%74%72%69%6e%67%3b%78%70%00%00%00%01%00%62%65%01%00%00%00%00%00%00%00%01%3f%80%00%00%40%00%00%00%00%00%00%00%74%00%03%61%61%61 HTTP/1.1
Host: monster
User-Agent: Chrome`,
`GET / HTTP/1.1
Host: a.cn
Content-Type: a
Origin: b
User-Agent: c
X-Forwarded-For: d
Cookie: FOOID=1;
A: AAAAAAAAA
Referer: %{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='ifconfig').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}`,
`GET / HTTP/1.1
Host: monster
User-Agent: Chrome
Content-Type: application/x-www-form-urlencoded
Content-Length: 55
<?php echo $a; ?>`,
`GET / HTTP/1.1
Host: monster
User-Agent: Chrome
Content-Type: application/x-www-form-urlencoded
Content-Length: 55
a=O:9:"FileClass":1:{s:8:"filename";s:10:"config.php";}`,
`GET / HTTP/1.1
Host: monster
User-Agent: () { :;}; echo; echo $(/bin/ls -al)
Referer: Chrome
Cookie: 2333;`,
`GET /somepath?q=1'%20or%201=1 HTTP/1.1`,
`GET /hello?content=hello&user=ftp://a.com HTTP/1.1
Host: www.baidu.com
User-Agent: Chrome
Referer: http://www.qq.com/abc.html`,
`GET /?flag=%7B%7Bconfig.__class__.__init__.__globals__%5B'os'%5D.popen('id').read()%7D%7D HTTP/1.1
Host: 1.2.3.4
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:97.0) Gecko/20100101 Firefox/97.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close`,
`POST / HTTP/1.1
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/fruits">
<xsl:value-of select="system-property('xsl:vendor')"/>
</xsl:template>
</xsl:stylesheet>`,
`GET /somepath?q=<script>alert('XSS')</script> HTTP/1.1`,
`POST / HTTP/1.1
<?xml version="1.0"?>
<!DOCTYPE data [
<!ELEMENT data (#ANY)>
<!ENTITY file SYSTEM "file:///etc/passwd">
]>
<data>&file;</data>`,
];
const BuiltinNonAttackSamples = [
`GET / HTTP/1.1
Host: 1.2.3.4`,
`POST / HTTP/1.1
name=haha&submit=submit`,
];
export { BuiltinAttackSamples, BuiltinNonAttackSamples };

View File

@@ -0,0 +1,3 @@
.detection-samples-accordion::before {
display: none;
}

View File

@@ -0,0 +1,23 @@
export type RecordSamplesType = Array<{
id: string;
size: number;
isAttack: boolean;
summary: string;
}>;
export type SampleDetailType = {
id: string;
size: number;
isAttack: boolean;
raw: string;
summary: string;
};
export type ResultRowsType = Array<{
engine: string;
version: string;
detectionRate: number;
failedRate: number;
accuracy: number;
cost: string;
}>;

View File

@@ -0,0 +1,35 @@
import ReactDOM, { type Root } from "react-dom";
const MARK = "__ct_react_root__";
type ContainerType = (Element | DocumentFragment) & {
[MARK]?: Root;
};
export function render(node: React.ReactElement, container: ContainerType) {
const root = container[MARK] || container;
ReactDOM.render(node, root);
container[MARK] = root;
}
export async function unmount(container: ContainerType) {
return Promise.resolve().then(() => {
container[MARK]?.unmount();
delete container[MARK];
});
}
export function sizeLength(l: number) {
return l > 1024 * 2 ? Math.round(l / 1024) + "KB" : l + "B";
}
export function sampleLength(s: string) {
const l = new Blob([s]).size;
return l > 1024 * 2 ? Math.round(l / 1024) + "KB" : l + "B";
}
export function sampleSummary(s: string) {
return s.split("\n").slice(0, 2).join(" ").slice(0, 60);
}

View File

@@ -0,0 +1,36 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
body {
font-size: 14px;
}
a:hover {
text-decoration: none;
}
.navbar.navbar--fixed-top {
background-color: #0f1935;
}
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #0fc6c2;
/* --ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
--ifm-color-primary-light: #33925d;
--ifm-color-primary-lighter: #359962;
--ifm-color-primary-lightest: #3cad6e; */
--ifm-breadcrumb-color-active: #0fc6c2;
--ifm-menu-color-active: #0fc6c2;
--ifm-link-hover-color: #0fc6c2;
--ifm-footer-link-hover-color: #0fc6c2;
--ifm-navbar-link-hover-color: #0fc6c2;
--ifm-navbar-link-color: white;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}

View File

@@ -0,0 +1,118 @@
import React, { useEffect, useState } from "react";
import Container from "@mui/material/Container";
import Result from "@site/src/components/detection/Result";
// import { useRouter } from "next/router";
import { useLocation } from "@docusaurus/router";
import { getSampleSet, getSampleSetResult } from "@site/src/api/detection";
import Message from "@site/src/components/Message";
import type {
RecordSamplesType,
ResultRowsType,
} from "@site/src/components/detection/types";
import Grid from "@mui/material/Grid";
import SampleList from "@site/src/components/detection/SampleList";
import { Typography } from "@mui/material";
import Layout from "@theme/Layout";
import ThemeProvider from "@site/src/components/Theme";
export default Detection;
function Detection() {
// const router = useRouter();
const location = useLocation();
const [samples, setSamples] = useState<RecordSamplesType>([]);
const [result, setResult] = useState<ResultRowsType>([]);
useEffect(() => {
// useRouter 中获取 参数会有延迟,所以先判断有没有 id 参数
const realSetId =
new URLSearchParams(location.search).get("id") || "default";
// const setId = (router.query.id as string) || "default";
const setId = "default";
if (setId !== realSetId) return;
// 查询样本集合
getSampleSet(setId).then((res) => {
if (res.code != 0) {
Message.error("测试集合 " + setId + ": " + res.msg);
return;
}
if (!res.data.data) {
Message.error("测试集合 " + setId + ": 获取结果为空");
return;
}
setSamples(
res.data.data?.map((i: any) => ({
id: i.id,
summary: i.summary,
size: i.length,
isAttack: i.tag == "black",
}))
);
});
// 查询样本集合结果
getSampleSetResult(setId).then(({ data, timeout }) => {
if (timeout) {
Message.error("获取检测集结果超时");
return;
}
setResult(
data.map((i: any) => ({
engine: i.engine,
version: i.version,
detectionRate: percent(i.recall),
failedRate: percent(i.fdr),
accuracy: percent(i.accuracy),
cost: i.elapsed > 0 ? i.elapsed + " 毫秒" : "小于 1 毫秒",
}))
);
});
}, []);
const handleSetId = (id: string) => {
// router.push({
// pathname: router.pathname,
// query: { id },
// });
};
return (
<Layout title="效果对比 - 长亭雷池 WAF 社区版">
<ThemeProvider>
<Container sx={{ mt: 2, mb: 2 }}>
<SampleList value={samples} onSetIdChange={handleSetId} />
<Result rows={result} />
<Grid
container
spacing={2}
sx={{ mt: 3, mb: 3, color: "text.auxiliary" }}
>
<Grid item md={3}>
<Typography>TP: 正确识别到攻击样本的数量</Typography>
<br />
<Typography> = TP / (TP + FN)</Typography>
</Grid>
<Grid item md={3}>
<Typography>TN: 正确识别到普通样本的数量</Typography>
<br />
<Typography> = FP / (TP + FP)</Typography>
</Grid>
<Grid item md={3}>
<Typography>FP: 将普通样本误报为攻击的数量</Typography>
<br />
<Typography> = (TP + TN) / (TP + TN + FP + FN)</Typography>
</Grid>
<Grid item md={3}>
<Typography>FN: 未识别到攻击样本的数量</Typography>
</Grid>
</Grid>
</Container>
</ThemeProvider>
</Layout>
);
}
function percent(v: number) {
return Math.round(v * 10000) / 100 + "%";
}

285
website/src/pages/index.tsx Normal file
View File

@@ -0,0 +1,285 @@
import React, { useEffect, useRef } from "react";
import { Carousel } from "react-responsive-carousel";
import Features from "@site/src/components/Features";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import { Box, Grid, Button, Typography, Container, Stack } from "@mui/material";
import Layout from "@theme/Layout";
import Title from "@site/src/components/Title";
import Version from "@site/src/components/Version";
import { getSetupCount } from "@site/src/api/home";
import "react-responsive-carousel/lib/styles/carousel.min.css";
import ThemeProvider from "@site/src/components/Theme";
import Head from "@docusaurus/Head";
const IMAGE_LIST = [
{
name: "可视化仪表盘",
url: "/images/album/0.png",
},
{
name: "登录页",
url: "/images/album/5.png",
},
{
name: "攻击检测列表",
url: "/images/album/1.png",
},
{
name: "攻击检测详情",
url: "/images/album/2.png",
},
{
name: "防护站点列表",
url: "/images/album/3.png",
},
{
name: "自定义规则列表",
url: "/images/album/3.png",
},
{
name: "攻击阻断页面",
url: "/images/album/block.png",
},
];
export default function Home(): JSX.Element {
const { siteConfig } = useDocusaurusContext();
const totalRef = useRef(null);
const initTotal = async (n: number) => {
const countUpModule = await import("countup.js");
const anim = new countUpModule.CountUp(totalRef.current!, Math.max(0, n), {
duration: 2,
});
anim.start();
};
useEffect(() => {
getSetupCount().then((d) => {
initTotal(d.total);
});
});
return (
<Layout title="" description="长亭雷池 Web 应用防火墙 | 长亭雷池 WAF">
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="keywords"
content="WAF,雷池,长亭,社区版,免费,开源,网站防护"
></meta>
</Head>
<ThemeProvider>
<Box sx={{ backgroundColor: "#F8F9FC" }}>
<Box
sx={{ pt: 16, pb: 18, backgroundColor: "#0F1935", color: "#fff" }}
>
<Container maxWidth="lg">
<Box
sx={{ display: { xs: "block", sm: "flex" }, flexWrap: "wrap" }}
>
<Grid container sx={{ flex: 1 }}>
<Grid item xs={12}>
<Typography variant="h3" sx={{ pb: 3 }}>
Web
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="subtitle1" sx={{ pb: 3 }}>
10
<Box component="span" color="primary.main" sx={{ px: 1 }}>
</Box>
</Typography>
</Grid>
<Grid container item xs={12} spacing={2}>
<Button
variant="contained"
target="_blank"
sx={{
width: { xs: "100%", sm: "auto" },
ml: { xs: 2, sm: 2 },
mb: { xs: 2, sm: 0 },
"&:hover": {
backgroundColor: "rgb(10, 138, 135)",
color: "white",
},
}}
href="https://stack.chaitin.com/tool/detail?id=717"
>
使
</Button>
<Button
variant="contained"
target="_blank"
sx={{
textTransform: "none",
backgroundColor: "#fff",
color: "#000",
ml: { xs: 2, sm: 2 },
mb: { xs: 2, sm: 0 },
width: { xs: "100%", sm: "auto" },
"&:hover": {
fontWeight: "500",
backgroundColor: "white",
},
}}
href="https://github.com/chaitin/safeline"
startIcon={
<img src="/images/github.png" width={16} height={16} />
}
>
GitHub
</Button>
<Button
variant="contained"
sx={{
backgroundColor: "#fff",
color: "#000",
ml: { xs: 2, sm: 2 },
width: { xs: "100%", sm: "auto" },
"&:hover": {
backgroundColor: "white",
},
}}
href="/#groupchat"
startIcon={
<img
src="/images/wechat-logo.png"
alt="Logo"
width={16}
height={16}
/>
}
>
</Button>
</Grid>
</Grid>
<Box
sx={{
display: "flex",
justifyContent: { xs: "center", sm: "right" },
pt: { xs: 3, sm: 0 },
ml: { xs: 0, sm: 3 },
}}
>
<img
src="/images/403.svg"
alt="Logo"
width={196}
height={196}
/>
</Box>
</Box>
</Container>
</Box>
<Container sx={{ mt: -10, color: "#000", pb: 3 }}>
<Features />
</Container>
<Container>
<Stack sx={{ pt: 15 }} spacing={3} alignItems="center">
<Title title="装机量" />
<Typography
sx={{
color: "primary.main",
fontSize: "96px",
letterSpacing: "10px",
}}
ref={totalRef}
>
-
</Typography>
</Stack>
</Container>
<Container
sx={{
color: "#000",
".carousel .control-dots .dot": {
backgroundColor: "#000",
},
".carousel .control-prev.control-arrow": {
padding: "20px",
borderRadius: "12px 0 0 12px",
},
".carousel .control-next.control-arrow": {
padding: "20px",
borderRadius: "0 12px 12px 0",
},
".carousel .control-prev.control-arrow:before": {
borderRightColor: "rgba(0,0,0,0.5)",
},
".carousel .control-next.control-arrow:before": {
borderLeftColor: "rgba(0,0,0,0.5)",
},
".carousel .slide .legend": {
width: "30%",
marginLeft: "-15%",
},
}}
>
<Stack sx={{ pt: 15 }} spacing={6} alignItems="center">
<Title title="产品展示" />
<Box sx={{ boxShadow: "0px 0px 6px #0fc6c2" }}>
<Carousel
interval={2000}
infiniteLoop
autoPlay
showStatus={false}
showThumbs={false}
>
{IMAGE_LIST.map((item) => (
<Box
key={item.url}
sx={{
borderRadius: "12px",
overflow: "hidden",
}}
>
<Box component="img" src={item.url} alt={item.name} />
<Box
className="legend"
sx={{
opacity: "0.40 !important",
py: "4px !important",
borderRadius: "4px !important",
}}
>
<Typography variant="h6" sx={{ fontSize: "14px" }}>
{item.name}
</Typography>
</Box>
</Box>
))}
</Carousel>
</Box>
</Stack>
</Container>
<Container sx={{ color: "#000", pb: 3 }}>
<Stack sx={{ pt: 15 }} spacing={3} alignItems="center">
<Title title="版本对比" />
<Version />
</Stack>
<Stack
sx={{ pt: 15 }}
id="groupchat"
spacing={6}
alignItems="center"
>
<Title title="加入讨论组" />
<img
src="/images/wechat-light.png"
alt="wechat"
width={300}
height={300}
/>
</Stack>
</Container>
</Box>
</ThemeProvider>
</Layout>
);
}

3
website/src/types/extenal.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare var hljs = {
highlight: any,
};

0
website/static/.nojekyll Normal file
View File

View File

@@ -0,0 +1 @@
window._iconfont_svg_string_4031246='<svg><symbol id="icon-bangzhu1" viewBox="0 0 1024 1024"><path d="M512 953.81818174c244.02272695 0 441.81818174-197.79545479 441.81818174-441.81818174C953.81818174 267.97727305 756.02272695 70.18181826 512 70.18181826 267.97727305 70.18181826 70.18181826 267.97727305 70.18181826 512c0 244.02272695 197.79545479 441.81818174 441.81818174 441.81818174z m0-65.45454522a376.36363653 376.36363653 0 1 1 0-752.72727305 376.36363653 376.36363653 0 0 1 0 752.72727305z" ></path><path d="M544.72727305 590.83181826v-5.56363652a90 90 0 0 1 41.1954539-75.64090869C638.69545479 475.59090869 667.45454521 435.5 667.45454521 389.27272695a155.45454521 155.45454521 0 0 0-310.90909042 0 32.72727305 32.72727305 0 1 0 65.45454521 0 90 90 0 1 1 180 0c0 20.37272695-15.62727305 42.17727305-51.54545479 65.37272784a155.45454521 155.45454521 0 0 0-71.18181826 130.62272695v5.56363652a32.72727305 32.72727305 0 1 0 65.4545461 0z" ></path><path d="M512 716.54545479m-40.90909131 0a40.90909131 40.90909131 0 1 0 81.81818262 0 40.90909131 40.90909131 0 1 0-81.81818262 0Z" ></path></symbol><symbol id="icon-duihao" viewBox="0 0 1024 1024"><path d="M515.2 26.66666666c266.496 0 483.2 216.704 483.2 483.2s-216.704 483.2-483.2 483.2S32 776.36266666 32 509.86666666 248.704 26.66666666 515.2 26.66666666z m202.24 380.16a35.2 35.2 0 0 0-49.728 0L470.784 603.62666666 364.48 497.32266666a35.2 35.2 0 0 0-49.792 49.792l131.2 131.2a35.072 35.072 0 0 0 21.952 10.24h6.016l5.888-1.088a35.008 35.008 0 0 0 16-9.152l221.696-221.76a35.2 35.2 0 0 0 0-49.728z" ></path></symbol><symbol id="icon-chahao" viewBox="0 0 1024 1024"><path d="M515.19999998 26.66666665c266.496 0 483.2 216.704 483.2 483.2s-216.704 483.2-483.2 483.2S31.99999998 776.36266665 31.99999998 509.86666665 248.70399998 26.66666665 515.19999998 26.66666665zM407.61599998 352.49066665a35.2 35.2 0 0 0-49.792 49.792L466.55999998 510.89066665 357.82399998 619.49866665a35.2 35.2 0 1 0 49.792 49.792l108.608-108.672 108.608 108.672a35.2 35.2 0 1 0 49.792-49.792L565.95199998 510.89066665l108.672-108.608a35.2 35.2 0 1 0-49.792-49.792L516.22399998 461.22666665z" ></path></symbol></svg>',function(n){var t=(t=document.getElementsByTagName("script"))[t.length-1],e=t.getAttribute("data-injectcss"),t=t.getAttribute("data-disable-injectsvg");if(!t){var o,a,i,c,d,s=function(t,e){e.parentNode.insertBefore(t,e)};if(e&&!n.__iconfont__svg__cssinject__){n.__iconfont__svg__cssinject__=!0;try{document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>")}catch(t){console&&console.log(t)}}o=function(){var t,e=document.createElement("div");e.innerHTML=n._iconfont_svg_string_4031246,(e=e.getElementsByTagName("svg")[0])&&(e.setAttribute("aria-hidden","true"),e.style.position="absolute",e.style.width=0,e.style.height=0,e.style.overflow="hidden",e=e,(t=document.body).firstChild?s(e,t.firstChild):t.appendChild(e))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(a=function(){document.removeEventListener("DOMContentLoaded",a,!1),o()},document.addEventListener("DOMContentLoaded",a,!1)):document.attachEvent&&(i=o,c=n.document,d=!1,r(),c.onreadystatechange=function(){"complete"==c.readyState&&(c.onreadystatechange=null,l())})}function l(){d||(d=!0,i())}function r(){try{c.documentElement.doScroll("left")}catch(t){return void setTimeout(r,50)}l()}}(window);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: a11y-light
Author: @ericwbailey
Maintainer: @ericwbailey
Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css
*/.hljs{background:#fefefe;color:#545454}.hljs-comment,.hljs-quote{color:#696969}.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#d91e18}.hljs-attribute,.hljs-built_in,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{color:#aa5d00}.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{color:green}.hljs-section,.hljs-title{color:#007faa}.hljs-keyword,.hljs-selector-tag{color:#7928a1}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}@media screen and (-ms-high-contrast:active){.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-comment,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-quote,.hljs-string,.hljs-symbol,.hljs-type{color:highlight}.hljs-keyword,.hljs-selector-tag{font-weight:700}}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="396px" height="407px" viewBox="0 0 396 407" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 12</title>
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
<stop stop-color="#4B4B4B" offset="0%"></stop>
<stop stop-color="#000000" offset="100%"></stop>
</linearGradient>
<filter x="-3.0%" y="-2.8%" width="106.1%" height="105.6%" filterUnits="objectBoundingBox" id="filter-2">
<feGaussianBlur stdDeviation="3" in="SourceGraphic"></feGaussianBlur>
</filter>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-3">
<stop stop-color="#24BC43" stop-opacity="0.8" offset="0%"></stop>
<stop stop-color="#3ACBAB" stop-opacity="0.7" offset="100%"></stop>
</linearGradient>
<path d="M110.049657,49.667649 C110.049657,49.667649 81.1358702,46.2263115 76.8,26.7636364 C72.4880848,46.2263115 43.5503431,49.667649 43.5503431,49.667649 C14.2053649,53.3001718 0,36.4567369 0,36.4567369 C13.941859,65.8036979 38.4,64.7712967 38.4,64.7712967 L115.2,64.7712967 C115.2,64.7712967 139.634186,65.8036979 153.6,36.4567369 C153.6,36.4567369 139.394635,53.3192904 110.049657,49.667649 Z" id="path-4"></path>
<filter x="-16.9%" y="-57.9%" width="133.9%" height="236.8%" filterUnits="objectBoundingBox" id="filter-5">
<feOffset dx="0" dy="4" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="8" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0.490319293 0 0 0 0 0.292243323 0 0 0 1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="编组-12" transform="translate(49.000000, 38.000000)">
<path d="M292.40836,59.04 C290.927217,51.9634286 285.002646,46.6971429 277.761503,46.368 C222.13636,44.8868571 176.385503,16.5805714 157.953503,3.08571429 C152.358074,-1.02857143 144.95236,-1.02857143 139.356931,3.08571429 C120.431217,16.5805714 75.1740742,44.8868571 19.5489314,46.368 C12.4723599,46.6971429 6.21864565,51.9634286 4.90207422,59.04 C-3.98478292,103.474286 -19.2899258,254.057143 148.902074,324 C316.60036,253.892571 300.966074,103.474286 292.40836,59.04 Z" id="路径" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
<path d="M292.40836,59.04 C290.927217,51.9634286 285.002646,46.6971429 277.761503,46.368 C222.13636,44.8868571 176.385503,16.5805714 157.953503,3.08571429 C152.358074,-1.02857143 144.95236,-1.02857143 139.356931,3.08571429 C120.431217,16.5805714 75.1740742,44.8868571 19.5489314,46.368 C12.4723599,46.6971429 6.21864565,51.9634286 4.90207422,59.04 C-3.98478292,103.474286 -19.2899258,254.057143 148.902074,324 C316.60036,253.892571 300.966074,103.474286 292.40836,59.04 Z" id="路径" fill="url(#linearGradient-1)" fill-rule="nonzero" filter="url(#filter-2)"></path>
<path d="M149,261.4 C205.553958,261.4 251.4,215.553958 251.4,159 C251.4,131.275004 240.381593,106.123494 222.484813,87.6855068 C209.900749,96.0964568 185.81512,106.024178 175.564259,100.853688 C166.334879,96.1984273 157.476591,88.4505652 148.989396,77.610101 C142.047769,88.5334102 134.670586,95.5517221 126.857848,98.6650367 C120.689419,101.123107 98.2592604,102.915695 75.4419467,87.761039 C57.5883513,106.192154 46.6,131.312844 46.6,159 C46.6,215.553958 92.4460416,261.4 149,261.4 Z" id="椭圆形备份-26" fill="url(#linearGradient-3)"></path>
<g id="编组-5备份-6" transform="translate(91.771423, 102.101722)" fill="#FFFFFF">
<polygon id="路径-130备份-29" transform="translate(57.217971, 95.920999) rotate(-180.000000) translate(-57.217971, -95.920999) " points="56.6651511 64.9496372 -7.57241738e-17 97.1108413 50.6084036 126.892361 68.8016729 117.264704 34.3433228 97.1108413 56.6651511 84.5503086 96.9001091 107.376711 96.9001091 114.88399 114.435942 125.435553 114.435942 97.1108413"></polygon>
<polygon id="路径-130备份-30" transform="translate(57.217971, 30.971362) rotate(-360.000000) translate(-57.217971, -30.971362) " points="56.6651511 2.84217094e-14 -7.57241738e-17 32.1612041 50.6084036 61.9427239 68.8016729 52.3150668 34.3433228 32.1612041 56.6651511 19.6006714 96.9001091 42.4270741 96.9001091 49.9343528 114.435942 60.4859155 114.435942 32.1612041"></polygon>
<polygon id="路径-130备份-29" opacity="0.40499442" transform="translate(57.217971, 95.920999) rotate(-180.000000) translate(-57.217971, -95.920999) " points="56.6651511 64.9496372 -7.57241738e-17 97.1108413 50.6084036 126.892361 68.8016729 117.264704 34.3433228 97.1108413 56.6651511 84.5503086 96.9001091 107.376711 96.9001091 114.88399 114.435942 125.435553 114.435942 97.1108413"></polygon>
<polygon id="路径-130备份-30" opacity="0.40499442" transform="translate(57.217971, 30.971362) rotate(-360.000000) translate(-57.217971, -30.971362) " points="56.6651511 4.8316906e-13 -7.57241738e-17 32.1612041 50.6084036 61.9427239 68.8016729 52.3150668 34.3433228 32.1612041 56.6651511 19.6006714 96.9001091 42.4270741 96.9001091 49.9343528 114.435942 60.4859155 114.435942 32.1612041"></polygon>
</g>
<g id="长亭logo备份-18" transform="translate(72.200000, 45.222222)" fill-rule="nonzero">
<g id="编组-7">
<path d="M96.7632666,18.0061837 C96.7632666,18.0061837 79.3862969,15.2966085 76.7907961,0 C74.1952953,15.2966085 56.8183256,18.0061837 56.8183256,18.0061837 C39.1836466,20.8694936 30.6424242,7.60987058 30.6424242,7.60987058 C39.0363842,30.6893013 53.7258141,29.862977 53.7258141,29.862977 L99.8741859,29.862977 C99.8741859,29.862977 114.563616,30.6700845 122.957576,7.60987058 C122.957576,7.60987058 114.416353,20.8694936 96.7816744,18.0061837 L96.7632666,18.0061837 Z" id="路径" fill="#27B876"></path>
<g id="路径">
<use fill="black" fill-opacity="1" filter="url(#filter-5)" xlink:href="#path-4"></use>
<use fill="#27B876" xlink:href="#path-4"></use>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="61px" height="68px" viewBox="0 0 61 68" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 4备份 4</title>
<defs>
<linearGradient x1="29.4327608%" y1="3.52079477e-12%" x2="77.5590451%" y2="100%" id="linearGradient-1">
<stop stop-color="#6FFECC" offset="0%"></stop>
<stop stop-color="#0FC6C2" offset="100%"></stop>
</linearGradient>
<linearGradient x1="9.5%" y1="7.78543896e-13%" x2="90.5%" y2="100%" id="linearGradient-2">
<stop stop-color="#FFFFFF" offset="0%"></stop>
<stop stop-color="#0FC6C2" offset="100%"></stop>
</linearGradient>
<path d="M25.4341763,6.37416481 L15.0880707,0.440979948 C14.1181233,-0.146993316 12.8787461,-0.146993316 11.8549127,0.440979948 L1.61657901,6.37416481 C0.646631602,6.96213807 0,7.97772828 0,9.15367481 L0,20.9131403 C0,22.0356347 0.592745625,23.1046771 1.61657901,23.6926503 L11.8549127,29.5723831 C12.3398864,29.8396437 12.932632,30 13.4714917,30 C14.0103514,30 14.603097,29.8396437 15.0880707,29.5723831 L25.3264044,23.6391982 C26.2963518,23.051225 26.9429834,22.0356347 26.9429834,20.9131403 L26.9968694,9.15367481 C27.0507554,7.97772825 26.4041238,6.90868595 25.4341763,6.37416481 Z" id="path-3"></path>
<filter x="-103.7%" y="-60.0%" width="307.4%" height="286.7%" filterUnits="objectBoundingBox" id="filter-4">
<feMorphology radius="0.5" operator="dilate" in="SourceAlpha" result="shadowSpreadOuter1"></feMorphology>
<feOffset dx="0" dy="10" in="shadowSpreadOuter1" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="7.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"></feComposite>
<feColorMatrix values="0 0 0 0 0.0588235294 0 0 0 0 0.776470588 0 0 0 0 0.760784314 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="画板" transform="translate(-366.000000, -633.000000)">
<g id="编组-4备份-4" transform="translate(367.000000, 633.000000)">
<g id="编组-3备份" fill="#D8D8D8" fill-opacity="0">
<rect id="矩形" x="0" y="0" width="60" height="60"></rect>
</g>
<rect id="矩形" fill-opacity="0" fill="#D8D8D8" x="0" y="0" width="60" height="60"></rect>
<path d="M24.4561176,11.7839344 L17.5587138,7.79637281 C16.9120822,7.40120906 16.0858307,7.40120906 15.4032751,7.79637281 L8.57771934,11.7839344 C7.93108773,12.1790981 7.5,12.8616537 7.5,13.6519812 L7.5,21.5552563 C7.5,22.3096599 7.89516375,23.0281394 8.57771934,23.4233032 L15.4032751,27.3749408 C15.7265909,27.5545606 16.1217547,27.6623326 16.4809945,27.6623326 C16.8402342,27.6623326 17.235398,27.5545606 17.5587138,27.3749408 L24.3842696,23.3873792 C25.0309012,22.9922155 25.4619889,22.3096599 25.4619889,21.5552563 L25.4979129,13.6519812 C25.5338369,12.8616537 25.1027492,12.1431741 24.4561176,11.7839344 Z" id="路径" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
<g id="编组-13备份-4" transform="translate(15.000000, 12.000000)" fill-rule="nonzero">
<g id="路径">
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-3"></use>
<use stroke="url(#linearGradient-2)" stroke-width="1" fill-opacity="0.3" fill="#0FC6C2" xlink:href="#path-3"></use>
</g>
</g>
<path d="M34.3321474,21.1810911 C34.9778424,20.8082989 35.8034894,21.0295304 36.1762816,21.6752254 C36.5490739,22.3209205 36.3278424,23.1465675 35.6821474,23.5193597 L35.6821474,23.5193597 L29.778,26.928 L29.7786864,33.4502254 C29.7786864,34.1958098 29.1742708,34.8002254 28.4286864,34.8002254 C27.683102,34.8002254 27.0786864,34.1958098 27.0786864,33.4502254 L27.078,26.927 L21.1752254,23.5193597 C20.5295304,23.1465675 20.3082989,22.3209205 20.6810911,21.6752254 C21.0538833,21.0295304 21.8795304,20.8082989 22.5252254,21.1810911 L28.429,24.589 Z" id="形状结合" fill="#FFFFFF"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

BIN
website/static/images/github.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
website/static/images/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="150px" height="42px" viewBox="0 0 150 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 5</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="编组-5" transform="translate(0.000000, -0.000000)" fill="#27B876" fill-rule="nonzero">
<path d="M145.703571,18.1714286 C145.703571,18.1714286 147.739286,20.0464286 149.303571,20.9571429 L150,19.9607143 C150,19.9607143 145.403571,17.025 145.703571,16.35 L145.703571,18.1607143 L145.703571,18.1714286 Z" id="路径"></path>
<rect id="矩形" x="80.6035714" y="26.2392857" width="8.43214286" height="1.38214286"></rect>
<rect id="矩形" x="122.432143" y="26.2071429" width="10.2214286" height="1.38214286"></rect>
<rect id="矩形" x="139.767857" y="26.2071429" width="1.38214286" height="8.325"></rect>
<rect id="矩形" x="100.821429" y="26.2071429" width="1.38214286" height="15.7821429"></rect>
<rect id="矩形" x="91.9821429" y="26.2071429" width="1.38214286" height="8.25"></rect>
<rect id="矩形" x="78.8035714" y="28.275" width="1.38214286" height="6.15"></rect>
<polygon id="路径" points="106.5 41.9892857 105.075 41.9892857 109.242857 26.2071429 110.667857 26.2071429"></polygon>
<g id="编组">
<path d="M45.975,10.0392857 C45.975,10.0392857 35.8607143,8.52857143 34.35,0 C32.8392857,8.52857143 22.725,10.0392857 22.725,10.0392857 C12.4607143,11.6357143 7.48928571,4.24285714 7.48928571,4.24285714 C12.375,17.1107143 20.925,16.65 20.925,16.65 L47.7857143,16.65 C47.7857143,16.65 56.3357143,17.1 61.2214286,4.24285714 C61.2214286,4.24285714 56.25,11.6357143 45.9857143,10.0392857 L45.975,10.0392857 Z" id="路径"></path>
<path d="M49.2214286,33.4607143 C49.2214286,33.4607143 36.2892857,31.5321429 34.35,20.625 C32.4214286,31.5321429 19.4785714,33.4607143 19.4785714,33.4607143 C6.35357143,35.4964286 0,26.0571429 0,26.0571429 C6.23571429,42.5035714 17.175,41.925 17.175,41.925 L51.525,41.925 C51.525,41.925 62.4535714,42.5035714 68.7,26.0571429 C68.7,26.0571429 62.3464286,35.5071429 49.2214286,33.4607143 Z" id="路径"></path>
</g>
<g id="编组" transform="translate(78.782143, 4.135714)">
<rect id="矩形" x="0" y="6.65357143" width="5.25" height="1.38214286"></rect>
<rect id="矩形" x="6.62142857" y="6.65357143" width="9.675" height="1.38214286"></rect>
<rect id="矩形" x="18.0857143" y="1.38214286" width="16.5428571" height="1.38214286"></rect>
<rect id="矩形" x="20.0892857" y="9.39642857" width="12.7928571" height="1.38214286"></rect>
<rect id="矩形" x="21.3214286" y="12.1607143" width="10.3392857" height="1.38214286"></rect>
<rect id="矩形" x="21.6214286" y="4.13571429" width="9.73928571" height="1.38214286"></rect>
<rect id="矩形" x="21.6214286" y="6.64285714" width="9.73928571" height="1.38214286"></rect>
<rect id="矩形" x="36.7607143" y="4.75809868e-16" width="6.87857143" height="1.38214286"></rect>
<rect id="矩形" x="36.7607143" y="2.76428571" width="6.87857143" height="1.38214286"></rect>
<rect id="矩形" x="54.8571429" y="1.38214286" width="5.85" height="1.38214286"></rect>
<rect id="矩形" x="62.625" y="1.38214286" width="8.58214286" height="1.38214286"></rect>
<rect id="矩形" x="3.86785714" y="0.0107142857" width="1.38214286" height="14.6785714"></rect>
<rect id="矩形" x="39.5035714" y="1.38214286" width="1.38214286" height="15.1821429"></rect>
<rect id="矩形" x="50.2821429" y="4.75809868e-16" width="1.38214286" height="16.8"></rect>
<rect id="矩形" x="57.0964286" y="4.75809868e-16" width="1.38214286" height="15.2571429"></rect>
<rect id="矩形" x="66.225" y="4.75809868e-16" width="1.38214286" height="7.2"></rect>
<rect id="矩形" x="62.7857143" y="5.81785714" width="8.25" height="1.38214286"></rect>
<rect id="矩形" x="25.6714286" y="4.75809868e-16" width="1.38214286" height="2.76428571"></rect>
<rect id="矩形" x="25.6714286" y="13.3821429" width="1.38214286" height="2.11071429"></rect>
<rect id="矩形" x="36.7607143" y="5.53928571" width="1.38214286" height="7.65"></rect>
<rect id="矩形" x="42.2464286" y="5.53928571" width="1.38214286" height="7.84285714"></rect>
<rect id="矩形" x="19.9285714" y="5.46428571" width="1.38214286" height="1.17857143"></rect>
<path d="M3.86785714,14.7 C3.86785714,14.7 3.66428571,16.8642857 6.61071429,16.8642857 L6.61071429,15.4928571 C6.61071429,15.4928571 5.25,15.5142857 5.25,14.7 L4.55357143,14.1321429 L3.86785714,14.7 Z" id="路径"></path>
<path d="M6.62142857,3.93214286 C6.62142857,3.93214286 13.1142857,2.775 16.2964286,0.0107142857 L16.2964286,1.55357143 C16.2964286,1.55357143 14.1428571,3.41785714 6.62142857,5.28214286 L6.62142857,3.92142857 L6.62142857,3.93214286 Z" id="路径"></path>
<path d="M16.2964286,13.2 C16.2964286,13.2 9.80357143,12.0428571 6.62142857,9.27857143 L6.62142857,10.8214286 C6.62142857,10.8214286 8.775,12.6857143 16.2964286,14.55 L16.2964286,13.1892857 L16.2964286,13.2 Z" id="路径"></path>
<path d="M21.6428571,4.13571429 C21.6428571,4.13571429 19.9285714,4.17857143 19.9285714,5.51785714 L21.6428571,5.51785714 L21.6428571,4.13571429 Z" id="路径"></path>
<path d="M21.6428571,8.025 C21.6428571,8.025 19.9285714,7.98214286 19.9285714,6.64285714 L21.6428571,6.64285714 L21.6428571,8.025 L21.6428571,8.025 Z" id="路径"></path>
<rect id="矩形" x="31.6607143" y="5.46428571" width="1.38214286" height="1.17857143"></rect>
<path d="M31.3392857,4.13571429 C31.3392857,4.13571429 33.0535714,4.17857143 33.0535714,5.51785714 L31.3392857,5.51785714 L31.3392857,4.13571429 Z" id="路径"></path>
<path d="M31.3392857,8.025 C31.3392857,8.025 33.0535714,7.98214286 33.0535714,6.64285714 L31.3392857,6.64285714 L31.3392857,8.025 L31.3392857,8.025 Z" id="路径"></path>
<path d="M20.0892857,9.39642857 C20.0892857,9.39642857 18.525,9.34285714 18.525,10.7785714 L18.525,12.1607143 L19.875,12.1607143 L19.875,11.0571429 C19.875,11.0571429 19.8642857,10.7785714 20.0892857,10.7785714 L20.5178571,9.99642857 L20.0892857,9.39642857 L20.0892857,9.39642857 Z" id="路径"></path>
<path d="M32.7964286,9.39642857 C32.7964286,9.39642857 34.3607143,9.34285714 34.3607143,10.7785714 L34.3607143,12.1607143 L33.0107143,12.1607143 L33.0107143,11.0571429 C33.0107143,11.0571429 33.0214286,10.7785714 32.7964286,10.7785714 L32.3678571,9.99642857 L32.7964286,9.39642857 L32.7964286,9.39642857 Z" id="路径"></path>
<path d="M27.0535714,15.4714286 C27.0535714,15.4714286 27.1071429,17.0357143 25.6714286,17.0357143 L24.2892857,17.0357143 L24.2892857,15.6857143 L25.3928571,15.6857143 C25.3928571,15.6857143 25.6714286,15.6964286 25.6714286,15.4714286 L26.4535714,15.0428571 L27.0535714,15.4714286 Z" id="路径"></path>
<path d="M58.4785714,15.2357143 C58.4785714,15.2357143 58.5321429,16.8 57.0964286,16.8 L55.7142857,16.8 L55.7142857,15.45 L56.8178571,15.45 C56.8178571,15.45 57.0964286,15.4607143 57.0964286,15.2357143 L57.8785714,14.8071429 L58.4785714,15.2357143 Z" id="路径"></path>
<polygon id="路径" points="45.3428571 4.75809868e-16 45.3428571 1.38214286 48.9 3.05357143 48.9 1.71428571"></polygon>
<polygon id="路径" points="45.2785714 5.52857143 45.2785714 6.91071429 48.8464286 8.58214286 48.8464286 7.23214286"></polygon>
<path d="M71.0357143,5.81785714 L71.2071429,5.81785714 C71.2071429,5.81785714 71.4535714,11.625 63.3214286,16.8 L62.625,15.8035714 C62.625,15.8035714 68.5071429,12.3321429 69.6964286,7.2 L71.0357143,5.81785714 L71.0357143,5.81785714 Z" id="路径"></path>
<path d="M62.7857143,5.81785714 L62.6357143,5.81785714 C62.6357143,5.81785714 62.4857143,8.58214286 65.1321429,12.0107143 C65.1321429,12.0107143 65.8178571,11.5928571 66.0428571,11.1107143 C66.0428571,11.1107143 64.7035714,9.53571429 64.1464286,7.2 L62.7964286,5.81785714 L62.7857143,5.81785714 Z" id="路径"></path>
<path d="M67.7892857,13.1142857 C67.7892857,13.1142857 69.975,15.1928571 71.2071429,15.8035714 L69.3,15.6428571 L67.5428571,13.5964286 L67.3928571,13.1785714 L67.7785714,13.1142857 L67.7892857,13.1142857 Z" id="路径"></path>
<path d="M60.7071429,5.81785714 L60.7071429,7.35 C60.7071429,7.35 56.5392857,9.36428571 54.8571429,10.0178571 L54.8571429,8.63571429 C54.8571429,8.63571429 58.425,7.125 60.7071429,5.80714286 L60.7071429,5.81785714 Z" id="路径"></path>
<path d="M52.8964286,9.53571429 L52.8964286,10.9071429 C52.8964286,10.9071429 50.1321429,11.9892857 45.3428571,13.1785714 L45.3428571,11.775 C45.3428571,11.775 47.4642857,11.4107143 52.8964286,9.53571429 Z" id="路径"></path>
<path d="M36.7607143,13.1142857 L36.7607143,14.5607143 C36.7607143,14.5607143 37.8535714,14.475 38.1428571,14.4428571 L38.1428571,13.1142857 L36.7607143,13.1142857 L36.7607143,13.1142857 Z" id="路径"></path>
<path d="M42.2464286,12.4071429 L42.2464286,13.8321429 C42.2464286,13.8321429 43.2642857,13.6392857 43.6285714,13.5535714 L43.6285714,12.15 L42.2464286,12.3964286 L42.2464286,12.4071429 Z" id="路径"></path>
</g>
<rect id="矩形" x="93.2892857" y="33.0857143" width="7.78928571" height="1.38214286"></rect>
<rect id="矩形" x="148.607143" y="26.2071429" width="1.38214286" height="15.7821429"></rect>
<rect id="矩形" x="135.525" y="26.2071429" width="1.38214286" height="15.7821429"></rect>
<rect id="矩形" x="127.017857" y="26.2071429" width="1.38214286" height="15.7821429"></rect>
<rect id="矩形" x="118.178571" y="26.2071429" width="1.38214286" height="15.7821429"></rect>
<rect id="矩形" x="80.2285714" y="40.6071429" width="8.80714286" height="1.38214286"></rect>
<polygon id="矩形" transform="translate(144.843906, 34.105836) rotate(-29.680000) translate(-144.843906, -34.105836) " points="144.152835 25.4111933 145.534977 25.4111933 145.534977 42.800479 144.152835 42.800479"></polygon>
<path d="M80.6035714,27.6214286 C80.6035714,27.6214286 80.1857143,27.7071429 80.1857143,28.275 L79.2642857,28.6928571 L78.8035714,28.275 C78.8035714,28.275 78.6,26.2392857 80.6035714,26.2392857" id="路径"></path>
<polygon id="路径" points="113.882143 41.9892857 115.317857 41.9892857 111.15 26.2071429 109.714286 26.2071429"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

BIN
website/static/images/qq.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Some files were not shown because too many files have changed in this diff Show More