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) }