package httputil import ( "bytes" "encoding/json" "fmt" "net" "net/http" "net/url" "strings" "time" ) const DefaultTimeout = 10 * time.Second type HTTPResponse struct { StatusCode int `json:"statusCode"` Status string `json:"status"` Headers map[string]string `json:"headers"` Body string `json:"body"` JSON map[string]interface{} `json:"json,omitempty"` Error string `json:"error,omitempty"` } type HTTPClient struct { client *http.Client timeout time.Duration } func NewHTTPClient(timeout time.Duration) *HTTPClient { if timeout <= 0 { timeout = DefaultTimeout } return &HTTPClient{ client: &http.Client{ Timeout: timeout, }, timeout: timeout, } } func (c *HTTPClient) Get(urlStr string, headers map[string]string) *HTTPResponse { return c.doRequest("GET", urlStr, headers, nil) } func (c *HTTPClient) Post(urlStr string, headers map[string]string, body interface{}) *HTTPResponse { return c.doRequest("POST", urlStr, headers, body) } func (c *HTTPClient) doRequest(method, urlStr string, headers map[string]string, body interface{}) *HTTPResponse { resp := &HTTPResponse{ Headers: make(map[string]string), } if err := validateURL(urlStr); err != nil { resp.Error = err.Error() return resp } var reqBody []byte var err error contentType := "application/json" if body != nil { switch v := body.(type) { case string: reqBody = []byte(v) contentType = "text/plain" case map[string]interface{}: reqBody, err = json.Marshal(v) if err != nil { resp.Error = fmt.Sprintf("failed to marshal body: %v", err) return resp } default: reqBody, err = json.Marshal(v) if err != nil { resp.Error = fmt.Sprintf("failed to marshal body: %v", err) return resp } } } var req *http.Request if reqBody != nil { req, err = http.NewRequest(method, urlStr, bytes.NewReader(reqBody)) } else { req, err = http.NewRequest(method, urlStr, nil) } if err != nil { resp.Error = fmt.Sprintf("failed to create request: %v", err) return resp } if headers != nil { for k, v := range headers { req.Header.Set(k, v) } } if body != nil && req.Header.Get("Content-Type") == "" { req.Header.Set("Content-Type", contentType) } httpResp, err := c.client.Do(req) if err != nil { resp.Error = fmt.Sprintf("request failed: %v", err) return resp } defer httpResp.Body.Close() resp.StatusCode = httpResp.StatusCode resp.Status = httpResp.Status for k, v := range httpResp.Header { if len(v) > 0 { resp.Headers[k] = v[0] } } buf := new(bytes.Buffer) if _, err := buf.ReadFrom(httpResp.Body); err != nil { resp.Error = fmt.Sprintf("failed to read response body: %v", err) return resp } resp.Body = buf.String() if strings.Contains(httpResp.Header.Get("Content-Type"), "application/json") { var jsonData map[string]interface{} if err := json.Unmarshal(buf.Bytes(), &jsonData); err == nil { resp.JSON = jsonData } } return resp } func validateURL(urlStr string) error { parsedURL, err := url.Parse(urlStr) if err != nil { return fmt.Errorf("invalid URL: %v", err) } if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { return fmt.Errorf("only http and https protocols are allowed") } host := parsedURL.Hostname() if host == "" { return fmt.Errorf("empty host") } if isPrivateIP(host) { return fmt.Errorf("access to private IP addresses is not allowed") } if isLocalhost(host) { return fmt.Errorf("access to localhost is not allowed") } return nil } func isLocalhost(host string) bool { lowerHost := strings.ToLower(host) return lowerHost == "localhost" || lowerHost == "127.0.0.1" || lowerHost == "::1" } func isPrivateIP(host string) bool { ip := net.ParseIP(host) if ip == nil { return false } if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { return true } privateBlocks := []string{ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "127.0.0.0/8", "::1/128", "fc00::/7", "fe80::/10", } for _, block := range privateBlocks { _, cidr, _ := net.ParseCIDR(block) if cidr != nil && cidr.Contains(ip) { return true } } return false }