Files
xk-hy-forward-go/internal/forward/testweb/govtest.go
2026-05-28 17:04:29 +08:00

152 lines
3.9 KiB
Go

package testweb
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"io"
"net"
"net/http"
"strings"
"time"
)
const (
connectDialTimeout = 5 * time.Second
connectReadTimeout = 15 * time.Second
replayTimeout = 120 * time.Second
)
func govHTTPClient(timeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // 政务网自签证书
DialContext: (&net.Dialer{Timeout: connectDialTimeout}).DialContext,
ResponseHeaderTimeout: connectReadTimeout,
},
}
}
// ConnectResult 政务云连通性探测结果。
type ConnectResult struct {
URL string `json:"url"`
Channel string `json:"channel"`
ElapsedMs int64 `json:"elapsedMs"`
HTTPStatus int `json:"httpStatus,omitempty"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Phase string `json:"phase,omitempty"`
}
// TestConnectivity 对 target URL 发起 POST 探测。
func TestConnectivity(channel, target string) ConnectResult {
res := ConnectResult{URL: target, Channel: channel}
start := time.Now()
defer func() { res.ElapsedMs = time.Since(start).Milliseconds() }()
ctx, cancel := context.WithTimeout(context.Background(), connectReadTimeout+connectDialTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, strings.NewReader("{}"))
if err != nil {
res.Error = err.Error()
res.Phase = "build_request"
return res
}
req.Header.Set("Content-Type", "application/json")
resp, err := govHTTPClient(connectReadTimeout + connectDialTimeout).Do(req)
if err != nil {
res.Error = err.Error()
if strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "deadline") {
res.Phase = "timeout"
} else if strings.Contains(err.Error(), "connection refused") {
res.Phase = "tcp"
} else if strings.Contains(err.Error(), "tls") || strings.Contains(err.Error(), "certificate") {
res.Phase = "tls"
} else {
res.Phase = "network"
}
return res
}
defer resp.Body.Close()
_, _ = io.Copy(io.Discard, resp.Body)
res.HTTPStatus = resp.StatusCode
res.OK = true
res.Phase = "http"
return res
}
// ReplayResult 回放请求结果。
type ReplayResult struct {
URL string `json:"url"`
ElapsedMs int64 `json:"elapsedMs"`
HTTPStatus int `json:"httpStatus"`
Headers map[string]string `json:"headers"`
Body string `json:"body"`
BodyJSON string `json:"bodyJSON,omitempty"`
Error string `json:"error,omitempty"`
}
// ReplayRequest 向政务云回放 POST。
func ReplayRequest(url string, headers map[string]string, body []byte) ReplayResult {
res := ReplayResult{URL: url, Headers: make(map[string]string)}
start := time.Now()
defer func() { res.ElapsedMs = time.Since(start).Milliseconds() }()
var bodyReader io.Reader
if len(body) > 0 {
bodyReader = bytes.NewReader(body)
} else {
bodyReader = strings.NewReader("")
}
req, err := http.NewRequest(http.MethodPost, url, bodyReader)
if err != nil {
res.Error = err.Error()
return res
}
for k, v := range headers {
if strings.EqualFold(k, "X-Forward-Token") {
continue
}
req.Header.Set(k, v)
}
resp, err := govHTTPClient(replayTimeout).Do(req)
if err != nil {
res.Error = err.Error()
return res
}
defer resp.Body.Close()
res.HTTPStatus = resp.StatusCode
for k, vs := range resp.Header {
if len(vs) > 0 {
res.Headers[k] = vs[0]
}
}
raw, _ := io.ReadAll(resp.Body)
res.Body = string(raw)
if strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "json") {
res.BodyJSON = formatJSON(raw)
}
return res
}
func formatJSON(raw []byte) string {
s := strings.TrimSpace(string(raw))
if s == "" {
return ""
}
var v any
if json.Unmarshal(raw, &v) != nil {
return s
}
b, err := json.MarshalIndent(v, "", " ")
if err != nil {
return s
}
return string(b)
}