496 lines
12 KiB
Go
496 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// HttpRequest 请求结构体
|
|
type HttpRequest struct {
|
|
Method string `json:"method"` // HTTP 方法
|
|
URL string `json:"url"` // 请求地址
|
|
Headers map[string]string `json:"headers"` // 请求头
|
|
Cookies map[string]string `json:"cookies"` // Cookie
|
|
Proxy string `json:"proxy"` // 代理地址
|
|
BodyType string `json:"bodyType"` // 数据类型: text, file, form, json
|
|
Body string `json:"body"` // 文本数据
|
|
FilePath string `json:"filePath"` // 文件路径
|
|
ContentType string `json:"contentType"` // Content-Type
|
|
Timeout int `json:"timeout"` // 超时时间(秒)
|
|
Insecure bool `json:"insecure"` // 忽略证书验证
|
|
}
|
|
|
|
// HttpResponse 响应结构体
|
|
type HttpResponse struct {
|
|
StatusCode int `json:"statusCode"` // 状态码
|
|
Status string `json:"status"` // 状态描述
|
|
Headers map[string]string `json:"headers"` // 响应头
|
|
Cookies []CookieInfo `json:"cookies"` // 响应 Cookie
|
|
Body string `json:"body"` // 响应正文
|
|
ContentType string `json:"contentType"` // Content-Type
|
|
ContentLength int64 `json:"contentLength"` // 内容长度
|
|
Time string `json:"time"` // 请求耗时
|
|
Size int64 `json:"size"` // 响应大小
|
|
Error string `json:"error"` // 错误信息
|
|
}
|
|
|
|
// CookieInfo Cookie 信息
|
|
type CookieInfo struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
Path string `json:"path"`
|
|
Domain string `json:"domain"`
|
|
Expires string `json:"expires"`
|
|
MaxAge int `json:"maxAge"`
|
|
Secure bool `json:"secure"`
|
|
HttpOnly bool `json:"httpOnly"`
|
|
SameSite string `json:"sameSite"`
|
|
}
|
|
|
|
// FileData 文件数据
|
|
type FileData struct {
|
|
FieldName string `json:"fieldName"` // 字段名
|
|
FileName string `json:"fileName"` // 文件名
|
|
ContentType string `json:"contentType"` // 文件类型
|
|
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
|
|
}
|
|
|
|
// 补全协议
|
|
if !strings.HasPrefix(req.URL, "http://") && !strings.HasPrefix(req.URL, "https://") {
|
|
req.URL = "https://" + req.URL
|
|
}
|
|
|
|
// 设置超时
|
|
timeout := time.Duration(req.Timeout) * time.Second
|
|
if timeout == 0 {
|
|
timeout = 30 * time.Second
|
|
}
|
|
|
|
// 创建 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,
|
|
},
|
|
}
|
|
}
|
|
|
|
// 创建请求体
|
|
var bodyReader io.Reader
|
|
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)
|
|
if err != nil {
|
|
resp.Error = fmt.Sprintf("打开文件失败: %v", err)
|
|
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 == "" {
|
|
contentType = "text/plain"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 创建请求
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 设置 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)
|
|
return resp
|
|
}
|
|
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())
|
|
|
|
return resp
|
|
}
|
|
|
|
// SendRequestWithFiles 带文件的请求
|
|
func SendRequestWithFiles(req *HttpRequest, files []FileData) *HttpResponse {
|
|
resp := &HttpResponse{
|
|
Headers: make(map[string]string),
|
|
}
|
|
|
|
if req.URL == "" {
|
|
resp.Error = "请求地址不能为空"
|
|
return resp
|
|
}
|
|
|
|
// 补全协议
|
|
if !strings.HasPrefix(req.URL, "http://") && !strings.HasPrefix(req.URL, "https://") {
|
|
req.URL = "https://" + req.URL
|
|
}
|
|
|
|
// 设置超时
|
|
timeout := time.Duration(req.Timeout) * time.Second
|
|
if timeout == 0 {
|
|
timeout = 30 * time.Second
|
|
}
|
|
|
|
// 创建 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 请求
|
|
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 {
|
|
resp.Error = fmt.Sprintf("打开文件 %s 失败: %v", file.Path, err)
|
|
return resp
|
|
}
|
|
defer f.Close()
|
|
|
|
part, err := writer.CreateFormFile(file.FieldName, file.FileName)
|
|
if err != nil {
|
|
resp.Error = fmt.Sprintf("创建文件字段失败: %v", err)
|
|
return resp
|
|
}
|
|
|
|
if _, err := io.Copy(part, f); err != nil {
|
|
resp.Error = fmt.Sprintf("复制文件内容失败: %v", err)
|
|
return resp
|
|
}
|
|
}
|
|
|
|
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())
|
|
|
|
// 设置自定义请求头(需要单独处理 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)
|
|
return resp
|
|
}
|
|
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())
|
|
|
|
return resp
|
|
}
|
|
|
|
// ParseHeaders 解析请求头字符串
|
|
func ParseHeaders(headerStr string) map[string]string {
|
|
headers := make(map[string]string)
|
|
if headerStr == "" {
|
|
return headers
|
|
}
|
|
|
|
lines := strings.Split(headerStr, "\n")
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) == 2 {
|
|
key := strings.TrimSpace(parts[0])
|
|
value := strings.TrimSpace(parts[1])
|
|
headers[key] = value
|
|
}
|
|
}
|
|
|
|
return headers
|
|
}
|
|
|
|
// ParseCookies 解析 Cookie 字符串
|
|
func ParseCookies(cookieStr string) map[string]string {
|
|
cookies := make(map[string]string)
|
|
if cookieStr == "" {
|
|
return cookies
|
|
}
|
|
|
|
pairs := strings.Split(cookieStr, ";")
|
|
for _, pair := range pairs {
|
|
pair = strings.TrimSpace(pair)
|
|
if pair == "" {
|
|
continue
|
|
}
|
|
|
|
parts := strings.SplitN(pair, "=", 2)
|
|
if len(parts) == 2 {
|
|
name := strings.TrimSpace(parts[0])
|
|
value := strings.TrimSpace(parts[1])
|
|
cookies[name] = value
|
|
}
|
|
}
|
|
|
|
return cookies
|
|
}
|
|
|
|
// convertCookies 将 http.Cookie 转换为 CookieInfo
|
|
func convertCookies(httpCookies []*http.Cookie) []CookieInfo {
|
|
var cookies []CookieInfo
|
|
for _, c := range httpCookies {
|
|
sameSite := ""
|
|
switch c.SameSite {
|
|
case http.SameSiteStrictMode:
|
|
sameSite = "Strict"
|
|
case http.SameSiteLaxMode:
|
|
sameSite = "Lax"
|
|
case http.SameSiteNoneMode:
|
|
sameSite = "None"
|
|
}
|
|
|
|
cookies = append(cookies, CookieInfo{
|
|
Name: c.Name,
|
|
Value: c.Value,
|
|
Path: c.Path,
|
|
Domain: c.Domain,
|
|
Expires: c.Expires.Format(time.RFC1123),
|
|
MaxAge: c.MaxAge,
|
|
Secure: c.Secure,
|
|
HttpOnly: c.HttpOnly,
|
|
SameSite: sameSite,
|
|
})
|
|
}
|
|
return cookies
|
|
}
|