fscan/Plugins/services/mysql/connector.go
ZacharyZcR de286026e8 refactor: 统一插件超时机制实现Context-First架构
- 重构SSH/MySQL/Redis插件超时控制,移除第三方库超时依赖
- 统一使用Go Context超时机制,提升超时控制可靠性和精确度
- 扩展MySQL/Redis/SSH插件默认端口支持,提升扫描覆盖率
- 修复插件系统中ConcurrentScanConfig超时配置缺失问题
- 优化插件检测逻辑,正确识别新架构插件并显示准确状态信息
- 解决插件在错误端口上长时间等待问题,显著提升扫描效率
2025-08-07 14:01:50 +08:00

187 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mysql
import (
"context"
"database/sql"
"fmt"
"net"
"strconv"
"time"
"github.com/go-sql-driver/mysql"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins/base"
)
// MySQLConnector 实现MySQL数据库服务连接器
// 遵循 base.ServiceConnector 接口规范提供标准化的MySQL连接和认证功能
// MySQLConnector MySQL数据库连接器
type MySQLConnector struct {
host string // 目标主机地址
port int // 目标端口号
}
// NewMySQLConnector 创建新的MySQL连接器实例
// 自动注册SOCKS代理支持统一使用Context超时控制
func NewMySQLConnector() *MySQLConnector {
connector := &MySQLConnector{}
// 注册SOCKS代理支持的dialer如果配置了代理
connector.registerProxyDialer()
return connector
}
// Connect 建立到MySQL服务的基础连接
// 实现 base.ServiceConnector 接口的 Connect 方法
func (c *MySQLConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) {
// 解析目标端口号
port, err := strconv.Atoi(info.Ports)
if err != nil {
return nil, fmt.Errorf("无效的端口号: %s", info.Ports)
}
// 缓存目标信息,供认证阶段使用
c.host = info.Host
c.port = port
// 构建基础连接字符串(无认证信息)
connStr := c.buildConnectionString(info.Host, port, "", "")
// 创建数据库连接实例
db, err := sql.Open("mysql", connStr)
if err != nil {
return nil, fmt.Errorf("创建连接失败: %v", err)
}
// 配置连接池参数
timeout := time.Duration(common.Timeout) * time.Second
db.SetConnMaxLifetime(timeout)
db.SetConnMaxIdleTime(timeout)
db.SetMaxIdleConns(0)
return db, nil
}
// Authenticate 使用凭据对MySQL服务进行身份认证
// 实现 base.ServiceConnector 接口的 Authenticate 方法
// 关键优化使用独立的Context避免上游超时问题并优化内存使用
func (c *MySQLConnector) Authenticate(ctx context.Context, conn interface{}, cred *base.Credential) error {
// 直接使用传入的Context它已经包含了正确的超时设置
// 内存优化:预构建连接字符串,避免重复分配
connStr := c.buildConnectionString(c.host, c.port, cred.Username, cred.Password)
common.LogDebug(fmt.Sprintf("MySQL尝试认证: %s@%s:%d", cred.Username, c.host, c.port))
// 内存优化:直接建立连接而不创建连接池
// 避免为单次认证创建不必要的连接池开销
rawConn, err := c.connectDirect(ctx, connStr)
if err != nil {
common.LogDebug(fmt.Sprintf("MySQL直连失败: %s@%s:%d - %v", cred.Username, c.host, c.port, err))
return fmt.Errorf("连接失败: %v", err)
}
defer rawConn.Close()
// 执行简单的认证验证
err = c.validateConnection(ctx, rawConn)
if err != nil {
common.LogDebug(fmt.Sprintf("MySQL认证失败: %s@%s:%d - %v", cred.Username, c.host, c.port, err))
return fmt.Errorf("认证失败: %v", err)
}
common.LogDebug(fmt.Sprintf("MySQL认证成功: %s@%s:%d", cred.Username, c.host, c.port))
return nil
}
// Close 关闭MySQL连接
// 实现 base.ServiceConnector 接口的 Close 方法
func (c *MySQLConnector) Close(conn interface{}) error {
if db, ok := conn.(*sql.DB); ok {
return db.Close()
}
return nil
}
// connectWithCredentials 使用凭据创建新连接
func (c *MySQLConnector) connectWithCredentials(ctx context.Context, originalDB *sql.DB, cred *base.Credential) (*sql.DB, error) {
// 从原始连接中提取主机和端口信息
// 这里简化处理,实际应该从原始连接字符串中解析
// 为了示例,我们假设可以从某种方式获取主机端口信息
// 临时解决方案:重新构建连接字符串
connStr := c.buildConnectionStringWithCredentials(cred)
db, err := sql.Open("mysql", connStr)
if err != nil {
return nil, fmt.Errorf("创建认证连接失败: %v", err)
}
return db, nil
}
// buildConnectionString 构建MySQL连接字符串
// 根据是否配置SOCKS代理选择合适的连接方式
// 移除timeout参数统一使用Context控制超时
func (c *MySQLConnector) buildConnectionString(host string, port int, username, password string) string {
// 根据代理配置选择网络类型
if common.Socks5Proxy != "" {
// SOCKS代理连接模式移除timeout参数由Context控制
return fmt.Sprintf("%v:%v@tcp-proxy(%v:%v)/mysql?charset=utf8",
username, password, host, port)
} else {
// 标准TCP直连模式移除timeout参数由Context控制
return fmt.Sprintf("%v:%v@tcp(%v:%v)/mysql?charset=utf8",
username, password, host, port)
}
}
// buildConnectionStringWithCredentials 构建带凭据的连接字符串
func (c *MySQLConnector) buildConnectionStringWithCredentials(cred *base.Credential) string {
// 使用保存的主机和端口信息
return c.buildConnectionString(c.host, c.port, cred.Username, cred.Password)
}
// connectDirect 内存优化直接建立MySQL连接避免连接池开销
// 用于单次认证场景,减少内存分配和资源浪费
func (c *MySQLConnector) connectDirect(ctx context.Context, connStr string) (*sql.Conn, error) {
// 创建最小化配置的临时数据库实例
db, err := sql.Open("mysql", connStr)
if err != nil {
return nil, fmt.Errorf("创建连接实例失败: %v", err)
}
defer db.Close() // 确保临时db实例被清理
// 禁用连接池以减少内存开销
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(0)
db.SetConnMaxLifetime(0)
// 获取原始连接
conn, err := db.Conn(ctx)
if err != nil {
return nil, fmt.Errorf("获取连接失败: %v", err)
}
return conn, nil
}
// validateConnection 内存优化:轻量级连接验证
// 使用最小开销的方式验证MySQL连接有效性
func (c *MySQLConnector) validateConnection(ctx context.Context, conn *sql.Conn) error {
// 使用传入的Context进行验证统一超时控制
return conn.PingContext(ctx)
}
// registerProxyDialer 注册SOCKS代理支持的网络拨号器
// 仅在配置了SOCKS代理时才注册避免不必要的开销
func (c *MySQLConnector) registerProxyDialer() {
if common.Socks5Proxy == "" {
return // 未配置代理,跳过注册
}
// 向MySQL驱动注册自定义的代理拨号器
mysql.RegisterDialContext("tcp-proxy", func(ctx context.Context, addr string) (net.Conn, error) {
return common.WrapperTcpWithContext(ctx, "tcp", addr)
})
}