feat: 重构HTTP/TCP客户端并增强全局设置与并发安全

This commit is contained in:
mogumc committed 2026-05-27 20:34:52 +08:00
1 parent ad5884804c
commit 2e9b2f8050
11 files changed
+580 -347

No files matched your search

+142 -212
View File
@@ -14,6 +14,17 @@ import (
"time"
)
// maxBodySize 限制响应体最大读取量为 50MB,防止 OOM
const maxBodySize = 50 << 20
// 默认 HTTP 客户端,复用连接池
var defaultHTTPClient = &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
},
}
// HttpRequest 请求结构体
type HttpRequest struct {
Method string `json:"method"` // HTTP 方法
@@ -64,39 +75,40 @@ type FileData struct {
Path string `json:"path"` // 文件路径
}
// SendRequest 发送 HTTP 请求
func SendRequest(req *HttpRequest) *HttpResponse {
resp := &HttpResponse{
Headers: make(map[string]string),
}
if req.URL == "" {
resp.Error = "请求地址不能为空"
return resp
}
// 补全协议
// normalizeURL 补全协议前缀
func normalizeURL(req *HttpRequest) {
if !strings.HasPrefix(req.URL, "http://") && !strings.HasPrefix(req.URL, "https://") {
req.URL = "https://" + req.URL
}
}
// 设置超时
// buildHTTPClient 创建带代理和 TLS 配置的 HTTP 客户端
// 无自定义代理且不跳过验证时复用默认客户端
func buildHTTPClient(req *HttpRequest) *http.Client {
// 无自定义设置时复用默认客户端(共享连接池)
if req.Proxy == "" && !req.Insecure {
if req.Timeout > 0 {
return &http.Client{
Timeout: time.Duration(req.Timeout) * time.Second,
Transport: defaultHTTPClient.Transport,
}
}
return defaultHTTPClient
}
// 有自定义设置时创建新客户端
timeout := time.Duration(req.Timeout) * time.Second
if timeout == 0 {
timeout = 30 * time.Second
}
// 创建 HTTP 客户端
client := &http.Client{
Timeout: timeout,
}
client := &http.Client{Timeout: timeout}
// 配置代理
if req.Proxy != "" {
proxyURL, err := url.Parse(req.Proxy)
if err != nil {
resp.Error = fmt.Sprintf("代理地址格式错误: %v", err)
return resp
// 代理格式错误,降级为默认客户端
return defaultHTTPClient
}
client.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyURL),
@@ -106,72 +118,108 @@ func SendRequest(req *HttpRequest) *HttpResponse {
}
} else if req.Insecure {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
// 创建请求体
return client
}
// setupCookies 将请求中的 Cookie 设置到客户端 Jar
func setupCookies(client *http.Client, req *HttpRequest) {
if len(req.Cookies) == 0 {
return
}
cookieJar, _ := cookiejar.New(nil)
var cookieList []*http.Cookie
for name, value := range req.Cookies {
cookieList = append(cookieList, &http.Cookie{Name: name, Value: value})
}
parsedURL, _ := url.Parse(req.URL)
cookieJar.SetCookies(parsedURL, cookieList)
client.Jar = cookieJar
}
// setRequestHeaders 设置自定义请求头(host 单独处理)
func setRequestHeaders(httpReq *http.Request, req *HttpRequest) {
for key, value := range req.Headers {
if strings.ToLower(key) == "host" {
httpReq.Host = value
} else {
httpReq.Header.Set(key, value)
}
}
}
// parseHTTPResponse 从 http.Response 解析到 HttpResponse
func parseHTTPResponse(httpResp *http.Response, resp *HttpResponse, start time.Time) {
resp.Time = time.Since(start).String()
// 限制最大读取量防止 OOM
body, err := io.ReadAll(io.LimitReader(httpResp.Body, maxBodySize))
if err != nil {
resp.Error = fmt.Sprintf("读取响应失败: %v", err)
return
}
resp.StatusCode = httpResp.StatusCode
resp.Status = httpResp.Status
resp.Body = string(body)
resp.ContentLength = httpResp.ContentLength
resp.Size = int64(len(body))
resp.ContentType = httpResp.Header.Get("Content-Type")
resp.Headers = make(map[string]string)
for key, values := range httpResp.Header {
resp.Headers[key] = strings.Join(values, "; ")
}
resp.Cookies = convertCookies(httpResp.Cookies())
}
// SendRequest 发送 HTTP 请求
func SendRequest(req *HttpRequest) *HttpResponse {
resp := &HttpResponse{}
if req.URL == "" {
resp.Error = "请求地址不能为空"
return resp
}
normalizeURL(req)
client := buildHTTPClient(req)
// 构造请求体
var bodyReader io.Reader
var err error
contentType := req.ContentType
if req.Method != http.MethodGet && req.Method != http.MethodHead {
switch req.BodyType {
case "file":
// 文件上传
if req.FilePath != "" {
file, err := os.Open(req.FilePath)
bodyReader, contentType, err = buildFileBody(req.FilePath)
if err != nil {
resp.Error = fmt.Sprintf("打开文件失败: %v", err)
resp.Error = err.Error()
return resp
}
defer file.Close()
// 获取文件信息
fileInfo, err := file.Stat()
if err != nil {
resp.Error = fmt.Sprintf("获取文件信息失败: %v", err)
return resp
}
// 使用 multipart 上传
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", fileInfo.Name())
if err != nil {
resp.Error = fmt.Sprintf("创建表单文件失败: %v", err)
return resp
}
if _, err := io.Copy(part, file); err != nil {
resp.Error = fmt.Sprintf("复制文件内容失败: %v", err)
return resp
}
writer.Close()
bodyReader = body
contentType = writer.FormDataContentType()
}
case "form":
// 表单数据
if req.Body != "" {
bodyReader = strings.NewReader(req.Body)
if contentType == "" {
contentType = "application/x-www-form-urlencoded"
}
}
case "json":
// JSON 数据
if req.Body != "" {
bodyReader = strings.NewReader(req.Body)
if contentType == "" {
contentType = "application/json"
}
}
default:
// 文本数据
if req.Body != "" {
bodyReader = strings.NewReader(req.Body)
if contentType == "" {
@@ -181,46 +229,20 @@ func SendRequest(req *HttpRequest) *HttpResponse {
}
}
// 创建请求
httpReq, err := http.NewRequest(req.Method, req.URL, bodyReader)
if err != nil {
resp.Error = fmt.Sprintf("创建请求失败: %v", err)
return resp
}
// 设置 Content-Type
if contentType != "" {
httpReq.Header.Set("Content-Type", contentType)
}
// 设置自定义请求头(需要单独处理 host)
for key, value := range req.Headers {
if strings.ToLower(key) == "host" {
httpReq.Host = value
} else {
httpReq.Header.Set(key, value)
}
}
setRequestHeaders(httpReq, req)
setupCookies(client, req)
// 设置 Cookie
if len(req.Cookies) > 0 {
cookieJar, _ := cookiejar.New(nil)
var cookieList []*http.Cookie
for name, value := range req.Cookies {
cookieList = append(cookieList, &http.Cookie{
Name: name,
Value: value,
})
}
parsedURL, _ := url.Parse(req.URL)
cookieJar.SetCookies(parsedURL, cookieList)
client.Jar = cookieJar
}
// 记录开始时间
start := time.Now()
// 发送请求
httpResp, err := client.Do(httpReq)
if err != nil {
resp.Error = fmt.Sprintf("请求失败: %v", err)
@@ -228,95 +250,31 @@ func SendRequest(req *HttpRequest) *HttpResponse {
}
defer httpResp.Body.Close()
// 记录耗时
resp.Time = time.Since(start).String()
// 读取响应体
body, err := io.ReadAll(httpResp.Body)
if err != nil {
resp.Error = fmt.Sprintf("读取响应失败: %v", err)
return resp
}
// 设置响应
resp.StatusCode = httpResp.StatusCode
resp.Status = httpResp.Status
resp.Body = string(body)
resp.ContentLength = httpResp.ContentLength
resp.Size = int64(len(body))
// 获取 Content-Type
resp.ContentType = httpResp.Header.Get("Content-Type")
// 获取响应头
for key, values := range httpResp.Header {
resp.Headers[key] = strings.Join(values, "; ")
}
// 获取响应 Cookie
resp.Cookies = convertCookies(httpResp.Cookies())
parseHTTPResponse(httpResp, resp, start)
return resp
}
// SendRequestWithFiles 带文件的请求
// SendRequestWithFiles 带多文件上传的请求
func SendRequestWithFiles(req *HttpRequest, files []FileData) *HttpResponse {
resp := &HttpResponse{
Headers: make(map[string]string),
}
resp := &HttpResponse{}
if req.URL == "" {
resp.Error = "请求地址不能为空"
return resp
}
// 补全协议
if !strings.HasPrefix(req.URL, "http://") && !strings.HasPrefix(req.URL, "https://") {
req.URL = "https://" + req.URL
}
normalizeURL(req)
// 设置超时
timeout := time.Duration(req.Timeout) * time.Second
if timeout == 0 {
timeout = 30 * time.Second
}
client := buildHTTPClient(req)
// 创建 HTTP 客户端
client := &http.Client{
Timeout: timeout,
}
// 配置代理
if req.Proxy != "" {
proxyURL, err := url.Parse(req.Proxy)
if err != nil {
resp.Error = fmt.Sprintf("代理地址格式错误: %v", err)
return resp
}
client.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{
InsecureSkipVerify: req.Insecure,
},
}
} else if req.Insecure {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
// 创建 multipart 请求
// 构造 multipart 请求体
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// 添加文本字段
if req.Body != "" {
_ = writer.WriteField("text", req.Body)
}
// 添加文件
for _, file := range files {
f, err := os.Open(file.Path)
if err != nil {
@@ -339,44 +297,17 @@ func SendRequestWithFiles(req *HttpRequest, files []FileData) *HttpResponse {
writer.Close()
// 创建请求
httpReq, err := http.NewRequest(req.Method, req.URL, body)
if err != nil {
resp.Error = fmt.Sprintf("创建请求失败: %v", err)
return resp
}
// 设置 Content-Type
httpReq.Header.Set("Content-Type", writer.FormDataContentType())
setRequestHeaders(httpReq, req)
setupCookies(client, req)
// 设置自定义请求头(需要单独处理 host)
for key, value := range req.Headers {
if strings.ToLower(key) == "host" {
httpReq.Host = value
} else {
httpReq.Header.Set(key, value)
}
}
// 设置 Cookie
if len(req.Cookies) > 0 {
cookieJar, _ := cookiejar.New(nil)
var cookieList []*http.Cookie
for name, value := range req.Cookies {
cookieList = append(cookieList, &http.Cookie{
Name: name,
Value: value,
})
}
parsedURL, _ := url.Parse(req.URL)
cookieJar.SetCookies(parsedURL, cookieList)
client.Jar = cookieJar
}
// 记录开始时间
start := time.Now()
// 发送请求
httpResp, err := client.Do(httpReq)
if err != nil {
resp.Error = fmt.Sprintf("请求失败: %v", err)
@@ -384,37 +315,36 @@ func SendRequestWithFiles(req *HttpRequest, files []FileData) *HttpResponse {
}
defer httpResp.Body.Close()
// 记录耗时
resp.Time = time.Since(start).String()
// 读取响应体
respBody, err := io.ReadAll(httpResp.Body)
if err != nil {
resp.Error = fmt.Sprintf("读取响应失败: %v", err)
return resp
}
// 设置响应
resp.StatusCode = httpResp.StatusCode
resp.Status = httpResp.Status
resp.Body = string(respBody)
resp.ContentLength = httpResp.ContentLength
resp.Size = int64(len(respBody))
// 获取 Content-Type
resp.ContentType = httpResp.Header.Get("Content-Type")
// 获取响应头
for key, values := range httpResp.Header {
resp.Headers[key] = strings.Join(values, "; ")
}
// 获取响应 Cookie
resp.Cookies = convertCookies(httpResp.Cookies())
parseHTTPResponse(httpResp, resp, start)
return resp
}
// buildFileBody 构造单文件上传的 multipart 请求体
func buildFileBody(filePath string) (io.Reader, string, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, "", fmt.Errorf("打开文件失败: %v", err)
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
return nil, "", fmt.Errorf("获取文件信息失败: %v", err)
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", fileInfo.Name())
if err != nil {
return nil, "", fmt.Errorf("创建表单文件失败: %v", err)
}
if _, err := io.Copy(part, file); err != nil {
return nil, "", fmt.Errorf("复制文件内容失败: %v", err)
}
writer.Close()
return body, writer.FormDataContentType(), nil
}
// ParseHeaders 解析请求头字符串
func ParseHeaders(headerStr string) map[string]string {
headers := make(map[string]string)
+33 -16
View File
@@ -7,6 +7,7 @@ import (
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
@@ -32,13 +33,17 @@ type RequestLog struct {
// LogManager 日志管理器
type LogManager struct {
mu sync.RWMutex
logDir string
logs []*RequestLog
maxLogs int
fileName string
}
var logManager *LogManager
var (
logManager *LogManager
logManagerOnce sync.Once
)
// InitLogManager 初始化日志管理器
func InitLogManager(logDir string) {
@@ -51,11 +56,13 @@ func InitLogManager(logDir string) {
logManager.loadLogs()
}
// GetLogManager 获取日志管理器
// GetLogManager 获取日志管理器(并发安全的单例)
func GetLogManager() *LogManager {
if logManager == nil {
InitLogManager("logs")
}
logManagerOnce.Do(func() {
if logManager == nil {
InitLogManager("logs")
}
})
return logManager
}
@@ -97,6 +104,9 @@ func (m *LogManager) saveLogs() error {
// AddLog 添加日志
func (m *LogManager) AddLog(log *RequestLog) error {
m.mu.Lock()
defer m.mu.Unlock()
// 生成序号
if len(m.logs) > 0 {
log.ID = m.logs[0].ID + 1
@@ -119,11 +129,15 @@ func (m *LogManager) AddLog(log *RequestLog) error {
// GetLogs 获取所有日志
func (m *LogManager) GetLogs() []*RequestLog {
m.mu.RLock()
defer m.mu.RUnlock()
return m.logs
}
// GetLog 获取单条日志
func (m *LogManager) GetLog(id int) *RequestLog {
m.mu.RLock()
defer m.mu.RUnlock()
for _, log := range m.logs {
if log.ID == id {
return log
@@ -134,6 +148,8 @@ func (m *LogManager) GetLog(id int) *RequestLog {
// DeleteLog 删除日志
func (m *LogManager) DeleteLog(id int) error {
m.mu.Lock()
defer m.mu.Unlock()
for i, log := range m.logs {
if log.ID == id {
m.logs = append(m.logs[:i], m.logs[i+1:]...)
@@ -145,12 +161,16 @@ func (m *LogManager) DeleteLog(id int) error {
// ClearLogs 清空日志
func (m *LogManager) ClearLogs() error {
m.mu.Lock()
defer m.mu.Unlock()
m.logs = make([]*RequestLog, 0)
return m.saveLogs()
}
// SearchLogs 搜索日志
func (m *LogManager) SearchLogs(keyword string) []*RequestLog {
m.mu.RLock()
defer m.mu.RUnlock()
var result []*RequestLog
keyword = strings.ToLower(keyword)
@@ -171,17 +191,14 @@ func CreateLogFromResponse(req *HttpRequest, resp *HttpResponse) *RequestLog {
// 解析 URL
host := ""
path := req.URL
if strings.HasPrefix(req.URL, "http://") {
parts := strings.SplitN(strings.TrimPrefix(req.URL, "http://"), "/", 2)
host = parts[0]
if len(parts) > 1 {
path = "/" + parts[1]
}
} else if strings.HasPrefix(req.URL, "https://") {
parts := strings.SplitN(strings.TrimPrefix(req.URL, "https://"), "/", 2)
host = parts[0]
if len(parts) > 1 {
path = "/" + parts[1]
for _, prefix := range []string{"http://", "https://"} {
if strings.HasPrefix(req.URL, prefix) {
parts := strings.SplitN(strings.TrimPrefix(req.URL, prefix), "/", 2)
host = parts[0]
if len(parts) > 1 {
path = "/" + parts[1]
}
break
}
}
+127 -14
View File
@@ -11,6 +11,7 @@ import (
"webhelper/global"
"github.com/gorilla/websocket"
"golang.org/x/net/proxy"
)
// WebSocketClient WebSocket 客户端
@@ -27,6 +28,7 @@ type WebSocketClient struct {
type WebSocketConfig struct {
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Proxy string `json:"proxy"` // 代理地址,支持 http:// 和 socks5://
Insecure bool `json:"insecure"`
}
@@ -37,7 +39,10 @@ type WebSocketMessage struct {
Time string `json:"time"`
}
var wsClient *WebSocketClient
var (
wsClient *WebSocketClient
wsClientOnce sync.Once
)
// NewWebSocketClient 创建 WebSocket 客户端
func NewWebSocketClient() *WebSocketClient {
@@ -67,13 +72,23 @@ func (c *WebSocketClient) Connect(config *WebSocketConfig, onMessage func(string
header.Set(key, value)
}
// 创建 Dialer(最简配置)
// 创建 Dialer
dialer := &websocket.Dialer{
HandshakeTimeout: 10 * time.Second,
}
// 配置代理
proxyAddr := global.NormalizeProxy(config.Proxy)
if proxyAddr != "" {
proxyURL, err := url.Parse(proxyAddr)
if err != nil {
return fmt.Errorf("代理地址格式错误: %v", err)
}
dialer.Proxy = http.ProxyURL(proxyURL)
}
// 连接
global.Log.Infof("正在连接 WebSocket: %s", u.String())
global.Log.Infof("正在连接 WebSocket: %s (proxy=%s)", u.String(), proxyAddr)
conn, _, err := dialer.Dial(u.String(), header)
if err != nil {
return fmt.Errorf("连接失败: %v", err)
@@ -150,11 +165,11 @@ func (c *WebSocketClient) IsConnected() bool {
return c.isConnected
}
// GetWebSocketClient 获取 WebSocket 客户端
// GetWebSocketClient 获取 WebSocket 客户端(并发安全的单例)
func GetWebSocketClient() *WebSocketClient {
if wsClient == nil {
wsClientOnce.Do(func() {
wsClient = NewWebSocketClient()
}
})
return wsClient
}
@@ -194,8 +209,9 @@ type TCPClient struct {
// TCPConfig TCP 配置
type TCPConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Host string `json:"host"`
Port int `json:"port"`
Proxy string `json:"proxy"` // 代理地址,支持 socks5://
}
// TCPMessage TCP 消息
@@ -205,7 +221,10 @@ type TCPMessage struct {
Time string `json:"time"`
}
var tcpClient *TCPClient
var (
tcpClient *TCPClient
tcpClientOnce sync.Once
)
// NewTCPClient 创建 TCP 客户端
func NewTCPClient() *TCPClient {
@@ -224,7 +243,43 @@ func (c *TCPClient) Connect(config *TCPConfig, onMessage func(string), onError f
}
addr := fmt.Sprintf("%s:%d", config.Host, config.Port)
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
// 配置代理
proxyAddr := global.NormalizeProxy(config.Proxy)
var conn net.Conn
var err error
if proxyAddr != "" {
// 通过代理连接
proxyURL, parseErr := url.Parse(proxyAddr)
if parseErr != nil {
return fmt.Errorf("代理地址格式错误: %v", parseErr)
}
switch proxyURL.Scheme {
case "socks5", "socks4":
// SOCKS 代理
auth := &proxy.Auth{}
if proxyURL.User != nil {
auth.User = proxyURL.User.Username()
if p, ok := proxyURL.User.Password(); ok {
auth.Password = p
}
}
dialer, proxyErr := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct)
if proxyErr != nil {
return fmt.Errorf("创建 SOCKS 代理失败: %v", proxyErr)
}
conn, err = dialer.Dial("tcp", addr)
default:
// HTTP 代理(通过 CONNECT 隧道)
conn, err = httpConnectProxy(addr, proxyAddr)
}
} else {
// 直连
conn, err = net.DialTimeout("tcp", addr, 10*time.Second)
}
if err != nil {
return fmt.Errorf("连接失败: %v", err)
}
@@ -235,12 +290,70 @@ func (c *TCPClient) Connect(config *TCPConfig, onMessage func(string), onError f
c.onError = onError
c.onClose = onClose
global.Log.Infof("TCP 连接成功: %s (proxy=%s)", addr, proxyAddr)
// 启动消息监听
go c.readMessages()
return nil
}
// httpConnectProxy 通过 HTTP CONNECT 代理建立 TCP 连接
func httpConnectProxy(target, proxyAddr string) (net.Conn, error) {
proxyURL, err := url.Parse(proxyAddr)
if err != nil {
return nil, err
}
// 连接代理
conn, err := net.DialTimeout("tcp", proxyURL.Host, 10*time.Second)
if err != nil {
return nil, err
}
// 发送 CONNECT 请求
connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", target, target)
_, err = conn.Write([]byte(connectReq))
if err != nil {
conn.Close()
return nil, err
}
// 读取响应
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
conn.Close()
return nil, err
}
// 检查 200 状态
resp := string(buf[:n])
if !contains(resp, "200") {
conn.Close()
return nil, fmt.Errorf("代理连接失败: %s", resp)
}
return conn, nil
}
// contains 检查字符串是否包含子串
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstr(s, substr))
}
func containsSubstr(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
// tcpBufSize TCP 读取缓冲区大小,64KB 覆盖绝大多数调试场景
const tcpBufSize = 64 << 10
// readMessages 读取消息
func (c *TCPClient) readMessages() {
defer func() {
@@ -252,7 +365,7 @@ func (c *TCPClient) readMessages() {
}
}()
buf := make([]byte, 4096)
buf := make([]byte, tcpBufSize)
for {
n, err := c.conn.Read(buf)
if err != nil {
@@ -300,11 +413,11 @@ func (c *TCPClient) IsConnected() bool {
return c.isConnected
}
// GetTCPClient 获取 TCP 客户端
// GetTCPClient 获取 TCP 客户端(并发安全的单例)
func GetTCPClient() *TCPClient {
if tcpClient == nil {
tcpClientOnce.Do(func() {
tcpClient = NewTCPClient()
}
})
return tcpClient
}