mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00

- 重构插件注册架构采用现代工厂模式和自动发现机制 - 新增完整的插件元数据管理系统支持版本能力标签等信息 - 实现智能插件适配器提供向后兼容的桥接功能 - 建立MySQL Redis SSH三个标准插件作为新架构参考实现 - 优化插件扫描逻辑支持按端口按类型的智能查询和过滤 - 添加国际化支持和完善的文档体系 - 代码量减少67%维护成本大幅降低扩展性显著提升 新架构特点: - 零配置插件注册import即用 - 工厂模式延迟初始化和依赖注入 - 丰富元数据系统和能力声明 - 完全解耦的模块化设计 - 面向未来的可扩展架构 测试验证: MySQL和Redis插件功能完整包括弱密码检测未授权访问检测和自动利用攻击
380 lines
12 KiB
Go
380 lines
12 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"net"
|
||
"time"
|
||
|
||
_ "github.com/go-sql-driver/mysql"
|
||
)
|
||
|
||
// FScanMySQLDiagnosis fscan MySQL连接问题诊断工具
|
||
type FScanMySQLDiagnosis struct {
|
||
host string
|
||
port int
|
||
username string
|
||
password string
|
||
timeout time.Duration
|
||
}
|
||
|
||
// NewDiagnosis 创建诊断工具
|
||
func NewDiagnosis(host string, port int, username, password string, timeout time.Duration) *FScanMySQLDiagnosis {
|
||
return &FScanMySQLDiagnosis{
|
||
host: host,
|
||
port: port,
|
||
username: username,
|
||
password: password,
|
||
timeout: timeout,
|
||
}
|
||
}
|
||
|
||
func main() {
|
||
fmt.Println("FScan MySQL连接问题诊断工具")
|
||
fmt.Println("===========================")
|
||
|
||
// 初始化诊断工具
|
||
diagnosis := NewDiagnosis("127.0.0.1", 3306, "root", "123456", 3*time.Second)
|
||
|
||
// 执行完整诊断
|
||
diagnosis.RunFullDiagnosis()
|
||
}
|
||
|
||
// RunFullDiagnosis 运行完整诊断
|
||
func (d *FScanMySQLDiagnosis) RunFullDiagnosis() {
|
||
fmt.Printf("目标: %s:%d\n", d.host, d.port)
|
||
fmt.Printf("用户: %s\n", d.username)
|
||
fmt.Printf("超时: %v\n", d.timeout)
|
||
fmt.Println()
|
||
|
||
// 步骤1: 网络连通性测试
|
||
fmt.Println("步骤1: 网络连通性测试")
|
||
if !d.testNetworkConnection() {
|
||
return
|
||
}
|
||
|
||
// 步骤2: 测试fscan当前的连接字符串格式
|
||
fmt.Println("\n步骤2: 测试fscan当前连接字符串格式")
|
||
d.testFScanCurrentFormat()
|
||
|
||
// 步骤3: 测试不同的超时配置
|
||
fmt.Println("\n步骤3: 测试不同超时配置")
|
||
d.testTimeoutConfigurations()
|
||
|
||
// 步骤4: 测试连接池配置的影响
|
||
fmt.Println("\n步骤4: 测试连接池配置")
|
||
d.testConnectionPoolSettings()
|
||
|
||
// 步骤5: 提供修复建议
|
||
fmt.Println("\n步骤5: 修复建议")
|
||
d.provideFixSuggestions()
|
||
}
|
||
|
||
// testNetworkConnection 测试网络连接
|
||
func (d *FScanMySQLDiagnosis) testNetworkConnection() bool {
|
||
fmt.Printf("正在测试TCP连接 %s:%d...\n", d.host, d.port)
|
||
|
||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", d.host, d.port), d.timeout)
|
||
if err != nil {
|
||
fmt.Printf("❌ TCP连接失败: %v\n", err)
|
||
fmt.Println("建议:")
|
||
fmt.Println("- 检查MySQL服务是否启动")
|
||
fmt.Println("- 检查端口3306是否开放")
|
||
fmt.Println("- 检查防火墙设置")
|
||
return false
|
||
}
|
||
defer conn.Close()
|
||
|
||
fmt.Println("✅ TCP连接成功")
|
||
return true
|
||
}
|
||
|
||
// testFScanCurrentFormat 测试fscan当前使用的连接字符串格式
|
||
func (d *FScanMySQLDiagnosis) testFScanCurrentFormat() {
|
||
timeoutStr := d.timeout.String()
|
||
|
||
// fscan当前使用的格式
|
||
connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8&timeout=%s",
|
||
d.username, d.password, d.host, d.port, timeoutStr)
|
||
|
||
fmt.Printf("连接字符串: %s\n", connStr)
|
||
|
||
success, elapsed, err := d.testConnectionString(connStr, "fscan当前格式")
|
||
|
||
if success {
|
||
fmt.Printf("✅ fscan格式连接成功 (耗时: %v)\n", elapsed)
|
||
} else {
|
||
fmt.Printf("❌ fscan格式连接失败 (耗时: %v): %v\n", elapsed, err)
|
||
|
||
// 尝试替代方案
|
||
fmt.Println("\n尝试替代连接字符串...")
|
||
d.testAlternativeFormats()
|
||
}
|
||
}
|
||
|
||
// testAlternativeFormats 测试替代连接字符串格式
|
||
func (d *FScanMySQLDiagnosis) testAlternativeFormats() {
|
||
timeoutStr := d.timeout.String()
|
||
|
||
alternatives := []struct {
|
||
name string
|
||
format string
|
||
desc string
|
||
}{
|
||
{
|
||
name: "不指定数据库",
|
||
format: fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr),
|
||
desc: "不连接到特定数据库",
|
||
},
|
||
{
|
||
name: "无数据库路径",
|
||
format: fmt.Sprintf("%s:%s@tcp(%s:%d)?charset=utf8&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr),
|
||
desc: "完全省略数据库路径",
|
||
},
|
||
{
|
||
name: "使用information_schema",
|
||
format: fmt.Sprintf("%s:%s@tcp(%s:%d)/information_schema?charset=utf8&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr),
|
||
desc: "连接到系统数据库",
|
||
},
|
||
{
|
||
name: "UTF8MB4字符集",
|
||
format: fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8mb4&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr),
|
||
desc: "使用完整UTF-8字符集",
|
||
},
|
||
}
|
||
|
||
for _, alt := range alternatives {
|
||
success, elapsed, err := d.testConnectionString(alt.format, alt.name)
|
||
if success {
|
||
fmt.Printf("✅ %s 成功 (耗时: %v)\n", alt.name, elapsed)
|
||
fmt.Printf(" 建议使用: %s\n", alt.format)
|
||
} else {
|
||
fmt.Printf("❌ %s 失败 (耗时: %v): %v\n", alt.name, elapsed, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// testConnectionString 测试指定的连接字符串
|
||
func (d *FScanMySQLDiagnosis) testConnectionString(connStr, name string) (bool, time.Duration, error) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), d.timeout+time.Second)
|
||
defer cancel()
|
||
|
||
start := time.Now()
|
||
|
||
db, err := sql.Open("mysql", connStr)
|
||
if err != nil {
|
||
return false, time.Since(start), err
|
||
}
|
||
defer db.Close()
|
||
|
||
// 模拟fscan的连接池设置
|
||
db.SetConnMaxLifetime(d.timeout)
|
||
db.SetConnMaxIdleTime(d.timeout)
|
||
db.SetMaxIdleConns(1)
|
||
db.SetMaxOpenConns(1)
|
||
|
||
err = db.PingContext(ctx)
|
||
elapsed := time.Since(start)
|
||
|
||
return err == nil, elapsed, err
|
||
}
|
||
|
||
// testTimeoutConfigurations 测试不同超时配置
|
||
func (d *FScanMySQLDiagnosis) testTimeoutConfigurations() {
|
||
timeouts := []time.Duration{
|
||
1 * time.Second,
|
||
3 * time.Second,
|
||
5 * time.Second,
|
||
10 * time.Second,
|
||
}
|
||
|
||
baseDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8", d.username, d.password, d.host, d.port)
|
||
|
||
for _, timeout := range timeouts {
|
||
fmt.Printf("测试超时: %v\n", timeout)
|
||
|
||
// DSN中设置timeout
|
||
dsnWithTimeout := baseDSN + "&timeout=" + timeout.String()
|
||
success, elapsed, err := d.testConnectionStringWithTimeout(dsnWithTimeout, timeout+time.Second)
|
||
|
||
if success {
|
||
fmt.Printf(" ✅ DSN超时%v 成功 (实际耗时: %v)\n", timeout, elapsed)
|
||
} else {
|
||
fmt.Printf(" ❌ DSN超时%v 失败 (实际耗时: %v): %v\n", timeout, elapsed, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// testConnectionStringWithTimeout 使用指定超时测试连接字符串
|
||
func (d *FScanMySQLDiagnosis) testConnectionStringWithTimeout(connStr string, contextTimeout time.Duration) (bool, time.Duration, error) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
|
||
defer cancel()
|
||
|
||
start := time.Now()
|
||
|
||
db, err := sql.Open("mysql", connStr)
|
||
if err != nil {
|
||
return false, time.Since(start), err
|
||
}
|
||
defer db.Close()
|
||
|
||
err = db.PingContext(ctx)
|
||
elapsed := time.Since(start)
|
||
|
||
return err == nil, elapsed, err
|
||
}
|
||
|
||
// testConnectionPoolSettings 测试连接池设置的影响
|
||
func (d *FScanMySQLDiagnosis) testConnectionPoolSettings() {
|
||
connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8&timeout=%s",
|
||
d.username, d.password, d.host, d.port, d.timeout.String())
|
||
|
||
configs := []struct {
|
||
name string
|
||
configure func(*sql.DB)
|
||
desc string
|
||
}{
|
||
{
|
||
name: "fscan默认设置",
|
||
configure: func(db *sql.DB) {
|
||
db.SetConnMaxLifetime(d.timeout)
|
||
db.SetConnMaxIdleTime(d.timeout)
|
||
db.SetMaxIdleConns(1)
|
||
db.SetMaxOpenConns(1)
|
||
},
|
||
desc: "模拟fscan当前的连接池配置",
|
||
},
|
||
{
|
||
name: "更长生命周期",
|
||
configure: func(db *sql.DB) {
|
||
db.SetConnMaxLifetime(30 * time.Second)
|
||
db.SetConnMaxIdleTime(30 * time.Second)
|
||
db.SetMaxIdleConns(1)
|
||
db.SetMaxOpenConns(1)
|
||
},
|
||
desc: "延长连接生命周期到30秒",
|
||
},
|
||
{
|
||
name: "无生命周期限制",
|
||
configure: func(db *sql.DB) {
|
||
db.SetMaxIdleConns(1)
|
||
db.SetMaxOpenConns(1)
|
||
},
|
||
desc: "不设置连接生命周期限制",
|
||
},
|
||
}
|
||
|
||
for _, config := range configs {
|
||
fmt.Printf("测试: %s\n", config.name)
|
||
fmt.Printf("描述: %s\n", config.desc)
|
||
|
||
success, elapsed, err := d.testWithPoolConfig(connStr, config.configure)
|
||
|
||
if success {
|
||
fmt.Printf(" ✅ 成功 (耗时: %v)\n", elapsed)
|
||
} else {
|
||
fmt.Printf(" ❌ 失败 (耗时: %v): %v\n", elapsed, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// testWithPoolConfig 使用指定连接池配置测试
|
||
func (d *FScanMySQLDiagnosis) testWithPoolConfig(connStr string, configure func(*sql.DB)) (bool, time.Duration, error) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), d.timeout+2*time.Second)
|
||
defer cancel()
|
||
|
||
start := time.Now()
|
||
|
||
db, err := sql.Open("mysql", connStr)
|
||
if err != nil {
|
||
return false, time.Since(start), err
|
||
}
|
||
defer db.Close()
|
||
|
||
configure(db)
|
||
|
||
err = db.PingContext(ctx)
|
||
elapsed := time.Since(start)
|
||
|
||
return err == nil, elapsed, err
|
||
}
|
||
|
||
// provideFixSuggestions 提供修复建议
|
||
func (d *FScanMySQLDiagnosis) provideFixSuggestions() {
|
||
fmt.Println("基于诊断结果的修复建议:")
|
||
fmt.Println()
|
||
|
||
fmt.Println("1. 连接字符串优化:")
|
||
fmt.Println(" 原始: user:pass@tcp(host:port)/mysql?charset=utf8&timeout=3s")
|
||
fmt.Println(" 建议: user:pass@tcp(host:port)/?charset=utf8mb4&timeout=5s&readTimeout=3s&writeTimeout=3s")
|
||
fmt.Println(" 原因: 不指定数据库减少权限要求,使用utf8mb4支持完整字符集")
|
||
fmt.Println()
|
||
|
||
fmt.Println("2. 超时配置优化:")
|
||
fmt.Println(" - Context超时应该 > DSN timeout")
|
||
fmt.Println(" - 建议Context超时 = DSN timeout + 2秒")
|
||
fmt.Println(" - 网络环境差时适当增加超时时间")
|
||
fmt.Println()
|
||
|
||
fmt.Println("3. 连接池配置优化:")
|
||
fmt.Println(" - SetConnMaxLifetime(10*time.Second) // 适当延长")
|
||
fmt.Println(" - SetMaxOpenConns(1) // 扫描场景保持1个连接")
|
||
fmt.Println(" - 确保每次使用后正确关闭连接")
|
||
fmt.Println()
|
||
|
||
fmt.Println("4. 错误处理改进:")
|
||
fmt.Println(" - 区分网络超时和认证失败")
|
||
fmt.Println(" - 实现重试机制")
|
||
fmt.Println(" - 记录详细的错误信息")
|
||
fmt.Println()
|
||
|
||
fmt.Println("5. 针对fscan的具体修改建议:")
|
||
d.provideFScanSpecificFixes()
|
||
}
|
||
|
||
// provideFScanSpecificFixes 提供fscan特定的修复建议
|
||
func (d *FScanMySQLDiagnosis) provideFScanSpecificFixes() {
|
||
fmt.Println("针对fscan代码的修改建议:")
|
||
fmt.Println()
|
||
|
||
fmt.Println("修改 plugins/services/mysql/connector.go:")
|
||
fmt.Println("```go")
|
||
fmt.Println("// buildConnectionString 构建连接字符串 - 优化版本")
|
||
fmt.Println("func (c *MySQLConnector) buildConnectionString(host string, port int, username, password string) string {")
|
||
fmt.Println(" timeoutStr := c.timeout.String()")
|
||
fmt.Println(" readTimeoutStr := (c.timeout - time.Second).String() // 读超时比总超时短1秒")
|
||
fmt.Println(" ")
|
||
fmt.Println(" var connStr string")
|
||
fmt.Println(" if common.Socks5Proxy != \"\" {")
|
||
fmt.Println(" // 代理连接 - 不指定具体数据库")
|
||
fmt.Println(" connStr = fmt.Sprintf(\"%v:%v@tcp-proxy(%v:%v)/?charset=utf8mb4&timeout=%s&readTimeout=%s&writeTimeout=%s\",")
|
||
fmt.Println(" username, password, host, port, timeoutStr, readTimeoutStr, readTimeoutStr)")
|
||
fmt.Println(" } else {")
|
||
fmt.Println(" // 标准连接 - 不指定具体数据库")
|
||
fmt.Println(" connStr = fmt.Sprintf(\"%v:%v@tcp(%v:%v)/?charset=utf8mb4&timeout=%s&readTimeout=%s&writeTimeout=%s\",")
|
||
fmt.Println(" username, password, host, port, timeoutStr, readTimeoutStr, readTimeoutStr)")
|
||
fmt.Println(" }")
|
||
fmt.Println(" ")
|
||
fmt.Println(" return connStr")
|
||
fmt.Println("}")
|
||
fmt.Println("```")
|
||
fmt.Println()
|
||
|
||
fmt.Println("修改认证函数中的Context超时:")
|
||
fmt.Println("```go")
|
||
fmt.Println("// Authenticate 认证 - 优化版本")
|
||
fmt.Println("func (c *MySQLConnector) Authenticate(ctx context.Context, conn interface{}, cred *base.Credential) error {")
|
||
fmt.Println(" // 创建更长的context超时,避免冲突")
|
||
fmt.Println(" authCtx, cancel := context.WithTimeout(ctx, c.timeout+2*time.Second)")
|
||
fmt.Println(" defer cancel()")
|
||
fmt.Println(" ")
|
||
fmt.Println(" // ... 其余代码保持不变")
|
||
fmt.Println(" err = db.PingContext(authCtx) // 使用新的context")
|
||
fmt.Println(" // ...")
|
||
fmt.Println("}")
|
||
fmt.Println("```")
|
||
fmt.Println()
|
||
|
||
fmt.Println("这些修改应该能解决'context deadline exceeded'错误。")
|
||
} |