470 lines
17 KiB
Markdown
470 lines
17 KiB
Markdown
# mysql-tunnel
|
||
|
||
Windows 本机 MySQL 协议代理,给 **影刀 RPA**(以及其它只认 MySQL 协议的客户端)用。
|
||
|
||
影刀连的是本机 `127.0.0.1:13306`。本进程把这条连接翻译成 **Navicat HTTP Tunnel** 协议,再 POST 到服务器上的 `ntunnel_mysql.php`,由 PHP 去连远程 MySQL。
|
||
|
||
```
|
||
影刀 RPA / mysql 客户端
|
||
│ MySQL 协议(握手、认证、COM_QUERY)
|
||
▼
|
||
127.0.0.1:13306
|
||
│
|
||
mysql-tunnel.exe ← 本仓库编译出的 Windows 程序
|
||
│ HTTP POST application/x-www-form-urlencoded
|
||
│ URL 带 ?token=...
|
||
▼
|
||
ntunnel_mysql.php ← 部署在能访问远程 MySQL 的 Web 主机上
|
||
│ mysqli_connect + mysqli_real_query
|
||
▼
|
||
远程 MySQL:3306
|
||
```
|
||
|
||
这 **不是** TCP-over-HTTP 端口转发,也 **不是** 业务 REST API。每一条 SQL 都走一次独立的 HTTP 请求。
|
||
|
||
协议细节见 [docs/PROTOCOL.md](docs/PROTOCOL.md)。开发 / 换图标 / 打包见 [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)。
|
||
|
||
---
|
||
|
||
## 当前状态
|
||
|
||
**可用(v0.5.0):** 本机 MySQL Server(默认 `127.0.0.1:13306`)已接通 Navicat HTTP 隧道。Handshake、`mysql_native_password`、`COM_QUERY` / `COM_PING` / `COM_QUIT` / `COM_INIT_DB` 可用。Windows 双击 exe 打开配置窗口(保存、测试、启动/停止、托盘)。可选安装为 Windows 服务。
|
||
|
||
---
|
||
|
||
## 工作原理
|
||
|
||
仓库里的 `ntunnel_mysql.php` 是 Navicat 16 官方隧道脚本(script version **206**),外加 GET `token` 门禁。
|
||
|
||
每次 HTTP POST,PHP 都会:
|
||
|
||
1. 校验 URL 上的 `?token=`
|
||
2. `mysqli_connect(host, login, password, …)`
|
||
3. `mysqli_set_charset(..., 'UTF8')`
|
||
4. 可选 `mysqli_select_db`
|
||
5. `actn=C` 返回服务器信息,或 `actn=Q` 执行 `q[]` 里的 SQL
|
||
6. 脚本结束,**远程 MySQL 连接被丢掉**
|
||
|
||
因此:
|
||
|
||
- **无持久会话。** `START TRANSACTION` / `COMMIT` 不能跨两次 HTTP 请求。
|
||
- `SET` 会话变量、`last_insert_id()` 只在同一次 POST 的 `q[]` 里有效。
|
||
- 影刀侧的 `USE db` 必须由 mysql-tunnel **记住**,并在后续请求里带上 `db=`。
|
||
- 大结果集会整包进 PHP 内存再作为 HTTP 响应返回。
|
||
|
||
`config.yaml` 里的 `charset` **不是** Navicat POST 字段。PHP 固定 UTF8;本客户端在 `actn=Q` 时会把 `SET NAMES utf8mb4`(若配置了非 utf8 字符集)作为第一条 SQL 一并发送。
|
||
|
||
---
|
||
|
||
## 你需要准备什么
|
||
|
||
| 角色 | 是什么 | 谁提供 |
|
||
|---|---|---|
|
||
| 本机程序 | `mysql-tunnel.exe` | 本仓库编译,或已有的 `dist\mysql-tunnel.exe` |
|
||
| 配置文件 | `config.yaml` | 从 `config.example.yaml` 复制后填写 |
|
||
| 隧道脚本 | `ntunnel_mysql.php` | 上传到能访问目标 MySQL 的 PHP 主机 |
|
||
| 访问令牌 | URL `?token=` 与 PHP `$accessToken` 必须一致 | 你自己设定 |
|
||
| 远程 MySQL | PHP 主机能连上的库(常是 `127.0.0.1` / Docker 名 `mysql`) | 运维 / 现有环境 |
|
||
| 影刀 | 把数据源指到本机监听地址 | 本机 `mysql-tunnel.exe` 保持运行 |
|
||
|
||
环境要求:
|
||
|
||
- **Windows**(目标运行环境)。Linux/macOS 可编译、跑单测,但产品面向影刀本机。
|
||
- **Go 1.24+**(仅构建需要;运行 `mysql-tunnel.exe` 不需要装 Go)。
|
||
- PHP 主机需启用 **mysqli**,并能访问目标 MySQL。
|
||
- 本机到隧道 URL 的 HTTP/HTTPS 网络畅通(公司代理走系统环境变量)。
|
||
|
||
第三方依赖:`gopkg.in/yaml.v3`(配置)、`github.com/lxn/walk`(Windows 配置窗口,无 CGO)。`github.com/go-sql-driver/mysql` 仅用于兼容性测试,不会链进 `mysql-tunnel.exe`。Windows 服务使用 `golang.org/x/sys/windows/svc`。
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
### 1. 编译
|
||
|
||
Go 已加入 `PATH` 后,在仓库根目录:
|
||
|
||
```bat
|
||
go mod tidy
|
||
go test ./...
|
||
build.bat
|
||
```
|
||
|
||
产物:`dist\mysql-tunnel.exe`。`build.bat` 会先跑完全部测试,再以 `CGO_ENABLED=0` 生成精简二进制。
|
||
|
||
等价命令:
|
||
|
||
```bat
|
||
go test ./...
|
||
go build -trimpath -ldflags="-s -w" -o dist\mysql-tunnel.exe .\cmd\tunnel
|
||
```
|
||
|
||
有 Make 时也可以 `make test` / `make build`(后者交叉编译 Windows amd64)。
|
||
|
||
### 2. 部署 PHP 隧道
|
||
|
||
把 `ntunnel_mysql.php` 放到 Web 根目录或由反向代理转到该文件。编辑文件顶部:
|
||
|
||
```php
|
||
$accessToken = '换成足够长的随机串';
|
||
$allowTestMenu = false;
|
||
```
|
||
|
||
浏览器访问(应看到 403 文本,说明门禁在工作):
|
||
|
||
```
|
||
https://你的域名/ntunnel_mysql.php
|
||
```
|
||
|
||
带正确 token、无 POST 时,脚本返回二进制错误包(errno 202),而不是 HTML 测试页。
|
||
|
||
URL 路径可以重写。客户端只要 POST 到配置里的完整 URL(**必须带** `?token=`)。例如:
|
||
|
||
```
|
||
https://example.com/ntunnel_mysql.php?token=YOUR_SECRET
|
||
https://ntunnel.example.com/?token=YOUR_SECRET
|
||
```
|
||
|
||
Token **只** 放在 query string,不要放 POST body,也不要用 `Authorization` 头。PHP 只读 `$_GET['token']`。
|
||
|
||
### 3. 填写配置
|
||
|
||
双击 `dist\mysql-tunnel.exe` 会打开配置窗口(无参数时默认如此)。把隧道 URL、远程库账号等填进表单,点 **保存**(写入 exe 旁的 `config.yaml`)。窗口可最小化到任务栏;点关闭会藏到托盘,监听可继续。托盘图标双击还原,右键退出。
|
||
|
||
也可以复制模板后用编辑器改:
|
||
|
||
```bat
|
||
copy config.example.yaml config.yaml
|
||
```
|
||
|
||
**不要把 `config.yaml` 提交进 Git**(已在 `.gitignore`)。密钥只放在 yaml 里,不会写进 exe。
|
||
|
||
### 4. 验证隧道
|
||
|
||
在配置窗口点 **测试连接**,或在仓库根目录(或把 `config.yaml` 拷到 exe 同级目录):
|
||
|
||
```bat
|
||
dist\mysql-tunnel.exe --test
|
||
```
|
||
|
||
成功时应类似:
|
||
|
||
```
|
||
mysql-tunnel --test
|
||
Tunnel URL: https://example.com/ntunnel_mysql.php?token=***
|
||
Tunnel: OK
|
||
Remote MySQL: OK
|
||
Query SELECT 1: OK
|
||
```
|
||
|
||
失败时先看「常见问题」。日志默认写到 `logs\tunnel.log`(密码和 token 会被打码)。
|
||
|
||
### 5. 日常启动
|
||
|
||
**推荐:** 双击 `mysql-tunnel.exe`,在窗口里点 **启动**。可最小化;关窗口进托盘,进程还在。
|
||
|
||
不要窗口、前台监听(关控制台即退出):
|
||
|
||
```bat
|
||
dist\mysql-tunnel.exe --console
|
||
dist\mysql-tunnel.exe --config config.yaml
|
||
```
|
||
|
||
**Windows 服务(开机自启、后台运行):** 先填好 `config.yaml`,再**以管理员**运行:
|
||
|
||
```bat
|
||
install-service.bat
|
||
```
|
||
|
||
或:
|
||
|
||
```bat
|
||
dist\mysql-tunnel.exe --install --config config.yaml
|
||
```
|
||
|
||
安装时会把 `config.yaml` **复制到 exe 同目录**(`dist\config.yaml`),服务始终读这份配置,不依赖当前工作目录。改配置后执行:
|
||
|
||
```bat
|
||
dist\mysql-tunnel.exe --restart
|
||
```
|
||
|
||
其它:
|
||
|
||
```bat
|
||
dist\mysql-tunnel.exe --stop
|
||
dist\mysql-tunnel.exe --start
|
||
dist\mysql-tunnel.exe --uninstall
|
||
```
|
||
|
||
卸载也可用 `uninstall-service.bat`(需管理员)。请先 `--test` 全绿再安装服务。
|
||
|
||
---
|
||
|
||
## 配置说明
|
||
|
||
完整示例见 `config.example.yaml`。
|
||
|
||
```yaml
|
||
listen:
|
||
host: 127.0.0.1
|
||
port: 13306
|
||
|
||
local_auth:
|
||
username: root
|
||
password: "123456"
|
||
|
||
tunnel:
|
||
url: "https://example.com/ntunnel_mysql.php?token=YOUR_TOKEN"
|
||
encode_base64: false
|
||
tls_insecure_skip_verify: false
|
||
|
||
remote:
|
||
host: "mysql"
|
||
port: 3306
|
||
username: "root"
|
||
password: "YOUR_PASSWORD"
|
||
database: "your_database"
|
||
charset: "utf8mb4"
|
||
|
||
options:
|
||
request_timeout: 30
|
||
max_connections: 5
|
||
log_level: info
|
||
log_sql: false
|
||
log_file: logs/tunnel.log
|
||
```
|
||
|
||
### `listen` — 本机给影刀听的地址
|
||
|
||
| 字段 | 默认 | 说明 |
|
||
|---|---|---|
|
||
| `host` | `127.0.0.1` | 只应绑回环。`0.0.0.0` / `::` 会被拒绝。非回环地址会打启动警告。 |
|
||
| `port` | `13306` | 影刀里填的端口。可改成未被占用的端口(如 `3309`)。 |
|
||
|
||
CLI `--host` / `--port` 会覆盖这两项。
|
||
|
||
### `local_auth` — 影刀连本机时用的账号
|
||
|
||
这是 **mysql-tunnel 自己校验** 的用户名/密码,**不必** 和远程 MySQL 相同。远程凭据只写在 `remote.*`。
|
||
|
||
影刀数据源填这里的用户名和密码(与远程 MySQL 账号可以不同)。
|
||
|
||
### `tunnel` — HTTP 隧道
|
||
|
||
| 字段 | 说明 |
|
||
|---|---|
|
||
| `url` | 完整 URL,**必须含** `?token=...`,且与 PHP `$accessToken` 一致。支持 `http` 和 `https`。 |
|
||
| `encode_base64` | `true` 时给每条 SQL 做 Base64 并带 `encodeBase64=1`,用于部分 WAF。默认 `false`。 |
|
||
| `tls_insecure_skip_verify` | `true` 时跳过 HTTPS 证书校验。仅排障用,启动会打 **SECURITY** 警告。生产保持 `false`。 |
|
||
|
||
### `remote` — PHP 主机眼里的 MySQL
|
||
|
||
**最容易配错的一组。** `host` 是 PHP **所在机器** 访问 MySQL 时用的地址,不是你笔记本上的公网 IP。
|
||
|
||
| 常见部署 | `remote.host` 应填 |
|
||
|---|---|
|
||
| PHP 与 MySQL 同机 | `127.0.0.1` 或 `localhost` |
|
||
| Docker Compose 服务名 `mysql` | `mysql` |
|
||
| 内网另一台库 | 那台机在 PHP 所在网段的内网 IP / 主机名 |
|
||
|
||
| 字段 | 说明 |
|
||
|---|---|
|
||
| `port` | 远程 MySQL 端口,默认 `3306`。 |
|
||
| `username` / `password` | 远程 MySQL 账号。密码可为空(若库允许)。 |
|
||
| `database` | 默认库;空字符串则 PHP 不 `select_db`。 |
|
||
| `charset` | 本客户端用 `SET NAMES` 补发。PHP 本身仍先 `UTF8`。`utf8` / `utf8mb3` 不再额外发 SET。 |
|
||
|
||
### `options`
|
||
|
||
| 字段 | 默认 | 说明 |
|
||
|---|---|---|
|
||
| `request_timeout` | `30` | 单次 HTTP 超时(秒),须 `> 0`。大查询可加大。 |
|
||
| `max_connections` | `5` | 本机并发连接上限。须 `>= 1`。 |
|
||
| `log_level` | `info` | `debug` / `info` / `warn` / `error`。 |
|
||
| `log_sql` | `false` | `true` 时记录 SQL 文本(仍会脱敏 token/密码)。排障后再关掉。 |
|
||
| `log_file` | `logs/tunnel.log` | 日志目录与前缀。实际文件为 `logs/tunnel-YYYY-MM-DD.log`,按日切割,**仅保留 7 天**。空路径则只打控制台。 |
|
||
|
||
Windows 启动时会提醒:`config.yaml` 含密钥,请用 NTFS 权限限制为本机账户可读。
|
||
|
||
---
|
||
|
||
## 命令行
|
||
|
||
```bat
|
||
mysql-tunnel.exe
|
||
mysql-tunnel.exe --gui
|
||
mysql-tunnel.exe --gui --config config.yaml
|
||
mysql-tunnel.exe --console
|
||
mysql-tunnel.exe --config config.yaml
|
||
mysql-tunnel.exe --host 127.0.0.1 --port 13306
|
||
mysql-tunnel.exe --test
|
||
mysql-tunnel.exe --version
|
||
mysql-tunnel.exe -h
|
||
```
|
||
|
||
Windows 下无参数(双击)打开配置窗口。`--config` 且未加 `--gui` 时仍走命令行监听。
|
||
|
||
| 参数 | 说明 |
|
||
|---|---|
|
||
| `--gui` | 打开配置窗口(编辑 yaml、测试、启动/停止、最小化/托盘)。 |
|
||
| `--console` | 前台监听,不打开窗口。 |
|
||
| `--config` | 配置文件路径。CLI 默认当前目录 `config.yaml`;GUI 默认 exe 旁。 |
|
||
| `--host` | 覆盖 `listen.host`。 |
|
||
| `--port` | 覆盖 `listen.port`。 |
|
||
| `--test` | POST `actn=C`,再 POST `actn=Q` + `SELECT 1`。不监听端口。 |
|
||
| `--version` | 打印版本并退出。 |
|
||
| `--install` / `--uninstall` / `--start` / `--stop` / `--restart` | Windows 服务(需管理员安装)。 |
|
||
|
||
退出码:`0` 成功,`1` 配置/隧道/查询失败,`2` 参数解析错误。
|
||
|
||
`--test` 分层含义:
|
||
|
||
| 输出 | 含义 |
|
||
|---|---|
|
||
| `Tunnel: OK` | HTTP 通了,响应是 Navicat 二进制头(magic=1111)。 |
|
||
| `Remote MySQL: OK` | PHP 连上了 `remote.*`。 |
|
||
| `Query SELECT 1: OK` | 查询返回一行一列 `"1"`。 |
|
||
| `Tunnel: FAIL` | 网络、403、token、HTML 拦截页、非法二进制。 |
|
||
| `Remote MySQL: FAIL` | 隧道层正常,但 MySQL 账号/主机/库不对(如 1045)。 |
|
||
|
||
脚本 version 不是 202/206 时会提示并继续(magic 仍须正确)。
|
||
|
||
---
|
||
|
||
## 给影刀配置
|
||
|
||
**不要** 把影刀的 MySQL 主机填成 HTTP 隧道 URL。影刀只会说 MySQL 协议。
|
||
|
||
| 影刀字段 | 填什么 |
|
||
|---|---|
|
||
| 主机 | `127.0.0.1`(与 `listen.host` 一致) |
|
||
| 端口 | `13306`(与 `listen.port` 一致) |
|
||
| 用户名 | `local_auth.username` |
|
||
| 密码 | `local_auth.password` |
|
||
| 数据库 | 可选;不填则用配置里的 `remote.database` |
|
||
|
||
流程:配置窗口「测试连接」全绿(或 `--test`)→ 点「启动」保持运行(可最小化/托盘)→ 影刀再连本机端口。
|
||
|
||
本机命令行客户端也可:
|
||
|
||
```bat
|
||
mysql -h 127.0.0.1 -P 13306 -u root -p
|
||
SELECT 1;
|
||
```
|
||
|
||
---
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
cmd/tunnel 入口:GUI / --console / --test / Windows 服务
|
||
internal/app 测试隧道、启动本机监听(CLI 与 GUI 共用)
|
||
internal/gui Windows 配置窗口 + 托盘
|
||
internal/config YAML 加载/保存、校验、URL 脱敏
|
||
internal/logger 文件日志,脱敏 token / 密码 / Authorization
|
||
internal/tunnel HTTP 客户端(TLS、超时、keep-alive)
|
||
internal/navicat actn=C / actn=Q、二进制解析、mock 隧道
|
||
internal/mysqlserver 本机 MySQL 握手 / 认证 / COM_QUERY / 结果集
|
||
internal/winsvc Windows 服务安装与运行
|
||
ntunnel_mysql.php 部署到远端的隧道脚本
|
||
config.example.yaml 配置模板
|
||
docs/PROTOCOL.md 协议对照 PHP 源码的说明
|
||
docs/DEVELOPMENT.md 开发说明(构建、换图标、服务、测试)
|
||
build.bat Windows 一键测试 + 编译
|
||
```
|
||
|
||
---
|
||
|
||
## 自动化测试
|
||
|
||
```bat
|
||
go test ./...
|
||
```
|
||
|
||
包内测试(`internal/.../*_test.go`):
|
||
|
||
- `config`:YAML、监听绑定规则、URL 脱敏
|
||
- `logger`:token / 密码脱敏
|
||
- `tunnel`:HTTP 客户端、Navicat magic 头嗅探
|
||
- `navicat`:GetBlock / 头解析、`actn=C/Q`、mock `ntunnel_mysql.php`
|
||
- `mysqlserver`:握手、native 密码、COM_QUERY/PING/QUIT、ResultSet、与 mock 隧道联调;另用官方 Go MySQL 驱动做兼容性测试
|
||
|
||
Mock 覆盖:`SELECT 1`、NULL、BIGINT(文本)、DECIMAL(文本)、utf8mb4 中文、SQL errno 1064、DML OK、错误 token(204)。
|
||
|
||
`--test` 才打真实隧道;单测默认不访问外网。
|
||
|
||
---
|
||
|
||
## 常见问题
|
||
|
||
| 现象 | 原因与处理 |
|
||
|---|---|
|
||
| `config error: ...` | YAML 缺必填项或非法。对照 `config.example.yaml`。 |
|
||
| HTTP 403 文本 `invalid or missing token` | GET 无 token 或 token 错。检查 URL `?token=` 与 `$accessToken`。 |
|
||
| 二进制 errno **204** `invalid access token` | POST 带了错误 token(HTTP 往往仍是 200)。 |
|
||
| 二进制 errno **202** `invalid parameters` | 缺 `actn` / `host` / `port` / `login`。 |
|
||
| MySQL **1045** | `remote.username` / `password` 被远程拒绝。 |
|
||
| `Tunnel: FAIL` 且提示 HTML | URL 指到了登录页、WAF 拦截页或错误的 PHP 路径。用浏览器看该 URL(无 token 应 403 纯文本)。 |
|
||
| 连得上隧道但连不上库 | `remote.host` 填了公网 IP。应填 PHP 主机访问 MySQL 的地址。 |
|
||
| 中文乱码 | 把 `remote.charset` 设为 `utf8mb4`(客户端会发 `SET NAMES utf8mb4`)。 |
|
||
| 超时 | 加大 `options.request_timeout`;大 `SELECT` 会占满 PHP 内存。 |
|
||
| 事务丢了 | 正常现象:每次 HTTP 都是新连接。事务必须写在同一次请求里(本机多语句映射是后续阶段)。 |
|
||
| `listen.host must not be 0.0.0.0` | 禁止对全网卡暴露本机 MySQL 口。保持 `127.0.0.1`。 |
|
||
| Go is not on PATH | 安装 [Go 1.24+](https://go.dev/dl/) 后重开终端再跑 `build.bat`。 |
|
||
|
||
隧道层 errno(PHP 定义,不是 MySQL):
|
||
|
||
| errno | 含义 |
|
||
|---|---|
|
||
| 201 | PHP 版本过低 |
|
||
| 202 | 参数无效 |
|
||
| 203 | 服务器无 MySQL 扩展 |
|
||
| 204 | token 无效 |
|
||
| 其它 | 一般为 `mysqli` 错误号(1045、1049、1146、1064…) |
|
||
|
||
---
|
||
|
||
## 安全
|
||
|
||
- 默认只监听 `127.0.0.1`。绑定非回环等于把远程库暴露给能连上该端口的人(仅靠 `local_auth`)。
|
||
- 日志不写 MySQL 密码、HTTP token、`Authorization`。出错信息也会脱敏。
|
||
- 默认校验证书。`tls_insecure_skip_verify: true` 仅临时排障。
|
||
- `ntunnel_mysql.php` 里的 `$accessToken` 视为密钥。**仓库里的默认值必须在部署前改掉。** 脚本泄露或曾公开过旧 token 时立刻轮换,并同步改 `config.yaml`。
|
||
- 不要提交 `config.yaml`。示例文件里只放占位符。本地 `config.yaml` / `logs/` / `dist/` 已在 `.gitignore`。
|
||
- Token 出现在 URL 中,可能进入 Web 访问日志。用 HTTPS,并限制 PHP 主机访问来源。
|
||
|
||
---
|
||
|
||
## 能力与限制
|
||
|
||
当前 `--test` / Navicat 客户端已支持:连接探测、任意 SQL 转发(`SHOW` / `DESC` / `SELECT` / DML 都是 `actn=Q`)。
|
||
|
||
已知限制(协议如此,不是漏实现):
|
||
|
||
- 无服务端会话;预处理语句(`COM_STMT_*`)要到更后阶段在本机模拟。
|
||
- `insert_id` / `affected_rows` 在 PHP 里是 32 位,超大 AUTO_INCREMENT 会截断。
|
||
- BIGINT / DECIMAL 以 **文本** 传递,客户端不解析成 Go 数值。
|
||
- 响应无 SQLSTATE;本机侧多数会用 `HY000`。
|
||
|
||
---
|
||
|
||
## 开发阶段(已完成)
|
||
|
||
开发约定、构建与 **更换图标** 见 [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)。
|
||
|
||
1. 协议调研 — 见 `docs/PROTOCOL.md`
|
||
2. Navicat 客户端 `actn=C` / `actn=Q` + 解析
|
||
3. 本机 MySQL 服务(握手、认证、`COM_QUERY`、PING、QUIT)
|
||
4. 接到隧道客户端 + ResultSet / DML / `COM_INIT_DB`
|
||
5. Windows GUI / 托盘 / 服务安装
|
||
6. 构建脚本与端到端测试(`go test` + `tests/`)
|
||
|
||
已知后续方向:预处理语句本地模拟、跨 HTTP 事务(协议本身不支持)、多语句拆分策略。
|
||
|
||
实现约定:只说 Navicat 隧道这一套协议——token 放 query、POST `application/x-www-form-urlencoded`、解析 16 字节 `EchoHeader` + `GetBlock`,不发明 JSON API。
|
||
|
||
## License
|
||
|
||
MIT — 见 [LICENSE](LICENSE)。
|
||
)
|