diff --git a/website/.dockerignore b/website/.dockerignore new file mode 100644 index 0000000..d6fb34a --- /dev/null +++ b/website/.dockerignore @@ -0,0 +1,3 @@ +node_modules +build +.docusaurus \ No newline at end of file diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 0000000..b2d6de3 --- /dev/null +++ b/website/.gitignore @@ -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* diff --git a/website/.npmrc b/website/.npmrc new file mode 100644 index 0000000..f7971ee --- /dev/null +++ b/website/.npmrc @@ -0,0 +1,4 @@ +strict-ssl=false +save-prefix="" +engine-strict=true +registry="https://registry.npmmirror.com" \ No newline at end of file diff --git a/website/Dockerfile b/website/Dockerfile new file mode 100644 index 0000000..8cbfee5 --- /dev/null +++ b/website/Dockerfile @@ -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 \ No newline at end of file diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..1a0d868 --- /dev/null +++ b/website/README.md @@ -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 . +``` \ No newline at end of file diff --git a/website/babel.config.js b/website/babel.config.js new file mode 100644 index 0000000..e00595d --- /dev/null +++ b/website/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [require.resolve('@docusaurus/core/lib/babel/preset')], +}; diff --git a/website/docs/01-上手指南/01-guide_introduction.md b/website/docs/01-上手指南/01-guide_introduction.md new file mode 100644 index 0000000..af2eac8 --- /dev/null +++ b/website/docs/01-上手指南/01-guide_introduction.md @@ -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) diff --git a/website/docs/01-上手指南/02-guide_install.md b/website/docs/01-上手指南/02-guide_install.md new file mode 100644 index 0000000..73967cd --- /dev/null +++ b/website/docs/01-上手指南/02-guide_install.md @@ -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 > .env +echo "REDIS_PASSWORD=$(LC_ALL=C tr -dc A-Za-z0-9 > .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) diff --git a/website/docs/01-上手指南/03-guide_login.md b/website/docs/01-上手指南/03-guide_login.md new file mode 100644 index 0000000..50de32d --- /dev/null +++ b/website/docs/01-上手指南/03-guide_login.md @@ -0,0 +1,11 @@ +--- +title: "登录雷池" +group: "上手指南" +order: 3 +--- + +# 登录雷池 + +浏览器打开后台管理页面 `https://:9443`。根据界面提示,使用 **支持 TOTP 的认证软件** 扫描二维码,然后输入动态口令登录: + +![login.gif](https://waf-ce.chaitin.cn/images/gif/login.gif) diff --git a/website/docs/01-上手指南/04-guide_config.md b/website/docs/01-上手指南/04-guide_config.md new file mode 100644 index 0000000..e714701 --- /dev/null +++ b/website/docs/01-上手指南/04-guide_config.md @@ -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://:<端口>` 应能获取到业务网站的响应。 + +## 将网站流量切到雷池 + +- 若网站通过域名访问,则可将域名的 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) diff --git a/website/docs/01-上手指南/05-guide_test.md b/website/docs/01-上手指南/05-guide_test.md new file mode 100644 index 0000000..63597aa --- /dev/null +++ b/website/docs/01-上手指南/05-guide_test.md @@ -0,0 +1,94 @@ +--- +title: "测试防护效果" +group: "上手指南" +order: 5 +--- + +# 测试防护效果 + +## 确认网站可以正常访问 + +根据雷池 WAF 配置的网站参数访问你的网站 + +打开浏览器访问 `http://:<端口>/` + +> 网站协议默认是 http,勾选 ssl 则为 https +> 主机名可以是雷池的 IP,也可以是网站的域名(确保域名已经解析到雷池) +> 端口是你在雷池页面中配置的网站端口 + +若网站访问不正常,请参考 [网站无法访问](/docs/02-常见问题排查/03-faq_access.md) + +## 尝试手动模拟攻击 + +打开浏览器,访问以下地址即可模拟出对应的攻击: + +- 模拟 SQL 注入,请访问 `http://:<端口>/?id=1%20AND%201=1` +- 模拟 XSS,请访问 `http://:<端口>/?html=` + +通过浏览器,你将会看到雷池已经发现并阻断了攻击请求。 + +若请求没有被阻断,请参考 [防护不生效](/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://:<端口> -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 +``` diff --git a/website/docs/01-上手指南/06-guide_upgrade.md b/website/docs/01-上手指南/06-guide_upgrade.md new file mode 100644 index 0000000..5e904ff --- /dev/null +++ b/website/docs/01-上手指南/06-guide_upgrade.md @@ -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 > ".env" +``` + +执行以下命令替换 Docker 容器 + +``` +docker compose down +docker compose up -d +``` + +OK, 你已经完成了升级 diff --git a/website/docs/02-常见问题排查/01-faq_install.md b/website/docs/02-常见问题排查/01-faq_install.md new file mode 100644 index 0000000..7a52378 --- /dev/null +++ b/website/docs/02-常见问题排查/01-faq_install.md @@ -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` 文件。 diff --git a/website/docs/02-常见问题排查/02-faq_login.md b/website/docs/02-常见问题排查/02-faq_login.md new file mode 100644 index 0000000..a385377 --- /dev/null +++ b/website/docs/02-常见问题排查/02-faq_login.md @@ -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. 将 “绑定二维码” 分享给其他人进行绑定,绑定后即可登录(注:“绑定二维码” 无绑定次数限制,无时效限制) + +**注意保存的 “绑定二维码” 千万别泄漏,任何人得到以后都可以绑定并登录** diff --git a/website/docs/02-常见问题排查/03-faq_access.md b/website/docs/02-常见问题排查/03-faq_access.md new file mode 100644 index 0000000..b26e48d --- /dev/null +++ b/website/docs/02-常见问题排查/03-faq_access.md @@ -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: " -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`
+ 如果 `time_namelookup` 时间过大,请检查 dns server 配置 + 如果 `time_connect` 时间过大,请检查与上游服务器之间的网络状态 + 如果 `time_starttransfer` 时间过大,请检查上游服务器状态,是否出现资源过载情况 diff --git a/website/docs/02-常见问题排查/04-faq_config.md b/website/docs/02-常见问题排查/04-faq_config.md new file mode 100644 index 0000000..9275e76 --- /dev/null +++ b/website/docs/02-常见问题排查/04-faq_config.md @@ -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: " -vv http://xiaoming.com:8888` 测一下是否能访问到。如果不行,需要自行排查为什么 SafeLine 的机器没法访问到。 + +注:这里需要 -H 指定 Host `Host: ` 进行连通性测试。收到比较多的反馈,在 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 提交反馈。 diff --git a/website/docs/02-常见问题排查/05-faq_other.md b/website/docs/02-常见问题排查/05-faq_other.md new file mode 100644 index 0000000..bdc33ca --- /dev/null +++ b/website/docs/02-常见问题排查/05-faq_other.md @@ -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 部分。 + +``` + + @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 mgt_detect_log_basic + update_column timestamp + time_column timestamp # optional +
+ + # detects all tables instead of sections + #all_tables + +``` + +之后,来编写我们的 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 +``` diff --git a/website/docs/03-关于雷池/01-about_syntaxanalysis.md b/website/docs/03-关于雷池/01-about_syntaxanalysis.md new file mode 100644 index 0000000..644f7b2 --- /dev/null +++ b/website/docs/03-关于雷池/01-about_syntaxanalysis.md @@ -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,雷池展现出了更多让安全产品 “更聪明” 的可能。除了形成了质变的检测引擎的精准程度,它可以通过插件形式灵活扩展、实现瑞士军刀般的功能增加,可以变形适配、安装部署进各种网络环境,可以跟机器学习等前沿技术更好的融合、增强流量分析的能力等。 diff --git a/website/docs/03-关于雷池/02-about_challenge.md b/website/docs/03-关于雷池/02-about_challenge.md new file mode 100644 index 0000000..ea59ab2 --- /dev/null +++ b/website/docs/03-关于雷池/02-about_challenge.md @@ -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) \ No newline at end of file diff --git a/website/docs/03-关于雷池/03-about_chaitin.md b/website/docs/03-关于雷池/03-about_chaitin.md new file mode 100644 index 0000000..3b30a47 --- /dev/null +++ b/website/docs/03-关于雷池/03-about_chaitin.md @@ -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 Tech:Web 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 演讲 diff --git a/website/docs/03-关于雷池/04-about_changelog.md b/website/docs/03-关于雷池/04-about_changelog.md new file mode 100644 index 0000000..95b63c3 --- /dev/null +++ b/website/docs/03-关于雷池/04-about_changelog.md @@ -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 登录 +- 攻击检测日志 +- 默认防护策略 diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js new file mode 100644 index 0000000..441e3e5 --- /dev/null +++ b/website/docusaurus.config.js @@ -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 = ` HTTP/1.1`, + `POST / HTTP/1.1 + + + + +]> +&file;`, +]; + +const BuiltinNonAttackSamples = [ + `GET / HTTP/1.1 + Host: 1.2.3.4`, + + `POST / HTTP/1.1 + +name=haha&submit=submit`, +]; + +export { BuiltinAttackSamples, BuiltinNonAttackSamples }; diff --git a/website/src/components/detection/detection.css b/website/src/components/detection/detection.css new file mode 100644 index 0000000..d7d86e4 --- /dev/null +++ b/website/src/components/detection/detection.css @@ -0,0 +1,3 @@ +.detection-samples-accordion::before { + display: none; +} \ No newline at end of file diff --git a/website/src/components/detection/types.ts b/website/src/components/detection/types.ts new file mode 100644 index 0000000..4282958 --- /dev/null +++ b/website/src/components/detection/types.ts @@ -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; +}>; diff --git a/website/src/components/utils.ts b/website/src/components/utils.ts new file mode 100644 index 0000000..a3eeb71 --- /dev/null +++ b/website/src/components/utils.ts @@ -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); +} diff --git a/website/src/css/custom.css b/website/src/css/custom.css new file mode 100644 index 0000000..6b96767 --- /dev/null +++ b/website/src/css/custom.css @@ -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); +} diff --git a/website/src/pages/detection.tsx b/website/src/pages/detection.tsx new file mode 100644 index 0000000..b9efa58 --- /dev/null +++ b/website/src/pages/detection.tsx @@ -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([]); + const [result, setResult] = useState([]); + + 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 ( + + + + + + + + TP: 正确识别到攻击样本的数量 +
+ 检出率 = TP / (TP + FN) +
+ + TN: 正确识别到普通样本的数量 +
+ 误报率 = FP / (TP + FP) +
+ + FP: 将普通样本误报为攻击的数量 +
+ 准确率 = (TP + TN) / (TP + TN + FP + FN) +
+ + FN: 未识别到攻击样本的数量 + +
+
+
+
+ ); +} + +function percent(v: number) { + return Math.round(v * 10000) / 100 + "%"; +} diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx new file mode 100644 index 0000000..a544415 --- /dev/null +++ b/website/src/pages/index.tsx @@ -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 ( + + + + + + + + + + + + + + 雷池 Web 应用防火墙 + + + + + 耗时近 10 年,长亭科技倾情打造,核心检测能力由 + + 智能语义分析算法 + + 驱动,专为社区而生,不让黑客越雷池半步。 + + + + + + + + + + Logo + + + + + + + + + + + <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> + ); +} diff --git a/website/src/types/extenal.d.ts b/website/src/types/extenal.d.ts new file mode 100644 index 0000000..8a93072 --- /dev/null +++ b/website/src/types/extenal.d.ts @@ -0,0 +1,3 @@ +declare var hljs = { + highlight: any, +}; diff --git a/website/static/.nojekyll b/website/static/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/website/static/fonts/iconfont.js b/website/static/fonts/iconfont.js new file mode 100644 index 0000000..feb36b4 --- /dev/null +++ b/website/static/fonts/iconfont.js @@ -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); \ No newline at end of file diff --git a/website/static/highlight/highlight.min.js b/website/static/highlight/highlight.min.js new file mode 100644 index 0000000..5a6bcc0 --- /dev/null +++ b/website/static/highlight/highlight.min.js @@ -0,0 +1,453 @@ +/*! + Highlight.js v11.7.0 (git: 82688fad18) + (c) 2006-2022 undefined and other contributors + License: BSD-3-Clause + */ +var hljs=function(){"use strict";var e={exports:{}};function t(e){ +return e instanceof Map?e.clear=e.delete=e.set=()=>{ +throw Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=()=>{ +throw Error("set is read-only") +}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((n=>{var i=e[n] +;"object"!=typeof i||Object.isFrozen(i)||t(i)})),e} +e.exports=t,e.exports.default=t;class n{constructor(e){ +void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} +ignoreMatch(){this.isMatchIgnored=!0}}function i(e){ +return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'") +}function r(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t] +;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n} +const s=e=>!!e.scope||e.sublanguage&&e.language;class o{constructor(e,t){ +this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){ +this.buffer+=i(e)}openNode(e){if(!s(e))return;let t="" +;t=e.sublanguage?"language-"+e.language:((e,{prefix:t})=>{if(e.includes(".")){ +const n=e.split(".") +;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ") +}return`${t}${e}`})(e.scope,{prefix:this.classPrefix}),this.span(t)} +closeNode(e){s(e)&&(this.buffer+="</span>")}value(){return this.buffer}span(e){ +this.buffer+=`<span class="${e}">`}}const a=(e={})=>{const t={children:[]} +;return Object.assign(t,e),t};class c{constructor(){ +this.rootNode=a(),this.stack=[this.rootNode]}get top(){ +return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ +this.top.children.push(e)}openNode(e){const t=a({scope:e}) +;this.add(t),this.stack.push(t)}closeNode(){ +if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ +for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} +walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){ +return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t), +t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){ +"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ +c._collapse(e)})))}}class l extends c{constructor(e){super(),this.options=e} +addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())} +addText(e){""!==e&&this.add(e)}addSublanguage(e,t){const n=e.root +;n.sublanguage=!0,n.language=t,this.add(n)}toHTML(){ +return new o(this,this.options).value()}finalize(){return!0}}function g(e){ +return e?"string"==typeof e?e:e.source:null}function d(e){return p("(?=",e,")")} +function u(e){return p("(?:",e,")*")}function h(e){return p("(?:",e,")?")} +function p(...e){return e.map((e=>g(e))).join("")}function f(...e){const t=(e=>{ +const t=e[e.length-1] +;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{} +})(e);return"("+(t.capture?"":"?:")+e.map((e=>g(e))).join("|")+")"} +function b(e){return RegExp(e.toString()+"|").exec("").length-1} +const m=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ +;function E(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n +;let i=g(e),r="";for(;i.length>0;){const e=m.exec(i);if(!e){r+=i;break} +r+=i.substring(0,e.index), +i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?r+="\\"+(Number(e[1])+t):(r+=e[0], +"("===e[0]&&n++)}return r})).map((e=>`(${e})`)).join(t)} +const x="[a-zA-Z]\\w*",w="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",_="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",O="\\b(0b[01]+)",v={ +begin:"\\\\[\\s\\S]",relevance:0},N={scope:"string",begin:"'",end:"'", +illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", +contains:[v]},M=(e,t,n={})=>{const i=r({scope:"comment",begin:e,end:t, +contains:[]},n);i.contains.push({scope:"doctag", +begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", +end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) +;const s=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) +;return i.contains.push({begin:p(/[ ]+/,"(",s,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i +},S=M("//","$"),R=M("/\\*","\\*/"),j=M("#","$");var A=Object.freeze({ +__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:x,UNDERSCORE_IDENT_RE:w, +NUMBER_RE:y,C_NUMBER_RE:_,BINARY_NUMBER_RE:O, +RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", +SHEBANG:(e={})=>{const t=/^#![ ]*\// +;return e.binary&&(e.begin=p(t,/.*\b/,e.binary,/\b.*/)),r({scope:"meta",begin:t, +end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)}, +BACKSLASH_ESCAPE:v,APOS_STRING_MODE:N,QUOTE_STRING_MODE:k,PHRASAL_WORDS_MODE:{ +begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ +},COMMENT:M,C_LINE_COMMENT_MODE:S,C_BLOCK_COMMENT_MODE:R,HASH_COMMENT_MODE:j, +NUMBER_MODE:{scope:"number",begin:y,relevance:0},C_NUMBER_MODE:{scope:"number", +begin:_,relevance:0},BINARY_NUMBER_MODE:{scope:"number",begin:O,relevance:0}, +REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{scope:"regexp",begin:/\//, +end:/\/[gimuy]*/,illegal:/\n/,contains:[v,{begin:/\[/,end:/\]/,relevance:0, +contains:[v]}]}]},TITLE_MODE:{scope:"title",begin:x,relevance:0}, +UNDERSCORE_TITLE_MODE:{scope:"title",begin:w,relevance:0},METHOD_GUARD:{ +begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:e=>Object.assign(e,{ +"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{ +t.data._beginMatch!==e[1]&&t.ignoreMatch()}})});function I(e,t){ +"."===e.input[e.index-1]&&t.ignoreMatch()}function T(e,t){ +void 0!==e.className&&(e.scope=e.className,delete e.className)}function L(e,t){ +t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", +e.__beforeBegin=I,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, +void 0===e.relevance&&(e.relevance=0))}function B(e,t){ +Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function D(e,t){ +if(e.match){ +if(e.begin||e.end)throw Error("begin & end are not supported with match") +;e.begin=e.match,delete e.match}}function H(e,t){ +void 0===e.relevance&&(e.relevance=1)}const P=(e,t)=>{if(!e.beforeMatch)return +;if(e.starts)throw Error("beforeMatch cannot be used with starts") +;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t] +})),e.keywords=n.keywords,e.begin=p(n.beforeMatch,d(n.begin)),e.starts={ +relevance:0,contains:[Object.assign(n,{endsParent:!0})] +},e.relevance=0,delete n.beforeMatch +},C=["of","and","for","in","not","or","if","then","parent","list","value"] +;function $(e,t,n="keyword"){const i=Object.create(null) +;return"string"==typeof e?r(n,e.split(" ")):Array.isArray(e)?r(n,e):Object.keys(e).forEach((n=>{ +Object.assign(i,$(e[n],t,n))})),i;function r(e,n){ +t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|") +;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){ +return t?Number(t):(e=>C.includes(e.toLowerCase()))(e)?0:1}const z={},K=e=>{ +console.error(e)},W=(e,...t)=>{console.log("WARN: "+e,...t)},X=(e,t)=>{ +z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0) +},G=Error();function Z(e,t,{key:n}){let i=0;const r=e[n],s={},o={} +;for(let e=1;e<=t.length;e++)o[e+i]=r[e],s[e+i]=!0,i+=b(t[e-1]) +;e[n]=o,e[n]._emit=s,e[n]._multi=!0}function F(e){(e=>{ +e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, +delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ +_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope +}),(e=>{if(Array.isArray(e.begin)){ +if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), +G +;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), +G;Z(e,e.begin,{key:"beginScope"}),e.begin=E(e.begin,{joinWith:""})}})(e),(e=>{ +if(Array.isArray(e.end)){ +if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), +G +;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), +G;Z(e,e.end,{key:"endScope"}),e.end=E(e.end,{joinWith:""})}})(e)}function V(e){ +function t(t,n){ +return RegExp(g(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":"")) +}class n{constructor(){ +this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} +addRule(e,t){ +t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]), +this.matchAt+=b(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) +;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(E(e,{joinWith:"|" +}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex +;const t=this.matcherRe.exec(e);if(!t)return null +;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n] +;return t.splice(0,n),Object.assign(t,i)}}class i{constructor(){ +this.rules=[],this.multiRegexes=[], +this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ +if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n +;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))), +t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){ +return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){ +this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){ +const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex +;let n=t.exec(e) +;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{ +const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)} +return n&&(this.regexIndex+=n.position+1, +this.regexIndex===this.count&&this.considerAll()),n}} +if(e.compilerExtensions||(e.compilerExtensions=[]), +e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") +;return e.classNameAliases=r(e.classNameAliases||{}),function n(s,o){const a=s +;if(s.isCompiled)return a +;[T,D,F,P].forEach((e=>e(s,o))),e.compilerExtensions.forEach((e=>e(s,o))), +s.__beforeBegin=null,[L,B,H].forEach((e=>e(s,o))),s.isCompiled=!0;let c=null +;return"object"==typeof s.keywords&&s.keywords.$pattern&&(s.keywords=Object.assign({},s.keywords), +c=s.keywords.$pattern, +delete s.keywords.$pattern),c=c||/\w+/,s.keywords&&(s.keywords=$(s.keywords,e.case_insensitive)), +a.keywordPatternRe=t(c,!0), +o&&(s.begin||(s.begin=/\B|\b/),a.beginRe=t(a.begin),s.end||s.endsWithParent||(s.end=/\B|\b/), +s.end&&(a.endRe=t(a.end)), +a.terminatorEnd=g(a.end)||"",s.endsWithParent&&o.terminatorEnd&&(a.terminatorEnd+=(s.end?"|":"")+o.terminatorEnd)), +s.illegal&&(a.illegalRe=t(s.illegal)), +s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>r(e,{ +variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?r(e,{ +starts:e.starts?r(e.starts):null +}):Object.isFrozen(e)?r(e):e))("self"===e?s:e)))),s.contains.forEach((e=>{n(e,a) +})),s.starts&&n(s.starts,o),a.matcher=(e=>{const t=new i +;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin" +}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end" +}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){ +return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{ +constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}} +const Y=i,Q=r,ee=Symbol("nomatch");var te=(t=>{ +const i=Object.create(null),r=Object.create(null),s=[];let o=!0 +;const a="Could not find the language '{}', did you forget to load/include a language module?",c={ +disableAutodetect:!0,name:"Plain text",contains:[]};let g={ +ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, +languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", +cssSelector:"pre code",languages:null,__emitter:l};function b(e){ +return g.noHighlightRe.test(e)}function m(e,t,n){let i="",r="" +;"object"==typeof t?(i=e, +n=t.ignoreIllegals,r=t.language):(X("10.7.0","highlight(lang, code, ...args) has been deprecated."), +X("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), +r=e,i=t),void 0===n&&(n=!0);const s={code:i,language:r};k("before:highlight",s) +;const o=s.result?s.result:E(s.language,s.code,n) +;return o.code=s.code,k("after:highlight",o),o}function E(e,t,r,s){ +const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(S) +;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(S),n="" +;for(;t;){n+=S.substring(e,t.index) +;const r=y.case_insensitive?t[0].toLowerCase():t[0],s=(i=r,N.keywords[i]);if(s){ +const[e,i]=s +;if(M.addText(n),n="",c[r]=(c[r]||0)+1,c[r]<=7&&(R+=i),e.startsWith("_"))n+=t[0];else{ +const n=y.classNameAliases[e]||e;M.addKeyword(t[0],n)}}else n+=t[0] +;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(S)}var i +;n+=S.substring(e),M.addText(n)}function d(){null!=N.subLanguage?(()=>{ +if(""===S)return;let e=null;if("string"==typeof N.subLanguage){ +if(!i[N.subLanguage])return void M.addText(S) +;e=E(N.subLanguage,S,!0,k[N.subLanguage]),k[N.subLanguage]=e._top +}else e=x(S,N.subLanguage.length?N.subLanguage:null) +;N.relevance>0&&(R+=e.relevance),M.addSublanguage(e._emitter,e.language) +})():l(),S=""}function u(e,t){let n=1;const i=t.length-1;for(;n<=i;){ +if(!e._emit[n]){n++;continue}const i=y.classNameAliases[e[n]]||e[n],r=t[n] +;i?M.addKeyword(r,i):(S=r,l(),S=""),n++}}function h(e,t){ +return e.scope&&"string"==typeof e.scope&&M.openNode(y.classNameAliases[e.scope]||e.scope), +e.beginScope&&(e.beginScope._wrap?(M.addKeyword(S,y.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), +S=""):e.beginScope._multi&&(u(e.beginScope,t),S="")),N=Object.create(e,{parent:{ +value:N}}),N}function p(e,t,i){let r=((e,t)=>{const n=e&&e.exec(t) +;return n&&0===n.index})(e.endRe,i);if(r){if(e["on:end"]){const i=new n(e) +;e["on:end"](t,i),i.isMatchIgnored&&(r=!1)}if(r){ +for(;e.endsParent&&e.parent;)e=e.parent;return e}} +if(e.endsWithParent)return p(e.parent,t,i)}function f(e){ +return 0===N.matcher.regexIndex?(S+=e[0],1):(I=!0,0)}function b(e){ +const n=e[0],i=t.substring(e.index),r=p(N,e,i);if(!r)return ee;const s=N +;N.endScope&&N.endScope._wrap?(d(), +M.addKeyword(n,N.endScope._wrap)):N.endScope&&N.endScope._multi?(d(), +u(N.endScope,e)):s.skip?S+=n:(s.returnEnd||s.excludeEnd||(S+=n), +d(),s.excludeEnd&&(S=n));do{ +N.scope&&M.closeNode(),N.skip||N.subLanguage||(R+=N.relevance),N=N.parent +}while(N!==r.parent);return r.starts&&h(r.starts,e),s.returnEnd?0:n.length} +let m={};function w(i,s){const a=s&&s[0];if(S+=i,null==a)return d(),0 +;if("begin"===m.type&&"end"===s.type&&m.index===s.index&&""===a){ +if(S+=t.slice(s.index,s.index+1),!o){const t=Error(`0 width match regex (${e})`) +;throw t.languageName=e,t.badRule=m.rule,t}return 1} +if(m=s,"begin"===s.type)return(e=>{ +const t=e[0],i=e.rule,r=new n(i),s=[i.__beforeBegin,i["on:begin"]] +;for(const n of s)if(n&&(n(e,r),r.isMatchIgnored))return f(t) +;return i.skip?S+=t:(i.excludeBegin&&(S+=t), +d(),i.returnBegin||i.excludeBegin||(S=t)),h(i,e),i.returnBegin?0:t.length})(s) +;if("illegal"===s.type&&!r){ +const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"<unnamed>")+'"') +;throw e.mode=N,e}if("end"===s.type){const e=b(s);if(e!==ee)return e} +if("illegal"===s.type&&""===a)return 1 +;if(A>1e5&&A>3*s.index)throw Error("potential infinite loop, way more iterations than matches") +;return S+=a,a.length}const y=O(e) +;if(!y)throw K(a.replace("{}",e)),Error('Unknown language: "'+e+'"') +;const _=V(y);let v="",N=s||_;const k={},M=new g.__emitter(g);(()=>{const e=[] +;for(let t=N;t!==y;t=t.parent)t.scope&&e.unshift(t.scope) +;e.forEach((e=>M.openNode(e)))})();let S="",R=0,j=0,A=0,I=!1;try{ +for(N.matcher.considerAll();;){ +A++,I?I=!1:N.matcher.considerAll(),N.matcher.lastIndex=j +;const e=N.matcher.exec(t);if(!e)break;const n=w(t.substring(j,e.index),e) +;j=e.index+n} +return w(t.substring(j)),M.closeAllNodes(),M.finalize(),v=M.toHTML(),{ +language:e,value:v,relevance:R,illegal:!1,_emitter:M,_top:N}}catch(n){ +if(n.message&&n.message.includes("Illegal"))return{language:e,value:Y(t), +illegal:!0,relevance:0,_illegalBy:{message:n.message,index:j, +context:t.slice(j-100,j+100),mode:n.mode,resultSoFar:v},_emitter:M};if(o)return{ +language:e,value:Y(t),illegal:!1,relevance:0,errorRaised:n,_emitter:M,_top:N} +;throw n}}function x(e,t){t=t||g.languages||Object.keys(i);const n=(e=>{ +const t={value:Y(e),illegal:!1,relevance:0,_top:c,_emitter:new g.__emitter(g)} +;return t._emitter.addText(e),t})(e),r=t.filter(O).filter(N).map((t=>E(t,e,!1))) +;r.unshift(n);const s=r.sort(((e,t)=>{ +if(e.relevance!==t.relevance)return t.relevance-e.relevance +;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1 +;if(O(t.language).supersetOf===e.language)return-1}return 0})),[o,a]=s,l=o +;return l.secondBest=a,l}function w(e){let t=null;const n=(e=>{ +let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"" +;const n=g.languageDetectRe.exec(t);if(n){const t=O(n[1]) +;return t||(W(a.replace("{}",n[1])), +W("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"} +return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return +;if(k("before:highlightElement",{el:e,language:n +}),e.children.length>0&&(g.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), +console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), +console.warn("The element with unescaped HTML:"), +console.warn(e)),g.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML) +;t=e;const i=t.textContent,s=n?m(i,{language:n,ignoreIllegals:!0}):x(i) +;e.innerHTML=s.value,((e,t,n)=>{const i=t&&r[t]||n +;e.classList.add("hljs"),e.classList.add("language-"+i) +})(e,n,s.language),e.result={language:s.language,re:s.relevance, +relevance:s.relevance},s.secondBest&&(e.secondBest={ +language:s.secondBest.language,relevance:s.secondBest.relevance +}),k("after:highlightElement",{el:e,result:s,text:i})}let y=!1;function _(){ +"loading"!==document.readyState?document.querySelectorAll(g.cssSelector).forEach(w):y=!0 +}function O(e){return e=(e||"").toLowerCase(),i[e]||i[r[e]]} +function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ +r[e.toLowerCase()]=t}))}function N(e){const t=O(e) +;return t&&!t.disableAutodetect}function k(e,t){const n=e;s.forEach((e=>{ +e[n]&&e[n](t)}))} +"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ +y&&_()}),!1),Object.assign(t,{highlight:m,highlightAuto:x,highlightAll:_, +highlightElement:w, +highlightBlock:e=>(X("10.7.0","highlightBlock will be removed entirely in v12.0"), +X("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{g=Q(g,e)}, +initHighlighting:()=>{ +_(),X("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, +initHighlightingOnLoad:()=>{ +_(),X("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") +},registerLanguage:(e,n)=>{let r=null;try{r=n(t)}catch(t){ +if(K("Language definition for '{}' could not be registered.".replace("{}",e)), +!o)throw t;K(t),r=c} +r.name||(r.name=e),i[e]=r,r.rawDefinition=n.bind(null,t),r.aliases&&v(r.aliases,{ +languageName:e})},unregisterLanguage:e=>{delete i[e] +;for(const t of Object.keys(r))r[t]===e&&delete r[t]}, +listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v, +autoDetection:N,inherit:Q,addPlugin:e=>{(e=>{ +e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{ +e["before:highlightBlock"](Object.assign({block:t.el},t)) +}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{ +e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),s.push(e)} +}),t.debugMode=()=>{o=!1},t.safeMode=()=>{o=!0 +},t.versionString="11.7.0",t.regex={concat:p,lookahead:d,either:f,optional:h, +anyNumberOfTimes:u};for(const t in A)"object"==typeof A[t]&&e.exports(A[t]) +;return Object.assign(t,A),t})({});return te}() +;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `xml` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const a=e.regex,n=a.concat(/[\p{L}_]/u,a.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),s={ +className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},t={begin:/\s/, +contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] +},i=e.inherit(t,{begin:/\(/,end:/\)/}),c=e.inherit(e.APOS_STRING_MODE,{ +className:"string"}),l=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),r={ +endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr", +begin:/[\p{L}0-9._:-]+/u,relevance:0},{begin:/=\s*/,relevance:0,contains:[{ +className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[s]},{ +begin:/'/,end:/'/,contains:[s]},{begin:/[^\s"'=<>`]+/}]}]}]};return{ +name:"HTML, XML", +aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], +case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin:/<![a-z]/, +end:/>/,relevance:10,contains:[t,l,c,i,{begin:/\[/,end:/\]/,contains:[{ +className:"meta",begin:/<![a-z]/,end:/>/,contains:[t,i,l,c]}]}] +},e.COMMENT(/<!--/,/-->/,{relevance:10}),{begin:/<!\[CDATA\[/,end:/\]\]>/, +relevance:10},s,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, +relevance:10,contains:[l]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", +begin:/<style(?=\s|>)/,end:/>/,keywords:{name:"style"},contains:[r],starts:{ +end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", +begin:/<script(?=\s|>)/,end:/>/,keywords:{name:"script"},contains:[r],starts:{ +end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ +className:"tag",begin:/<>|<\/>/},{className:"tag", +begin:a.concat(/</,a.lookahead(a.concat(n,a.either(/\/>/,/>/,/\s/)))), +end:/\/?>/,contains:[{className:"name",begin:n,relevance:0,starts:r}]},{ +className:"tag",begin:a.concat(/<\//,a.lookahead(a.concat(n,/>/))),contains:[{ +className:"name",begin:n,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}} +})();hljs.registerLanguage("xml",e)})();/*! `http` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const n="HTTP/(2|1\\.[01])",a={ +className:"attribute", +begin:e.regex.concat("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{ +contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$", +relevance:0}}]}},s=[a,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0} +}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{ +begin:"^(?="+n+" \\d{3})",end:/$/,contains:[{className:"meta",begin:n},{ +className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/, +contains:s}},{begin:"(?=^[A-Z]+ (.*?) "+n+"$)",end:/$/,contains:[{ +className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{ +className:"meta",begin:n},{className:"keyword",begin:"[A-Z]+"}],starts:{ +end:/\b\B/,illegal:/\S/,contains:s}},e.inherit(a,{relevance:0})]}}})() +;hljs.registerLanguage("http",e)})();/*! `javascript` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],r=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],c=["arguments","this","super","console","window","document","localStorage","module","global"],i=[].concat(r,t,s) +;return o=>{const l=o.regex,b=e,d={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="</"+e[0].slice(1) +;return-1!==e.input.indexOf(a,n)})(e,{after:a})||n.ignoreMatch()) +;const r=e.input.substring(a) +;((s=r.match(/^\s*=/))||(s=r.match(/^\s+extends\s+/))&&0===s.index)&&n.ignoreMatch() +}},g={$pattern:e,keyword:n,literal:a,built_in:i,"variable.language":c +},u="\\.([0-9](_?[0-9])*)",m="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",E={ +className:"number",variants:[{ +begin:`(\\b(${m})((${u})|\\.)?|(${u}))[eE][+-]?([0-9](_?[0-9])*)\\b`},{ +begin:`\\b(${m})\\b((${u})\\b|\\.)?|(${u})\\b`},{ +begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{ +begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{ +begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{ +begin:"\\b0[0-7]+n?\\b"}],relevance:0},A={className:"subst",begin:"\\$\\{", +end:"\\}",keywords:g,contains:[]},y={begin:"html`",end:"",starts:{end:"`", +returnEnd:!1,contains:[o.BACKSLASH_ESCAPE,A],subLanguage:"xml"}},N={ +begin:"css`",end:"",starts:{end:"`",returnEnd:!1, +contains:[o.BACKSLASH_ESCAPE,A],subLanguage:"css"}},_={className:"string", +begin:"`",end:"`",contains:[o.BACKSLASH_ESCAPE,A]},h={className:"comment", +variants:[o.COMMENT(/\/\*\*(?!\/)/,"\\*/",{relevance:0,contains:[{ +begin:"(?=@[A-Za-z]+)",relevance:0,contains:[{className:"doctag", +begin:"@[A-Za-z]+"},{className:"type",begin:"\\{",end:"\\}",excludeEnd:!0, +excludeBegin:!0,relevance:0},{className:"variable",begin:b+"(?=\\s*(-)|$)", +endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}] +}),o.C_BLOCK_COMMENT_MODE,o.C_LINE_COMMENT_MODE] +},f=[o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,y,N,_,{match:/\$\d+/},E] +;A.contains=f.concat({begin:/\{/,end:/\}/,keywords:g,contains:["self"].concat(f) +});const v=[].concat(h,A.contains),p=v.concat([{begin:/\(/,end:/\)/,keywords:g, +contains:["self"].concat(v)}]),S={className:"params",begin:/\(/,end:/\)/, +excludeBegin:!0,excludeEnd:!0,keywords:g,contains:p},w={variants:[{ +match:[/class/,/\s+/,b,/\s+/,/extends/,/\s+/,l.concat(b,"(",l.concat(/\./,b),")*")], +scope:{1:"keyword",3:"title.class",5:"keyword",7:"title.class.inherited"}},{ +match:[/class/,/\s+/,b],scope:{1:"keyword",3:"title.class"}}]},R={relevance:0, +match:l.either(/\bJSON/,/\b[A-Z][a-z]+([A-Z][a-z]*|\d)*/,/\b[A-Z]{2,}([A-Z][a-z]+|\d)+([A-Z][a-z]*)*/,/\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\d)*([A-Z][a-z]*)*/), +className:"title.class",keywords:{_:[...t,...s]}},O={variants:[{ +match:[/function/,/\s+/,b,/(?=\s*\()/]},{match:[/function/,/\s*(?=\()/]}], +className:{1:"keyword",3:"title.function"},label:"func.def",contains:[S], +illegal:/%/},k={ +match:l.concat(/\b/,(I=[...r,"super","import"],l.concat("(?!",I.join("|"),")")),b,l.lookahead(/\(/)), +className:"title.function",relevance:0};var I;const x={ +begin:l.concat(/\./,l.lookahead(l.concat(b,/(?![0-9A-Za-z$_(])/))),end:b, +excludeBegin:!0,keywords:"prototype",className:"property",relevance:0},T={ +match:[/get|set/,/\s+/,b,/(?=\()/],className:{1:"keyword",3:"title.function"}, +contains:[{begin:/\(\)/},S] +},C="(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+o.UNDERSCORE_IDENT_RE+")\\s*=>",M={ +match:[/const|var|let/,/\s+/,b,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(C)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[S]} +;return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:p,CLASS_REFERENCE:R},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,y,N,_,h,{match:/\$\d+/},E,R,{ +className:"attr",begin:b+l.lookahead(":"),relevance:0},M,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[h,o.REGEXP_MODE,{ +className:"function",begin:C,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:g,contains:p}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:"</>"},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:d.begin, +"on:begin":d.isTrulyOpeningTag,end:d.end}],subLanguage:"xml",contains:[{ +begin:d.begin,end:d.end,skip:!0,contains:["self"]}]}]},O,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[S,o.inherit(o.TITLE_MODE,{begin:b, +className:"title.function"})]},{match:/\.\.\./,relevance:0},x,{match:"\\$"+b, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[S]},k,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},w,T,{match:/\$[(.]/}]}}})() +;hljs.registerLanguage("javascript",e)})();/*! `css` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],i=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],r=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],t=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],o=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse() +;return n=>{const a=n.regex,l=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z][A-Za-z0-9_-]*/} +}))(n),s=[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE];return{name:"CSS", +case_insensitive:!0,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"}, +classNameAliases:{keyframePosition:"selector-tag"},contains:[l.BLOCK_COMMENT,{ +begin:/-(webkit|moz|ms|o)-(?=[a-z])/},l.CSS_NUMBER_MODE,{ +className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0},{ +className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 +},l.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ +begin:":("+r.join("|")+")"},{begin:":(:)?("+t.join("|")+")"}]},l.CSS_VARIABLE,{ +className:"attribute",begin:"\\b("+o.join("|")+")\\b"},{begin:/:/,end:/[;}{]/, +contains:[l.BLOCK_COMMENT,l.HEXCOLOR,l.IMPORTANT,l.CSS_NUMBER_MODE,...s,{ +begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" +},contains:[...s,{className:"string",begin:/[^)]/,endsWithParent:!0, +excludeEnd:!0}]},l.FUNCTION_DISPATCH]},{begin:a.lookahead(/@/),end:"[{;]", +relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ +},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ +$pattern:/[a-z-]+/,keyword:"and or not only",attribute:i.join(" ")},contains:[{ +begin:/[a-z-]+(?=:)/,className:"attribute"},...s,l.CSS_NUMBER_MODE]}]},{ +className:"selector-tag",begin:"\\b("+e.join("|")+")\\b"}]}}})() +;hljs.registerLanguage("css",e)})(); \ No newline at end of file diff --git a/website/static/highlight/styles/a11y-light.min.css b/website/static/highlight/styles/a11y-light.min.css new file mode 100644 index 0000000..8b5ab90 --- /dev/null +++ b/website/static/highlight/styles/a11y-light.min.css @@ -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}} \ No newline at end of file diff --git a/website/static/images/403.svg b/website/static/images/403.svg new file mode 100644 index 0000000..c7c2935 --- /dev/null +++ b/website/static/images/403.svg @@ -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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/static/images/album/0.png b/website/static/images/album/0.png new file mode 100644 index 0000000..4c45b86 Binary files /dev/null and b/website/static/images/album/0.png differ diff --git a/website/static/images/album/1.png b/website/static/images/album/1.png new file mode 100644 index 0000000..a194980 Binary files /dev/null and b/website/static/images/album/1.png differ diff --git a/website/static/images/album/2.png b/website/static/images/album/2.png new file mode 100644 index 0000000..7eced47 Binary files /dev/null and b/website/static/images/album/2.png differ diff --git a/website/static/images/album/3.png b/website/static/images/album/3.png new file mode 100644 index 0000000..1b808ce Binary files /dev/null and b/website/static/images/album/3.png differ diff --git a/website/static/images/album/4.png b/website/static/images/album/4.png new file mode 100644 index 0000000..f3b9828 Binary files /dev/null and b/website/static/images/album/4.png differ diff --git a/website/static/images/album/5.png b/website/static/images/album/5.png new file mode 100644 index 0000000..4c1a6d5 Binary files /dev/null and b/website/static/images/album/5.png differ diff --git a/website/static/images/album/block.png b/website/static/images/album/block.png new file mode 100644 index 0000000..3be281d Binary files /dev/null and b/website/static/images/album/block.png differ diff --git a/website/static/images/class.png b/website/static/images/class.png new file mode 100644 index 0000000..3a6191d Binary files /dev/null and b/website/static/images/class.png differ diff --git a/website/static/images/docs/DNS.png b/website/static/images/docs/DNS.png new file mode 100644 index 0000000..a6838c2 Binary files /dev/null and b/website/static/images/docs/DNS.png differ diff --git a/website/static/images/docs/LoadBlance.png b/website/static/images/docs/LoadBlance.png new file mode 100644 index 0000000..5e915b1 Binary files /dev/null and b/website/static/images/docs/LoadBlance.png differ diff --git a/website/static/images/docs/Untitled10.png b/website/static/images/docs/Untitled10.png new file mode 100644 index 0000000..e52f85f Binary files /dev/null and b/website/static/images/docs/Untitled10.png differ diff --git a/website/static/images/docs/Untitled11.png b/website/static/images/docs/Untitled11.png new file mode 100644 index 0000000..ed68f33 Binary files /dev/null and b/website/static/images/docs/Untitled11.png differ diff --git a/website/static/images/docs/Untitled12.png b/website/static/images/docs/Untitled12.png new file mode 100644 index 0000000..995e452 Binary files /dev/null and b/website/static/images/docs/Untitled12.png differ diff --git a/website/static/images/docs/Untitled13.png b/website/static/images/docs/Untitled13.png new file mode 100644 index 0000000..c61b6a7 Binary files /dev/null and b/website/static/images/docs/Untitled13.png differ diff --git a/website/static/images/docs/about_changelog/map.png b/website/static/images/docs/about_changelog/map.png new file mode 100644 index 0000000..2ff4613 Binary files /dev/null and b/website/static/images/docs/about_changelog/map.png differ diff --git a/website/static/images/docs/add_challenge.png b/website/static/images/docs/add_challenge.png new file mode 100644 index 0000000..1510707 Binary files /dev/null and b/website/static/images/docs/add_challenge.png differ diff --git a/website/static/images/docs/challenge.png b/website/static/images/docs/challenge.png new file mode 100644 index 0000000..645ecd6 Binary files /dev/null and b/website/static/images/docs/challenge.png differ diff --git a/website/static/images/docs/config_access_log.png b/website/static/images/docs/config_access_log.png new file mode 100644 index 0000000..2598dbe Binary files /dev/null and b/website/static/images/docs/config_access_log.png differ diff --git a/website/static/images/docs/flow.png b/website/static/images/docs/flow.png new file mode 100644 index 0000000..0429f24 Binary files /dev/null and b/website/static/images/docs/flow.png differ diff --git a/website/static/images/docs/get_source_ip.png b/website/static/images/docs/get_source_ip.png new file mode 100644 index 0000000..ccbfba1 Binary files /dev/null and b/website/static/images/docs/get_source_ip.png differ diff --git a/website/static/images/docs/guide_install/collie_apps.png b/website/static/images/docs/guide_install/collie_apps.png new file mode 100644 index 0000000..28ab326 Binary files /dev/null and b/website/static/images/docs/guide_install/collie_apps.png differ diff --git a/website/static/images/docs/guide_introduction/website_with_safeline.png b/website/static/images/docs/guide_introduction/website_with_safeline.png new file mode 100644 index 0000000..da7eb20 Binary files /dev/null and b/website/static/images/docs/guide_introduction/website_with_safeline.png differ diff --git a/website/static/images/docs/guide_introduction/website_without_safeline.png b/website/static/images/docs/guide_introduction/website_without_safeline.png new file mode 100644 index 0000000..1231bbc Binary files /dev/null and b/website/static/images/docs/guide_introduction/website_without_safeline.png differ diff --git a/website/static/images/docs/manual.png b/website/static/images/docs/manual.png new file mode 100644 index 0000000..581794c Binary files /dev/null and b/website/static/images/docs/manual.png differ diff --git a/website/static/images/docs/safeline_https_website.gif b/website/static/images/docs/safeline_https_website.gif new file mode 100644 index 0000000..dc3c4d0 Binary files /dev/null and b/website/static/images/docs/safeline_https_website.gif differ diff --git a/website/static/images/docs/server_index01.png b/website/static/images/docs/server_index01.png new file mode 100644 index 0000000..c6a8e72 Binary files /dev/null and b/website/static/images/docs/server_index01.png differ diff --git a/website/static/images/docs/server_index02.png b/website/static/images/docs/server_index02.png new file mode 100644 index 0000000..8958ef7 Binary files /dev/null and b/website/static/images/docs/server_index02.png differ diff --git a/website/static/images/docs/tengine_502.png b/website/static/images/docs/tengine_502.png new file mode 100644 index 0000000..43432c5 Binary files /dev/null and b/website/static/images/docs/tengine_502.png differ diff --git a/website/static/images/favicon.ico b/website/static/images/favicon.ico new file mode 100644 index 0000000..bcba385 Binary files /dev/null and b/website/static/images/favicon.ico differ diff --git a/website/static/images/feature.svg b/website/static/images/feature.svg new file mode 100644 index 0000000..16ec807 --- /dev/null +++ b/website/static/images/feature.svg @@ -0,0 +1,40 @@ + + + 编组 4备份 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/static/images/gif/config_site.gif b/website/static/images/gif/config_site.gif new file mode 100644 index 0000000..41d4b51 Binary files /dev/null and b/website/static/images/gif/config_site.gif differ diff --git a/website/static/images/gif/detect_log.gif b/website/static/images/gif/detect_log.gif new file mode 100644 index 0000000..9193696 Binary files /dev/null and b/website/static/images/gif/detect_log.gif differ diff --git a/website/static/images/gif/login.gif b/website/static/images/gif/login.gif new file mode 100644 index 0000000..3f5c7c1 Binary files /dev/null and b/website/static/images/gif/login.gif differ diff --git a/website/static/images/github.png b/website/static/images/github.png new file mode 100755 index 0000000..9490ffc Binary files /dev/null and b/website/static/images/github.png differ diff --git a/website/static/images/logo.png b/website/static/images/logo.png new file mode 100755 index 0000000..1ef614b Binary files /dev/null and b/website/static/images/logo.png differ diff --git a/website/static/images/logo.svg b/website/static/images/logo.svg new file mode 100644 index 0000000..2253368 --- /dev/null +++ b/website/static/images/logo.svg @@ -0,0 +1,74 @@ + + + 编组 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/static/images/qq.png b/website/static/images/qq.png new file mode 100755 index 0000000..5abcb10 Binary files /dev/null and b/website/static/images/qq.png differ diff --git a/website/static/images/safeline.png b/website/static/images/safeline.png new file mode 100755 index 0000000..3a3485d Binary files /dev/null and b/website/static/images/safeline.png differ diff --git a/website/static/images/wechat-230717.png b/website/static/images/wechat-230717.png new file mode 100644 index 0000000..8dbffe3 Binary files /dev/null and b/website/static/images/wechat-230717.png differ diff --git a/website/static/images/wechat-light.png b/website/static/images/wechat-light.png new file mode 100644 index 0000000..8dbffe3 Binary files /dev/null and b/website/static/images/wechat-light.png differ diff --git a/website/static/images/wechat-logo.png b/website/static/images/wechat-logo.png new file mode 100644 index 0000000..07cb558 Binary files /dev/null and b/website/static/images/wechat-logo.png differ diff --git a/website/static/images/wechat.png b/website/static/images/wechat.png new file mode 100644 index 0000000..8dbffe3 Binary files /dev/null and b/website/static/images/wechat.png differ diff --git a/website/tsconfig.json b/website/tsconfig.json new file mode 100644 index 0000000..6f47569 --- /dev/null +++ b/website/tsconfig.json @@ -0,0 +1,7 @@ +{ + // This file is not used in compilation. It is here just for a nice editor experience. + "extends": "@tsconfig/docusaurus/tsconfig.json", + "compilerOptions": { + "baseUrl": "." + } +}