post inicial

This commit is contained in:
lastdragon
2026-09-27 16:04:02 -06:00
parent d84587e1f3
commit 52864794f5
67 changed files with 15023 additions and 2 deletions
+98
View File
@@ -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
}