post inicial
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewHTTPClient crea el único cliente HTTP de las herramientas (§67).
|
||||
//
|
||||
// No tiene timeout global porque las descargas grandes pueden durar mucho;
|
||||
// cada petición lleva su propio contexto con timeout. El transporte sí
|
||||
// limita las fases que nunca deberían tardar: conexión, TLS y cabeceras.
|
||||
// Reutiliza conexiones (pool) y exige TLS 1.2 o superior.
|
||||
func NewHTTPClient() *http.Client {
|
||||
tr := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment, // respeta HTTPS_PROXY/NO_PROXY
|
||||
DialContext: (&net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
||||
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
|
||||
TLSHandshakeTimeout: 15 * time.Second,
|
||||
ResponseHeaderTimeout: 30 * time.Second,
|
||||
MaxIdleConns: 20,
|
||||
MaxIdleConnsPerHost: 4,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
ForceAttemptHTTP2: true,
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: tr,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 10 {
|
||||
return fmt.Errorf("stopped after 10 redirects")
|
||||
}
|
||||
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
|
||||
return fmt.Errorf("redirect to unsupported scheme %q", req.URL.Scheme)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// validateURL solo admite http(s) con host.
|
||||
func validateURL(raw string) (*url.URL, error) {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
|
||||
return nil, fmt.Errorf("invalid URL %q: only http and https are supported", raw)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// httpGet hace un GET con timeout y devuelve como máximo maxBytes del cuerpo.
|
||||
// truncated indica que el cuerpo era más largo.
|
||||
func httpGet(ctx context.Context, env *Env, rawURL string, timeout time.Duration, maxBytes int64, accept string) (body []byte, resp *http.Response, truncated bool, err error) {
|
||||
if _, err := validateURL(rawURL); err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to build request for %s: %w", rawURL, err)
|
||||
}
|
||||
req.Header.Set("User-Agent", env.userAgent())
|
||||
if accept != "" {
|
||||
req.Header.Set("Accept", accept)
|
||||
}
|
||||
resp, err = env.httpClient().Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("GET %s: %w", rawURL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err = io.ReadAll(io.LimitReader(resp.Body, maxBytes+1))
|
||||
if err != nil {
|
||||
return nil, resp, false, fmt.Errorf("failed to read %s: %w", rawURL, err)
|
||||
}
|
||||
if int64(len(body)) > maxBytes {
|
||||
body, truncated = body[:maxBytes], true
|
||||
}
|
||||
return body, resp, truncated, nil
|
||||
}
|
||||
|
||||
func (e *Env) httpClient() *http.Client {
|
||||
if e.HTTP == nil {
|
||||
e.HTTP = NewHTTPClient()
|
||||
}
|
||||
return e.HTTP
|
||||
}
|
||||
|
||||
func (e *Env) userAgent() string {
|
||||
if e.UserAgent == "" {
|
||||
return "sysagent/0.1"
|
||||
}
|
||||
return e.UserAgent
|
||||
}
|
||||
Reference in New Issue
Block a user