From ad2b0a7d3e4837cdd1f83865eb0e27691ae3c5e8 Mon Sep 17 00:00:00 2001 From: Yangxt Date: Mon, 21 Sep 2026 16:46:55 +0800 Subject: [PATCH] app-init --- .gitignore | 36 ++ LICENSE | 21 + Makefile | 10 + README.md | 8 +- build.bat | 32 ++ cmd/tunnel/app.ico | Bin 0 -> 61420 bytes cmd/tunnel/app.manifest | 15 + cmd/tunnel/main.go | 219 ++++++++ cmd/tunnel/rsrc_windows_amd64.syso | Bin 0 -> 62576 bytes config.example.yaml | 30 ++ docs/PROTOCOL.md | 303 +++++++++++ go.mod | 16 + go.sum | 17 + install-service.bat | 26 + internal/app/app.go | 139 ++++++ internal/config/config.go | 296 +++++++++++ internal/config/config_test.go | 154 ++++++ internal/config/path.go | 62 +++ internal/config/path_test.go | 50 ++ internal/config/save.go | 99 ++++ internal/config/save_test.go | 71 +++ internal/config/yaml.go | 7 + internal/gui/gui.go | 13 + internal/gui/gui_windows.go | 692 ++++++++++++++++++++++++++ internal/gui/logo.png | Bin 0 -> 186603 bytes internal/logger/logger.go | 262 ++++++++++ internal/logger/logger_test.go | 87 ++++ internal/mysqlserver/auth.go | 44 ++ internal/mysqlserver/conn.go | 105 ++++ internal/mysqlserver/doc.go | 3 + internal/mysqlserver/driver_test.go | 57 +++ internal/mysqlserver/handshake.go | 138 +++++ internal/mysqlserver/protocol.go | 263 ++++++++++ internal/mysqlserver/protocol_test.go | 122 +++++ internal/mysqlserver/result.go | 95 ++++ internal/mysqlserver/server.go | 194 ++++++++ internal/mysqlserver/server_test.go | 423 ++++++++++++++++ internal/mysqlserver/session.go | 168 +++++++ internal/navicat/binary.go | 76 +++ internal/navicat/client.go | 177 +++++++ internal/navicat/client_test.go | 196 ++++++++ internal/navicat/doc.go | 26 + internal/navicat/error.go | 45 ++ internal/navicat/mock.go | 228 +++++++++ internal/navicat/parser.go | 116 +++++ internal/navicat/parser_test.go | 205 ++++++++ internal/navicat/request.go | 63 +++ internal/navicat/response.go | 266 ++++++++++ internal/tunnel/client.go | 131 +++++ internal/tunnel/client_test.go | 88 ++++ internal/winsvc/name.go | 7 + internal/winsvc/other.go | 40 ++ internal/winsvc/windows.go | 212 ++++++++ tests/http_tunnel.py | 635 +++++++++++++++++++++++ tests/requirements.txt | 1 + tests/rpa_tunnel.py | 31 ++ tools/mkico/main.go | 69 +++ uninstall-service.bat | 20 + 58 files changed, 6906 insertions(+), 3 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 build.bat create mode 100644 cmd/tunnel/app.ico create mode 100644 cmd/tunnel/app.manifest create mode 100644 cmd/tunnel/main.go create mode 100644 cmd/tunnel/rsrc_windows_amd64.syso create mode 100644 config.example.yaml create mode 100644 docs/PROTOCOL.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 install-service.bat create mode 100644 internal/app/app.go create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/config/path.go create mode 100644 internal/config/path_test.go create mode 100644 internal/config/save.go create mode 100644 internal/config/save_test.go create mode 100644 internal/config/yaml.go create mode 100644 internal/gui/gui.go create mode 100644 internal/gui/gui_windows.go create mode 100644 internal/gui/logo.png create mode 100644 internal/logger/logger.go create mode 100644 internal/logger/logger_test.go create mode 100644 internal/mysqlserver/auth.go create mode 100644 internal/mysqlserver/conn.go create mode 100644 internal/mysqlserver/doc.go create mode 100644 internal/mysqlserver/driver_test.go create mode 100644 internal/mysqlserver/handshake.go create mode 100644 internal/mysqlserver/protocol.go create mode 100644 internal/mysqlserver/protocol_test.go create mode 100644 internal/mysqlserver/result.go create mode 100644 internal/mysqlserver/server.go create mode 100644 internal/mysqlserver/server_test.go create mode 100644 internal/mysqlserver/session.go create mode 100644 internal/navicat/binary.go create mode 100644 internal/navicat/client.go create mode 100644 internal/navicat/client_test.go create mode 100644 internal/navicat/doc.go create mode 100644 internal/navicat/error.go create mode 100644 internal/navicat/mock.go create mode 100644 internal/navicat/parser.go create mode 100644 internal/navicat/parser_test.go create mode 100644 internal/navicat/request.go create mode 100644 internal/navicat/response.go create mode 100644 internal/tunnel/client.go create mode 100644 internal/tunnel/client_test.go create mode 100644 internal/winsvc/name.go create mode 100644 internal/winsvc/other.go create mode 100644 internal/winsvc/windows.go create mode 100644 tests/http_tunnel.py create mode 100644 tests/requirements.txt create mode 100644 tests/rpa_tunnel.py create mode 100644 tools/mkico/main.go create mode 100644 uninstall-service.bat diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c349ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Secrets & local runtime (any path named config.yaml) +config.yaml +/dist/ +/logs/ +*.log + +# Binaries & test artifacts +*.exe +*.test +*.out +*.dll + +# Python (tests/) +__pycache__/ +*.py[cod] +.venv/ +venv/ + +# Editor / OS +.idea/ +.vscode/ +.cursor/ +*.swp +*~ +.DS_Store +Thumbs.db +Desktop.ini + +# Go +vendor/ +go.work +go.work.sum + +# Env files (defensive; project uses YAML) +.env +.env.* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a9c97d7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 mysql-tunnel contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..371ab26 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY: test build tidy + +tidy: + go mod tidy + +test: + go test ./... + +build: + GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o dist/mysql-tunnel.exe ./cmd/tunnel diff --git a/README.md b/README.md index d75b1cd..d28dba7 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Windows 本机 MySQL 协议代理,给 **影刀 RPA**(以及其它只认 MySQ 这 **不是** TCP-over-HTTP 端口转发,也 **不是** 业务 REST API。每一条 SQL 都走一次独立的 HTTP 请求。 -协议细节见 [docs/PROTOCOL.md](docs/PROTOCOL.md)。 +协议细节见 [docs/PROTOCOL.md](docs/PROTOCOL.md)。开发 / 换图标 / 打包见 [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)。 --- @@ -282,7 +282,7 @@ CLI `--host` / `--port` 会覆盖这两项。 | `max_connections` | `5` | 本机并发连接上限。须 `>= 1`。 | | `log_level` | `info` | `debug` / `info` / `warn` / `error`。 | | `log_sql` | `false` | `true` 时记录 SQL 文本(仍会脱敏 token/密码)。排障后再关掉。 | -| `log_file` | `logs/tunnel.log` | 同时写 stdout 和该文件。空路径则只打控制台。 | +| `log_file` | `logs/tunnel.log` | 日志目录与前缀。实际文件为 `logs/tunnel-YYYY-MM-DD.log`,按日切割,**仅保留 7 天**。空路径则只打控制台。 | Windows 启动时会提醒:`config.yaml` 含密钥,请用 NTFS 权限限制为本机账户可读。 @@ -369,7 +369,7 @@ internal/winsvc Windows 服务安装与运行 ntunnel_mysql.php 部署到远端的隧道脚本 config.example.yaml 配置模板 docs/PROTOCOL.md 协议对照 PHP 源码的说明 -tests/README.md 单测范围说明 +docs/DEVELOPMENT.md 开发说明(构建、换图标、服务、测试) build.bat Windows 一键测试 + 编译 ``` @@ -450,6 +450,8 @@ Mock 覆盖:`SELECT 1`、NULL、BIGINT(文本)、DECIMAL(文本)、utf ## 开发阶段(已完成) +开发约定、构建与 **更换图标** 见 [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)。 + 1. 协议调研 — 见 `docs/PROTOCOL.md` 2. Navicat 客户端 `actn=C` / `actn=Q` + 解析 3. 本机 MySQL 服务(握手、认证、`COM_QUERY`、PING、QUIT) diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..de09d9c --- /dev/null +++ b/build.bat @@ -0,0 +1,32 @@ +@echo off +setlocal +cd /d "%~dp0" + +REM Prefer a typical Windows install; do not rely on where.exe (it may be missing from PATH). +if exist "%ProgramFiles%\Go\bin\go.exe" set "PATH=%ProgramFiles%\Go\bin;%PATH%" + +go version >nul 2>nul +if errorlevel 1 ( + echo Go is not on PATH. Install Go 1.24+ and re-run build.bat. + echo If Go is already installed, add its bin directory to PATH and open a new terminal. + exit /b 1 +) + +if not exist dist mkdir dist + +REM rsrc_windows_amd64.syso embeds Common Controls 6 and the app icon (required by the GUI). +REM After changing cmd\tunnel\app.ico or app.manifest, regenerate (see docs\DEVELOPMENT.md): +REM go run github.com/akavel/rsrc@latest -manifest cmd\tunnel\app.manifest -ico cmd\tunnel\app.ico -arch amd64 -o cmd\tunnel\rsrc_windows_amd64.syso +REM Optional PNG-to-ICO: go run .\tools\mkico +REM Keep internal\gui\logo.png in sync with app.ico for tray / in-window icon. + +set CGO_ENABLED=0 +go test ./... +if errorlevel 1 exit /b 1 + +go build -trimpath -ldflags="-s -w" -o dist\mysql-tunnel.exe .\cmd\tunnel +if errorlevel 1 exit /b 1 + +echo Built dist\mysql-tunnel.exe +dir dist\mysql-tunnel.exe +endlocal \ No newline at end of file diff --git a/cmd/tunnel/app.ico b/cmd/tunnel/app.ico new file mode 100644 index 0000000000000000000000000000000000000000..0309dfefc7ffb52b908bdeba17c527b2990d8508 GIT binary patch literal 61420 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 literal 0 HcmV?d00001 diff --git a/cmd/tunnel/app.manifest b/cmd/tunnel/app.manifest new file mode 100644 index 0000000..94a5aae --- /dev/null +++ b/cmd/tunnel/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + PerMonitorV2, PerMonitor + True + + + diff --git a/cmd/tunnel/main.go b/cmd/tunnel/main.go new file mode 100644 index 0000000..bdaa174 --- /dev/null +++ b/cmd/tunnel/main.go @@ -0,0 +1,219 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "os/signal" + "path/filepath" + "runtime" + "strconv" + "strings" + + "mysql-tunnel/internal/app" + "mysql-tunnel/internal/config" + "mysql-tunnel/internal/gui" + "mysql-tunnel/internal/logger" + "mysql-tunnel/internal/winsvc" +) + +const version = "0.5.0" + +func main() { + os.Exit(run(os.Args[1:])) +} + +func run(args []string) int { + fs := flag.NewFlagSet("mysql-tunnel", flag.ContinueOnError) + fs.SetOutput(os.Stderr) + configPath := fs.String("config", "config.yaml", "path to config.yaml") + listenHost := fs.String("host", "", "override listen.host") + listenPort := fs.Int("port", 0, "override listen.port") + doTest := fs.Bool("test", false, "POST actn=C and SELECT 1 through the HTTP tunnel") + showVersion := fs.Bool("version", false, "print version and exit") + doGUI := fs.Bool("gui", false, "open the configuration window (Windows)") + doConsole := fs.Bool("console", false, "listen in the foreground without a window") + doInstall := fs.Bool("install", false, "install Windows service (Administrator)") + doUninstall := fs.Bool("uninstall", false, "uninstall Windows service (Administrator)") + doStart := fs.Bool("start", false, "start Windows service") + doStop := fs.Bool("stop", false, "stop Windows service") + doRestart := fs.Bool("restart", false, "restart Windows service") + if err := fs.Parse(args); err != nil { + return 2 + } + + if *showVersion { + fmt.Println("mysql-tunnel", version) + return 0 + } + if *doInstall { + return cmdInstall(*configPath) + } + if *doUninstall { + return report(winsvc.Uninstall()) + } + if *doStart { + return report(winsvc.Start()) + } + if *doStop { + return report(winsvc.Stop()) + } + if *doRestart { + _ = winsvc.Stop() + return report(winsvc.Start()) + } + + if winsvc.IsService() { + return runHeadless(*configPath, *listenHost, *listenPort, false) + } + + if wantGUI(fs, *doGUI, *doConsole, *doTest) { + return gui.Run(*configPath) + } + + return runHeadless(*configPath, *listenHost, *listenPort, *doTest) +} + +func wantGUI(fs *flag.FlagSet, gui, console, doTest bool) bool { + if console || doTest { + return false + } + if gui { + return true + } + if runtime.GOOS != "windows" { + return false + } + // Double-click / no flags → GUI. `--config` without `--gui` stays CLI. + if flagWasSet(fs, "config") || flagWasSet(fs, "host") || flagWasSet(fs, "port") { + return false + } + return true +} + +func flagWasSet(fs *flag.FlagSet, name string) bool { + found := false + fs.Visit(func(f *flag.Flag) { + if f.Name == name { + found = true + } + }) + return found +} + +func runHeadless(configPath, listenHost string, listenPort int, doTest bool) int { + resolved, err := config.ResolveConfigPath(configPath) + if err != nil { + fmt.Fprintf(os.Stderr, "config error: %s\n", err) + return 1 + } + cfg, err := config.Load(resolved, config.Overrides{ + ListenHost: listenHost, + ListenPort: listenPort, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "config error: %s\n", logger.RedactError(err)) + return 1 + } + cfg.ResolvePaths() + + log, err := logger.New(cfg.LogFile, cfg.LogLevel, cfg.LogSQL) + if err != nil { + fmt.Fprintf(os.Stderr, "logger error: %v\n", err) + return 1 + } + defer log.Close() + + for _, w := range cfg.Warnings { + log.Warn(w) + } + + if doTest { + fmt.Println("mysql-tunnel --test") + r := app.Test(context.Background(), cfg, log) + fmt.Println(strings.ReplaceAll(r.Text(), "\r\n", "\n")) + if !r.OK() { + return 1 + } + return 0 + } + + runSrv := func(ctx context.Context) error { + return app.Serve(ctx, cfg, log) + } + + if winsvc.IsService() { + if err := winsvc.Run(runSrv); err != nil { + log.Error("service stopped", logger.RedactError(err)) + return 1 + } + return 0 + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + fmt.Printf("mysql-tunnel listening on %s:%d\n", cfg.ListenHost, cfg.ListenPort) + fmt.Println("Yingdao: Host=127.0.0.1 Port=" + strconv.Itoa(cfg.ListenPort) + " User=" + cfg.LocalUsername) + fmt.Println("Tunnel:", config.RedactedTunnelURL(cfg.TunnelURL)) + fmt.Println("Config:", cfg.Path) + fmt.Println("Log:", cfg.LogFile) + if err := runSrv(ctx); err != nil { + log.Error("server stopped", logger.RedactError(err)) + return 1 + } + log.Info("server stopped") + return 0 +} + +func cmdInstall(configFlag string) int { + exe, err := winsvc.ExePath() + if err != nil { + fmt.Fprintf(os.Stderr, "install: cannot locate exe: %v\n", err) + return 1 + } + src, err := config.ResolveConfigPath(configFlag) + if err != nil { + fmt.Fprintf(os.Stderr, "install: %s\n", err) + fmt.Fprintln(os.Stderr, "put a filled config.yaml next to mysql-tunnel.exe or pass --config") + return 1 + } + if _, err := config.Load(src, config.Overrides{}); err != nil { + fmt.Fprintf(os.Stderr, "install: invalid config: %s\n", logger.RedactError(err)) + return 1 + } + dest := filepath.Join(filepath.Dir(exe), "config.yaml") + srcAbs, _ := filepath.Abs(src) + destAbs, _ := filepath.Abs(dest) + if filepath.Clean(srcAbs) != filepath.Clean(destAbs) { + if err := winsvc.CopyFile(srcAbs, destAbs); err != nil { + fmt.Fprintf(os.Stderr, "install: copy config: %v\n", err) + return 1 + } + fmt.Println("copied config.yaml ->", destAbs) + } + if err := winsvc.Install(exe, destAbs); err != nil { + fmt.Fprintf(os.Stderr, "install: %v\n", err) + return 1 + } + if err := winsvc.Start(); err != nil { + fmt.Fprintf(os.Stderr, "service installed but start failed: %v\n", err) + fmt.Println("try: mysql-tunnel.exe --start") + return 1 + } + fmt.Println("Windows service installed and started:", winsvc.Name) + fmt.Println(" exe: ", exe) + fmt.Println(" config:", destAbs) + fmt.Println(" stop: mysql-tunnel.exe --stop") + fmt.Println(" uninstall: mysql-tunnel.exe --uninstall") + return 0 +} + +func report(err error) int { + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + return 1 + } + fmt.Println("ok") + return 0 +} diff --git a/cmd/tunnel/rsrc_windows_amd64.syso b/cmd/tunnel/rsrc_windows_amd64.syso new file mode 100644 index 0000000000000000000000000000000000000000..4f7b884ffa99f3a6a6ae5a0029b72ae6e959e545 GIT binary patch literal 62576 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 a`A6#Sf2X+IA@0C`<^Ivi5CDj<-~RIu literal 0 HcmV?d00001 diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..58b36b5 --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,30 @@ +# Copy to config.yaml and fill in real values. +# Do not commit config.yaml. Do not put tokens or passwords in logs. + +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 # writes logs/tunnel-YYYY-MM-DD.log; keep 7 days diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md new file mode 100644 index 0000000..cfd82ad --- /dev/null +++ b/docs/PROTOCOL.md @@ -0,0 +1,303 @@ +# Navicat HTTP Tunnel Protocol + +This document is derived from the project’s actual tunnel script: + +`ntunnel_mysql.php` + +That file is Navicat 16’s `ntunnel_mysql.php` (script version **206**), plus a GET `token` gate. It is **not** a custom JSON/business API. + +Wire format below is taken from the PHP source. Items that the PHP does not define are marked **TODO**. + +## 1. Compatibility with a deployed `ntunnel_mysql.php` + +**Compatible at the protocol layer**, with high confidence: + +| Observation | Meaning | +|---|---| +| Same binary header (`magic=1111`, version field, errno) | Standard Navicat tunnel response | +| Same POST fields (`actn`, `host`, `port`, `login`, `password`, `db`, `q[]`) | Standard Navicat tunnel request | +| Comment in PHP: `Navicat URL: https://ntunnel.example.com/index.php?token=YOUR_SECRET` | The `?token=` URL is an intentional extension | +| `$allowTestMenu = false` | Browser GET does not show the HTML tester unless token + missing POST is handled as binary error | + +The public hostname/path (with or without `/ntunnel_mysql.php`) is a deployment detail (reverse proxy / rewrite). The client must POST to the configured URL **including** the `token` query string. + +`--test` POSTs `actn=C` then `actn=Q` with `SELECT 1` (implemented in Phase 2). + +## 2. HTTP layer + +| Item | Value | Source | +|---|---|---| +| Method | `POST` | tester JS `xmlhttp.open("POST", ...)` and `$_POST[...]` | +| Request `Content-Type` | `application/x-www-form-urlencoded` | tester JS | +| Response `Content-Type` | `text/plain; charset=x-user-defined` | PHP `header(...)` | +| Body | **binary**, not JSON | `EchoHeader` / `GetBlock` | +| URL query | `token` is **required** | `isValidAccessToken()` reads `$_GET['token']` only | +| Token location | Query string only. Not POST, not `Authorization` | PHP | +| TLS | Use HTTPS when the URL is `https://`. Do not disable certificate verification unless explicitly configured. | product requirement | + +GET without POST: + +- Missing/wrong token → HTTP `403` text `403 Forbidden: invalid or missing token` +- Valid token, `$allowTestMenu = false` → binary header errno **202** `invalid parameters` + +Tunnel POST with missing/wrong token → HTTP 200 (typical PHP) with binary errno **204** `invalid access token`. + +## 3. POST parameters + +Required by PHP (`isset`): + +- `actn` +- `host` +- `port` +- `login` + +Used if present: + +| Field | Role | Notes | +|---|---|---| +| `actn` | Action | Only `"C"` and `"Q"` are handled | +| `host` | MySQL host **as seen from the PHP server** | Often `127.0.0.1` / `localhost` / docker name `mysql` | +| `port` | MySQL port | Empty is allowed; mysqli then uses default port | +| `login` | Remote MySQL user | | +| `password` | Remote MySQL password | May be empty. PHP does not `isset`-check it | +| `db` | Default database | Empty string skips `mysqli_select_db` | +| `q[]` | SQL list | Used only when `actn=Q`. Empty items are skipped | +| `encodeBase64` | `"1"` to Base64-decode each `q[]` | Optional. Intended for WAF bypass | + +**Not a POST field in this script:** + +- `charset` — PHP hardcodes `mysqli_set_charset($conn, 'UTF8')` after connect +- session id / cookie — none +- close / disconnect action — none + +**TODO:** If Yingdao needs `utf8mb4`, send `SET NAMES utf8mb4` as the first `q[]` (or a preceding query in the same HTTP request). Do not invent a `charset` POST field unless a later capture shows Navicat sending one (this PHP ignores unknown fields). + +## 4. `actn` values + +Observed in this file: + +| `actn` | Meaning | +|---|---| +| `C` | Connect + return server info. No SQL. | +| `Q` | Connect + execute each `q[]` | + +Any other `actn` (including missing after the `isset` check has passed): PHP still connects, writes `EchoHeader`, then writes **no** conn-info and **no** result set. + +There is **no** `actn=D` / close. The HTTP request is the entire session. + +**TODO:** Confirm whether the live Navicat client ever sends other `actn` values. This PHP would ignore them after a successful connect. + +## 5. Session / transaction model + +**Stateless.** Every POST: + +1. Validates token +2. `mysqli_connect(...)` +3. `mysqli_set_charset(..., 'UTF8')` +4. optional `mysqli_select_db` +5. `C` or `Q` +6. Script ends; PHP drops the MySQL connection + +Consequences (not assumptions — this is what the PHP does): + +- `START TRANSACTION` / `COMMIT` **cannot** span two HTTP requests +- `SET` session variables do not persist across COM_QUERY unless we put them in the same POST `q[]` list +- `last_insert_id` is only meaningful inside that same HTTP response +- Local Yingdao connection state (`USE db`) must be stored **in mysql-tunnel** and sent as `db=` on every later request + +Phase 1/2 client will therefore send `host/port/login/password/db` on **every** request. + +## 6. Binary encoding primitives + +All integers are **big-endian**. + +``` +GetLongBinary(n) = pack("N", n) // 4 bytes +GetShortBinary(n) = pack("n", n) // 2 bytes +GetDummy(k) = k zero bytes +``` + +`GetBlock(val)` (length-prefixed bytes, `strlen` = byte length): + +| First byte | Meaning | +|---|---| +| `0x00`–`0xFD` | `len` then `len` bytes of data (`len < 254`) | +| `0xFE` | next 4 bytes = `len` (uint32 BE), then `len` bytes | +| `0xFF` | **NULL** (only used for row field values in `EchoData`) | + +Empty string is `0x00` (length 0), not NULL. + +**Limitation:** `pack("N", $insertid)` and `pack("N", $affectedrows)` are 32-bit. Values that do not fit are truncated/wrapped by PHP. + +**TODO:** If a table uses `BIGINT AUTO_INCREMENT` beyond 2^32-1, the tunnel cannot return the full id. mysql-tunnel should surface the 32-bit value it actually received, not invent a 64-bit value. + +## 7. Response: common header (`EchoHeader`) + +Always 16 bytes, then an optional error block: + +``` +offset 0 uint32 BE magic = 1111 (0x00000457) +offset 4 uint16 BE version = 206 (0x00CE) // this script; older copies used 202 +offset 6 uint32 BE errno +offset 10 6 bytes zeros +offset 16 [GetBlock] error message if errno != 0 +``` + +The built-in tester reads errno at offset 6 and the message at offset 16. mysql-tunnel must do the same. + +Tunnel-level errno values defined by this PHP (not MySQL): + +| errno | Message | +|---|---| +| 201 | unsupported php version | +| 202 | invalid parameters | +| 203 | MySQL not supported on the server | +| 204 | invalid access token | +| other | `mysqli_connect_errno` / `mysqli_errno` (real MySQL errors: 1045, 1049, 1146, 1064, ...) | + +Parser should accept version **206** (this file) and **202** (older public copies). Reject unknown magic. **TODO:** whether to hard-fail on unexpected version numbers, or warn and continue if magic matches. + +## 8. `actn=C` response + +If header errno == 0: + +``` +GetBlock(host_info) // e.g. "127.0.0.1 via TCP/IP" +GetBlock(proto_info) // mysqli_get_proto_info is an int; PHP stringifies it, e.g. "10" +GetBlock(server_info) // e.g. "8.0.36" +``` + +If header errno != 0: only the error `GetBlock` after the 16-byte header. No conn info. + +## 9. `actn=Q` response + +After a successful connect header (errno 0): + +For each **non-empty** `q[i]`: + +``` +EchoResultSetHeader: + uint32 BE errno + uint32 BE affected_rows + uint32 BE insert_id + uint32 BE num_fields + uint32 BE num_rows + 12 zero bytes + +if errno > 0: + GetBlock(mysql_error) +else if num_fields > 0: + EchoFieldsHeader (num_fields times) + EchoData (num_rows times) +else: + GetBlock(mysqli_info) or GetBlock("") // OK-style extra info string + +separator: + 0x01 if this is not the last q[] index + 0x00 if this is the last q[] index +``` + +Empty `q[]` entries are `continue`’d **before** a result is written. If the last array slot is empty, the previous result may end with `0x01` and the stream never gets a final `0x00`. + +**Implementation rule:** send only non-empty queries; prefer a single `q[]` per local `COM_QUERY`. + +### Field header (`EchoFieldsHeader`, mysqli path) + +Per column: + +``` +GetBlock(name) +GetBlock(table) +uint32 BE type // mysqli_fetch_field_direct()->type (MYSQLI_TYPE_*) +uint32 BE flags // mysqli flags bitfield +uint32 BE length // display/octet length +``` + +`MYSQLI_TYPE_*` values are the same numbers as MySQL protocol field types (e.g. `3=LONG`, `8=LONGLONG`, `246=NEWDECIMAL`, `253=VAR_STRING`, `245=JSON`). mysql-tunnel should copy these into Column Definition packets. + +Legacy `mysql_field_type()` string mapping exists only on the old `mysql_*` path. Prefer treating incoming type as a numeric MySQL type. + +### Row data (`EchoData`) + +Per row, per field: + +- SQL `NULL` → single byte `0xFF` +- otherwise → `GetBlock(byte string)` + +`mysqli_fetch_row` returns strings (and NULL). **BIGINT and DECIMAL stay text.** Do not parse them as Go `int64` / `float64`. + +## 10. Mapping to local MySQL protocol (later phases) + +| Tunnel result | Local packet | +|---|---| +| `num_fields > 0` | Result Set: column count, column defs, rows, EOF/OK | +| `num_fields == 0` and errno 0 | OK packet: `affected_rows`, `last_insert_id` | +| query errno > 0 | ERR packet: SQLSTATE unknown unless we map common errno; message from `GetBlock` | +| connect errno > 0 | ERR packet to Yingdao (or fail `--test`) | +| HTTP 403 / non-binary body | Local ERR with a tunnel-layer message; never include token/password | + +**TODO:** SQLSTATE is not in the tunnel response. Use `HY000` unless we add a local errno→SQLSTATE table. + +**TODO:** `mysqli_info()` string (`Records: n Duplicates: m ...`) is not required by the MySQL OK packet. Can be ignored or logged. + +## 11. SHOW / DESC / DML + +This PHP runs **any** SQL string through `mysqli_real_query`. `SHOW DATABASES`, `SHOW TABLES`, `DESC t`, `SHOW COLUMNS`, `INSERT/UPDATE/DELETE/REPLACE` are all just `actn=Q`. mysql-tunnel should forward them unchanged. + +`mysqli_real_query` + `mysqli_store_result` loads the full result into PHP memory. Large `SELECT`s become large HTTP bodies. + +**TODO:** max result size / timeout behavior of the live gateway. Client-side `request_timeout` is our safety valve. + +## 12. Sequence + +``` +Yingdao mysql-tunnel ntunnel_mysql.php MySQL + | | | | + |-- TCP connect ------------->| | | + |<-- Handshake ---------------| | | + |-- Auth -------------------->| | | + |<-- OK (local auth) ---------| | | + | | | | + |-- COM_QUERY SELECT 1 ------>| | | + | |-- POST actn=Q + token ------>| | + | | |-- connect ------------>| + | | |-- SET charset UTF8 --->| + | | |-- select_db ---------->| + | | |-- real_query --------->| + | | |<-- rows ---------------| + | |<-- binary result set --------| | + |<-- MySQL result set --------| | | + | | | (PHP connection dropped) +``` + +Connect check (`--test` / first use): + +``` +mysql-tunnel --test + POST actn=C (same URL + token + host/port/login/password/db) + -> header errno 0 + ConnInfo => Tunnel + Remote MySQL OK + POST actn=Q q[]=SELECT 1 + -> 1 field, 1 row => Query OK +``` + +## 13. Open TODOs (do not guess) + +1. Does a reverse proxy / WAF wrap or modify the PHP output (gzip, extra HTTP headers, HTML error pages)? +2. Does Navicat send `encodeBase64=1` by default? Default in mysql-tunnel: **off**, configurable. +3. Does Navicat send a `charset` POST field? This PHP ignores it. +4. Should `utf8mb4` be applied via `SET NAMES` in the same `q[]` batch? (Client already does this when charset is not utf8/utf8mb3.) +5. Multi-statement in one `COM_QUERY` (`a; b;`) vs one `q[]` vs multiple `q[]`. +6. `COM_INIT_DB` / `USE`: local session `db` vs extra SQL. Current behavior: update session `db` and send `db=` on later requests. +7. Prepared statements: tunnel has no binary protocol. Later: emulate prepare locally or send equivalent SQL. +8. `insert_id` / `affected_rows` 32-bit wrap. +9. Trailing `0x01` when the last `q[]` is empty. +10. Whether HTTP keep-alive to the PHP front-end is allowed (PHP itself is still one MySQL connect per request). + +## 14. Implementation rule for mysql-tunnel + +Speak **this** protocol only: + +- Keep `token` on the URL query string +- POST `application/x-www-form-urlencoded` +- Parse binary `EchoHeader` / `GetBlock` +- Never log token, password, or the raw query string of the tunnel URL diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c092948 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module mysql-tunnel + +go 1.24.0 + +require ( + github.com/go-sql-driver/mysql v1.9.3 + github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 + github.com/lxn/win v0.0.0-20210218163916-a377121e959e + golang.org/x/sys v0.36.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1424abd --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/install-service.bat b/install-service.bat new file mode 100644 index 0000000..529b420 --- /dev/null +++ b/install-service.bat @@ -0,0 +1,26 @@ +@echo off +setlocal +cd /d "%~dp0" + +net session >nul 2>&1 +if %errorLevel% neq 0 ( + echo Requesting Administrator... + powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" + exit /b +) + +if not exist "dist\mysql-tunnel.exe" ( + echo dist\mysql-tunnel.exe not found. Run build.bat first. + pause + exit /b 1 +) + +if not exist "config.yaml" ( + echo config.yaml not found in %cd% + pause + exit /b 1 +) + +dist\mysql-tunnel.exe --install --config "%cd%\config.yaml" +pause +endlocal diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..3f16bfd --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,139 @@ +package app + +import ( + "context" + "fmt" + + "mysql-tunnel/internal/config" + "mysql-tunnel/internal/logger" + "mysql-tunnel/internal/mysqlserver" + "mysql-tunnel/internal/navicat" + "mysql-tunnel/internal/tunnel" +) + +type TestReport struct { + TunnelOK bool + RemoteOK bool + QueryOK bool + Lines []string +} + +func (r TestReport) OK() bool { return r.TunnelOK && r.RemoteOK && r.QueryOK } + +func (r TestReport) Text() string { + s := "" + for i, line := range r.Lines { + if i > 0 { + s += "\r\n" + } + s += line + } + return s +} + +func NewTunnelClient(cfg *config.Config) *navicat.Client { + httpClient := tunnel.NewHTTPClient(tunnel.Options{ + Timeout: cfg.RequestTimeout, + TLSInsecureSkipVerify: cfg.TLSInsecureSkipVerify, + }) + return navicat.NewClient(httpClient, navicat.ClientOptions{ + URL: cfg.TunnelURL, + EncodeBase64: cfg.EncodeBase64, + Retries: 1, + Target: navicat.Target{ + Host: cfg.RemoteHost, + Port: cfg.RemotePort, + User: cfg.RemoteUsername, + Password: cfg.RemotePassword, + Database: cfg.RemoteDatabase, + Charset: cfg.RemoteCharset, + }, + }) +} + +func Serve(ctx context.Context, cfg *config.Config, log *logger.Logger) error { + navi := NewTunnelClient(cfg) + srv := mysqlserver.New(mysqlserver.Options{ + Host: cfg.ListenHost, + Port: cfg.ListenPort, + Username: cfg.LocalUsername, + Password: cfg.LocalPassword, + DefaultDB: cfg.RemoteDatabase, + MaxConnections: cfg.MaxConnections, + Timeout: cfg.RequestTimeout, + LogSQL: cfg.LogSQL, + }, navi, log) + if log != nil { + log.Info("server starting", fmt.Sprintf("%s:%d", cfg.ListenHost, cfg.ListenPort)) + } + return srv.ListenAndServe(ctx) +} + +func Test(ctx context.Context, cfg *config.Config, log *logger.Logger) TestReport { + var r TestReport + navi := NewTunnelClient(cfg) + r.Lines = append(r.Lines, "Tunnel URL: "+config.RedactedTunnelURL(cfg.TunnelURL)) + + conn, err := navi.Connect(ctx) + switch { + case err == nil: + r.TunnelOK = true + r.RemoteOK = true + r.Lines = append(r.Lines, "Tunnel: OK", "Remote MySQL: OK") + if conn != nil && conn.Header.UncertainVersion { + r.Lines = append(r.Lines, fmt.Sprintf(" (script version %d is not 202/206; continuing)", conn.Header.Version)) + } + if log != nil && conn != nil { + log.Info("tunnel connect ok", conn.Conn.ServerInfo) + } + case isTunnelLayer(err): + r.Lines = append(r.Lines, "Tunnel: FAIL", " "+logger.RedactError(err), "Remote MySQL: FAIL", "Query SELECT 1: FAIL") + if log != nil { + log.Error("tunnel connect failed", logger.RedactError(err)) + } + return r + default: + r.TunnelOK = true + r.Lines = append(r.Lines, "Tunnel: OK", "Remote MySQL: FAIL", " "+logger.RedactError(err), "Query SELECT 1: FAIL") + if log != nil { + log.Error("remote mysql connect failed", logger.RedactError(err)) + } + return r + } + + q, err := navi.Query(ctx, cfg.RemoteDatabase, "SELECT 1") + if err != nil { + r.Lines = append(r.Lines, "Query SELECT 1: FAIL", " "+logger.RedactError(err)) + return r + } + res, ok := navicat.LastResultSet(q) + if !ok || res.IsError() || len(res.Rows) != 1 || len(res.Rows[0]) != 1 || res.Rows[0][0].Text() != "1" { + msg := "unexpected result" + if res.IsError() { + msg = fmt.Sprintf("mysql errno %d: %s", res.Errno, res.Message) + } + r.Lines = append(r.Lines, "Query SELECT 1: FAIL", " "+msg) + return r + } + r.QueryOK = true + r.Lines = append(r.Lines, "Query SELECT 1: OK") + if log != nil { + log.Info("query completed rows=1") + } + return r +} + +func isTunnelLayer(err error) bool { + ne, ok := err.(*navicat.Error) + if !ok { + return true + } + if ne.Kind == "http" { + return true + } + switch ne.Errno { + case navicat.ErrnoUnsupportedPHP, navicat.ErrnoInvalidParams, navicat.ErrnoMySQLMissing, navicat.ErrnoInvalidToken: + return true + } + return false +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..73949fd --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,296 @@ +package config + +import ( + "errors" + "fmt" + "net" + "net/url" + "os" + "runtime" + "strings" + "time" +) + +const ( + DefaultListenHost = "127.0.0.1" + DefaultListenPort = 13306 + DefaultTimeoutSec = 30 + DefaultMaxConnections = 5 + DefaultLogLevel = "info" + DefaultLogFile = "logs/tunnel.log" + DefaultCharset = "utf8mb4" +) + +// File holds the on-disk YAML shape. Nested pointers distinguish "unset" from zero values. +type File struct { + Listen ListenFile `yaml:"listen"` + LocalAuth LocalAuthFile `yaml:"local_auth"` + Tunnel TunnelFile `yaml:"tunnel"` + Remote RemoteFile `yaml:"remote"` + Options OptionsFile `yaml:"options"` +} + +type ListenFile struct { + Host string `yaml:"host"` + Port int `yaml:"port"` +} + +type LocalAuthFile struct { + Username string `yaml:"username"` + Password string `yaml:"password"` +} + +type TunnelFile struct { + URL string `yaml:"url"` + EncodeBase64 bool `yaml:"encode_base64"` + TLSInsecureSkipVerify bool `yaml:"tls_insecure_skip_verify"` +} + +type RemoteFile struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Username string `yaml:"username"` + Password string `yaml:"password"` + Database string `yaml:"database"` + Charset string `yaml:"charset"` +} + +type OptionsFile struct { + RequestTimeout int `yaml:"request_timeout"` + MaxConnections int `yaml:"max_connections"` + LogLevel string `yaml:"log_level"` + LogSQL bool `yaml:"log_sql"` + LogFile string `yaml:"log_file"` +} + +// Config is the validated runtime configuration. +type Config struct { + Path string + + ListenHost string + ListenPort int + + LocalUsername string + LocalPassword string + + TunnelURL string + EncodeBase64 bool + TLSInsecureSkipVerify bool + TLSInsecureSkipVerifyWarned bool + + RemoteHost string + RemotePort int + RemoteUsername string + RemotePassword string + RemoteDatabase string + RemoteCharset string + + RequestTimeout time.Duration + MaxConnections int + LogLevel string + LogSQL bool + LogFile string + + Warnings []string +} + +type Overrides struct { + ListenHost string + ListenPort int +} + +func Defaults() Config { + return Config{ + ListenHost: DefaultListenHost, + ListenPort: DefaultListenPort, + RequestTimeout: time.Duration(DefaultTimeoutSec) * time.Second, + MaxConnections: DefaultMaxConnections, + LogLevel: DefaultLogLevel, + LogFile: DefaultLogFile, + RemotePort: 3306, + RemoteCharset: DefaultCharset, + } +} + +func Parse(path string, ov Overrides) (*Config, error) { + raw, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read config %s: %w", path, err) + } + + var file File + if err := unmarshalYAML(raw, &file); err != nil { + return nil, fmt.Errorf("parse config %s: %w", path, err) + } + + cfg := Defaults() + cfg.Path = path + applyFile(&cfg, file) + applyOverrides(&cfg, ov) + return &cfg, nil +} + +func Load(path string, ov Overrides) (*Config, error) { + cfg, err := Parse(path, ov) + if err != nil { + return nil, err + } + if err := cfg.Validate(); err != nil { + return nil, err + } + cfg.warnFilePermissions() + return cfg, nil +} + +func applyFile(cfg *Config, file File) { + if file.Listen.Host != "" { + cfg.ListenHost = file.Listen.Host + } + if file.Listen.Port != 0 { + cfg.ListenPort = file.Listen.Port + } + cfg.LocalUsername = file.LocalAuth.Username + cfg.LocalPassword = file.LocalAuth.Password + cfg.TunnelURL = strings.TrimSpace(file.Tunnel.URL) + cfg.EncodeBase64 = file.Tunnel.EncodeBase64 + cfg.TLSInsecureSkipVerify = file.Tunnel.TLSInsecureSkipVerify + cfg.RemoteHost = file.Remote.Host + if file.Remote.Port != 0 { + cfg.RemotePort = file.Remote.Port + } + cfg.RemoteUsername = file.Remote.Username + cfg.RemotePassword = file.Remote.Password + cfg.RemoteDatabase = file.Remote.Database + if file.Remote.Charset != "" { + cfg.RemoteCharset = file.Remote.Charset + } + if file.Options.RequestTimeout != 0 { + cfg.RequestTimeout = time.Duration(file.Options.RequestTimeout) * time.Second + } + if file.Options.MaxConnections != 0 { + cfg.MaxConnections = file.Options.MaxConnections + } + if file.Options.LogLevel != "" { + cfg.LogLevel = strings.ToLower(strings.TrimSpace(file.Options.LogLevel)) + } + cfg.LogSQL = file.Options.LogSQL + if file.Options.LogFile != "" { + cfg.LogFile = file.Options.LogFile + } +} + +func applyOverrides(cfg *Config, ov Overrides) { + if ov.ListenHost != "" { + cfg.ListenHost = ov.ListenHost + } + if ov.ListenPort != 0 { + cfg.ListenPort = ov.ListenPort + } +} + +func (c *Config) Validate() error { + var errs []string + + if c.ListenHost == "" { + errs = append(errs, "listen.host is required") + } else if ip := net.ParseIP(c.ListenHost); ip != nil && ip.IsUnspecified() { + errs = append(errs, "listen.host must not be 0.0.0.0/::; bind loopback only unless you fully understand the risk") + } else if !isLoopbackHost(c.ListenHost) { + c.Warnings = append(c.Warnings, fmt.Sprintf("listen.host %q is not loopback; the local MySQL port will be reachable beyond this machine", c.ListenHost)) + } + if c.ListenPort < 1 || c.ListenPort > 65535 { + errs = append(errs, "listen.port must be 1-65535") + } + if strings.TrimSpace(c.LocalUsername) == "" { + errs = append(errs, "local_auth.username is required") + } + if c.TunnelURL == "" { + errs = append(errs, "tunnel.url is required") + } else { + u, err := url.Parse(c.TunnelURL) + if err != nil { + errs = append(errs, "tunnel.url is not a valid URL") + } else { + if u.Scheme != "http" && u.Scheme != "https" { + errs = append(errs, "tunnel.url scheme must be http or https") + } + if u.Host == "" { + errs = append(errs, "tunnel.url must include a host") + } + if u.Query().Get("token") == "" { + c.Warnings = append(c.Warnings, "tunnel.url has no token query parameter; ntunnel_mysql.php will reject requests with errno 204") + } + } + } + if strings.TrimSpace(c.RemoteHost) == "" { + errs = append(errs, "remote.host is required") + } + if c.RemotePort < 1 || c.RemotePort > 65535 { + errs = append(errs, "remote.port must be 1-65535") + } + if strings.TrimSpace(c.RemoteUsername) == "" { + errs = append(errs, "remote.username is required") + } + if c.RequestTimeout <= 0 { + errs = append(errs, "options.request_timeout must be > 0") + } + if c.MaxConnections < 1 { + errs = append(errs, "options.max_connections must be >= 1") + } + switch c.LogLevel { + case "debug", "info", "warn", "error": + default: + errs = append(errs, "options.log_level must be debug, info, warn, or error") + } + + if c.TLSInsecureSkipVerify { + c.TLSInsecureSkipVerifyWarned = true + c.Warnings = append(c.Warnings, "SECURITY: tls_insecure_skip_verify is true; TLS certificate verification is disabled") + } + + if len(errs) == 0 { + return nil + } + return errors.New(strings.Join(errs, "; ")) +} + +func (c *Config) warnFilePermissions() { + info, err := os.Stat(c.Path) + if err != nil { + return + } + if runtime.GOOS != "windows" { + if info.Mode().Perm()&0o004 != 0 { + c.Warnings = append(c.Warnings, fmt.Sprintf("%s is world-readable; it contains a tunnel token and database password", c.Path)) + } + } else { + c.Warnings = append(c.Warnings, fmt.Sprintf("%s contains secrets (token, passwords); restrict NTFS permissions to your account", c.Path)) + } +} + +func isLoopbackHost(host string) bool { + h := strings.TrimSpace(host) + if strings.EqualFold(h, "localhost") { + return true + } + ip := net.ParseIP(h) + return ip != nil && ip.IsLoopback() +} + +// RedactedTunnelURL strips userinfo and token/password query values for logs. +func RedactedTunnelURL(raw string) string { + u, err := url.Parse(raw) + if err != nil { + return "(invalid-url)" + } + u.User = nil + q := u.Query() + for _, key := range []string{"token", "password", "pass", "access_token", "accessToken"} { + if q.Has(key) { + q.Set(key, "***") + } + } + u.RawQuery = q.Encode() + u.Fragment = "" + return u.String() +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..880ef05 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,154 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func writeTempConfig(t *testing.T, body string) string { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + if err := os.WriteFile(path, []byte(body), 0o600); err != nil { + t.Fatal(err) + } + return path +} + +func TestLoadValidConfig(t *testing.T) { + path := writeTempConfig(t, ` +listen: + host: 127.0.0.1 + port: 13306 +local_auth: + username: root + password: "local-secret" +tunnel: + url: "https://example.invalid/ntunnel?token=abc" +remote: + host: mysql + port: 3306 + username: remote-root + password: "remote-secret" + database: app + charset: utf8mb4 +options: + request_timeout: 15 + max_connections: 3 + log_level: debug + log_sql: false +`) + cfg, err := Load(path, Overrides{}) + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.ListenHost != "127.0.0.1" || cfg.ListenPort != 13306 { + t.Fatalf("listen = %s:%d", cfg.ListenHost, cfg.ListenPort) + } + if cfg.RemoteHost != "mysql" || cfg.RemoteUsername != "remote-root" { + t.Fatalf("remote mismatch") + } + if cfg.RequestTimeout.Seconds() != 15 { + t.Fatalf("timeout = %s", cfg.RequestTimeout) + } +} + +func TestRejectUnspecifiedListen(t *testing.T) { + path := writeTempConfig(t, ` +listen: + host: 0.0.0.0 + port: 13306 +local_auth: + username: root + password: x +tunnel: + url: "https://example.invalid/?token=t" +remote: + host: mysql + username: root + password: x +`) + _, err := Load(path, Overrides{}) + if err == nil || !strings.Contains(err.Error(), "0.0.0.0") { + t.Fatalf("expected unspecified listen error, got %v", err) + } +} + +func TestMissingTunnelURL(t *testing.T) { + path := writeTempConfig(t, ` +local_auth: + username: root + password: x +remote: + host: mysql + username: root + password: x +`) + _, err := Load(path, Overrides{}) + if err == nil || !strings.Contains(err.Error(), "tunnel.url") { + t.Fatalf("expected tunnel.url error, got %v", err) + } +} + +func TestCLIOverrides(t *testing.T) { + path := writeTempConfig(t, ` +listen: + host: 127.0.0.1 + port: 13306 +local_auth: + username: root + password: x +tunnel: + url: "https://example.invalid/?token=t" +remote: + host: mysql + username: root + password: x +`) + cfg, err := Load(path, Overrides{ListenHost: "127.0.0.1", ListenPort: 23306}) + if err != nil { + t.Fatal(err) + } + if cfg.ListenPort != 23306 { + t.Fatalf("port override failed: %d", cfg.ListenPort) + } +} + +func TestRedactedTunnelURL(t *testing.T) { + got := RedactedTunnelURL("https://bth-ntunnel.example/?token=super-secret&x=1") + if strings.Contains(got, "super-secret") { + t.Fatalf("token leaked: %s", got) + } + if !strings.Contains(got, "token=%2A%2A%2A") && !strings.Contains(got, "token=***") { + t.Fatalf("token not redacted: %s", got) + } +} + +func TestWarnWhenTokenMissing(t *testing.T) { + path := writeTempConfig(t, ` +local_auth: + username: root + password: x +tunnel: + url: "https://example.invalid/ntunnel" +remote: + host: mysql + username: root + password: x +`) + cfg, err := Load(path, Overrides{}) + if err != nil { + t.Fatal(err) + } + found := false + for _, w := range cfg.Warnings { + if strings.Contains(w, "token") { + found = true + } + } + if !found { + t.Fatalf("expected token warning, got %#v", cfg.Warnings) + } +} diff --git a/internal/config/path.go b/internal/config/path.go new file mode 100644 index 0000000..42b8263 --- /dev/null +++ b/internal/config/path.go @@ -0,0 +1,62 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" +) + +// ResolveConfigPath finds config.yaml: --config, then next to the exe, then cwd. +func ResolveConfigPath(flagPath string) (string, error) { + var cands []string + if flagPath != "" { + cands = append(cands, flagPath) + } + if exe, err := os.Executable(); err == nil { + dir := filepath.Dir(exe) + cands = append(cands, filepath.Join(dir, "config.yaml")) + cands = append(cands, filepath.Join(dir, "..", "config.yaml")) + } + cands = append(cands, "config.yaml") + seen := map[string]bool{} + for _, p := range cands { + abs, err := filepath.Abs(p) + if err != nil { + continue + } + abs = filepath.Clean(abs) + if seen[abs] { + continue + } + seen[abs] = true + st, err := os.Stat(abs) + if err == nil && !st.IsDir() { + return abs, nil + } + } + if flagPath == "" { + flagPath = "config.yaml" + } + abs, err := filepath.Abs(flagPath) + if err != nil { + return flagPath, fmt.Errorf("config not found: %s", flagPath) + } + return abs, fmt.Errorf("config not found: %s", abs) +} + +// ResolvePaths makes LogFile absolute relative to the config file directory. +func (c *Config) ResolvePaths() { + if c == nil { + return + } + if p, err := filepath.Abs(c.Path); err == nil { + c.Path = p + } + if c.LogFile == "" { + c.LogFile = DefaultLogFile + } + if !filepath.IsAbs(c.LogFile) { + c.LogFile = filepath.Join(filepath.Dir(c.Path), c.LogFile) + } + c.LogFile = filepath.Clean(c.LogFile) +} diff --git a/internal/config/path_test.go b/internal/config/path_test.go new file mode 100644 index 0000000..1e1668e --- /dev/null +++ b/internal/config/path_test.go @@ -0,0 +1,50 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestResolvePathsMakesLogAbsolute(t *testing.T) { + path := writeTempConfig(t, ` +local_auth: + username: root + password: x +tunnel: + url: "https://example.invalid/?token=t" +remote: + host: mysql + username: root + password: x +options: + log_file: logs/tunnel.log +`) + cfg, err := Load(path, Overrides{}) + if err != nil { + t.Fatal(err) + } + cfg.ResolvePaths() + want := filepath.Join(filepath.Dir(path), "logs", "tunnel.log") + if cfg.LogFile != want { + t.Fatalf("LogFile=%s want %s", cfg.LogFile, want) + } +} + +func TestResolveConfigPathPrefersExisting(t *testing.T) { + dir := t.TempDir() + p := filepath.Join(dir, "config.yaml") + if err := os.WriteFile(p, []byte("x"), 0o600); err != nil { + t.Fatal(err) + } + got, err := ResolveConfigPath(p) + if err != nil { + t.Fatal(err) + } + if got != p && got != filepath.Clean(p) { + // Abs may change separators on Windows + if filepath.Clean(got) != filepath.Clean(p) { + t.Fatalf("got %s", got) + } + } +} diff --git a/internal/config/save.go b/internal/config/save.go new file mode 100644 index 0000000..4e64e2d --- /dev/null +++ b/internal/config/save.go @@ -0,0 +1,99 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +func (c *Config) ToFile() File { + if c == nil { + return File{} + } + return File{ + Listen: ListenFile{Host: c.ListenHost, Port: c.ListenPort}, + LocalAuth: LocalAuthFile{Username: c.LocalUsername, Password: c.LocalPassword}, + Tunnel: TunnelFile{ + URL: c.TunnelURL, + EncodeBase64: c.EncodeBase64, + TLSInsecureSkipVerify: c.TLSInsecureSkipVerify, + }, + Remote: RemoteFile{ + Host: c.RemoteHost, + Port: c.RemotePort, + Username: c.RemoteUsername, + Password: c.RemotePassword, + Database: c.RemoteDatabase, + Charset: c.RemoteCharset, + }, + Options: OptionsFile{ + RequestTimeout: int(c.RequestTimeout.Seconds()), + MaxConnections: c.MaxConnections, + LogLevel: c.LogLevel, + LogSQL: c.LogSQL, + LogFile: relativeLogFile(c.Path, c.LogFile), + }, + } +} + +func relativeLogFile(cfgPath, logFile string) string { + if logFile == "" { + return DefaultLogFile + } + if !filepath.IsAbs(logFile) { + return filepath.ToSlash(filepath.Clean(logFile)) + } + dir := filepath.Dir(cfgPath) + rel, err := filepath.Rel(dir, logFile) + if err != nil { + return logFile + } + return filepath.ToSlash(rel) +} + +func Save(path string, c *Config) error { + if path == "" { + return fmt.Errorf("config path is empty") + } + if dir := filepath.Dir(path); dir != "" && dir != "." { + if err := os.MkdirAll(dir, 0o750); err != nil { + return err + } + } + raw, err := yaml.Marshal(c.ToFile()) + if err != nil { + return err + } + out := append([]byte("# Written by mysql-tunnel. Contains secrets; do not commit.\n"), raw...) + return os.WriteFile(path, out, 0o600) +} + +func DefaultSavePath() string { + if exe, err := os.Executable(); err == nil { + return filepath.Join(filepath.Dir(exe), "config.yaml") + } + return "config.yaml" +} + +// LoadOrEmpty returns a parsed config, or defaults if the file is missing or invalid. +func LoadOrEmpty(path string) *Config { + if path == "" { + path = DefaultSavePath() + } + if _, err := os.Stat(path); err != nil { + c := Defaults() + c.Path = path + return &c + } + cfg, err := Parse(path, Overrides{}) + if err != nil { + c := Defaults() + c.Path = path + c.Warnings = append(c.Warnings, err.Error()) + return &c + } + cfg.ResolvePaths() + return cfg +} diff --git a/internal/config/save_test.go b/internal/config/save_test.go new file mode 100644 index 0000000..1627de4 --- /dev/null +++ b/internal/config/save_test.go @@ -0,0 +1,71 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestSaveRoundTrip(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + in := Defaults() + in.Path = path + in.LocalUsername = "root" + in.LocalPassword = "local-secret" + in.TunnelURL = "https://example.invalid/ntunnel?token=abc" + in.RemoteHost = "mysql" + in.RemoteUsername = "remote-root" + in.RemotePassword = "remote-secret" + in.RemoteDatabase = "app" + in.LogFile = filepath.Join(dir, "logs", "tunnel.log") + in.EncodeBase64 = true + in.LogSQL = true + if err := Save(path, &in); err != nil { + t.Fatal(err) + } + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(raw), "token=abc") { + t.Fatalf("saved yaml missing url: %s", raw) + } + if !strings.Contains(string(raw), "logs/tunnel.log") && !strings.Contains(string(raw), `log_file: logs/tunnel.log`) { + if !strings.Contains(string(raw), "log_file:") { + t.Fatalf("log_file missing: %s", raw) + } + } + cfg, err := Load(path, Overrides{}) + if err != nil { + t.Fatal(err) + } + if cfg.LocalPassword != "local-secret" || cfg.RemotePassword != "remote-secret" { + t.Fatalf("passwords not round-tripped") + } + if cfg.TunnelURL != in.TunnelURL { + t.Fatalf("url=%s", cfg.TunnelURL) + } + if !cfg.EncodeBase64 || !cfg.LogSQL { + t.Fatalf("bools not round-tripped") + } +} + +func TestLoadOrEmptyMissing(t *testing.T) { + path := filepath.Join(t.TempDir(), "nope.yaml") + cfg := LoadOrEmpty(path) + if cfg.ListenPort != DefaultListenPort { + t.Fatalf("port=%d", cfg.ListenPort) + } + if cfg.Path != path { + t.Fatalf("path=%s", cfg.Path) + } +} + +func TestRelativeLogFileKeepsRelative(t *testing.T) { + got := relativeLogFile(filepath.Join(t.TempDir(), "c.yaml"), "logs/tunnel.log") + if got != "logs/tunnel.log" { + t.Fatalf("got %s", got) + } +} diff --git a/internal/config/yaml.go b/internal/config/yaml.go new file mode 100644 index 0000000..cc3a021 --- /dev/null +++ b/internal/config/yaml.go @@ -0,0 +1,7 @@ +package config + +import "gopkg.in/yaml.v3" + +func unmarshalYAML(raw []byte, v any) error { + return yaml.Unmarshal(raw, v) +} diff --git a/internal/gui/gui.go b/internal/gui/gui.go new file mode 100644 index 0000000..ef84a94 --- /dev/null +++ b/internal/gui/gui.go @@ -0,0 +1,13 @@ +//go:build !windows + +package gui + +import ( + "fmt" + "os" +) + +func Run(string) int { + fmt.Fprintln(os.Stderr, "GUI is only available on Windows; use --console to listen") + return 1 +} diff --git a/internal/gui/gui_windows.go b/internal/gui/gui_windows.go new file mode 100644 index 0000000..bda7e30 --- /dev/null +++ b/internal/gui/gui_windows.go @@ -0,0 +1,692 @@ +//go:build windows + +package gui + +import ( + "bytes" + "context" + _ "embed" + "fmt" + "image/png" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + "unsafe" + + "github.com/lxn/walk" + . "github.com/lxn/walk/declarative" + "github.com/lxn/win" + "golang.org/x/sys/windows" + + "mysql-tunnel/internal/app" + "mysql-tunnel/internal/config" + "mysql-tunnel/internal/logger" +) + +//go:embed logo.png +var logoPNG []byte + +func init() { + runtime.LockOSThread() +} + +func Run(configFlag string) int { + hideConsoleIfStandalone() + path := strings.TrimSpace(configFlag) + if path == "" { + path = "config.yaml" + } + if resolved, err := config.ResolveConfigPath(path); err == nil { + path = resolved + } else if path == "config.yaml" { + path = config.DefaultSavePath() + } else if abs, err := filepath.Abs(path); err == nil { + path = abs + } + if err := runWindow(config.LoadOrEmpty(path)); err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + return 0 +} + +type ui struct { + mw *walk.MainWindow + ni *walk.NotifyIcon + + listenHost *walk.LineEdit + listenPort *walk.NumberEdit + localUser *walk.LineEdit + localPass *walk.LineEdit + tunnelURL *walk.LineEdit + encodeB64 *walk.CheckBox + tlsSkip *walk.CheckBox + remoteHost *walk.LineEdit + remotePort *walk.NumberEdit + remoteUser *walk.LineEdit + remotePass *walk.LineEdit + remoteDB *walk.LineEdit + charset *walk.LineEdit + timeout *walk.NumberEdit + maxConn *walk.NumberEdit + logLevel *walk.ComboBox + logSQL *walk.CheckBox + logFile *walk.LineEdit + status *walk.TextEdit + saveBtn *walk.PushButton + testBtn *walk.PushButton + startBtn *walk.PushButton + stopBtn *walk.PushButton + pathLabel *walk.Label + hint *walk.StatusBarItem + logoView *walk.ImageView + appIcon *walk.Icon + appBmp *walk.Bitmap + + cfgPath string + + mu sync.Mutex + running bool + busy bool + cancel context.CancelFunc + log *logger.Logger + exiting bool + hidOnce bool +} + +func runWindow(cfg *config.Config) error { + u := &ui{cfgPath: cfg.Path} + if err := u.build(cfg); err != nil { + return err + } + defer u.dispose() + u.mw.Run() + return nil +} + +func (u *ui) dispose() { + u.stopServer() + if u.ni != nil { + _ = u.ni.Dispose() + } +} + +func (u *ui) build(cfg *config.Config) error { + logFile := relativeLogFile(cfg) + levels := []string{"debug", "info", "warn", "error"} + levelIdx := 1 + for i, s := range levels { + if s == cfg.LogLevel { + levelIdx = i + break + } + } + + if err := (MainWindow{ + AssignTo: &u.mw, + Title: "MySQL Tunnel", + MinSize: Size{Width: 780, Height: 560}, + Size: Size{Width: 840, Height: 620}, + Layout: VBox{Margins: Margins{Left: 12, Top: 10, Right: 12, Bottom: 8}, Spacing: 8}, + StatusBarItems: []StatusBarItem{ + {AssignTo: &u.hint, Text: "就绪", Width: 800}, + }, + Children: []Widget{ + Composite{ + Layout: HBox{Spacing: 10, MarginsZero: true}, + Children: []Widget{ + ImageView{ + AssignTo: &u.logoView, + MinSize: Size{Width: 40, Height: 40}, + MaxSize: Size{Width: 40, Height: 40}, + Mode: ImageViewModeZoom, + }, + Composite{ + Layout: VBox{MarginsZero: true, Spacing: 2}, + Children: []Widget{ + Label{Text: "MySQL Tunnel", Font: Font{Family: "Segoe UI", PointSize: 13, Bold: true}}, + Label{AssignTo: &u.pathLabel, Text: cfg.Path}, + }, + }, + HSpacer{}, + }, + }, + Composite{ + Layout: HBox{Spacing: 10, MarginsZero: true}, + Children: []Widget{ + Composite{ + StretchFactor: 1, + MinSize: Size{Width: 380}, + Layout: VBox{MarginsZero: true, Spacing: 8}, + Children: []Widget{ + GroupBox{ + Title: "本机监听(影刀连这里)", + Layout: Grid{Columns: 2, Spacing: 6}, + Children: []Widget{ + fieldLabel("地址"), + LineEdit{AssignTo: &u.listenHost, Text: cfg.ListenHost}, + fieldLabel("端口"), + NumberEdit{AssignTo: &u.listenPort, Value: float64(cfg.ListenPort), MinValue: 1, MaxValue: 65535, Decimals: 0, MinSize: Size{Width: 90}}, + fieldLabel("本机用户"), + LineEdit{AssignTo: &u.localUser, Text: cfg.LocalUsername}, + fieldLabel("本机密码"), + LineEdit{AssignTo: &u.localPass, Text: cfg.LocalPassword, PasswordMode: true}, + }, + }, + GroupBox{ + Title: "HTTP 隧道", + StretchFactor: 1, + Layout: Grid{Columns: 2, Spacing: 6}, + Children: []Widget{ + fieldLabel("URL"), + LineEdit{AssignTo: &u.tunnelURL, Text: cfg.TunnelURL}, + CheckBox{AssignTo: &u.encodeB64, Text: "SQL Base64(WAF)", Checked: cfg.EncodeBase64, ColumnSpan: 2}, + CheckBox{AssignTo: &u.tlsSkip, Text: "跳过 TLS 证书校验(仅排障)", Checked: cfg.TLSInsecureSkipVerify, ColumnSpan: 2}, + }, + }, + }, + }, + Composite{ + StretchFactor: 1, + MinSize: Size{Width: 380}, + Layout: VBox{MarginsZero: true, Spacing: 8}, + Children: []Widget{ + GroupBox{ + Title: "远程 MySQL(PHP 主机访问)", + Layout: Grid{Columns: 2, Spacing: 6}, + Children: []Widget{ + fieldLabel("主机"), + LineEdit{AssignTo: &u.remoteHost, Text: cfg.RemoteHost}, + fieldLabel("端口"), + NumberEdit{AssignTo: &u.remotePort, Value: float64(nonzero(cfg.RemotePort, 3306)), MinValue: 1, MaxValue: 65535, Decimals: 0, MinSize: Size{Width: 90}}, + fieldLabel("用户名"), + LineEdit{AssignTo: &u.remoteUser, Text: cfg.RemoteUsername}, + fieldLabel("密码"), + LineEdit{AssignTo: &u.remotePass, Text: cfg.RemotePassword, PasswordMode: true}, + fieldLabel("默认库"), + LineEdit{AssignTo: &u.remoteDB, Text: cfg.RemoteDatabase}, + fieldLabel("字符集"), + LineEdit{AssignTo: &u.charset, Text: orDefault(cfg.RemoteCharset, "utf8mb4")}, + }, + }, + GroupBox{ + Title: "选项", + StretchFactor: 1, + Layout: Grid{Columns: 2, Spacing: 6}, + Children: []Widget{ + fieldLabel("超时(秒)"), + NumberEdit{AssignTo: &u.timeout, Value: float64(timeoutSec(cfg)), MinValue: 1, MaxValue: 600, Decimals: 0, MinSize: Size{Width: 90}}, + fieldLabel("最大连接"), + NumberEdit{AssignTo: &u.maxConn, Value: float64(nonzero(cfg.MaxConnections, 5)), MinValue: 1, MaxValue: 256, Decimals: 0, MinSize: Size{Width: 90}}, + fieldLabel("日志级别"), + ComboBox{AssignTo: &u.logLevel, Model: levels, CurrentIndex: levelIdx, Editable: false}, + fieldLabel("日志文件"), + LineEdit{AssignTo: &u.logFile, Text: logFile}, + CheckBox{AssignTo: &u.logSQL, Text: "记录 SQL(仍会脱敏)", Checked: cfg.LogSQL, ColumnSpan: 2}, + }, + }, + }, + }, + }, + }, + Composite{ + Layout: HBox{Spacing: 8}, + Children: []Widget{ + PushButton{AssignTo: &u.saveBtn, Text: "保存", MinSize: Size{Width: 88}}, + PushButton{AssignTo: &u.testBtn, Text: "测试连接", MinSize: Size{Width: 88}}, + PushButton{AssignTo: &u.startBtn, Text: "启动", MinSize: Size{Width: 88}}, + PushButton{AssignTo: &u.stopBtn, Text: "停止", Enabled: false, MinSize: Size{Width: 88}}, + HSpacer{}, + }, + }, + GroupBox{ + Title: "运行日志", + StretchFactor: 1, + MinSize: Size{Height: 140}, + Layout: VBox{MarginsZero: true}, + Children: []Widget{ + TextEdit{AssignTo: &u.status, ReadOnly: true, VScroll: true, MaxLength: 64 * 1024}, + }, + }, + }, + }).Create(); err != nil { + return err + } + + if u.saveBtn != nil { + u.saveBtn.Clicked().Attach(u.onSave) + } + if u.testBtn != nil { + u.testBtn.Clicked().Attach(u.onTest) + } + if u.startBtn != nil { + u.startBtn.Clicked().Attach(u.onStart) + } + if u.stopBtn != nil { + u.stopBtn.Clicked().Attach(u.onStop) + u.stopBtn.SetEnabled(false) + } + u.appendStatus("填写后点「保存」,再「测试连接」。通过后点「启动」,影刀连 本机地址:端口。") + u.appendStatus("最小化或关闭窗口会藏到系统托盘(监听可继续)。托盘左键打开,右键退出。") + for _, w := range cfg.Warnings { + u.appendStatus("注意: " + w) + } + + if err := u.applyBranding(); err != nil { + return err + } + + ni, err := walk.NewNotifyIcon(u.mw) + if err != nil { + return err + } + u.ni = ni + if u.appIcon != nil { + _ = ni.SetIcon(u.appIcon) + _ = u.mw.SetIcon(u.appIcon) + } + _ = ni.SetToolTip("MySQL Tunnel") + _ = ni.SetVisible(true) + ni.MouseDown().Attach(func(x, y int, button walk.MouseButton) { + if button == walk.LeftButton { + u.showWindow() + } + }) + + showAct := walk.NewAction() + _ = showAct.SetText("打开窗口") + showAct.Triggered().Attach(u.showWindow) + exitAct := walk.NewAction() + _ = exitAct.SetText("退出") + exitAct.Triggered().Attach(u.onExit) + if menu := ni.ContextMenu(); menu != nil { + _ = menu.Actions().Add(showAct) + _ = menu.Actions().Add(exitAct) + } + + // Minimize → tray (not taskbar). Close also → tray unless exiting. + u.mw.SizeChanged().Attach(func() { + hwnd := win.HWND(u.mw.Handle()) + if win.IsIconic(hwnd) { + u.hideToTray() + } + }) + u.mw.Closing().Attach(func(canceled *bool, reason walk.CloseReason) { + u.mu.Lock() + exiting := u.exiting + u.mu.Unlock() + if exiting { + return + } + *canceled = true + u.hideToTray() + }) + return nil +} + +func (u *ui) hideToTray() { + if u.mw == nil { + return + } + u.mw.Hide() + hwnd := win.HWND(u.mw.Handle()) + win.ShowWindow(hwnd, win.SW_HIDE) + if u.ni != nil && !u.hidOnce { + u.hidOnce = true + _ = u.ni.ShowInfo("MySQL Tunnel", "已放到系统托盘。左键打开窗口,右键退出。") + } +} + +func (u *ui) showWindow() { + if u.mw == nil { + return + } + u.mw.Show() + hwnd := win.HWND(u.mw.Handle()) + win.ShowWindow(hwnd, win.SW_RESTORE) + win.SetForegroundWindow(hwnd) +} + +func (u *ui) onExit() { + u.mu.Lock() + u.exiting = true + u.mu.Unlock() + u.stopServer() + if u.ni != nil { + _ = u.ni.SetVisible(false) + } + u.mw.Close() +} + +func (u *ui) collect() (*config.Config, error) { + c := config.Defaults() + c.Path = u.cfgPath + c.ListenHost = strings.TrimSpace(lineText(u.listenHost)) + c.ListenPort = numValue(u.listenPort, config.DefaultListenPort) + c.LocalUsername = strings.TrimSpace(lineText(u.localUser)) + c.LocalPassword = lineText(u.localPass) + c.TunnelURL = strings.TrimSpace(lineText(u.tunnelURL)) + c.EncodeBase64 = boxChecked(u.encodeB64) + c.TLSInsecureSkipVerify = boxChecked(u.tlsSkip) + c.RemoteHost = strings.TrimSpace(lineText(u.remoteHost)) + c.RemotePort = numValue(u.remotePort, 3306) + c.RemoteUsername = strings.TrimSpace(lineText(u.remoteUser)) + c.RemotePassword = lineText(u.remotePass) + c.RemoteDatabase = strings.TrimSpace(lineText(u.remoteDB)) + c.RemoteCharset = strings.TrimSpace(lineText(u.charset)) + c.RequestTimeout = time.Duration(numValue(u.timeout, config.DefaultTimeoutSec)) * time.Second + c.MaxConnections = numValue(u.maxConn, config.DefaultMaxConnections) + if u.logLevel != nil { + c.LogLevel = strings.ToLower(strings.TrimSpace(u.logLevel.Text())) + } + if c.LogLevel == "" { + c.LogLevel = config.DefaultLogLevel + } + c.LogSQL = boxChecked(u.logSQL) + c.LogFile = strings.TrimSpace(lineText(u.logFile)) + if c.LogFile == "" { + c.LogFile = config.DefaultLogFile + } + if err := c.Validate(); err != nil { + return &c, err + } + c.ResolvePaths() + return &c, nil +} + +func (u *ui) onSave() { + c, err := u.collect() + if err != nil { + u.appendStatus("保存失败: " + err.Error()) + walk.MsgBox(u.mw, "MySQL Tunnel", err.Error(), walk.MsgBoxIconError) + return + } + if err := config.Save(u.cfgPath, c); err != nil { + u.appendStatus("保存失败: " + err.Error()) + walk.MsgBox(u.mw, "MySQL Tunnel", err.Error(), walk.MsgBoxIconError) + return + } + u.appendStatus("已保存 " + u.cfgPath) + for _, w := range c.Warnings { + u.appendStatus("注意: " + w) + } +} + +func (u *ui) onTest() { + c, err := u.collect() + if err != nil { + u.appendStatus("无法测试: " + err.Error()) + walk.MsgBox(u.mw, "MySQL Tunnel", "无法测试:\n"+err.Error(), walk.MsgBoxIconError) + return + } + u.setBusy(true) + u.setHint("正在测试隧道…") + u.appendStatus("正在测试 " + config.RedactedTunnelURL(c.TunnelURL) + " …") + go func() { + defer func() { + if rec := recover(); rec != nil { + u.mw.Synchronize(func() { + msg := fmt.Sprintf("测试异常: %v", rec) + u.appendStatus(msg) + u.setHint("测试异常") + walk.MsgBox(u.mw, "测试结果", msg, walk.MsgBoxIconError) + u.setBusy(false) + }) + } + }() + log, _ := logger.New(c.LogFile, c.LogLevel, c.LogSQL) + r := app.Test(context.Background(), c, log) + if log != nil { + _ = log.Close() + } + u.mw.Synchronize(func() { + u.appendStatus(strings.ReplaceAll(r.Text(), "\n", "\r\n")) + icon := walk.MsgBoxIconError + hint := "测试未通过" + if r.OK() { + icon = walk.MsgBoxIconInformation + hint = "测试通过" + u.appendStatus("测试通过,可以启动监听。") + } else { + u.appendStatus("测试未通过。") + } + u.setHint(hint) + walk.MsgBox(u.mw, "测试结果", strings.ReplaceAll(r.Text(), "\r\n", "\n"), icon) + u.setBusy(false) + }) + }() +} + +func (u *ui) onStart() { + u.mu.Lock() + if u.running { + u.mu.Unlock() + return + } + u.mu.Unlock() + c, err := u.collect() + if err != nil { + u.appendStatus("无法启动: " + err.Error()) + walk.MsgBox(u.mw, "MySQL Tunnel", err.Error(), walk.MsgBoxIconError) + return + } + log, err := logger.New(c.LogFile, c.LogLevel, c.LogSQL) + if err != nil { + u.appendStatus("日志: " + err.Error()) + return + } + ctx, cancel := context.WithCancel(context.Background()) + u.mu.Lock() + u.cancel = cancel + u.log = log + u.running = true + u.mu.Unlock() + u.refreshButtons() + u.appendStatus(fmt.Sprintf("正在监听 %s:%d 影刀用户=%s", c.ListenHost, c.ListenPort, c.LocalUsername)) + u.appendStatus("隧道: " + config.RedactedTunnelURL(c.TunnelURL)) + if u.ni != nil { + _ = u.ni.SetToolTip(fmt.Sprintf("MySQL Tunnel · %s:%d", c.ListenHost, c.ListenPort)) + } + go func() { + err := app.Serve(ctx, c, log) + u.mw.Synchronize(func() { + u.mu.Lock() + u.running = false + u.cancel = nil + if u.log != nil { + _ = u.log.Close() + u.log = nil + } + u.mu.Unlock() + if err != nil && ctx.Err() == nil { + u.appendStatus("监听结束: " + logger.RedactError(err)) + } else { + u.appendStatus("监听已停止") + } + if u.ni != nil { + _ = u.ni.SetToolTip("MySQL Tunnel") + } + u.refreshButtons() + }) + }() +} + +func (u *ui) onStop() { + u.stopServer() +} + +func (u *ui) stopServer() { + u.mu.Lock() + cancel := u.cancel + u.mu.Unlock() + if cancel != nil { + cancel() + } +} + +func (u *ui) setBusy(b bool) { + u.mu.Lock() + u.busy = b + u.mu.Unlock() + u.refreshButtons() +} + +func (u *ui) refreshButtons() { + u.mu.Lock() + running, busy := u.running, u.busy + u.mu.Unlock() + if u.saveBtn != nil { + u.saveBtn.SetEnabled(!busy) + } + if u.testBtn != nil { + u.testBtn.SetEnabled(!busy) + if busy { + _ = u.testBtn.SetText("测试中…") + } else { + _ = u.testBtn.SetText("测试连接") + } + } + if u.startBtn != nil { + u.startBtn.SetEnabled(!running && !busy) + } + if u.stopBtn != nil { + u.stopBtn.SetEnabled(running) + } +} + +func (u *ui) setHint(s string) { + if u.hint == nil { + return + } + _ = u.hint.SetText(s) +} + +func (u *ui) appendStatus(line string) { + line = strings.ReplaceAll(strings.TrimRight(line, "\r\n"), "\n", "\r\n") + if u.status != nil { + prev := u.status.Text() + if prev != "" { + prev += "\r\n" + } + text := prev + time.Now().Format("15:04:05") + " " + line + _ = u.status.SetText(text) + end := len([]rune(text)) + u.status.SetTextSelection(end, end) + } + first := line + if i := strings.Index(first, "\r\n"); i >= 0 { + first = first[:i] + } + if len([]rune(first)) > 80 { + first = string([]rune(first)[:80]) + "…" + } + u.setHint(first) +} + +func relativeLogFile(cfg *config.Config) string { + if cfg == nil || cfg.LogFile == "" { + return config.DefaultLogFile + } + if !filepath.IsAbs(cfg.LogFile) { + return filepath.ToSlash(cfg.LogFile) + } + rel, err := filepath.Rel(filepath.Dir(cfg.Path), cfg.LogFile) + if err != nil { + return cfg.LogFile + } + return filepath.ToSlash(rel) +} + +func lineText(e *walk.LineEdit) string { + if e == nil { + return "" + } + return e.Text() +} + +func numValue(e *walk.NumberEdit, def int) int { + if e == nil { + return def + } + v := int(e.Value()) + if v == 0 { + return def + } + return v +} + +func boxChecked(b *walk.CheckBox) bool { + return b != nil && b.Checked() +} + +func nonzero(v, def int) int { + if v == 0 { + return def + } + return v +} + +func orDefault(s, def string) string { + if strings.TrimSpace(s) == "" { + return def + } + return s +} + +func fieldLabel(text string) Label { + return Label{Text: text, Alignment: AlignHFarVCenter, MinSize: Size{Width: 72}} +} + +func (u *ui) applyBranding() error { + if len(logoPNG) == 0 { + return nil + } + img, err := png.Decode(bytes.NewReader(logoPNG)) + if err != nil { + return nil + } + if ico, err := walk.NewIconFromImage(img); err == nil { + u.appIcon = ico + if u.mw != nil { + _ = u.mw.SetIcon(ico) + } + } + if bmp, err := walk.NewBitmapFromImage(img); err == nil { + u.appBmp = bmp + if u.logoView != nil { + _ = u.logoView.SetImage(bmp) + } + } + return nil +} + +func timeoutSec(cfg *config.Config) int { + if cfg == nil || cfg.RequestTimeout <= 0 { + return config.DefaultTimeoutSec + } + return int(cfg.RequestTimeout.Seconds()) +} + +func hideConsoleIfStandalone() { + var pids [16]uint32 + n, _, _ := procGetConsoleProcessList.Call(uintptr(unsafe.Pointer(&pids[0])), uintptr(len(pids))) + if n == 0 || n > 1 { + return + } + hwnd, _, _ := procGetConsoleWindow.Call() + if hwnd == 0 { + return + } + procShowWindow.Call(hwnd, 0) +} + +var ( + user32 = windows.NewLazySystemDLL("user32.dll") + kernel32 = windows.NewLazySystemDLL("kernel32.dll") + procShowWindow = user32.NewProc("ShowWindow") + procGetConsoleWindow = kernel32.NewProc("GetConsoleWindow") + procGetConsoleProcessList = kernel32.NewProc("GetConsoleProcessList") +) diff --git a/internal/gui/logo.png b/internal/gui/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4332a4117af81d45fae77e62f29f4c705e0c77b6 GIT binary patch literal 186603 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%0A+-?N`jEdBE_i$J)e1~fdz9!BlEjq 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�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%0ASfs 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 literal 0 HcmV?d00001 diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..a0d489f --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,262 @@ +package logger + +import ( + "fmt" + "io" + "net/url" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "time" +) + +const ( + RetainDays = 7 + dateLayout = "2006-01-02" +) + +type Level int + +const ( + LevelDebug Level = iota + LevelInfo + LevelWarn + LevelError +) + +func ParseLevel(s string) Level { + switch strings.ToLower(strings.TrimSpace(s)) { + case "debug": + return LevelDebug + case "warn", "warning": + return LevelWarn + case "error": + return LevelError + default: + return LevelInfo + } +} + +func (l Level) String() string { + switch l { + case LevelDebug: + return "DEBUG" + case LevelWarn: + return "WARN" + case LevelError: + return "ERROR" + default: + return "INFO" + } +} + +type Logger struct { + mu sync.Mutex + level Level + out io.Writer + file *os.File + basePath string // e.g. .../logs/tunnel.log → files tunnel-YYYY-MM-DD.log + day string // current file date YYYY-MM-DD + logSQL bool +} + +func New(path string, level string, logSQL bool) (*Logger, error) { + l := &Logger{ + level: ParseLevel(level), + logSQL: logSQL, + } + if strings.TrimSpace(path) == "" { + l.out = os.Stdout + return l, nil + } + l.basePath = path + if err := l.rotateLocked(time.Now()); err != nil { + return nil, err + } + return l, nil +} + +func (l *Logger) Close() error { + if l == nil { + return nil + } + l.mu.Lock() + defer l.mu.Unlock() + if l.file == nil { + return nil + } + err := l.file.Close() + l.file = nil + l.out = os.Stdout + return err +} + +func (l *Logger) LogSQLEnabled() bool { + return l.logSQL +} + +func (l *Logger) Debug(msg string, args ...any) { l.log(LevelDebug, msg, args...) } +func (l *Logger) Info(msg string, args ...any) { l.log(LevelInfo, msg, args...) } +func (l *Logger) Warn(msg string, args ...any) { l.log(LevelWarn, msg, args...) } +func (l *Logger) Error(msg string, args ...any) { l.log(LevelError, msg, args...) } + +func (l *Logger) log(level Level, msg string, args ...any) { + if l == nil { + return + } + if level < l.level { + return + } + now := time.Now() + line := fmt.Sprintf("%s %s %s", now.Format("2006-01-02 15:04:05"), level.String(), format(msg, args...)) + l.mu.Lock() + defer l.mu.Unlock() + if l.basePath != "" { + _ = l.rotateLocked(now) + } + fmt.Fprintln(l.out, line) +} + +// rotateLocked opens today's file when the calendar day changes. Caller holds mu. +func (l *Logger) rotateLocked(now time.Time) error { + day := now.Format(dateLayout) + if l.file != nil && l.day == day { + return nil + } + path := DatedPath(l.basePath, now) + if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { + return fmt.Errorf("create log dir: %w", err) + } + f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o640) + if err != nil { + return fmt.Errorf("open log file: %w", err) + } + if l.file != nil { + _ = l.file.Close() + } + l.file = f + l.day = day + l.out = io.MultiWriter(os.Stdout, f) + _ = purgeOld(l.basePath, now, RetainDays) + return nil +} + +// DatedPath turns logs/tunnel.log into logs/tunnel-2006-01-02.log. +func DatedPath(base string, day time.Time) string { + dir := filepath.Dir(base) + ext := filepath.Ext(base) + stem := strings.TrimSuffix(filepath.Base(base), ext) + if stem == "" || stem == "." { + stem = "tunnel" + } + if ext == "" { + ext = ".log" + } + return filepath.Join(dir, stem+"-"+day.Format(dateLayout)+ext) +} + +func purgeOld(base string, now time.Time, retainDays int) error { + if retainDays < 1 { + retainDays = RetainDays + } + dir := filepath.Dir(base) + ext := filepath.Ext(base) + stem := strings.TrimSuffix(filepath.Base(base), ext) + if stem == "" || stem == "." { + stem = "tunnel" + } + if ext == "" { + ext = ".log" + } + re := regexp.MustCompile("^" + regexp.QuoteMeta(stem) + `-(\d{4}-\d{2}-\d{2})` + regexp.QuoteMeta(ext) + `$`) + // Keep retainDays calendar days including today (e.g. 7 → today back through today-6). + oldest := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()). + AddDate(0, 0, -(retainDays - 1)) + + entries, err := os.ReadDir(dir) + if err != nil { + return err + } + for _, e := range entries { + if e.IsDir() { + continue + } + m := re.FindStringSubmatch(e.Name()) + if m == nil { + continue + } + d, err := time.ParseInLocation(dateLayout, m[1], now.Location()) + if err != nil { + continue + } + if d.Before(oldest) { + _ = os.Remove(filepath.Join(dir, e.Name())) + } + } + return nil +} + +func format(msg string, args ...any) string { + if len(args) == 0 { + return msg + } + return msg + " " + strings.TrimSpace(fmt.Sprint(args...)) +} + +// RedactURL removes userinfo and known secret query keys. +func RedactURL(raw string) string { + u, err := url.Parse(raw) + if err != nil || raw == "" { + return "(redacted-url)" + } + u.User = nil + q := u.Query() + changed := false + for key := range q { + lk := strings.ToLower(key) + if lk == "token" || lk == "password" || lk == "pass" || strings.Contains(lk, "secret") || strings.Contains(lk, "access") { + q.Set(key, "***") + changed = true + } + } + if changed { + u.RawQuery = q.Encode() + } + u.Fragment = "" + return u.String() +} + +var secretKV = regexp.MustCompile(`(?i)(token|password|passwd|authorization)\s*[=:]\s*[^\s&;]+`) + +func RedactError(err error) string { + if err == nil { + return "" + } + return RedactText(err.Error()) +} + +func RedactText(s string) string { + out := secretKV.ReplaceAllString(s, "${1}=***") + if u := extractURL(out); u != "" { + out = strings.ReplaceAll(out, u, RedactURL(u)) + } + return out +} + +func extractURL(s string) string { + lower := strings.ToLower(s) + start := strings.Index(lower, "https://") + if start < 0 { + start = strings.Index(lower, "http://") + } + if start < 0 { + return "" + } + end := start + for end < len(s) && !strings.ContainsRune(" \t\r\n<>\"'", rune(s[end])) { + end++ + } + return s[start:end] +} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go new file mode 100644 index 0000000..7e02ea3 --- /dev/null +++ b/internal/logger/logger_test.go @@ -0,0 +1,87 @@ +package logger + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestRedactURLToken(t *testing.T) { + raw := "http://ntunnel.example.com/?token=secret-token-value" + got := RedactURL(raw) + if strings.Contains(got, "secret-token-value") { + t.Fatalf("token leaked: %s", got) + } +} + +func TestRedactTextPasswordAndToken(t *testing.T) { + in := `login failed password=hunter2 token=abc123 url=https://x.example/?token=zzz` + got := RedactText(in) + for _, secret := range []string{"hunter2", "abc123", "zzz"} { + if strings.Contains(got, secret) { + t.Fatalf("secret %q leaked in %q", secret, got) + } + } +} + +func TestParseLevel(t *testing.T) { + if ParseLevel("DEBUG") != LevelDebug { + t.Fatal("debug") + } + if ParseLevel("nope") != LevelInfo { + t.Fatal("default") + } +} + +func TestDatedPath(t *testing.T) { + day := time.Date(2026, 9, 21, 15, 0, 0, 0, time.Local) + got := DatedPath(filepath.Join("logs", "tunnel.log"), day) + want := filepath.Join("logs", "tunnel-2026-09-21.log") + if got != want { + t.Fatalf("got %s want %s", got, want) + } +} + +func TestDailyRotateAndPurge(t *testing.T) { + dir := t.TempDir() + base := filepath.Join(dir, "tunnel.log") + now := time.Date(2026, 9, 21, 10, 0, 0, 0, time.Local) + + keepDay := now.AddDate(0, 0, -3) + dropDay := now.AddDate(0, 0, -8) + keepPath := DatedPath(base, keepDay) + dropPath := DatedPath(base, dropDay) + if err := os.WriteFile(keepPath, []byte("keep\n"), 0o640); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(dropPath, []byte("drop\n"), 0o640); err != nil { + t.Fatal(err) + } + + l, err := New(base, "info", false) + if err != nil { + t.Fatal(err) + } + defer l.Close() + + l.mu.Lock() + if err := l.rotateLocked(now); err != nil { + l.mu.Unlock() + t.Fatal(err) + } + l.mu.Unlock() + l.Info("hello") + + today := DatedPath(base, now) + if _, err := os.Stat(today); err != nil { + t.Fatalf("today log missing: %v", err) + } + if _, err := os.Stat(keepPath); err != nil { + t.Fatalf("recent log should remain: %v", err) + } + if _, err := os.Stat(dropPath); !os.IsNotExist(err) { + t.Fatalf("old log should be purged, err=%v", err) + } +} diff --git a/internal/mysqlserver/auth.go b/internal/mysqlserver/auth.go new file mode 100644 index 0000000..192eba0 --- /dev/null +++ b/internal/mysqlserver/auth.go @@ -0,0 +1,44 @@ +package mysqlserver + +import ( + "crypto/sha1" + "crypto/subtle" +) + +func nativePasswordToken(password string, scramble []byte) []byte { + if password == "" { + return []byte{} + } + if len(scramble) > 20 { + scramble = scramble[:20] + } + hash1 := sha1.Sum([]byte(password)) + hash2 := sha1.Sum(hash1[:]) + h := sha1.New() + h.Write(scramble) + h.Write(hash2[:]) + hash3 := h.Sum(nil) + out := make([]byte, 20) + for i := 0; i < 20; i++ { + out[i] = hash1[i] ^ hash3[i] + } + return out +} + +func verifyNativePassword(password string, scramble, auth []byte) bool { + if password == "" { + return len(auth) == 0 + } + want := nativePasswordToken(password, scramble) + if len(auth) != len(want) { + return false + } + return subtle.ConstantTimeCompare(auth, want) == 1 +} + +func usernamesEqual(a, b string) bool { + if len(a) != len(b) { + return false + } + return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 +} diff --git a/internal/mysqlserver/conn.go b/internal/mysqlserver/conn.go new file mode 100644 index 0000000..33f7e3d --- /dev/null +++ b/internal/mysqlserver/conn.go @@ -0,0 +1,105 @@ +package mysqlserver + +import ( + "fmt" + "io" + "net" + "time" +) + +type packetConn struct { + c net.Conn + seq uint8 + max int + readTO time.Duration + writeTO time.Duration +} + +func newPacketConn(c net.Conn, timeout time.Duration) *packetConn { + return &packetConn{c: c, max: MaxPacketSize, readTO: timeout, writeTO: timeout} +} + +func (p *packetConn) resetSeq() { p.seq = 0 } + +func (p *packetConn) readPacket() ([]byte, error) { + var payload []byte + for { + if p.readTO > 0 { + _ = p.c.SetReadDeadline(time.Now().Add(p.readTO)) + } + var hdr [4]byte + if _, err := io.ReadFull(p.c, hdr[:]); err != nil { + return nil, err + } + n := int(readU24(hdr[:3])) + seq := hdr[3] + if seq != p.seq { + return nil, fmt.Errorf("sequence mismatch: got %d want %d", seq, p.seq) + } + p.seq++ + if n == 0 { + return payload, nil + } + if n > p.max { + return nil, fmt.Errorf("packet too large: %d", n) + } + chunk := make([]byte, n) + if _, err := io.ReadFull(p.c, chunk); err != nil { + return nil, err + } + payload = append(payload, chunk...) + if n < 0xffffff { + return payload, nil + } + } +} + +func (p *packetConn) writePacket(payload []byte) error { + off := 0 + for { + n := len(payload) - off + if n > 0xffffff { + n = 0xffffff + } + var hdr [4]byte + hdr[0] = byte(n) + hdr[1] = byte(n >> 8) + hdr[2] = byte(n >> 16) + hdr[3] = p.seq + p.seq++ + if p.writeTO > 0 { + _ = p.c.SetWriteDeadline(time.Now().Add(p.writeTO)) + } + if _, err := p.c.Write(hdr[:]); err != nil { + return err + } + if n > 0 { + if _, err := p.c.Write(payload[off : off+n]); err != nil { + return err + } + } + off += n + if n < 0xffffff { + return nil + } + } +} + +func (p *packetConn) writeOK(affected, lastInsertID uint64, status, warnings uint16) error { + return p.writePacket(encodeOK(affected, lastInsertID, status, warnings)) +} + +func (p *packetConn) writeERR(errno uint16, msg string) error { + return p.writePacket(encodeERR(errno, msg)) +} + +func (p *packetConn) remote() string { + if p.c.RemoteAddr() == nil { + return "" + } + return p.c.RemoteAddr().String() +} + +func (p *packetConn) close() error { + return p.c.Close() +} diff --git a/internal/mysqlserver/doc.go b/internal/mysqlserver/doc.go new file mode 100644 index 0000000..2e35ead --- /dev/null +++ b/internal/mysqlserver/doc.go @@ -0,0 +1,3 @@ +// Package mysqlserver implements a loopback MySQL protocol server. +// Yingdao connects here; COM_QUERY is forwarded through the Navicat HTTP tunnel. +package mysqlserver diff --git a/internal/mysqlserver/driver_test.go b/internal/mysqlserver/driver_test.go new file mode 100644 index 0000000..5deb4eb --- /dev/null +++ b/internal/mysqlserver/driver_test.go @@ -0,0 +1,57 @@ +package mysqlserver + +import ( + "database/sql" + "fmt" + "net" + "testing" + + _ "github.com/go-sql-driver/mysql" +) + +func TestGoSQLDriverCompat(t *testing.T) { + addr, _ := startServer(t, stubBackend{}, "root", "123456") + host, port, err := net.SplitHostPort(addr) + if err != nil { + t.Fatal(err) + } + dsn := fmt.Sprintf("root:123456@tcp(%s:%s)/app?timeout=5s&readTimeout=5s&writeTimeout=5s&charset=utf8mb4&parseTime=false", host, port) + db, err := sql.Open("mysql", dsn) + if err != nil { + t.Fatal(err) + } + defer db.Close() + db.SetMaxOpenConns(1) + + var v string + if err := db.QueryRow("SELECT 1").Scan(&v); err != nil { + t.Fatal(err) + } + if v != "1" { + t.Fatalf("got %q", v) + } + + var n sql.NullString + if err := db.QueryRow("SELECT NULL AS n").Scan(&n); err != nil { + t.Fatal(err) + } + if n.Valid { + t.Fatal("expected NULL") + } + + var big string + if err := db.QueryRow("SELECT bigint_val").Scan(&big); err != nil { + t.Fatal(err) + } + if big != "18446744073709551615" { + t.Fatalf("bigint %q", big) + } + + var zh string + if err := db.QueryRow("SELECT zh_val").Scan(&zh); err != nil { + t.Fatal(err) + } + if zh != "你好世界" { + t.Fatalf("zh %q", zh) + } +} diff --git a/internal/mysqlserver/handshake.go b/internal/mysqlserver/handshake.go new file mode 100644 index 0000000..b266f23 --- /dev/null +++ b/internal/mysqlserver/handshake.go @@ -0,0 +1,138 @@ +package mysqlserver + +import "crypto/rand" + +func buildHandshake(connID uint32, scramble []byte) []byte { + if len(scramble) < 20 { + s := make([]byte, 20) + copy(s, scramble) + scramble = s + } + b := []byte{ProtocolVersion} + b = appendNUL(b, ServerVersion) + b = appendU32(b, connID) + b = append(b, scramble[:8]...) + b = append(b, 0x00) + b = appendU16(b, uint16(serverCapabilities)) + b = append(b, charsetUTF8MB4) + b = appendU16(b, serverStatusAutocommit) + b = appendU16(b, uint16(serverCapabilities>>16)) + b = append(b, 21) // auth plugin data len: 20 bytes + NUL + b = append(b, make([]byte, 10)...) + b = append(b, scramble[8:20]...) + b = append(b, 0x00) + b = appendNUL(b, AuthPluginNative) + return b +} + +func randomScramble() ([]byte, error) { + s := make([]byte, 20) + if _, err := rand.Read(s); err != nil { + return nil, err + } + for i, c := range s { + if c == 0 { + s[i] = 1 + } + } + return s, nil +} + +type handshakeResponse struct { + Capabilities uint32 + MaxPacket uint32 + Charset byte + Username string + Auth []byte + Database string + Plugin string + SSL bool +} + +func parseHandshakeResponse(payload []byte) (handshakeResponse, error) { + var hr handshakeResponse + if len(payload) < 32 { + return hr, errPacket("handshake response too short") + } + hr.Capabilities = uint32(payload[0]) | uint32(payload[1])<<8 | uint32(payload[2])<<16 | uint32(payload[3])<<24 + hr.MaxPacket = uint32(payload[4]) | uint32(payload[5])<<8 | uint32(payload[6])<<16 | uint32(payload[7])<<24 + hr.Charset = payload[8] + hr.SSL = hr.Capabilities&clientSSL != 0 + if hr.SSL && len(payload) <= 32 { + return hr, nil + } + if hr.Capabilities&clientProtocol41 == 0 { + return hr, errPacket("CLIENT_PROTOCOL_41 required") + } + off := 32 + user, off, err := readNUL(payload, off) + if err != nil { + return hr, err + } + hr.Username = user + + switch { + case hr.Capabilities&clientPluginAuthLenencClientData != 0: + n, nOff, err := readLenencInt(payload, off) + if err != nil { + return hr, err + } + off = nOff + end := off + int(n) + if end > len(payload) { + return hr, errPacket("auth-response truncated") + } + hr.Auth = append([]byte(nil), payload[off:end]...) + off = end + case hr.Capabilities&clientSecureConnection != 0: + if off >= len(payload) { + return hr, errPacket("auth-response truncated") + } + n := int(payload[off]) + off++ + end := off + n + if end > len(payload) { + return hr, errPacket("auth-response truncated") + } + hr.Auth = append([]byte(nil), payload[off:end]...) + off = end + default: + s, nOff, err := readNUL(payload, off) + if err != nil { + return hr, err + } + hr.Auth = []byte(s) + off = nOff + } + + if hr.Capabilities&clientConnectWithDB != 0 && off < len(payload) { + db, nOff, err := readNUL(payload, off) + if err != nil { + return hr, err + } + hr.Database = db + off = nOff + } + if hr.Capabilities&clientPluginAuth != 0 && off < len(payload) { + plugin, _, err := readNUL(payload, off) + if err != nil { + return hr, err + } + hr.Plugin = plugin + } + return hr, nil +} + +func encodeAuthSwitch(plugin string, scramble []byte) []byte { + b := []byte{markerEOF} // 0xFE AuthSwitchRequest + b = appendNUL(b, plugin) + b = append(b, scramble...) + b = append(b, 0x00) + return b +} + +type protoError struct{ s string } + +func (e protoError) Error() string { return e.s } + +func errPacket(s string) error { return protoError{s} } diff --git a/internal/mysqlserver/protocol.go b/internal/mysqlserver/protocol.go new file mode 100644 index 0000000..194c7d2 --- /dev/null +++ b/internal/mysqlserver/protocol.go @@ -0,0 +1,263 @@ +package mysqlserver + +import ( + "encoding/binary" + "fmt" + "io" +) + +const ( + ProtocolVersion = 10 + ServerVersion = "8.0.0-mysql-tunnel" + AuthPluginNative = "mysql_native_password" + AuthPluginSHA2 = "caching_sha2_password" + + MaxPacketSize = 16 << 20 +) + +const ( + ComQuit byte = 0x01 + ComInitDB byte = 0x02 + ComQuery byte = 0x03 + ComFieldList byte = 0x04 + ComPing byte = 0x0e + ComStmtPrepare byte = 0x16 + ComStmtExecute byte = 0x17 + ComStmtClose byte = 0x19 + ComResetConnection byte = 0x1f +) + +const ( + clientLongPassword uint32 = 1 + clientFoundRows uint32 = 1 << 1 + clientLongFlag uint32 = 1 << 2 + clientConnectWithDB uint32 = 1 << 3 + clientProtocol41 uint32 = 1 << 9 + clientSSL uint32 = 1 << 11 + clientTransactions uint32 = 1 << 13 + clientSecureConnection uint32 = 1 << 15 + clientMultiStatements uint32 = 1 << 16 + clientMultiResults uint32 = 1 << 17 + clientPSMultiResults uint32 = 1 << 18 + clientPluginAuth uint32 = 1 << 19 + clientConnectAttrs uint32 = 1 << 20 + clientPluginAuthLenencClientData uint32 = 1 << 21 + clientSessionTrack uint32 = 1 << 23 + clientDeprecateEOF uint32 = 1 << 24 +) + +const ( + serverStatusAutocommit uint16 = 0x0002 +) + +const ( + charsetUTF8MB4 uint8 = 45 + charsetBinary uint8 = 63 +) + +const ( + typeDecimal byte = 0 + typeTiny byte = 1 + typeShort byte = 2 + typeLong byte = 3 + typeFloat byte = 4 + typeDouble byte = 5 + typeNull byte = 6 + typeTimestamp byte = 7 + typeLongLong byte = 8 + typeInt24 byte = 9 + typeDate byte = 10 + typeTime byte = 11 + typeDatetime byte = 12 + typeYear byte = 13 + typeJSON byte = 245 + typeNewDecimal byte = 246 + typeTinyBlob byte = 249 + typeMediumBlob byte = 250 + typeLongBlob byte = 251 + typeBlob byte = 252 + typeVarString byte = 253 + typeString byte = 254 +) + +const ( + flagNotNull uint16 = 1 + flagPriKey uint16 = 2 + flagUniqueKey uint16 = 4 + flagMultipleKey uint16 = 8 + flagBlob uint16 = 16 + flagUnsigned uint16 = 32 + flagZerofill uint16 = 64 + flagBinary uint16 = 128 + flagEnum uint16 = 256 + flagAutoIncrement uint16 = 512 + flagTimestamp uint16 = 1024 + flagSet uint16 = 2048 +) + +const ( + errAccessDenied uint16 = 1045 + errUnknownCommand uint16 = 1047 + errNoDB uint16 = 1046 + errParseError uint16 = 1064 + errUnknown uint16 = 1105 + errNotSupported uint16 = 1235 +) + +const ( + markerOK byte = 0x00 + markerEOF byte = 0xfe + markerERR byte = 0xff + markerNull byte = 0xfb +) + +var serverCapabilities = clientLongPassword | clientFoundRows | clientLongFlag | + clientConnectWithDB | clientProtocol41 | clientTransactions | clientSecureConnection | + clientMultiResults | clientPluginAuth | clientPluginAuthLenencClientData | + clientConnectAttrs | clientDeprecateEOF | clientSessionTrack + +func appendU24(dst []byte, n uint32) []byte { + return append(dst, byte(n), byte(n>>8), byte(n>>16)) +} + +func appendU32(dst []byte, n uint32) []byte { + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, n) + return append(dst, b...) +} + +func appendU16(dst []byte, n uint16) []byte { + return append(dst, byte(n), byte(n>>8)) +} + +func appendNUL(dst []byte, s string) []byte { + dst = append(dst, s...) + return append(dst, 0) +} + +func appendLenencInt(dst []byte, n uint64) []byte { + switch { + case n < 251: + return append(dst, byte(n)) + case n < 1<<16: + return append(dst, 0xfc, byte(n), byte(n>>8)) + case n < 1<<24: + return append(dst, 0xfd, byte(n), byte(n>>8), byte(n>>16)) + default: + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, n) + return append(append(dst, 0xfe), b...) + } +} + +func appendLenencString(dst []byte, s []byte) []byte { + dst = appendLenencInt(dst, uint64(len(s))) + return append(dst, s...) +} + +func readU24(b []byte) uint32 { + return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 +} + +func readLenencInt(b []byte, off int) (uint64, int, error) { + if off >= len(b) { + return 0, off, io.ErrUnexpectedEOF + } + switch b[off] { + case 0xfb: + return 0, off + 1, fmt.Errorf("NULL lenenc") + case 0xfc: + if off+3 > len(b) { + return 0, off, io.ErrUnexpectedEOF + } + return uint64(b[off+1]) | uint64(b[off+2])<<8, off + 3, nil + case 0xfd: + if off+4 > len(b) { + return 0, off, io.ErrUnexpectedEOF + } + return uint64(b[off+1]) | uint64(b[off+2])<<8 | uint64(b[off+3])<<16, off + 4, nil + case 0xfe: + if off+9 > len(b) { + return 0, off, io.ErrUnexpectedEOF + } + return binary.LittleEndian.Uint64(b[off+1 : off+9]), off + 9, nil + default: + return uint64(b[off]), off + 1, nil + } +} + +func readLenencString(b []byte, off int) ([]byte, int, error) { + n, off, err := readLenencInt(b, off) + if err != nil { + return nil, off, err + } + end := off + int(n) + if end > len(b) { + return nil, off, io.ErrUnexpectedEOF + } + return b[off:end], end, nil +} + +func readNUL(b []byte, off int) (string, int, error) { + for i := off; i < len(b); i++ { + if b[i] == 0 { + return string(b[off:i]), i + 1, nil + } + } + return string(b[off:]), len(b), nil +} + +func sqlState(errno uint16) string { + switch errno { + case errAccessDenied: + return "28000" + case errParseError: + return "42000" + case errNoDB: + return "3D000" + case errUnknownCommand, errNotSupported: + return "HY000" + default: + return "HY000" + } +} + +func encodeOK(affected, lastInsertID uint64, status, warnings uint16) []byte { + b := []byte{markerOK} + b = appendLenencInt(b, affected) + b = appendLenencInt(b, lastInsertID) + b = appendU16(b, status) + b = appendU16(b, warnings) + return b +} + +func encodeERR(errno uint16, msg string) []byte { + b := []byte{markerERR} + b = appendU16(b, errno) + b = append(b, '#') + state := sqlState(errno) + if len(state) != 5 { + state = "HY000" + } + b = append(b, state...) + b = append(b, msg...) + return b +} + +func encodeEOF(warnings, status uint16) []byte { + b := []byte{markerEOF} + b = appendU16(b, warnings) + b = appendU16(b, status) + return b +} + +func isNumericType(t byte) bool { + switch t { + case typeDecimal, typeTiny, typeShort, typeLong, typeFloat, typeDouble, + typeNull, typeLongLong, typeInt24, typeYear, typeNewDecimal, typeBit: + return true + } + return false +} + +const typeBit byte = 16 diff --git a/internal/mysqlserver/protocol_test.go b/internal/mysqlserver/protocol_test.go new file mode 100644 index 0000000..3e5ae6b --- /dev/null +++ b/internal/mysqlserver/protocol_test.go @@ -0,0 +1,122 @@ +package mysqlserver + +import ( + "bytes" + "testing" + + "mysql-tunnel/internal/navicat" +) + +func TestLenencInt(t *testing.T) { + cases := []uint64{0, 1, 250, 251, 0xffff, 0x10000, 1 << 32} + for _, n := range cases { + enc := appendLenencInt(nil, n) + got, _, err := readLenencInt(enc, 0) + if err != nil || got != n { + t.Fatalf("n=%d got=%d err=%v", n, got, err) + } + } +} + +func TestOKERRPackets(t *testing.T) { + ok := encodeOK(3, 9, serverStatusAutocommit, 0) + if ok[0] != markerOK { + t.Fatal("ok header") + } + n, off, err := readLenencInt(ok, 1) + if err != nil || n != 3 { + t.Fatal(n, err) + } + id, _, err := readLenencInt(ok, off) + if err != nil || id != 9 { + t.Fatal(id, err) + } + + er := encodeERR(1064, "syntax") + if er[0] != markerERR || er[1] != 0x28 || er[2] != 0x04 { + t.Fatalf("%x", er) + } + if !bytes.Contains(er, []byte("syntax")) { + t.Fatal(string(er)) + } +} + +func TestNativePasswordRoundtrip(t *testing.T) { + scramble := bytes.Repeat([]byte{3}, 20) + tok := nativePasswordToken("123456", scramble) + if !verifyNativePassword("123456", scramble, tok) { + t.Fatal("should match") + } + if verifyNativePassword("wrong", scramble, tok) { + t.Fatal("should fail") + } + if !verifyNativePassword("", scramble, nil) { + t.Fatal("empty") + } +} + +func TestHandshakeBuildParseResponse(t *testing.T) { + scramble := bytes.Repeat([]byte{7}, 20) + hs := buildHandshake(42, scramble) + if hs[0] != ProtocolVersion { + t.Fatal("proto") + } + if !bytes.Contains(hs, []byte(ServerVersion)) { + t.Fatal("version") + } + if !bytes.Contains(hs, []byte(AuthPluginNative)) { + t.Fatal("plugin") + } + + token := nativePasswordToken("p", scramble) + resp := buildClientAuth("root", token, "app", clientProtocol41|clientSecureConnection|clientPluginAuth|clientConnectWithDB|clientPluginAuthLenencClientData) + hr, err := parseHandshakeResponse(resp) + if err != nil { + t.Fatal(err) + } + if hr.Username != "root" || hr.Database != "app" || !bytes.Equal(hr.Auth, token) { + t.Fatalf("%+v", hr) + } +} + +func TestTextRowNULLAndBigint(t *testing.T) { + row := encodeTextRow([]navicat.Cell{ + {Null: true}, + {Bytes: []byte("18446744073709551615")}, + {Bytes: []byte("1.50")}, + {Bytes: []byte("你好")}, + }) + if row[0] != markerNull { + t.Fatal("null marker") + } + off := 1 + b, off, err := readLenencString(row, off) + if err != nil || string(b) != "18446744073709551615" { + t.Fatal(string(b), err) + } + b, off, err = readLenencString(row, off) + if string(b) != "1.50" { + t.Fatal(string(b)) + } + b, _, err = readLenencString(row, off) + if string(b) != "你好" { + t.Fatal(string(b)) + } +} + +func buildClientAuth(user string, token []byte, db string, caps uint32) []byte { + b := appendU32(nil, caps) + b = appendU32(b, MaxPacketSize) + b = append(b, charsetUTF8MB4) + b = append(b, make([]byte, 23)...) + b = appendNUL(b, user) + b = appendLenencInt(b, uint64(len(token))) + b = append(b, token...) + if caps&clientConnectWithDB != 0 { + b = appendNUL(b, db) + } + if caps&clientPluginAuth != 0 { + b = appendNUL(b, AuthPluginNative) + } + return b +} diff --git a/internal/mysqlserver/result.go b/internal/mysqlserver/result.go new file mode 100644 index 0000000..503e9a2 --- /dev/null +++ b/internal/mysqlserver/result.go @@ -0,0 +1,95 @@ +package mysqlserver + +import ( + "mysql-tunnel/internal/navicat" +) + +func encodeColumnCount(n int) []byte { + return appendLenencInt(nil, uint64(n)) +} + +func encodeColumn(f navicat.Field) []byte { + typ := byte(f.Type) + charset := uint16(charsetUTF8MB4) + if isNumericType(typ) || f.Flags&uint32(flagBinary) != 0 && isBlobType(typ) { + charset = uint16(charsetBinary) + } + if isNumericType(typ) { + charset = uint16(charsetBinary) + } + length := f.Length + if length == 0 { + length = uint32(len(f.Name)) + if length == 0 { + length = 64 + } + } + flags := uint16(f.Flags) + decimals := byte(0) + if typ == typeFloat || typ == typeDouble || typ == typeNewDecimal || typ == typeDecimal { + decimals = 31 + } + + b := appendLenencString(nil, []byte("def")) + b = appendLenencString(b, nil) // schema + b = appendLenencString(b, []byte(f.Table)) + b = appendLenencString(b, []byte(f.Table)) + b = appendLenencString(b, []byte(f.Name)) + b = appendLenencString(b, []byte(f.Name)) + b = appendLenencInt(b, 0x0c) + b = appendU16(b, charset) + b = appendU32(b, length) + b = append(b, typ) + b = appendU16(b, flags) + b = append(b, decimals, 0x00, 0x00) + return b +} + +func encodeTextRow(row []navicat.Cell) []byte { + var b []byte + for _, c := range row { + if c.Null { + b = append(b, markerNull) + continue + } + b = appendLenencString(b, c.Bytes) + } + return b +} + +func isBlobType(t byte) bool { + switch t { + case typeTinyBlob, typeMediumBlob, typeLongBlob, typeBlob: + return true + } + return false +} + +func (p *packetConn) writeResultSet(res navicat.Result, caps uint32, status uint16) error { + if err := p.writePacket(encodeColumnCount(len(res.Fields))); err != nil { + return err + } + for _, f := range res.Fields { + if err := p.writePacket(encodeColumn(f)); err != nil { + return err + } + } + if caps&clientDeprecateEOF != 0 { + if err := p.writeOK(0, 0, status, 0); err != nil { + return err + } + } else { + if err := p.writePacket(encodeEOF(0, status)); err != nil { + return err + } + } + for _, row := range res.Rows { + if err := p.writePacket(encodeTextRow(row)); err != nil { + return err + } + } + if caps&clientDeprecateEOF != 0 { + return p.writeOK(uint64(res.AffectedRows), uint64(res.InsertID), status, 0) + } + return p.writePacket(encodeEOF(0, status)) +} diff --git a/internal/mysqlserver/server.go b/internal/mysqlserver/server.go new file mode 100644 index 0000000..21ab517 --- /dev/null +++ b/internal/mysqlserver/server.go @@ -0,0 +1,194 @@ +package mysqlserver + +import ( + "context" + "fmt" + "net" + "strconv" + "sync" + "sync/atomic" + "time" + + "mysql-tunnel/internal/logger" +) + +type Options struct { + Host string + Port int + Username string + Password string + DefaultDB string + MaxConnections int + Timeout time.Duration + LogSQL bool +} + +type Server struct { + opt Options + backend Backend + log *logger.Logger + + ln net.Listener + connID atomic.Uint32 + + sem chan struct{} + wg sync.WaitGroup +} + +func New(opt Options, backend Backend, log *logger.Logger) *Server { + if opt.Host == "" { + opt.Host = "127.0.0.1" + } + if opt.Port == 0 { + opt.Port = 13306 + } + if opt.MaxConnections < 1 { + opt.MaxConnections = 5 + } + if opt.Timeout <= 0 { + opt.Timeout = 30 * time.Second + } + return &Server{ + opt: opt, + backend: backend, + log: log, + sem: make(chan struct{}, opt.MaxConnections), + } +} + +func (s *Server) Addr() string { + if s.ln != nil { + return s.ln.Addr().String() + } + return net.JoinHostPort(s.opt.Host, strconv.Itoa(s.opt.Port)) +} + +func (s *Server) ListenAndServe(ctx context.Context) error { + ln, err := net.Listen("tcp", net.JoinHostPort(s.opt.Host, strconv.Itoa(s.opt.Port))) + if err != nil { + return err + } + return s.Serve(ctx, ln) +} + +func (s *Server) Serve(ctx context.Context, ln net.Listener) error { + s.ln = ln + if s.log != nil { + s.log.Info("server started", ln.Addr().String()) + } + go func() { + <-ctx.Done() + _ = ln.Close() + }() + for { + c, err := ln.Accept() + if err != nil { + select { + case <-ctx.Done(): + s.wg.Wait() + return nil + default: + return err + } + } + select { + case s.sem <- struct{}{}: + default: + _ = c.Close() + if s.log != nil { + s.log.Warn("max connections reached, rejected", c.RemoteAddr().String()) + } + continue + } + s.wg.Add(1) + go func(c net.Conn) { + defer s.wg.Done() + defer func() { <-s.sem }() + s.handle(ctx, c) + }(c) + } +} + +func (s *Server) handle(ctx context.Context, c net.Conn) { + pc := newPacketConn(c, s.opt.Timeout) + remote := pc.remote() + if s.log != nil { + s.log.Info("client connected", remote) + } + defer func() { + _ = pc.close() + if s.log != nil { + s.log.Info("client disconnected", remote) + } + }() + + scramble, err := randomScramble() + if err != nil { + return + } + id := s.connID.Add(1) + if err := pc.writePacket(buildHandshake(id, scramble)); err != nil { + return + } + payload, err := pc.readPacket() + if err != nil { + return + } + hr, err := parseHandshakeResponse(payload) + if err != nil { + _ = pc.writeERR(errUnknown, "bad handshake") + return + } + if hr.SSL { + _ = pc.writeERR(errNotSupported, "SSL is not supported on the local tunnel") + return + } + auth := hr.Auth + plugin := hr.Plugin + if plugin == "" { + plugin = AuthPluginNative + } + if plugin != AuthPluginNative { + if err := pc.writePacket(encodeAuthSwitch(AuthPluginNative, scramble)); err != nil { + return + } + auth, err = pc.readPacket() + if err != nil { + return + } + if len(auth) > 0 && auth[len(auth)-1] == 0 && len(auth) != 20 { + auth = auth[:len(auth)-1] + } + } + db := hr.Database + if db == "" { + db = s.opt.DefaultDB + } + if len(auth) == 21 && auth[20] == 0 { + auth = auth[:20] + } + if !usernamesEqual(hr.Username, s.opt.Username) || !verifyNativePassword(s.opt.Password, scramble, auth) { + _ = pc.writeERR(errAccessDenied, fmt.Sprintf("Access denied for user '%s'@'localhost'", hr.Username)) + return + } + if err := pc.writeOK(0, 0, serverStatusAutocommit, 0); err != nil { + return + } + + sess := &session{ + pc: pc, + backend: s.backend, + log: s.log, + logSQL: s.opt.LogSQL, + timeout: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(ctx, s.opt.Timeout) + }, + user: hr.Username, + db: db, + defaultDB: s.opt.DefaultDB, + caps: hr.Capabilities, + status: serverStatusAutocommit, + scramble: scramble, + } + sess.serve(ctx) +} diff --git a/internal/mysqlserver/server_test.go b/internal/mysqlserver/server_test.go new file mode 100644 index 0000000..9664e61 --- /dev/null +++ b/internal/mysqlserver/server_test.go @@ -0,0 +1,423 @@ +package mysqlserver + +import ( + "context" + "fmt" + "net" + "net/http/httptest" + "strings" + "testing" + "time" + + "mysql-tunnel/internal/navicat" + "mysql-tunnel/internal/tunnel" +) + +type stubBackend struct{} + +func (stubBackend) Query(_ context.Context, _ string, sqls ...string) (*navicat.QueryResponse, error) { + var results []navicat.Result + for _, sql := range sqls { + s := strings.TrimSpace(sql) + u := strings.ToUpper(s) + switch { + case strings.HasPrefix(u, "SET NAMES"), strings.HasPrefix(u, "USE "): + results = append(results, navicat.Result{}) + case u == "SELECT 1" || strings.HasPrefix(u, "SELECT 1 "): + results = append(results, navicat.Result{ + NumFields: 1, NumRows: 1, + Fields: []navicat.Field{{Name: "1", Type: 8, Length: 20}}, + Rows: [][]navicat.Cell{{{Bytes: []byte("1")}}}, + }) + case strings.Contains(u, "SELECT NULL"): + results = append(results, navicat.Result{ + NumFields: 1, NumRows: 1, + Fields: []navicat.Field{{Name: "n", Type: 6}}, + Rows: [][]navicat.Cell{{{Null: true}}}, + }) + case strings.Contains(strings.ToLower(s), "bigint_val"): + results = append(results, navicat.Result{ + NumFields: 1, NumRows: 1, + Fields: []navicat.Field{{Name: "bigint_val", Type: 8, Flags: 32, Length: 20}}, + Rows: [][]navicat.Cell{{{Bytes: []byte("18446744073709551615")}}}, + }) + case strings.Contains(strings.ToLower(s), "dec_val"): + results = append(results, navicat.Result{ + NumFields: 1, NumRows: 1, + Fields: []navicat.Field{{Name: "dec_val", Type: 246, Length: 32}}, + Rows: [][]navicat.Cell{{{Bytes: []byte("1234567890.123456789")}}}, + }) + case strings.Contains(s, "你好") || strings.Contains(strings.ToLower(s), "zh_val"): + results = append(results, navicat.Result{ + NumFields: 1, NumRows: 1, + Fields: []navicat.Field{{Name: "zh_val", Type: 253, Length: 12}}, + Rows: [][]navicat.Cell{{{Bytes: []byte("你好世界")}}}, + }) + case strings.HasPrefix(u, "UPDATE"), strings.HasPrefix(u, "INSERT"): + results = append(results, navicat.Result{AffectedRows: 2, InsertID: 7}) + case strings.Contains(u, "SYNTAX"): + results = append(results, navicat.Result{Errno: 1064, Message: "You have an error in your SQL syntax"}) + default: + results = append(results, navicat.Result{Errno: 1064, Message: "stub: unhandled SQL"}) + } + } + return &navicat.QueryResponse{Results: results}, nil +} + +func startServer(t *testing.T, backend Backend, user, pass string) (addr string, cancel context.CancelFunc) { + t.Helper() + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + s := New(Options{ + Host: "127.0.0.1", Port: 1, Username: user, Password: pass, + DefaultDB: "app", MaxConnections: 5, Timeout: 5 * time.Second, + }, backend, nil) + errCh := make(chan error, 1) + go func() { errCh <- s.Serve(ctx, ln) }() + t.Cleanup(func() { + cancel() + _ = ln.Close() + }) + return ln.Addr().String(), cancel +} + +type testClient struct { + pc *packetConn + caps uint32 +} + +func dialTest(t *testing.T, addr, user, pass, db string) *testClient { + t.Helper() + c, err := net.DialTimeout("tcp", addr, 3*time.Second) + if err != nil { + t.Fatal(err) + } + pc := newPacketConn(c, 5*time.Second) + hs, err := pc.readPacket() + if err != nil { + t.Fatal(err) + } + scramble, err := scrambleFromHandshake(hs) + if err != nil { + t.Fatal(err) + } + caps := clientProtocol41 | clientSecureConnection | clientPluginAuth | + clientConnectWithDB | clientPluginAuthLenencClientData | clientDeprecateEOF | clientMultiResults + token := nativePasswordToken(pass, scramble) + if err := pc.writePacket(buildClientAuth(user, token, db, caps)); err != nil { + t.Fatal(err) + } + ok, err := pc.readPacket() + if err != nil { + t.Fatal(err) + } + if len(ok) == 0 || ok[0] != markerOK { + t.Fatalf("auth failed: %x %q", ok, ok) + } + return &testClient{pc: pc, caps: caps} +} + +func scrambleFromHandshake(payload []byte) ([]byte, error) { + if len(payload) < 20 || payload[0] != ProtocolVersion { + return nil, fmt.Errorf("bad handshake") + } + off := 1 + _, off, err := readNUL(payload, off) + if err != nil { + return nil, err + } + off += 4 // conn id + part1 := payload[off : off+8] + off += 8 + 1 + 2 + 1 + 2 + 2 + 1 + 10 + if off+13 > len(payload) { + return nil, fmt.Errorf("short scramble") + } + part2 := payload[off : off+12] + s := append(append([]byte{}, part1...), part2...) + return s, nil +} + +func (c *testClient) query(t *testing.T, sql string) (cols []string, rows [][]*string, affected, lastID uint64, errno int, errMsg string) { + t.Helper() + c.pc.resetSeq() + payload := append([]byte{ComQuery}, []byte(sql)...) + if err := c.pc.writePacket(payload); err != nil { + t.Fatal(err) + } + pkt, err := c.pc.readPacket() + if err != nil { + t.Fatal(err) + } + if len(pkt) == 0 { + t.Fatal("empty") + } + if pkt[0] == markerERR { + errno = int(pkt[1]) | int(pkt[2])<<8 + if len(pkt) > 9 { + errMsg = string(pkt[9:]) + } + return + } + if pkt[0] == markerOK { + n, off, _ := readLenencInt(pkt, 1) + id, _, _ := readLenencInt(pkt, off) + return nil, nil, n, id, 0, "" + } + nCols, _, err := readLenencInt(pkt, 0) + if err != nil { + t.Fatal(err) + } + for i := uint64(0); i < nCols; i++ { + colPkt, err := c.pc.readPacket() + if err != nil { + t.Fatal(err) + } + name := columnName(colPkt) + cols = append(cols, name) + } + // metadata end OK/EOF + if _, err := c.pc.readPacket(); err != nil { + t.Fatal(err) + } + for { + rowPkt, err := c.pc.readPacket() + if err != nil { + t.Fatal(err) + } + if len(rowPkt) == 0 { + break + } + if rowPkt[0] == markerERR { + errno = int(rowPkt[1]) | int(rowPkt[2])<<8 + return + } + if rowPkt[0] == markerEOF && len(rowPkt) < 9 { + break + } + if rowPkt[0] == markerOK && len(rowPkt) >= 7 { + break + } + off := 0 + row := make([]*string, nCols) + for i := uint64(0); i < nCols; i++ { + if off >= len(rowPkt) { + t.Fatal("short row") + } + if rowPkt[off] == markerNull { + row[i] = nil + off++ + continue + } + b, nOff, err := readLenencString(rowPkt, off) + if err != nil { + t.Fatal(err) + } + s := string(b) + row[i] = &s + off = nOff + } + rows = append(rows, row) + } + return +} + +func (c *testClient) ping(t *testing.T) { + t.Helper() + c.pc.resetSeq() + if err := c.pc.writePacket([]byte{ComPing}); err != nil { + t.Fatal(err) + } + pkt, err := c.pc.readPacket() + if err != nil { + t.Fatal(err) + } + if pkt[0] != markerOK { + t.Fatalf("ping %x", pkt) + } +} + +func (c *testClient) quit() { + c.pc.resetSeq() + _ = c.pc.writePacket([]byte{ComQuit}) + _ = c.pc.close() +} + +func columnName(pkt []byte) string { + off := 0 + skip := func() { + _, n, err := readLenencString(pkt, off) + if err != nil { + return + } + off = n + } + skip() // catalog + skip() // schema + skip() // table + skip() // org table + b, _, err := readLenencString(pkt, off) + if err != nil { + return "" + } + return string(b) +} + +func TestServerAuthPingQueryQuit(t *testing.T) { + addr, _ := startServer(t, stubBackend{}, "root", "123456") + cl := dialTest(t, addr, "root", "123456", "app") + cl.ping(t) + _, rows, _, _, errno, msg := cl.query(t, "SELECT 1") + if errno != 0 { + t.Fatalf("errno %d %s", errno, msg) + } + if len(rows) != 1 || rows[0][0] == nil || *rows[0][0] != "1" { + t.Fatalf("rows=%v", rows) + } + cl.quit() +} + +func TestServerBadPassword(t *testing.T) { + addr, _ := startServer(t, stubBackend{}, "root", "123456") + c, err := net.DialTimeout("tcp", addr, 3*time.Second) + if err != nil { + t.Fatal(err) + } + defer c.Close() + pc := newPacketConn(c, 5*time.Second) + hs, err := pc.readPacket() + if err != nil { + t.Fatal(err) + } + scramble, err := scrambleFromHandshake(hs) + if err != nil { + t.Fatal(err) + } + caps := clientProtocol41 | clientSecureConnection | clientPluginAuth | clientPluginAuthLenencClientData + tok := nativePasswordToken("wrong", scramble) + if err := pc.writePacket(buildClientAuth("root", tok, "", caps)); err != nil { + t.Fatal(err) + } + pkt, err := pc.readPacket() + if err != nil { + t.Fatal(err) + } + if pkt[0] != markerERR { + t.Fatalf("want ERR got %x", pkt) + } + errno := int(pkt[1]) | int(pkt[2])<<8 + if errno != int(errAccessDenied) { + t.Fatalf("errno %d", errno) + } +} + +func TestServerResultTypes(t *testing.T) { + addr, _ := startServer(t, stubBackend{}, "root", "p") + cl := dialTest(t, addr, "root", "p", "app") + defer cl.quit() + + _, rows, _, _, errno, _ := cl.query(t, "SELECT NULL AS n") + if errno != 0 || rows[0][0] != nil { + t.Fatalf("null errno=%d row=%v", errno, rows) + } + + _, rows, _, _, errno, _ = cl.query(t, "SELECT bigint_val") + if errno != 0 || *rows[0][0] != "18446744073709551615" { + t.Fatalf("bigint %v errno=%d", rows, errno) + } + + _, rows, _, _, errno, _ = cl.query(t, "SELECT dec_val") + if errno != 0 || *rows[0][0] != "1234567890.123456789" { + t.Fatalf("dec %v", rows) + } + + _, rows, _, _, errno, _ = cl.query(t, "SELECT zh_val") + if errno != 0 || *rows[0][0] != "你好世界" { + t.Fatalf("zh %v", rows) + } + + _, _, affected, lastID, errno, msg := cl.query(t, "UPDATE t SET x=1") + if errno != 0 { + t.Fatal(msg) + } + if affected != 2 || lastID != 7 { + t.Fatalf("affected=%d id=%d", affected, lastID) + } + + _, _, _, _, errno, msg = cl.query(t, "SELECT SYNTAX") + if errno != 1064 || !strings.Contains(msg, "syntax") { + t.Fatalf("errno=%d msg=%s", errno, msg) + } +} + +func TestServerAuthSwitch(t *testing.T) { + addr, _ := startServer(t, stubBackend{}, "root", "p") + c, err := net.DialTimeout("tcp", addr, 3*time.Second) + if err != nil { + t.Fatal(err) + } + defer c.Close() + pc := newPacketConn(c, 5*time.Second) + hs, err := pc.readPacket() + if err != nil { + t.Fatal(err) + } + scramble, err := scrambleFromHandshake(hs) + if err != nil { + t.Fatal(err) + } + caps := clientProtocol41 | clientSecureConnection | clientPluginAuth | clientPluginAuthLenencClientData + b := appendU32(nil, caps) + b = appendU32(b, MaxPacketSize) + b = append(b, charsetUTF8MB4) + b = append(b, make([]byte, 23)...) + b = appendNUL(b, "root") + b = appendLenencInt(b, 0) // empty, will switch + b = appendNUL(b, AuthPluginSHA2) + if err := pc.writePacket(b); err != nil { + t.Fatal(err) + } + sw, err := pc.readPacket() + if err != nil { + t.Fatal(err) + } + if sw[0] != markerEOF { + t.Fatalf("want auth switch %x", sw) + } + tok := nativePasswordToken("p", scramble) + if err := pc.writePacket(tok); err != nil { + t.Fatal(err) + } + ok, err := pc.readPacket() + if err != nil { + t.Fatal(err) + } + if ok[0] != markerOK { + t.Fatalf("auth after switch %x", ok) + } +} + +func TestPhase4NavicatStack(t *testing.T) { + mock := httptest.NewServer(navicat.NewMockHandler(navicat.WithToken("t"))) + t.Cleanup(mock.Close) + httpClient := tunnel.NewHTTPClient(tunnel.Options{Timeout: 5 * time.Second}) + n := navicat.NewClient(httpClient, navicat.ClientOptions{ + URL: mock.URL + "?token=t", + Target: navicat.Target{ + Host: "mysql", Port: 3306, User: "remote", Password: "rp", + Database: "app", Charset: "utf8mb4", + }, + }) + addr, _ := startServer(t, n, "root", "local") + cl := dialTest(t, addr, "root", "local", "app") + defer cl.quit() + _, rows, _, _, errno, msg := cl.query(t, "SELECT 1") + if errno != 0 { + t.Fatal(msg) + } + if *rows[0][0] != "1" { + t.Fatalf("%v", rows) + } +} diff --git a/internal/mysqlserver/session.go b/internal/mysqlserver/session.go new file mode 100644 index 0000000..b67a596 --- /dev/null +++ b/internal/mysqlserver/session.go @@ -0,0 +1,168 @@ +package mysqlserver + +import ( + "context" + "fmt" + "regexp" + "strings" + + "mysql-tunnel/internal/logger" + "mysql-tunnel/internal/navicat" +) + +var useDBRe = regexp.MustCompile(`(?is)^\s*USE\s+(?:` + "`" + `([^` + "`" + `]+)` + "`" + `|([A-Za-z0-9_]+))\s*;?\s*$`) + +type Backend interface { + Query(ctx context.Context, database string, sqls ...string) (*navicat.QueryResponse, error) +} + +type session struct { + pc *packetConn + backend Backend + log *logger.Logger + logSQL bool + timeout ctxTimeout + user string + db string + defaultDB string + caps uint32 + status uint16 + scramble []byte +} + +type ctxTimeout func() (context.Context, context.CancelFunc) + +func (s *session) serve(ctx context.Context) { + defer s.pc.close() + for { + select { + case <-ctx.Done(): + return + default: + } + s.pc.resetSeq() + payload, err := s.pc.readPacket() + if err != nil { + return + } + if len(payload) == 0 { + _ = s.pc.writeERR(errUnknownCommand, "empty command") + continue + } + if err := s.dispatch(ctx, payload[0], payload[1:]); err != nil { + return + } + } +} + +func (s *session) dispatch(parent context.Context, cmd byte, body []byte) error { + switch cmd { + case ComQuit: + return fmt.Errorf("quit") + case ComPing: + return s.pc.writeOK(0, 0, s.status, 0) + case ComInitDB: + db := strings.TrimSpace(string(body)) + return s.changeDB(parent, db) + case ComQuery: + return s.query(parent, string(body)) + case ComResetConnection: + s.db = s.defaultDB + return s.pc.writeOK(0, 0, s.status, 0) + case ComFieldList, ComStmtPrepare, ComStmtExecute, ComStmtClose: + return s.pc.writeERR(errNotSupported, "command not implemented yet") + default: + return s.pc.writeERR(errUnknownCommand, "unknown command") + } +} + +func (s *session) changeDB(parent context.Context, db string) error { + if db == "" { + return s.pc.writeERR(errNoDB, "no database selected") + } + s.db = db + return s.pc.writeOK(0, 0, s.status, 0) +} + +func (s *session) query(parent context.Context, sql string) error { + sql = strings.TrimRight(sql, "\x00") + s.logQuery(sql) + pendingDB := "" + if m := useDBRe.FindStringSubmatch(sql); m != nil { + pendingDB = m[1] + if pendingDB == "" { + pendingDB = m[2] + } + } + + ctx, cancel := s.timeout() + defer cancel() + if ctx == nil { + ctx = parent + } + + resp, err := s.backend.Query(ctx, s.db, sql) + if err != nil { + return s.writeBackendErr(err) + } + for _, r := range resp.Results { + if r.IsError() { + return s.pc.writeERR(uint16(r.Errno), r.Message) + } + } + if pendingDB != "" { + s.db = pendingDB + } + res, ok := navicat.LastResultSet(resp) + if !ok { + return s.pc.writeOK(0, 0, s.status, 0) + } + if res.IsError() { + return s.pc.writeERR(uint16(res.Errno), res.Message) + } + if res.IsResultSet() { + if s.log != nil { + s.log.Info("query completed", fmt.Sprintf("rows=%d", len(res.Rows))) + } + return s.pc.writeResultSet(res, s.caps, s.status) + } + if s.log != nil { + s.log.Info("query completed", fmt.Sprintf("affected=%d", res.AffectedRows)) + } + return s.pc.writeOK(uint64(res.AffectedRows), uint64(res.InsertID), s.status, 0) +} + +func (s *session) writeBackendErr(err error) error { + msg := logger.RedactError(err) + if ne, ok := err.(*navicat.Error); ok { + errno := uint16(ne.Errno) + if errno == 0 { + errno = errUnknown + } + if ne.Kind == "http" || ne.Kind == "tunnel" { + errno = errUnknown + } + return s.pc.writeERR(errno, msg) + } + return s.pc.writeERR(errUnknown, msg) +} + +func (s *session) logQuery(sql string) { + if s.log == nil { + return + } + if !s.logSQL { + s.log.Info("query") + return + } + s.log.Info("query", trimSQL(sql)) +} + +func trimSQL(sql string) string { + s := logger.RedactText(sql) + s = strings.ReplaceAll(s, "\n", " ") + if len(s) > 240 { + return s[:240] + "..." + } + return s +} diff --git a/internal/navicat/binary.go b/internal/navicat/binary.go new file mode 100644 index 0000000..4584317 --- /dev/null +++ b/internal/navicat/binary.go @@ -0,0 +1,76 @@ +package navicat + +import ( + "encoding/binary" + "fmt" +) + +func putU32(n uint32) []byte { + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, n) + return b +} + +func putU16(n uint16) []byte { + b := make([]byte, 2) + binary.BigEndian.PutUint16(b, n) + return b +} + +// AppendBlock encodes GetBlock() from ntunnel_mysql.php. +// NULL is not represented here; use AppendNull for row cells. +func AppendBlock(dst []byte, val []byte) []byte { + if val == nil { + val = []byte{} + } + n := len(val) + if n < 254 { + dst = append(dst, byte(n)) + return append(dst, val...) + } + dst = append(dst, markLong) + dst = append(dst, putU32(uint32(n))...) + return append(dst, val...) +} + +func AppendNull(dst []byte) []byte { + return append(dst, markNull) +} + +func AppendHeader(dst []byte, version uint16, errno uint32) []byte { + dst = append(dst, putU32(Magic)...) + dst = append(dst, putU16(version)...) + dst = append(dst, putU32(errno)...) + dst = append(dst, 0, 0, 0, 0, 0, 0) + return dst +} + +func AppendResultSetHeader(dst []byte, errno, affected, insertID, numFields, numRows uint32) []byte { + dst = append(dst, putU32(errno)...) + dst = append(dst, putU32(affected)...) + dst = append(dst, putU32(insertID)...) + dst = append(dst, putU32(numFields)...) + dst = append(dst, putU32(numRows)...) + dst = append(dst, make([]byte, 12)...) + return dst +} + +func AppendField(dst []byte, name, table string, typ, flags, length uint32) []byte { + dst = AppendBlock(dst, []byte(name)) + dst = AppendBlock(dst, []byte(table)) + dst = append(dst, putU32(typ)...) + dst = append(dst, putU32(flags)...) + dst = append(dst, putU32(length)...) + return dst +} + +func knownVersion(v uint16) bool { + return v == Version202 || v == Version206 +} + +func fmtErrno(errno uint32, msg string) string { + if msg == "" { + return fmt.Sprintf("tunnel errno %d", errno) + } + return fmt.Sprintf("tunnel errno %d: %s", errno, msg) +} diff --git a/internal/navicat/client.go b/internal/navicat/client.go new file mode 100644 index 0000000..2e16ca5 --- /dev/null +++ b/internal/navicat/client.go @@ -0,0 +1,177 @@ +package navicat + +import ( + "context" + "fmt" + "net/http" + "regexp" + "strings" + + "mysql-tunnel/internal/logger" + "mysql-tunnel/internal/tunnel" +) + +var charsetIdent = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*$`) + +// Target is the remote MySQL as seen by ntunnel_mysql.php. +type Target struct { + Host string + Port int + User string + Password string + Database string + Charset string +} + +// Client talks Navicat HTTP Tunnel over the generic HTTP client. +type Client struct { + http *tunnel.HTTPClient + url string + target Target + encodeBase64 bool + retries int +} + +type ClientOptions struct { + URL string + Target Target + EncodeBase64 bool + // Retries is extra attempts on transport failure only (not SQL errors). 0 means a single try. + Retries int +} + +func NewClient(httpClient *tunnel.HTTPClient, opt ClientOptions) *Client { + retries := opt.Retries + if retries < 0 { + retries = 0 + } + return &Client{ + http: httpClient, + url: opt.URL, + target: opt.Target, + encodeBase64: opt.EncodeBase64, + retries: retries, + } +} + +func (c *Client) Connect(ctx context.Context) (*ConnectResponse, error) { + body, status, err := c.post(ctx, Request{ + Action: ActionConnect, + Host: c.target.Host, + Port: c.target.Port, + Login: c.target.User, + Password: c.target.Password, + Database: c.target.Database, + EncodeBase64: c.encodeBase64, + }) + if err != nil { + return nil, err + } + resp, err := ParseConnect(body) + if err != nil { + return nil, err + } + if !resp.Header.OK() { + return resp, headerError(resp.Header, status) + } + return resp, nil +} + +func (c *Client) Query(ctx context.Context, database string, sqls ...string) (*QueryResponse, error) { + db := database + if db == "" { + db = c.target.Database + } + queries := make([]string, 0, len(sqls)+1) + if s := charsetSQL(c.target.Charset); s != "" { + queries = append(queries, s) + } + queries = append(queries, nonEmpty(sqls)...) + if len(queries) == 0 { + return nil, fmt.Errorf("no SQL to execute") + } + body, status, err := c.post(ctx, Request{ + Action: ActionQuery, + Host: c.target.Host, + Port: c.target.Port, + Login: c.target.User, + Password: c.target.Password, + Database: db, + Queries: queries, + EncodeBase64: c.encodeBase64, + }) + if err != nil { + return nil, err + } + resp, err := ParseQuery(body) + if err != nil { + return nil, err + } + if !resp.Header.OK() { + return resp, headerError(resp.Header, status) + } + return resp, nil +} + +func (c *Client) post(ctx context.Context, req Request) (body []byte, status int, err error) { + encoded := req.Encode() + attempts := 1 + c.retries + var last error + for i := 0; i < attempts; i++ { + if err := ctx.Err(); err != nil { + return nil, 0, err + } + httpResp, err := c.http.Do(ctx, http.MethodPost, c.url, "application/x-www-form-urlencoded", strings.NewReader(encoded)) + if err != nil { + last = err + continue + } + status = httpResp.StatusCode + body = httpResp.Body + if status == http.StatusForbidden { + return body, status, &Error{Kind: "http", HTTPStatus: status, Errno: ErrnoInvalidToken, Message: "invalid or missing token"} + } + if status < 200 || status >= 300 { + return body, status, &Error{Kind: "http", HTTPStatus: status, Message: "unexpected status"} + } + if looksHTML(body) { + return body, status, &Error{Kind: "http", HTTPStatus: status, Message: "tunnel returned HTML, not Navicat binary"} + } + return body, status, nil + } + return nil, 0, last +} + +func charsetSQL(cs string) string { + cs = strings.TrimSpace(cs) + if cs == "" { + return "" + } + if strings.EqualFold(cs, "utf8") || strings.EqualFold(cs, "utf8mb3") { + return "" + } + if !charsetIdent.MatchString(cs) { + return "" + } + return "SET NAMES " + cs +} + +// LastResultSet returns the last result that has columns (skipping SET NAMES OK packets). +func LastResultSet(resp *QueryResponse) (Result, bool) { + if resp == nil { + return Result{}, false + } + for i := len(resp.Results) - 1; i >= 0; i-- { + if resp.Results[i].IsResultSet() { + return resp.Results[i], true + } + } + if len(resp.Results) == 0 { + return Result{}, false + } + return resp.Results[len(resp.Results)-1], true +} + +func RedactedURL(raw string) string { + return logger.RedactURL(raw) +} diff --git a/internal/navicat/client_test.go b/internal/navicat/client_test.go new file mode 100644 index 0000000..0ca421d --- /dev/null +++ b/internal/navicat/client_test.go @@ -0,0 +1,196 @@ +package navicat + +import ( + "context" + "net/http/httptest" + "strconv" + "strings" + "testing" + "time" + + "mysql-tunnel/internal/tunnel" +) + +func testClient(t *testing.T, opts ...MockOption) (*Client, *httptest.Server) { + t.Helper() + srv := httptest.NewServer(NewMockHandler(opts...)) + t.Cleanup(srv.Close) + httpClient := tunnel.NewHTTPClient(tunnel.Options{Timeout: 5 * time.Second}) + c := NewClient(httpClient, ClientOptions{ + URL: srv.URL + "?token=secret", + Target: Target{ + Host: "mysql", + Port: 3306, + User: "root", + Password: "remote-pass", + Database: "app", + Charset: "utf8mb4", + }, + }) + return c, srv +} + +func TestClientConnectAndSelect1(t *testing.T) { + var cap CapturedRequest + c, _ := testClient(t, WithToken("secret"), WithCapture(&cap)) + ctx := context.Background() + + conn, err := c.Connect(ctx) + if err != nil { + t.Fatal(err) + } + if conn.Conn.ServerInfo != "8.0.36-mock" { + t.Fatalf("%+v", conn.Conn) + } + if cap.Action != ActionConnect || cap.Host != "mysql" || cap.Login != "root" { + t.Fatalf("connect capture %+v", cap) + } + if cap.Port != "3306" { + t.Fatalf("port %s", cap.Port) + } + if !cap.HasPassword { + t.Fatal("password field missing") + } + + q, err := c.Query(ctx, "", "SELECT 1") + if err != nil { + t.Fatal(err) + } + res, ok := LastResultSet(q) + if !ok || res.Rows[0][0].Text() != "1" { + t.Fatalf("select 1: %+v", q) + } + if cap.Action != ActionQuery { + t.Fatalf("query capture %+v", cap) + } + joined := strings.Join(cap.Queries, " | ") + if !strings.Contains(joined, "SET NAMES utf8mb4") { + t.Fatalf("expected SET NAMES in %q", joined) + } + if !strings.Contains(joined, "SELECT 1") { + t.Fatalf("expected SELECT 1 in %q", joined) + } +} + +func TestClientTypesOverHTTP(t *testing.T) { + c, _ := testClient(t, WithToken("secret")) + ctx := context.Background() + + q, err := c.Query(ctx, "app", "SELECT NULL AS n") + if err != nil { + t.Fatal(err) + } + res, _ := LastResultSet(q) + if !res.Rows[0][0].Null { + t.Fatal("NULL") + } + + q, err = c.Query(ctx, "", "SELECT bigint_val") + if err != nil { + t.Fatal(err) + } + res, _ = LastResultSet(q) + got := res.Rows[0][0].Text() + if got != "18446744073709551615" { + t.Fatalf("bigint %q", got) + } + if _, err := strconv.ParseInt(got, 10, 64); err == nil { + t.Fatal("test value should overflow int64") + } + + q, err = c.Query(ctx, "", "SELECT dec_val") + if err != nil { + t.Fatal(err) + } + res, _ = LastResultSet(q) + if res.Fields[0].Type != 246 || res.Rows[0][0].Text() != "1234567890.123456789" { + t.Fatalf("decimal %+v", res) + } + + q, err = c.Query(ctx, "", "SELECT zh_val") + if err != nil { + t.Fatal(err) + } + res, _ = LastResultSet(q) + if res.Rows[0][0].Text() != "你好世界" { + t.Fatalf("zh %q", res.Rows[0][0].Text()) + } +} + +func TestClientSQLError(t *testing.T) { + c, _ := testClient(t, WithToken("secret")) + q, err := c.Query(context.Background(), "", "SELECT SYNTAX") + if err != nil { + t.Fatal(err) + } + res, ok := LastResultSet(q) + if !ok || !res.IsError() || res.Errno != 1064 { + t.Fatalf("%+v", q) + } +} + +func TestClientDML(t *testing.T) { + c, _ := testClient(t, WithToken("secret")) + q, err := c.Query(context.Background(), "", "UPDATE product SET price=1") + if err != nil { + t.Fatal(err) + } + res := q.Results[len(q.Results)-1] + if res.IsResultSet() || res.AffectedRows != 1 || res.InsertID != 42 { + t.Fatalf("%+v", res) + } +} + +func TestClientBadTokenPOST(t *testing.T) { + srv := httptest.NewServer(NewMockHandler(WithToken("right"))) + t.Cleanup(srv.Close) + httpClient := tunnel.NewHTTPClient(tunnel.Options{Timeout: 5 * time.Second}) + c := NewClient(httpClient, ClientOptions{ + URL: srv.URL + "?token=wrong", + Target: Target{Host: "mysql", Port: 3306, User: "root"}, + }) + _, err := c.Connect(context.Background()) + if err == nil { + t.Fatal("expected token error") + } + ne, ok := err.(*Error) + if !ok || ne.Errno != ErrnoInvalidToken { + t.Fatalf("%v", err) + } +} + +func TestClientConnectDenied(t *testing.T) { + c, _ := testClient(t, WithToken("secret"), WithConnectError(1045, "Access denied for user 'root'@'localhost'")) + _, err := c.Connect(context.Background()) + if err == nil { + t.Fatal("expected deny") + } + if !strings.Contains(err.Error(), "1045") { + t.Fatal(err) + } + if strings.Contains(err.Error(), "remote-pass") { + t.Fatal("password leaked") + } +} + +func TestEncodeBase64Query(t *testing.T) { + var cap CapturedRequest + srv := httptest.NewServer(NewMockHandler(WithToken("secret"), WithCapture(&cap))) + t.Cleanup(srv.Close) + httpClient := tunnel.NewHTTPClient(tunnel.Options{Timeout: 5 * time.Second}) + c := NewClient(httpClient, ClientOptions{ + URL: srv.URL + "?token=secret", + Target: Target{Host: "mysql", Port: 3306, User: "root", Charset: "utf8"}, + EncodeBase64: true, + }) + _, err := c.Query(context.Background(), "", "SELECT 1") + if err != nil { + t.Fatal(err) + } + if cap.EncodeBase64 != "1" { + t.Fatalf("%+v", cap) + } + if strings.Contains(strings.Join(cap.Queries, ""), "SELECT 1") { + t.Fatalf("wire should be base64, got %v", cap.Queries) + } +} diff --git a/internal/navicat/doc.go b/internal/navicat/doc.go new file mode 100644 index 0000000..4806d39 --- /dev/null +++ b/internal/navicat/doc.go @@ -0,0 +1,26 @@ +// Package navicat implements the Navicat HTTP Tunnel protocol used by +// ntunnel_mysql.php (script version 206 in this repository). +// +// Every request is stateless: the PHP script opens a new MySQL connection, +// runs actn=C or actn=Q, and disconnects. See docs/PROTOCOL.md. +package navicat + +const ( + Magic uint32 = 1111 + Version202 uint16 = 202 + Version206 uint16 = 206 + HeaderSize = 16 + + ActionConnect = "C" + ActionQuery = "Q" + + ErrnoUnsupportedPHP uint32 = 201 + ErrnoInvalidParams uint32 = 202 + ErrnoMySQLMissing uint32 = 203 + ErrnoInvalidToken uint32 = 204 + + sepMore byte = 0x01 + sepLast byte = 0x00 + markLong byte = 0xFE + markNull byte = 0xFF +) diff --git a/internal/navicat/error.go b/internal/navicat/error.go new file mode 100644 index 0000000..cb25107 --- /dev/null +++ b/internal/navicat/error.go @@ -0,0 +1,45 @@ +package navicat + +import "fmt" + +// Error is a tunnel-layer or remote MySQL error. It never includes passwords or tokens. +type Error struct { + Kind string // http, tunnel, mysql + HTTPStatus int + Errno uint32 + Message string +} + +func (e *Error) Error() string { + if e == nil { + return "" + } + msg := e.Message + if msg == "" { + msg = "unknown error" + } + switch e.Kind { + case "http": + if e.HTTPStatus != 0 { + return fmt.Sprintf("tunnel http %d: %s", e.HTTPStatus, msg) + } + return "tunnel http: " + msg + case "mysql": + return fmt.Sprintf("mysql errno %d: %s", e.Errno, msg) + default: + return fmtErrno(e.Errno, msg) + } +} + +func headerError(h Header, httpStatus int) *Error { + kind := "tunnel" + switch h.Errno { + case ErrnoUnsupportedPHP, ErrnoInvalidParams, ErrnoMySQLMissing, ErrnoInvalidToken: + kind = "tunnel" + default: + if h.Errno > 0 { + kind = "mysql" + } + } + return &Error{Kind: kind, HTTPStatus: httpStatus, Errno: h.Errno, Message: h.Message} +} diff --git a/internal/navicat/mock.go b/internal/navicat/mock.go new file mode 100644 index 0000000..3a77a43 --- /dev/null +++ b/internal/navicat/mock.go @@ -0,0 +1,228 @@ +package navicat + +import ( + "encoding/base64" + "net/http" + "strings" +) + +// MockOption configures the in-process ntunnel_mysql.php stand-in. +type MockOption func(*mockState) + +type mockState struct { + token string + connectFail *Error + queryFn func(sql string) Result + capture *CapturedRequest +} + +// CapturedRequest is the last POST seen by the mock. +type CapturedRequest struct { + Action string + Host string + Port string + Login string + Database string + Queries []string + EncodeBase64 string + HasPassword bool +} + +func WithToken(token string) MockOption { + return func(m *mockState) { m.token = token } +} + +func WithConnectError(errno uint32, msg string) MockOption { + return func(m *mockState) { + m.connectFail = &Error{Kind: "mysql", Errno: errno, Message: msg} + } +} + +func WithQueryHandler(fn func(sql string) Result) MockOption { + return func(m *mockState) { m.queryFn = fn } +} + +func WithCapture(dst *CapturedRequest) MockOption { + return func(m *mockState) { m.capture = dst } +} + +// NewMockHandler implements a subset of ntunnel_mysql.php for tests. +func NewMockHandler(opts ...MockOption) http.Handler { + st := &mockState{queryFn: defaultQuery} + for _, o := range opts { + o(st) + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = r.ParseForm() + if st.token != "" && r.URL.Query().Get("token") != st.token { + if r.PostForm.Get("actn") != "" { + writeBytes(w, encodeHeaderError(ErrnoInvalidToken, "invalid access token")) + return + } + http.Error(w, "403 Forbidden: invalid or missing token", http.StatusForbidden) + return + } + if r.Method != http.MethodPost { + writeBytes(w, encodeHeaderError(ErrnoInvalidParams, "invalid parameters")) + return + } + actn := r.PostForm.Get("actn") + host := r.PostForm.Get("host") + login := r.PostForm.Get("login") + if actn == "" || host == "" || !r.PostForm.Has("port") || login == "" { + writeBytes(w, encodeHeaderError(ErrnoInvalidParams, "invalid parameters")) + return + } + if st.capture != nil { + *st.capture = CapturedRequest{ + Action: actn, + Host: host, + Port: r.PostForm.Get("port"), + Login: login, + Database: r.PostForm.Get("db"), + Queries: append([]string(nil), r.PostForm["q[]"]...), + EncodeBase64: r.PostForm.Get("encodeBase64"), + HasPassword: r.PostForm.Has("password"), + } + } + if st.connectFail != nil { + writeBytes(w, encodeHeaderError(st.connectFail.Errno, st.connectFail.Message)) + return + } + switch actn { + case ActionConnect: + writeBytes(w, encodeConnectOK("127.0.0.1 via TCP/IP", "10", "8.0.36-mock")) + case ActionQuery: + queries := append([]string(nil), r.PostForm["q[]"]...) + if r.PostForm.Get("encodeBase64") == "1" { + decoded := make([]string, 0, len(queries)) + for _, q := range queries { + b, err := base64.StdEncoding.DecodeString(q) + if err != nil { + writeBytes(w, encodeQueryResults([]Result{{Errno: 1064, Message: "bad base64 query"}})) + return + } + decoded = append(decoded, string(b)) + } + queries = decoded + } + var results []Result + for _, q := range queries { + if q == "" { + continue + } + results = append(results, st.queryFn(q)) + } + writeBytes(w, encodeQueryResults(results)) + default: + writeBytes(w, AppendHeader(nil, Version206, 0)) + } + }) +} + +func defaultQuery(sql string) Result { + s := strings.TrimSpace(sql) + u := strings.ToUpper(s) + switch { + case strings.HasPrefix(u, "SET NAMES"): + return Result{} + case u == "SELECT 1" || strings.HasPrefix(u, "SELECT 1 "): + return selectLiteral("1", "1", 8, 20) + case strings.Contains(u, "SELECT NULL"): + return Result{ + NumFields: 1, + NumRows: 1, + Fields: []Field{{Name: "n", Type: 6, Length: 0}}, + Rows: [][]Cell{{{Null: true}}}, + } + case strings.Contains(strings.ToLower(s), "bigint_val"): + r := selectLiteral("bigint_val", "18446744073709551615", 8, 20) + r.Fields[0].Flags = 32 // UNSIGNED + return r + case strings.Contains(strings.ToLower(s), "dec_val"): + return selectLiteral("dec_val", "1234567890.123456789", 246, 32) + case strings.Contains(s, "你好") || strings.Contains(strings.ToLower(s), "zh_val"): + return selectLiteral("zh_val", "你好世界", 253, 12) + case strings.HasPrefix(u, "INSERT") || strings.HasPrefix(u, "UPDATE") || strings.HasPrefix(u, "DELETE") || strings.HasPrefix(u, "REPLACE"): + return Result{AffectedRows: 1, InsertID: 42, Info: "Rows matched: 1 Changed: 1 Warnings: 0"} + case strings.Contains(u, "SYNTAX"): + return Result{Errno: 1064, Message: "You have an error in your SQL syntax"} + default: + return Result{Errno: 1064, Message: "mock: unhandled SQL"} + } +} + +func selectLiteral(name, value string, typ, length uint32) Result { + return Result{ + NumFields: 1, + NumRows: 1, + Fields: []Field{{Name: name, Type: typ, Length: length}}, + Rows: [][]Cell{{{Bytes: []byte(value)}}}, + } +} + +func encodeHeaderError(errno uint32, msg string) []byte { + b := AppendHeader(nil, Version206, errno) + return AppendBlock(b, []byte(msg)) +} + +func encodeConnectOK(host, proto, server string) []byte { + b := AppendHeader(nil, Version206, 0) + b = AppendBlock(b, []byte(host)) + b = AppendBlock(b, []byte(proto)) + b = AppendBlock(b, []byte(server)) + return b +} + +func encodeQueryResults(results []Result) []byte { + b := AppendHeader(nil, Version206, 0) + if len(results) == 0 { + return b + } + for i, res := range results { + b = appendQueryResult(b, res) + if i < len(results)-1 { + b = append(b, sepMore) + } else { + b = append(b, sepLast) + } + } + return b +} + +func appendQueryResult(b []byte, res Result) []byte { + nfields := res.NumFields + nrows := res.NumRows + if nfields == 0 { + nfields = uint32(len(res.Fields)) + } + if nrows == 0 && len(res.Rows) > 0 { + nrows = uint32(len(res.Rows)) + } + b = AppendResultSetHeader(b, res.Errno, res.AffectedRows, res.InsertID, nfields, nrows) + if res.Errno > 0 { + return AppendBlock(b, []byte(res.Message)) + } + if nfields == 0 { + return AppendBlock(b, []byte(res.Info)) + } + for _, f := range res.Fields { + b = AppendField(b, f.Name, f.Table, f.Type, f.Flags, f.Length) + } + for _, row := range res.Rows { + for _, cell := range row { + if cell.Null { + b = AppendNull(b) + } else { + b = AppendBlock(b, cell.Bytes) + } + } + } + return b +} + +func writeBytes(w http.ResponseWriter, body []byte) { + w.Header().Set("Content-Type", "text/plain; charset=x-user-defined") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(body) +} diff --git a/internal/navicat/parser.go b/internal/navicat/parser.go new file mode 100644 index 0000000..3728b30 --- /dev/null +++ b/internal/navicat/parser.go @@ -0,0 +1,116 @@ +package navicat + +import ( + "encoding/binary" + "fmt" + "io" +) + +type reader struct { + buf []byte + off int +} + +func newReader(b []byte) *reader { + return &reader{buf: b} +} + +func (r *reader) remaining() int { + return len(r.buf) - r.off +} + +func (r *reader) peek() (byte, error) { + if r.off >= len(r.buf) { + return 0, io.EOF + } + return r.buf[r.off], nil +} + +func (r *reader) readByte() (byte, error) { + if r.off >= len(r.buf) { + return 0, io.ErrUnexpectedEOF + } + b := r.buf[r.off] + r.off++ + return b, nil +} + +func (r *reader) readN(n int) ([]byte, error) { + if n < 0 { + return nil, fmt.Errorf("negative length") + } + if r.off+n > len(r.buf) { + return nil, io.ErrUnexpectedEOF + } + s := r.buf[r.off : r.off+n] + r.off += n + return s, nil +} + +func (r *reader) u16() (uint16, error) { + b, err := r.readN(2) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint16(b), nil +} + +func (r *reader) u32() (uint32, error) { + b, err := r.readN(4) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint32(b), nil +} + +// block reads GetBlock. nullable allows 0xFF as SQL NULL (EchoData only). +func (r *reader) block(nullable bool) (val []byte, isNull bool, err error) { + if r.off >= len(r.buf) { + return nil, false, io.ErrUnexpectedEOF + } + first := r.buf[r.off] + if nullable && first == markNull { + r.off++ + return nil, true, nil + } + r.off++ + var n int + if first < 254 { + n = int(first) + } else if first == markLong { + ln, err := r.u32() + if err != nil { + return nil, false, err + } + if ln > 64<<20 { + return nil, false, fmt.Errorf("block length %d exceeds 64MiB limit", ln) + } + n = int(ln) + } else { + return nil, false, fmt.Errorf("invalid block marker 0x%02x", first) + } + b, err := r.readN(n) + if err != nil { + return nil, false, err + } + return b, false, nil +} + +func (r *reader) blockBytes() ([]byte, error) { + b, isNull, err := r.block(false) + if err != nil { + return nil, err + } + if isNull { + return nil, fmt.Errorf("unexpected NULL block") + } + return b, nil +} + +func (r *reader) blockString() (string, error) { + b, err := r.blockBytes() + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/internal/navicat/parser_test.go b/internal/navicat/parser_test.go new file mode 100644 index 0000000..a1836b2 --- /dev/null +++ b/internal/navicat/parser_test.go @@ -0,0 +1,205 @@ +package navicat + +import ( + "bytes" + "strings" + "testing" +) + +func TestBlockRoundTripShortAndLong(t *testing.T) { + cases := [][]byte{ + {}, + []byte("a"), + bytes.Repeat([]byte("x"), 253), + bytes.Repeat([]byte("y"), 254), + bytes.Repeat([]byte("z"), 1024), + []byte("你好世界"), + } + for _, in := range cases { + enc := AppendBlock(nil, in) + r := newReader(enc) + got, isNull, err := r.block(false) + if err != nil { + t.Fatalf("len=%d: %v", len(in), err) + } + if isNull { + t.Fatal("null") + } + if !bytes.Equal(got, in) { + t.Fatalf("mismatch len=%d", len(in)) + } + } +} + +func TestNullCell(t *testing.T) { + enc := AppendNull(nil) + r := newReader(enc) + _, isNull, err := r.block(true) + if err != nil || !isNull { + t.Fatalf("err=%v null=%v", err, isNull) + } +} + +func TestHeader202And206(t *testing.T) { + for _, ver := range []uint16{Version202, Version206} { + body := AppendHeader(nil, ver, 0) + h, _, err := ParseHeader(body) + if err != nil { + t.Fatal(err) + } + if h.Version != ver || !h.OK() { + t.Fatalf("%+v", h) + } + } +} + +func TestHeaderError204(t *testing.T) { + body := encodeHeaderError(ErrnoInvalidToken, "invalid access token") + h, _, err := ParseHeader(body) + if err != nil { + t.Fatal(err) + } + if h.Errno != ErrnoInvalidToken || h.Message != "invalid access token" { + t.Fatalf("%+v", h) + } +} + +func TestConnectParse(t *testing.T) { + body := encodeConnectOK("127.0.0.1 via TCP/IP", "10", "8.0.36") + resp, err := ParseConnect(body) + if err != nil { + t.Fatal(err) + } + if resp.Conn.ProtoInfo != "10" || resp.Conn.ServerInfo != "8.0.36" { + t.Fatalf("%+v", resp.Conn) + } +} + +func TestQuerySelect1(t *testing.T) { + body := encodeQueryResults([]Result{selectLiteral("1", "1", 8, 20)}) + resp, err := ParseQuery(body) + if err != nil { + t.Fatal(err) + } + if len(resp.Results) != 1 { + t.Fatalf("results=%d", len(resp.Results)) + } + r := resp.Results[0] + if !r.IsResultSet() || r.NumRows != 1 { + t.Fatalf("%+v", r) + } + if r.Rows[0][0].Null || r.Rows[0][0].Text() != "1" { + t.Fatalf("cell=%+v", r.Rows[0][0]) + } +} + +func TestNullDecimalBigintUTF8(t *testing.T) { + body := encodeQueryResults([]Result{ + { + NumFields: 4, + NumRows: 1, + Fields: []Field{ + {Name: "n", Type: 6}, + {Name: "b", Type: 8, Length: 20}, + {Name: "d", Type: 246, Length: 32}, + {Name: "z", Type: 253, Length: 12}, + }, + Rows: [][]Cell{{ + {Null: true}, + {Bytes: []byte("18446744073709551615")}, + {Bytes: []byte("1234567890.123456789")}, + {Bytes: []byte("你好世界")}, + }}, + }, + }) + resp, err := ParseQuery(body) + if err != nil { + t.Fatal(err) + } + row := resp.Results[0].Rows[0] + if !row[0].Null { + t.Fatal("expected NULL") + } + if row[1].Text() != "18446744073709551615" { + t.Fatalf("bigint parsed badly: %q", row[1].Text()) + } + if row[2].Text() != "1234567890.123456789" { + t.Fatalf("decimal parsed badly: %q", row[2].Text()) + } + if row[3].Text() != "你好世界" { + t.Fatalf("utf8: %q", row[3].Text()) + } +} + +func TestSQLError(t *testing.T) { + body := encodeQueryResults([]Result{{Errno: 1064, Message: "You have an error in your SQL syntax"}}) + resp, err := ParseQuery(body) + if err != nil { + t.Fatal(err) + } + if !resp.Results[0].IsError() || !strings.Contains(resp.Results[0].Message, "syntax") { + t.Fatalf("%+v", resp.Results[0]) + } +} + +func TestDMLOk(t *testing.T) { + body := encodeQueryResults([]Result{{AffectedRows: 3, InsertID: 9, Info: "Rows matched: 3"}}) + resp, err := ParseQuery(body) + if err != nil { + t.Fatal(err) + } + r := resp.Results[0] + if r.IsResultSet() || r.AffectedRows != 3 || r.InsertID != 9 { + t.Fatalf("%+v", r) + } +} + +func TestHTMLRejected(t *testing.T) { + _, err := ParseConnect([]byte("nope")) + if err == nil { + t.Fatal("expected error") + } +} + +func TestRequestOmitsEmptyQueriesAndEncodesArray(t *testing.T) { + enc := Request{ + Action: ActionQuery, + Host: "mysql", + Port: 3306, + Login: "root", + Queries: []string{"SELECT 1", "", "SELECT 2"}, + }.Encode() + if strings.Contains(enc, "password=secret") { + t.Fatal("unexpected") + } + if !strings.Contains(enc, "q%5B%5D=SELECT+1") && !strings.Contains(enc, "q%5B%5D=SELECT%201") { + t.Fatalf("q[] missing: %s", enc) + } + if strings.Count(enc, "q%5B%5D=") != 2 { + t.Fatalf("empty query not skipped: %s", enc) + } +} + +func TestRequestBase64(t *testing.T) { + enc := Request{ + Action: ActionQuery, + Host: "mysql", + Port: 3306, + Login: "root", + Queries: []string{"SELECT 1"}, + EncodeBase64: true, + }.Encode() + if !strings.Contains(enc, "encodeBase64=1") { + t.Fatal(enc) + } + if strings.Contains(enc, "SELECT+1") { + t.Fatalf("sql should be base64: %s", enc) + } +} + +func TestErrorDoesNotEmbedPassword(t *testing.T) { + e := &Error{Kind: "mysql", Errno: 1045, Message: "Access denied for user 'root'@'localhost'"} + if strings.Contains(e.Error(), "password") && strings.Contains(e.Error(), "=") { + t.Fatal(e.Error()) + } +} diff --git a/internal/navicat/request.go b/internal/navicat/request.go new file mode 100644 index 0000000..e40b401 --- /dev/null +++ b/internal/navicat/request.go @@ -0,0 +1,63 @@ +package navicat + +import ( + "encoding/base64" + "net/url" + "strconv" + "strings" +) + +// Request is one Navicat HTTP Tunnel POST body. +type Request struct { + Action string + Host string + Port int + Login string + Password string + Database string + Queries []string + EncodeBase64 bool +} + +// Form builds application/x-www-form-urlencoded fields matching $_POST in ntunnel_mysql.php. +func (r Request) Form() url.Values { + v := url.Values{} + v.Set("actn", r.Action) + v.Set("host", r.Host) + if r.Port > 0 { + v.Set("port", strconv.Itoa(r.Port)) + } else { + v.Set("port", "") + } + v.Set("login", r.Login) + v.Set("password", r.Password) + v.Set("db", r.Database) + + queries := nonEmpty(r.Queries) + if r.EncodeBase64 { + v.Set("encodeBase64", "1") + for _, q := range queries { + v.Add("q[]", base64.StdEncoding.EncodeToString([]byte(q))) + } + } else { + for _, q := range queries { + v.Add("q[]", q) + } + } + return v +} + +func (r Request) Encode() string { + return r.Form().Encode() +} + +func nonEmpty(in []string) []string { + out := make([]string, 0, len(in)) + for _, s := range in { + if strings.TrimSpace(s) == "" { + continue + } + out = append(out, s) + } + return out +} diff --git a/internal/navicat/response.go b/internal/navicat/response.go new file mode 100644 index 0000000..d5bab50 --- /dev/null +++ b/internal/navicat/response.go @@ -0,0 +1,266 @@ +package navicat + +import ( + "fmt" + "strings" +) + +// Header is EchoHeader from ntunnel_mysql.php (16 bytes). +type Header struct { + Magic uint32 + Version uint16 + Errno uint32 + Message string + UncertainVersion bool +} + +func (h Header) OK() bool { return h.Errno == 0 } + +// ConnInfo is actn=C payload after a successful header. +type ConnInfo struct { + HostInfo string + ProtoInfo string + ServerInfo string +} + +// Field is one EchoFieldsHeader column. Type/flags/length are mysqli numeric values. +type Field struct { + Name string + Table string + Type uint32 + Flags uint32 + Length uint32 +} + +// Cell is one row field. Bytes are kept as the PHP string; never parsed as int64/float64. +type Cell struct { + Null bool + Bytes []byte +} + +func (c Cell) Text() string { + if c.Null { + return "" + } + return string(c.Bytes) +} + +// Result is one query in an actn=Q response. +type Result struct { + Errno uint32 + Message string + AffectedRows uint32 + InsertID uint32 // protocol is 32-bit; do not invent high bits + NumFields uint32 + NumRows uint32 + Info string + Fields []Field + Rows [][]Cell +} + +func (r Result) IsError() bool { return r.Errno > 0 } +func (r Result) IsResultSet() bool { + return r.Errno == 0 && r.NumFields > 0 +} + +// ConnectResponse is a parsed actn=C reply. +type ConnectResponse struct { + Header Header + Conn ConnInfo +} + +// QueryResponse is a parsed actn=Q reply. +type QueryResponse struct { + Header Header + Results []Result +} + +func ParseHeader(body []byte) (Header, *reader, error) { + var h Header + if len(body) < HeaderSize { + if looksHTML(body) { + return h, nil, &Error{Kind: "http", Message: "tunnel returned HTML, not Navicat binary"} + } + return h, nil, fmt.Errorf("response too short (%d bytes)", len(body)) + } + r := newReader(body) + magic, err := r.u32() + if err != nil { + return h, nil, err + } + if magic != Magic { + if looksHTML(body) { + return h, nil, &Error{Kind: "http", Message: "tunnel returned HTML, not Navicat binary"} + } + return h, nil, fmt.Errorf("invalid Navicat magic %d (want %d)", magic, Magic) + } + ver, err := r.u16() + if err != nil { + return h, nil, err + } + errno, err := r.u32() + if err != nil { + return h, nil, err + } + if _, err := r.readN(6); err != nil { + return h, nil, err + } + h.Magic = magic + h.Version = ver + h.Errno = errno + h.UncertainVersion = !knownVersion(ver) + if errno != 0 { + msg, err := r.blockString() + if err != nil { + return h, nil, fmt.Errorf("read error message: %w", err) + } + h.Message = msg + } + return h, r, nil +} + +func ParseConnect(body []byte) (*ConnectResponse, error) { + h, r, err := ParseHeader(body) + if err != nil { + return nil, err + } + out := &ConnectResponse{Header: h} + if !h.OK() { + return out, nil + } + host, err := r.blockString() + if err != nil { + return nil, fmt.Errorf("conn host_info: %w", err) + } + proto, err := r.blockString() + if err != nil { + return nil, fmt.Errorf("conn proto_info: %w", err) + } + server, err := r.blockString() + if err != nil { + return nil, fmt.Errorf("conn server_info: %w", err) + } + out.Conn = ConnInfo{HostInfo: host, ProtoInfo: proto, ServerInfo: server} + return out, nil +} + +func ParseQuery(body []byte) (*QueryResponse, error) { + h, r, err := ParseHeader(body) + if err != nil { + return nil, err + } + out := &QueryResponse{Header: h} + if !h.OK() { + return out, nil + } + for { + if r.remaining() == 0 { + break + } + res, err := parseOneResult(r) + if err != nil { + return nil, err + } + out.Results = append(out.Results, res) + if r.remaining() == 0 { + break + } + sep, err := r.readByte() + if err != nil { + return nil, err + } + if sep == sepLast { + break + } + if sep != sepMore { + return nil, fmt.Errorf("invalid query separator 0x%02x", sep) + } + } + return out, nil +} + +func parseOneResult(r *reader) (Result, error) { + var res Result + var err error + if res.Errno, err = r.u32(); err != nil { + return res, err + } + if res.AffectedRows, err = r.u32(); err != nil { + return res, err + } + if res.InsertID, err = r.u32(); err != nil { + return res, err + } + if res.NumFields, err = r.u32(); err != nil { + return res, err + } + if res.NumRows, err = r.u32(); err != nil { + return res, err + } + if _, err = r.readN(12); err != nil { + return res, err + } + if res.Errno > 0 { + res.Message, err = r.blockString() + return res, err + } + if res.NumFields == 0 { + res.Info, err = r.blockString() + return res, err + } + res.Fields = make([]Field, 0, res.NumFields) + for i := uint32(0); i < res.NumFields; i++ { + f, err := parseField(r) + if err != nil { + return res, fmt.Errorf("field %d: %w", i, err) + } + res.Fields = append(res.Fields, f) + } + res.Rows = make([][]Cell, 0, res.NumRows) + for i := uint32(0); i < res.NumRows; i++ { + row := make([]Cell, res.NumFields) + for j := uint32(0); j < res.NumFields; j++ { + val, isNull, err := r.block(true) + if err != nil { + return res, fmt.Errorf("row %d col %d: %w", i, j, err) + } + row[j] = Cell{Null: isNull, Bytes: val} + } + res.Rows = append(res.Rows, row) + } + return res, nil +} + +func parseField(r *reader) (Field, error) { + var f Field + var err error + if f.Name, err = r.blockString(); err != nil { + return f, err + } + if f.Table, err = r.blockString(); err != nil { + return f, err + } + if f.Type, err = r.u32(); err != nil { + return f, err + } + if f.Flags, err = r.u32(); err != nil { + return f, err + } + if f.Length, err = r.u32(); err != nil { + return f, err + } + return f, nil +} + +func looksHTML(body []byte) bool { + s := strings.TrimSpace(string(body)) + if len(s) < 5 { + return false + } + n := 64 + if len(s) < n { + n = len(s) + } + ls := strings.ToLower(s[:n]) + return strings.HasPrefix(ls, " Any: + v = raw.strip() + if v == "": + return "" + if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")): + return v[1:-1] + low = v.lower() + if low in ("true", "yes"): + return True + if low in ("false", "no"): + return False + try: + return int(v) + except ValueError: + return v + + +def load_project_yaml(path: Path) -> dict[str, Any]: + root: dict[str, Any] = {} + section: str | None = None + text = path.read_text(encoding="utf-8") + for line in text.splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + indent = len(line) - len(line.lstrip(" \t")) + if indent == 0 and stripped.endswith(":") and ":" not in stripped[:-1]: + section = stripped[:-1] + root[section] = {} + continue + if section is None or ":" not in stripped: + continue + key, _, val = stripped.partition(":") + root[section][key.strip()] = parse_scalar(val) + return root + + +def redact_url(raw: str) -> str: + u = urlparse(raw) + q = [] + for k, v in parse_qsl(u.query, keep_blank_values=True): + if k.lower() in ("token", "password", "pass", "access_token", "accesstoken"): + q.append((k, "***")) + else: + q.append((k, v)) + return urlunparse(u._replace(query=urlencode(q), fragment="")) + + +def looks_html(body: bytes) -> bool: + s = body.lstrip()[:64].decode("latin-1", errors="replace").lower() + return s.startswith(" bool: + return self.errno == 0 + + +@dataclass +class Field: + name: str + table: str + type: int = 0 + flags: int = 0 + length: int = 0 + + +@dataclass +class Result: + errno: int = 0 + message: str = "" + affected_rows: int = 0 + insert_id: int = 0 + num_fields: int = 0 + num_rows: int = 0 + info: str = "" + fields: list[Field] = field(default_factory=list) + rows: list[list[str | None]] = field(default_factory=list) + + @property + def is_error(self) -> bool: + return self.errno > 0 + + @property + def is_result_set(self) -> bool: + return self.errno == 0 and self.num_fields > 0 + + +class Reader: + def __init__(self, buf: bytes) -> None: + self.buf = buf + self.off = 0 + + def remaining(self) -> int: + return len(self.buf) - self.off + + def read(self, n: int) -> bytes: + if n < 0 or self.off + n > len(self.buf): + raise TunnelError("truncated Navicat binary") + s = self.buf[self.off : self.off + n] + self.off += n + return s + + def u8(self) -> int: + return self.read(1)[0] + + def u16(self) -> int: + return struct.unpack(">H", self.read(2))[0] + + def u32(self) -> int: + return struct.unpack(">I", self.read(4))[0] + + def block(self, nullable: bool = False) -> tuple[bytes | None, bool]: + if self.remaining() == 0: + raise TunnelError("truncated GetBlock") + first = self.buf[self.off] + if nullable and first == MARK_NULL: + self.off += 1 + return None, True + self.off += 1 + if first < 254: + n = first + elif first == MARK_LONG: + n = self.u32() + if n > MAX_BLOCK: + raise TunnelError(f"block length {n} exceeds 64MiB") + else: + raise TunnelError(f"invalid block marker 0x{first:02x}") + return self.read(n), False + + def block_str(self) -> str: + val, is_null = self.block(False) + if is_null or val is None: + raise TunnelError("unexpected NULL block") + return val.decode("utf-8", errors="replace") + + +def parse_header(body: bytes) -> tuple[Header, Reader]: + if looks_html(body): + raise TunnelError("tunnel returned HTML, not Navicat binary") + if len(body) < 16: + raise TunnelError(f"response too short ({len(body)} bytes)") + r = Reader(body) + magic = r.u32() + if magic != MAGIC: + raise TunnelError(f"invalid Navicat magic {magic} (want {MAGIC})") + version = r.u16() + errno = r.u32() + r.read(6) + msg = "" + if errno != 0: + msg = r.block_str() + return Header(magic, version, errno, msg), r + + +def parse_connect(body: bytes) -> tuple[Header, dict[str, str]]: + h, r = parse_header(body) + if not h.ok: + return h, {} + return h, { + "host_info": r.block_str(), + "proto_info": r.block_str(), + "server_info": r.block_str(), + } + + +def parse_one_result(r: Reader) -> Result: + res = Result( + errno=r.u32(), + affected_rows=r.u32(), + insert_id=r.u32(), + num_fields=r.u32(), + num_rows=r.u32(), + ) + r.read(12) + if res.errno > 0: + res.message = r.block_str() + return res + if res.num_fields == 0: + res.info = r.block_str() + return res + for _ in range(res.num_fields): + res.fields.append(Field(r.block_str(), r.block_str(), r.u32(), r.u32(), r.u32())) + for _ in range(res.num_rows): + row: list[str | None] = [] + for _c in range(res.num_fields): + val, is_null = r.block(True) + row.append(None if is_null else (val or b"").decode("utf-8", errors="replace")) + res.rows.append(row) + return res + + +def parse_query(body: bytes) -> tuple[Header, list[Result]]: + h, r = parse_header(body) + if not h.ok: + return h, [] + results: list[Result] = [] + while r.remaining() > 0: + results.append(parse_one_result(r)) + if r.remaining() == 0: + break + sep = r.u8() + if sep == SEP_LAST: + break + if sep != SEP_MORE: + raise TunnelError(f"invalid query separator 0x{sep:02x}") + return h, results + + +@dataclass +class Config: + path: Path + listen_host: str + listen_port: int + local_user: str + local_password: str + url: str + encode_base64: bool + tls_insecure: bool + remote_host: str + remote_port: int + remote_user: str + remote_password: str + database: str + charset: str + timeout: int + + @classmethod + def load(cls, path: Path) -> Config: + data = load_project_yaml(path) + listen = data.get("listen") or {} + local = data.get("local_auth") or {} + tunnel = data.get("tunnel") or {} + remote = data.get("remote") or {} + options = data.get("options") or {} + url = str(tunnel.get("url") or "").strip() + if not url: + raise TunnelError(f"{path}: tunnel.url is required") + local_user = str(local.get("username") or "").strip() + if not local_user: + raise TunnelError(f"{path}: local_auth.username is required") + timeout = int(options.get("request_timeout") or 30) + if timeout <= 0: + timeout = 30 + return cls( + path=path, + listen_host=str(listen.get("host") or "127.0.0.1"), + listen_port=int(listen.get("port") or 13306), + local_user=local_user, + local_password=str(local.get("password") or ""), + url=url, + encode_base64=bool(tunnel.get("encode_base64")), + tls_insecure=bool(tunnel.get("tls_insecure_skip_verify")), + remote_host=str(remote.get("host") or ""), + remote_port=int(remote.get("port") or 3306), + remote_user=str(remote.get("username") or ""), + remote_password=str(remote.get("password") or ""), + database=str(remote.get("database") or ""), + charset=str(remote.get("charset") or "utf8mb4"), + timeout=timeout, + ) + + +class HTTPClient: + """POST actn=C/Q to ntunnel_mysql.php using remote.* (not local_auth).""" + + def __init__(self, cfg: Config) -> None: + self.cfg = cfg + ctx = ssl.create_default_context() + if cfg.tls_insecure: + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + self._ssl = ctx + + def post(self, actn: str, queries: list[str] | None = None) -> bytes: + fields: list[tuple[str, str]] = [ + ("actn", actn), + ("host", self.cfg.remote_host), + ("port", str(self.cfg.remote_port) if self.cfg.remote_port > 0 else ""), + ("login", self.cfg.remote_user), + ("password", self.cfg.remote_password), + ("db", self.cfg.database), + ] + qs = [q for q in (queries or []) if q.strip()] + if self.cfg.encode_base64 and qs: + fields.append(("encodeBase64", "1")) + for q in qs: + fields.append(("q[]", base64.b64encode(q.encode("utf-8")).decode("ascii"))) + else: + for q in qs: + fields.append(("q[]", q)) + body = urlencode(fields).encode("utf-8") + req = Request( + self.cfg.url, + data=body, + method="POST", + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "mysql-tunnel-python/0.5", + }, + ) + kw: dict[str, Any] = {"timeout": self.cfg.timeout} + if urlparse(self.cfg.url).scheme == "https": + kw["context"] = self._ssl + try: + with urlopen(req, **kw) as resp: + status = resp.status + data = resp.read() + except HTTPError as e: + if e.code == 403: + raise TunnelError("HTTP 403: invalid or missing token") from None + raise TunnelError(f"HTTP {e.code}") from None + except URLError as e: + raise TunnelError(f"HTTP transport: {e.reason}") from None + if status < 200 or status >= 300: + raise TunnelError(f"HTTP {status}") + if looks_html(data): + raise TunnelError("tunnel returned HTML, not Navicat binary") + return data + + def connect(self) -> tuple[Header, dict[str, str]]: + return parse_connect(self.post("C")) + + def query(self, *sqls: str) -> tuple[Header, list[Result]]: + qs: list[str] = [] + cs = (self.cfg.charset or "").strip() + if cs and cs.lower() not in ("utf8", "utf8mb3") and cs.replace("_", "").isalnum(): + qs.append("SET NAMES " + cs) + qs.extend(sqls) + return parse_query(self.post("Q", qs)) + + +def mysql_connect(cfg: Config): + try: + import pymysql + except ImportError as e: + raise TunnelError("install PyMySQL: pip install pymysql") from e + import warnings + + # Same signature as 影刀: host/port/user/password/db → listen + local_auth + remote.database + try: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return pymysql.connect( + host=cfg.listen_host, + port=cfg.listen_port, + user=cfg.local_user, + password=cfg.local_password, + db=cfg.database, + ) + except pymysql.err.OperationalError as e: + code = e.args[0] if e.args else 0 + if code in (2003, 2002): + raise TunnelError( + f"cannot connect {cfg.listen_host}:{cfg.listen_port} — " + "start mysql-tunnel.exe first (GUI 「启动」 or --console)" + ) from None + if code == 1045: + raise TunnelError( + f"local_auth rejected ({cfg.local_user}) — " + "影刀/本脚本的 user/password 必须等于 config.yaml 的 local_auth" + ) from None + raise TunnelError(f"mysql connect: {e}") from None + + +def query_mysql(conn, sql: str) -> Result: + import pymysql + + cur = conn.cursor() + try: + cur.execute(sql) + rows = cur.fetchall() + desc = cur.description or () + res = Result( + affected_rows=cur.rowcount if not desc else 0, + insert_id=int(getattr(cur, "lastrowid", 0) or 0), + num_fields=len(desc), + num_rows=len(rows) if desc else 0, + ) + if desc: + res.fields = [Field(str(col[0]), "") for col in desc] + for row in rows: + out: list[str | None] = [] + for v in row: + if v is None: + out.append(None) + elif isinstance(v, bytes): + out.append(v.decode("utf-8", errors="replace")) + else: + out.append(str(v)) + res.rows.append(out) + return res + except pymysql.MySQLError as e: + errno = int(e.args[0]) if e.args else 0 + msg = str(e.args[1]) if len(e.args) > 1 else str(e) + return Result(errno=errno, message=msg) + finally: + cur.close() + + +def find_config(explicit: str | None) -> Path: + cands: list[Path] = [] + if explicit: + cands.append(Path(explicit)) + cands.extend( + [ + ROOT / "config.yaml", + Path.cwd() / "config.yaml", + ROOT / "dist" / "config.yaml", + ] + ) + for p in cands: + if p.is_file(): + return p.resolve() + raise TunnelError("config.yaml not found; pass --config") + + +def last_result_set(results: list[Result]) -> Result | None: + for res in reversed(results): + if res.is_result_set: + return res + return results[-1] if results else None + + +def print_header(title: str) -> None: + print() + print("== " + title) + + +def print_result(res: Result, max_rows: int = 40) -> None: + if res.is_error: + print(f" mysql errno {res.errno}: {res.message}") + return + if not res.is_result_set: + extra = f" info={res.info!r}" if res.info else "" + print(f" OK affected={res.affected_rows} insert_id={res.insert_id}{extra}") + return + names = [f.name or "?" for f in res.fields] + print(f" {res.num_rows} row(s), columns: {', '.join(names)}") + widths = [len(n) for n in names] + shown = res.rows[:max_rows] + for row in shown: + for i, cell in enumerate(row): + text = "NULL" if cell is None else cell.replace("\n", "\\n") + widths[i] = max(widths[i], min(len(text), 48)) + + def fmt(val: str, w: int) -> str: + if len(val) > w: + val = val[: w - 1] + "…" + return val.ljust(w) + + print(" " + " | ".join(fmt(n, widths[i]) for i, n in enumerate(names))) + print(" " + "-+-".join("-" * w for w in widths)) + for row in shown: + cells = [] + for i, cell in enumerate(row): + text = "NULL" if cell is None else cell.replace("\n", "\\n") + cells.append(fmt(text, widths[i])) + print(" " + " | ".join(cells)) + if res.num_rows > max_rows: + print(f" … {res.num_rows - max_rows} more row(s)") + + +def default_sql() -> list[str]: + return [ + "SELECT 1 AS ok", + "SELECT VERSION() AS version, DATABASE() AS db, USER() AS user, " + "@@character_set_connection AS charset", + "SHOW TABLES", + ] + + +def run_local(cfg: Config, extra_sql: list[str]) -> int: + print("config:", cfg.path) + print("pymysql.connect(") + print(f' host="{cfg.listen_host}",') + print(f" port={cfg.listen_port},") + print(f' user="{cfg.local_user}",') + print(' password="***",') + print(f' db="{cfg.database}",') + print(")") + print("via: mysql-tunnel.exe →", redact_url(cfg.url)) + + try: + conn = mysql_connect(cfg) + except TunnelError as e: + print() + print(" Connect: FAIL") + print(" ", e) + return 1 + + print() + print("MySQL数据库连接成功") + print(" Connect: OK (local_auth)") + code = 0 + try: + for sql in extra_sql or default_sql(): + print_header(sql.replace("\n", " ")) + res = query_mysql(conn, sql) + print_result(res) + if res.is_error: + code = 1 + finally: + conn.close() + return code + + +def run_direct(cfg: Config, extra_sql: list[str]) -> int: + print("config:", cfg.path) + print("mode: --direct (POST ntunnel, remote.* not local_auth)") + print("tunnel:", redact_url(cfg.url)) + print("remote:", f"{cfg.remote_user}@{cfg.remote_host}:{cfg.remote_port}/{cfg.database}") + + client = HTTPClient(cfg) + code = 0 + print_header("actn=C connect") + try: + h, conninfo = client.connect() + except TunnelError as e: + print(" Tunnel: FAIL") + print(" ", e) + return 1 + if not h.ok: + print(f" errno {h.errno}: {h.message}") + print(" Remote MySQL: FAIL") + return 1 + print(f" Tunnel: OK (script version {h.version})") + print(" Remote MySQL: OK") + print(f" server_info: {conninfo.get('server_info', '')}") + + for sql in extra_sql or default_sql(): + print_header("actn=Q " + sql.replace("\n", " ")) + try: + h, results = client.query(sql) + except TunnelError as e: + print(" FAIL", e) + code = 1 + continue + if not h.ok: + print(f" header errno {h.errno}: {h.message}") + code = 1 + continue + res = last_result_set(results) + if res is None: + print(" no result") + code = 1 + continue + print_result(res) + if res.is_error: + code = 1 + return code + + +def main(argv: list[str]) -> int: + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + ap = argparse.ArgumentParser( + description="Connect through mysql-tunnel using local_auth (same as Yingdao)" + ) + ap.add_argument("--config", default="", help="path to config.yaml") + ap.add_argument("--sql", action="append", default=[], help="SQL to run (repeatable)") + ap.add_argument( + "--direct", + action="store_true", + help="POST ntunnel_mysql.php with remote.* instead of local_auth", + ) + args = ap.parse_args(argv) + try: + cfg = Config.load(find_config(args.config or None)) + if args.direct: + return run_direct(cfg, args.sql) + return run_local(cfg, args.sql) + except TunnelError as e: + print("error:", e, file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..4696e4a --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1 @@ +pymysql>=1.1 diff --git a/tests/rpa_tunnel.py b/tests/rpa_tunnel.py new file mode 100644 index 0000000..b87157d --- /dev/null +++ b/tests/rpa_tunnel.py @@ -0,0 +1,31 @@ +"""影刀 RPA:用 local_auth 连本机 mysql-tunnel,返回 conn。 + +须先启动 mysql-tunnel.exe(窗口点「启动」),listen / local_auth 与下面一致。 +默认值对齐 config.example.yaml;按你的 config.yaml 改 port / password / db。 +""" + +import pymysql + + +def main(args): + conn = pymysql.connect( + host="127.0.0.1", + port=13306, + user="root", + password="123456", + db="your_database", + ) + print("MySQL数据库连接成功") + args["conn"] = conn + return args + + +if __name__ == "__main__": + out = main({}) + conn = out["conn"] + try: + with conn.cursor() as cur: + cur.execute("SELECT 1") + print("SELECT 1 ->", cur.fetchone()) + finally: + conn.close() diff --git a/tools/mkico/main.go b/tools/mkico/main.go new file mode 100644 index 0000000..e0dc55e --- /dev/null +++ b/tools/mkico/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "encoding/binary" + "fmt" + "os" + "path/filepath" +) + +// mkico packs internal/gui/logo.png into a single-image ICO at cmd/tunnel/app.ico. +// Prefer replacing app.ico with a proper multi-size .ico from a designer tool; +// this helper is a quick path when you only have a PNG. +func main() { + root, err := findRoot() + if err != nil { + panic(err) + } + pngPath := filepath.Join(root, "internal", "gui", "logo.png") + icoPath := filepath.Join(root, "cmd", "tunnel", "app.ico") + + png, err := os.ReadFile(pngPath) + if err != nil { + panic(err) + } + var w, h uint32 + if len(png) >= 24 { + w = binary.BigEndian.Uint32(png[16:20]) + h = binary.BigEndian.Uint32(png[20:24]) + } + wb, hb := byte(w), byte(h) + if w >= 256 { + wb = 0 + } + if h >= 256 { + hb = 0 + } + out := make([]byte, 22+len(png)) + binary.LittleEndian.PutUint16(out[2:], 1) + binary.LittleEndian.PutUint16(out[4:], 1) + out[6] = wb + out[7] = hb + binary.LittleEndian.PutUint16(out[10:], 1) + binary.LittleEndian.PutUint16(out[12:], 32) + binary.LittleEndian.PutUint32(out[14:], uint32(len(png))) + binary.LittleEndian.PutUint32(out[18:], 22) + copy(out[22:], png) + if err := os.WriteFile(icoPath, out, 0o644); err != nil { + panic(err) + } + fmt.Println("wrote", icoPath) +} + +func findRoot() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + dir := wd + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, nil + } + parent := filepath.Dir(dir) + if parent == dir { + return "", fmt.Errorf("go.mod not found from %s", wd) + } + dir = parent + } +} diff --git a/uninstall-service.bat b/uninstall-service.bat new file mode 100644 index 0000000..e9f9782 --- /dev/null +++ b/uninstall-service.bat @@ -0,0 +1,20 @@ +@echo off +setlocal +cd /d "%~dp0" + +net session >nul 2>&1 +if %errorLevel% neq 0 ( + echo Requesting Administrator... + powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" + exit /b +) + +if exist "dist\mysql-tunnel.exe" ( + dist\mysql-tunnel.exe --uninstall +) else ( + echo dist\mysql-tunnel.exe not found; trying sc.exe + sc stop mysql-tunnel + sc delete mysql-tunnel +) +pause +endlocal