feat: 重构HTTP/TCP客户端并增强全局设置与并发安全
This commit is contained in:
1 parent
ad5884804c
commit
2e9b2f8050
11 files changed
+580
-347
No files matched your search
+142
-212
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,51 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import { GetALLLang, GetCurrentLang, SetLanguage, GetLangTextMap } from '../../wailsjs/go/service/App'
|
||||
import {
|
||||
GetALLLang, GetCurrentLang, SetLanguage, GetLangTextMap,
|
||||
GetProxy, SetProxy, GetTimeout, SetTimeout,
|
||||
GetLogLevel, SetLogLevel
|
||||
} from '../../wailsjs/go/service/App'
|
||||
|
||||
const { t, textMap } = useI18n()
|
||||
|
||||
const settings = ref({
|
||||
proxyEnabled: false,
|
||||
proxyHost: '',
|
||||
proxyPort: 7890
|
||||
proxyURL: '',
|
||||
timeout: 30,
|
||||
logLevel: 'info'
|
||||
})
|
||||
|
||||
const languages = ref([])
|
||||
const currentLang = ref('')
|
||||
|
||||
// 加载语言列表
|
||||
const logLevels = [
|
||||
{ label: 'Debug', value: 'debug' },
|
||||
{ label: 'Info', value: 'info' },
|
||||
{ label: 'Warn', value: 'warn' },
|
||||
{ label: 'Error', value: 'error' }
|
||||
]
|
||||
|
||||
// 加载所有设置
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const langInfos = await GetALLLang()
|
||||
const [langInfos, proxy, timeoutCfg, logLevel] = await Promise.all([
|
||||
GetALLLang(),
|
||||
GetProxy(),
|
||||
GetTimeout(),
|
||||
GetLogLevel()
|
||||
])
|
||||
|
||||
languages.value = langInfos.map(info => ({
|
||||
code: info.language_code,
|
||||
name: info.language_name
|
||||
}))
|
||||
currentLang.value = await GetCurrentLang() || 'zh-CN'
|
||||
|
||||
if (proxy) settings.value.proxyURL = proxy.url || ''
|
||||
if (timeoutCfg) settings.value.timeout = timeoutCfg.timeout || 30
|
||||
settings.value.logLevel = logLevel || 'info'
|
||||
} catch (e) {
|
||||
console.error('获取语言列表失败:', e)
|
||||
console.error('获取设置失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -40,19 +61,46 @@ const handleLangChange = async (langCode) => {
|
||||
}
|
||||
}
|
||||
|
||||
const saveSettings = () => {
|
||||
localStorage.setItem('webhelper-settings', JSON.stringify(settings.value))
|
||||
ElMessage.success(t('settings.msg.saved'))
|
||||
// 保存所有设置
|
||||
const saveSettings = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
SetProxy(settings.value.proxyURL),
|
||||
SetTimeout(settings.value.timeout),
|
||||
SetLogLevel(settings.value.logLevel)
|
||||
])
|
||||
ElMessage.success(t('settings.msg.saved'))
|
||||
} catch (e) {
|
||||
ElMessage.error('保存设置失败: ' + e)
|
||||
}
|
||||
}
|
||||
|
||||
const resetSettings = () => {
|
||||
// 恢复默认
|
||||
const resetSettings = async () => {
|
||||
settings.value = {
|
||||
proxyEnabled: false,
|
||||
proxyHost: '',
|
||||
proxyPort: 7890
|
||||
proxyURL: '',
|
||||
timeout: 30,
|
||||
logLevel: 'info'
|
||||
}
|
||||
|
||||
const defaultLang = 'zh-CN'
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
SetProxy(''),
|
||||
SetTimeout(30),
|
||||
SetLogLevel('info'),
|
||||
SetLanguage(defaultLang)
|
||||
])
|
||||
|
||||
currentLang.value = defaultLang
|
||||
const map = await GetLangTextMap()
|
||||
textMap.value = map || {}
|
||||
|
||||
ElMessage.info(t('settings.msg.reset'))
|
||||
} catch (e) {
|
||||
ElMessage.error('恢复默认失败: ' + e)
|
||||
}
|
||||
localStorage.removeItem('webhelper-settings')
|
||||
ElMessage.info(t('settings.msg.reset'))
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -62,7 +110,10 @@ const resetSettings = () => {
|
||||
<span>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
|
||||
<el-form :model="settings" label-width="100px" class="settings-form">
|
||||
<el-form :model="settings" label-width="120px" class="settings-form">
|
||||
<!-- 外观 -->
|
||||
<el-divider content-position="left">{{ t('settings.form.appearance_divider') }}</el-divider>
|
||||
|
||||
<el-form-item :label="t('settings.form.language_label')">
|
||||
<el-select v-model="currentLang" style="width: 200px" @change="handleLangChange">
|
||||
<el-option
|
||||
@@ -74,18 +125,37 @@ const resetSettings = () => {
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">{{ t('settings.form.proxy_divider') }}</el-divider>
|
||||
<!-- 网络 -->
|
||||
<el-divider content-position="left">{{ t('settings.form.network_divider') }}</el-divider>
|
||||
|
||||
<el-form-item :label="t('settings.form.enable_proxy_label')">
|
||||
<el-switch v-model="settings.proxyEnabled" />
|
||||
<el-form-item :label="t('settings.form.proxy_label')">
|
||||
<el-input
|
||||
v-model="settings.proxyURL"
|
||||
:placeholder="t('settings.form.proxy_placeholder')"
|
||||
style="width: 320px"
|
||||
clearable
|
||||
/>
|
||||
<div class="form-tip">{{ t('settings.form.proxy_tip') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="settings.proxyEnabled" :label="t('settings.form.proxy_address_label')">
|
||||
<el-input v-model="settings.proxyHost" :placeholder="t('settings.form.proxy_address_placeholder')" style="width: 200px" />
|
||||
<el-form-item :label="t('settings.form.timeout_label')">
|
||||
<el-input-number v-model="settings.timeout" :min="1" :max="300" />
|
||||
<span class="form-unit">{{ t('settings.form.timeout_unit') }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="settings.proxyEnabled" :label="t('settings.form.proxy_port_label')">
|
||||
<el-input-number v-model="settings.proxyPort" :min="1" :max="65535" />
|
||||
<!-- 日志 -->
|
||||
<el-divider content-position="left">{{ t('settings.form.log_divider') }}</el-divider>
|
||||
|
||||
<el-form-item :label="t('settings.form.log_level_label')">
|
||||
<el-select v-model="settings.logLevel" style="width: 200px">
|
||||
<el-option
|
||||
v-for="level in logLevels"
|
||||
:key="level.value"
|
||||
:label="level.label"
|
||||
:value="level.value"
|
||||
/>
|
||||
</el-select>
|
||||
<span class="form-tip-inline">{{ t('settings.form.log_level_tip') }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider />
|
||||
@@ -119,4 +189,22 @@ const resetSettings = () => {
|
||||
.settings-form {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-unit {
|
||||
margin-left: 8px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.form-tip-inline {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,14 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import {
|
||||
SendHttpRequest,
|
||||
OpenFileSelect,
|
||||
GetRequestLogs,
|
||||
DeleteRequestLog,
|
||||
import {
|
||||
SendHttpRequest,
|
||||
OpenFileSelect,
|
||||
GetRequestLogs,
|
||||
DeleteRequestLog,
|
||||
ClearRequestLogs,
|
||||
SearchRequestLogs
|
||||
SearchRequestLogs,
|
||||
GetTimeout
|
||||
} from '../../wailsjs/go/service/App'
|
||||
import { Delete, Upload, Notebook, Search } from '@element-plus/icons-vue'
|
||||
|
||||
@@ -29,6 +30,18 @@ const timeout = ref(30)
|
||||
const insecure = ref(false)
|
||||
const saveLog = ref(true)
|
||||
|
||||
// 加载全局默认设置
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const cfg = await GetTimeout()
|
||||
if (cfg) {
|
||||
timeout.value = cfg.timeout || 30
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取默认设置失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
// 响应展示相关
|
||||
const responseTab = ref('body')
|
||||
|
||||
|
||||
@@ -198,28 +198,28 @@ export namespace global {
|
||||
|
||||
export namespace service {
|
||||
|
||||
export class SystemInfo {
|
||||
os: string;
|
||||
arch: string;
|
||||
num_cpu: number;
|
||||
hostname: string;
|
||||
go_ver: string;
|
||||
time: string;
|
||||
process_name: string;
|
||||
export class ProxyConfig {
|
||||
url: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SystemInfo(source);
|
||||
return new ProxyConfig(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.os = source["os"];
|
||||
this.arch = source["arch"];
|
||||
this.num_cpu = source["num_cpu"];
|
||||
this.hostname = source["hostname"];
|
||||
this.go_ver = source["go_ver"];
|
||||
this.time = source["time"];
|
||||
this.process_name = source["process_name"];
|
||||
this.url = source["url"];
|
||||
}
|
||||
}
|
||||
export class TimeoutConfig {
|
||||
timeout: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new TimeoutConfig(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.timeout = source["timeout"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+10
-6
@@ -14,8 +14,6 @@ export function DisconnectSocket(arg1:string):Promise<boolean>;
|
||||
|
||||
export function ExecuteJs(arg1:string):Promise<api.JsExecResult>;
|
||||
|
||||
export function Flashtime():Promise<void>;
|
||||
|
||||
export function GetALLLang():Promise<Array<global.LanguageInfo>>;
|
||||
|
||||
export function GetCurrentLang():Promise<string>;
|
||||
@@ -32,15 +30,17 @@ export function GetLogLevel():Promise<string>;
|
||||
|
||||
export function GetProcessName():Promise<string>;
|
||||
|
||||
export function GetProxy():Promise<service.ProxyConfig>;
|
||||
|
||||
export function GetProxyAddress():Promise<string>;
|
||||
|
||||
export function GetRequestLog(arg1:number):Promise<api.RequestLog>;
|
||||
|
||||
export function GetRequestLogs():Promise<Array<api.RequestLog>>;
|
||||
|
||||
export function GetSystemInfo():Promise<service.SystemInfo>;
|
||||
export function GetTimeout():Promise<service.TimeoutConfig>;
|
||||
|
||||
export function Gettestjson():Promise<string>;
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
export function GetVersion():Promise<string>;
|
||||
|
||||
export function IsSocketConnected(arg1:string):Promise<boolean>;
|
||||
|
||||
@@ -64,6 +64,10 @@ export function SetLanguage(arg1:string):Promise<boolean>;
|
||||
|
||||
export function SetLogLevel(arg1:string):Promise<boolean>;
|
||||
|
||||
export function SetProxy(arg1:string):Promise<boolean>;
|
||||
|
||||
export function SetTimeout(arg1:number):Promise<boolean>;
|
||||
|
||||
export function WindowClose():Promise<void>;
|
||||
|
||||
export function WindowMinimise():Promise<void>;
|
||||
|
||||
@@ -22,10 +22,6 @@ export function ExecuteJs(arg1) {
|
||||
return window['go']['service']['App']['ExecuteJs'](arg1);
|
||||
}
|
||||
|
||||
export function Flashtime() {
|
||||
return window['go']['service']['App']['Flashtime']();
|
||||
}
|
||||
|
||||
export function GetALLLang() {
|
||||
return window['go']['service']['App']['GetALLLang']();
|
||||
}
|
||||
@@ -58,6 +54,14 @@ export function GetProcessName() {
|
||||
return window['go']['service']['App']['GetProcessName']();
|
||||
}
|
||||
|
||||
export function GetProxy() {
|
||||
return window['go']['service']['App']['GetProxy']();
|
||||
}
|
||||
|
||||
export function GetProxyAddress() {
|
||||
return window['go']['service']['App']['GetProxyAddress']();
|
||||
}
|
||||
|
||||
export function GetRequestLog(arg1) {
|
||||
return window['go']['service']['App']['GetRequestLog'](arg1);
|
||||
}
|
||||
@@ -66,16 +70,12 @@ export function GetRequestLogs() {
|
||||
return window['go']['service']['App']['GetRequestLogs']();
|
||||
}
|
||||
|
||||
export function GetSystemInfo() {
|
||||
return window['go']['service']['App']['GetSystemInfo']();
|
||||
export function GetTimeout() {
|
||||
return window['go']['service']['App']['GetTimeout']();
|
||||
}
|
||||
|
||||
export function Gettestjson() {
|
||||
return window['go']['service']['App']['Gettestjson']();
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['service']['App']['Greet'](arg1);
|
||||
export function GetVersion() {
|
||||
return window['go']['service']['App']['GetVersion']();
|
||||
}
|
||||
|
||||
export function IsSocketConnected(arg1) {
|
||||
@@ -122,6 +122,14 @@ export function SetLogLevel(arg1) {
|
||||
return window['go']['service']['App']['SetLogLevel'](arg1);
|
||||
}
|
||||
|
||||
export function SetProxy(arg1) {
|
||||
return window['go']['service']['App']['SetProxy'](arg1);
|
||||
}
|
||||
|
||||
export function SetTimeout(arg1) {
|
||||
return window['go']['service']['App']['SetTimeout'](arg1);
|
||||
}
|
||||
|
||||
export function WindowClose() {
|
||||
return window['go']['service']['App']['WindowClose']();
|
||||
}
|
||||
|
||||
+29
-1
@@ -1,11 +1,39 @@
|
||||
package global
|
||||
|
||||
import "strings"
|
||||
|
||||
type GConfig struct {
|
||||
Language string `json:"language"`
|
||||
LogDir string `json:"log_dir"`
|
||||
ProxyURL string `json:"proxy_url"` // 代理地址,支持 http:// 和 socks5://
|
||||
Timeout int `json:"timeout"` // 默认请求超时(秒)
|
||||
LogLevel string `json:"log_level"` // 日志等级:debug/info/warn/error
|
||||
}
|
||||
|
||||
var GlobalConfig = &GConfig{
|
||||
Language: "zh-CN",
|
||||
LogDir: "logs",
|
||||
}
|
||||
ProxyURL: "",
|
||||
Timeout: 30,
|
||||
LogLevel: "info",
|
||||
}
|
||||
|
||||
// GetProxy 获取全局代理地址,未设置或为空时返回空字符串
|
||||
func GetProxy() string {
|
||||
return NormalizeProxy(GlobalConfig.ProxyURL)
|
||||
}
|
||||
|
||||
// NormalizeProxy 规范化代理地址:无协议前缀时默认 http://
|
||||
func NormalizeProxy(addr string) string {
|
||||
addr = strings.TrimSpace(addr)
|
||||
if addr == "" {
|
||||
return ""
|
||||
}
|
||||
// 已有协议前缀,直接返回
|
||||
if strings.HasPrefix(addr, "http://") || strings.HasPrefix(addr, "https://") ||
|
||||
strings.HasPrefix(addr, "socks5://") || strings.HasPrefix(addr, "socks4://") {
|
||||
return addr
|
||||
}
|
||||
// 无前缀,默认 http://
|
||||
return "http://" + addr
|
||||
}
|
||||
@@ -7,6 +7,7 @@ require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/wailsapp/wails/v2 v2.12.0
|
||||
golang.org/x/net v0.35.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -37,7 +38,6 @@ require (
|
||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
|
||||
+71
-39
@@ -5,10 +5,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
goruntime "runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"webhelper/api"
|
||||
"webhelper/global"
|
||||
@@ -29,18 +27,6 @@ func (a *App) Startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
}
|
||||
|
||||
func (a *App) Greet(name string) string {
|
||||
return greet(name)
|
||||
}
|
||||
|
||||
func (a *App) Flashtime() {
|
||||
flashtime(a)
|
||||
}
|
||||
|
||||
func (a *App) Gettestjson() string {
|
||||
return getJSONString()
|
||||
}
|
||||
|
||||
func (a *App) GetLangTextMap() map[string]string {
|
||||
return global.GetLangTextMap()
|
||||
}
|
||||
@@ -69,6 +55,52 @@ func (a *App) GetCurrentLang() string {
|
||||
return global.GlobalConfig.Language
|
||||
}
|
||||
|
||||
// ProxyConfig 代理配置
|
||||
type ProxyConfig struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// TimeoutConfig 超时配置
|
||||
type TimeoutConfig struct {
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
|
||||
// GetProxy 获取全局代理配置
|
||||
func (a *App) GetProxy() ProxyConfig {
|
||||
return ProxyConfig{
|
||||
URL: global.GlobalConfig.ProxyURL,
|
||||
}
|
||||
}
|
||||
|
||||
// SetProxy 设置全局代理配置
|
||||
func (a *App) SetProxy(url string) bool {
|
||||
global.GlobalConfig.ProxyURL = url
|
||||
global.Log.Infof("全局代理已更新: %s", url)
|
||||
return true
|
||||
}
|
||||
|
||||
// GetProxyAddress 获取全局代理地址字符串(已规范化),供 HTTP 请求使用
|
||||
func (a *App) GetProxyAddress() string {
|
||||
return global.GetProxy()
|
||||
}
|
||||
|
||||
// GetTimeout 获取默认超时设置
|
||||
func (a *App) GetTimeout() TimeoutConfig {
|
||||
return TimeoutConfig{
|
||||
Timeout: global.GlobalConfig.Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// SetTimeout 设置默认超时
|
||||
func (a *App) SetTimeout(timeout int) bool {
|
||||
if timeout < 1 {
|
||||
timeout = 30
|
||||
}
|
||||
global.GlobalConfig.Timeout = timeout
|
||||
global.Log.Infof("默认超时已更新: %d", timeout)
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *App) GetLogFiles() []string {
|
||||
logDir := global.GlobalConfig.LogDir
|
||||
entries, err := os.ReadDir(logDir)
|
||||
@@ -108,28 +140,26 @@ func (a *App) SetLogLevel(level string) bool {
|
||||
switch strings.ToLower(level) {
|
||||
case "debug":
|
||||
global.SetLogLevel(logrus.DebugLevel)
|
||||
global.Log.Debug("日志等级已切换为 Debug")
|
||||
global.GlobalConfig.LogLevel = "debug"
|
||||
case "info":
|
||||
global.SetLogLevel(logrus.InfoLevel)
|
||||
global.Log.Info("日志等级已切换为 Info")
|
||||
global.GlobalConfig.LogLevel = "info"
|
||||
case "warn":
|
||||
global.SetLogLevel(logrus.WarnLevel)
|
||||
global.Log.Warn("日志等级已切换为 Warn")
|
||||
global.GlobalConfig.LogLevel = "warn"
|
||||
case "error":
|
||||
global.SetLogLevel(logrus.ErrorLevel)
|
||||
global.Log.Error("日志等级已切换为 Error")
|
||||
global.GlobalConfig.LogLevel = "error"
|
||||
default:
|
||||
global.Log.Warnf("未知的日志等级: %s", level)
|
||||
return false
|
||||
}
|
||||
global.Log.Infof("日志等级已切换为: %s", strings.ToUpper(global.GlobalConfig.LogLevel))
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *App) GetLogLevel() string {
|
||||
if global.Log == nil {
|
||||
return "info"
|
||||
}
|
||||
return strings.ToUpper(global.Log.GetLevel().String())
|
||||
return global.GlobalConfig.LogLevel
|
||||
}
|
||||
|
||||
func (a *App) WindowMinimise() {
|
||||
@@ -144,20 +174,6 @@ func (a *App) WindowClose() {
|
||||
runtime.Quit(a.ctx)
|
||||
}
|
||||
|
||||
func (a *App) GetSystemInfo() SystemInfo {
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
return SystemInfo{
|
||||
OS: goruntime.GOOS,
|
||||
Arch: goruntime.GOARCH,
|
||||
NumCPU: goruntime.NumCPU(),
|
||||
Hostname: hostname,
|
||||
GoVer: goruntime.Version(),
|
||||
Time: time.Now().Format(time.DateTime),
|
||||
ProcessName: global.GetProcessName(),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) GetProcessName() string {
|
||||
return global.GetProcessName()
|
||||
}
|
||||
@@ -248,12 +264,26 @@ func (a *App) SendHttpRequest(method, url, headers, cookies, proxy, bodyType, bo
|
||||
// 解析 Cookie
|
||||
cookieMap := api.ParseCookies(cookies)
|
||||
|
||||
// 代理优先级:请求级代理 > 全局代理
|
||||
requestProxy := global.NormalizeProxy(proxy)
|
||||
if requestProxy == "" {
|
||||
requestProxy = global.GetProxy()
|
||||
}
|
||||
|
||||
// 超时优先级:请求级 > 全局默认
|
||||
if timeout <= 0 {
|
||||
timeout = global.GlobalConfig.Timeout
|
||||
}
|
||||
|
||||
// 日志保存优先级:显式传入 > 全局默认
|
||||
// saveLog 由前端传入,这里不覆盖
|
||||
|
||||
req := &api.HttpRequest{
|
||||
Method: method,
|
||||
URL: url,
|
||||
Headers: headerMap,
|
||||
Cookies: cookieMap,
|
||||
Proxy: proxy,
|
||||
Proxy: requestProxy,
|
||||
BodyType: bodyType,
|
||||
Body: body,
|
||||
FilePath: filePath,
|
||||
@@ -366,8 +396,9 @@ func (a *App) ConnectSocket(protocol, host string, port int, path string, header
|
||||
if protocol == "tcp" {
|
||||
// TCP 连接
|
||||
config := &api.TCPConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Host: host,
|
||||
Port: port,
|
||||
Proxy: global.GetProxy(),
|
||||
}
|
||||
|
||||
err := api.ConnectTCP(config,
|
||||
@@ -409,6 +440,7 @@ func (a *App) ConnectSocket(protocol, host string, port int, path string, header
|
||||
config := &api.WebSocketConfig{
|
||||
URL: url,
|
||||
Headers: headerMap,
|
||||
Proxy: global.GetProxy(),
|
||||
}
|
||||
|
||||
err := api.ConnectWebSocket(config,
|
||||
|
||||
Reference in new issue
Block a user