e019f6a55c
- 将硬编码的代理地址改为从环境变量读取 - 使用 http.ProxyFromEnvironment 让代理配置更灵活 - 服务器环境不设置代理环境变量即可直连
205 lines
4.3 KiB
Go
205 lines
4.3 KiB
Go
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
|
|
}
|
|
|
|
transport := &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
}
|
|
|
|
return &HTTPClient{
|
|
client: &http.Client{
|
|
Timeout: timeout,
|
|
Transport: transport,
|
|
},
|
|
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
|
|
}
|