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 }