Files
mysql-tunnel/internal/app/app.go
T
2026-09-21 16:46:55 +08:00

140 lines
3.7 KiB
Go

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
}