- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
148 lines
3.8 KiB
Go
148 lines
3.8 KiB
Go
package fetchguard
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/compass"
|
|
)
|
|
|
|
// Mode selects host-allow-list vs any-public-host. The private/loopback/
|
|
// reserved IP block is always on regardless of Mode (D-11).
|
|
type Mode int
|
|
|
|
const (
|
|
AllowHostsMode Mode = iota
|
|
PublicOnlyMode
|
|
)
|
|
|
|
// Reason is the closed set of Fetch failure reasons, matching PHP's
|
|
// invalid_url/scheme/unresolvable/private_ip/network_error/too_large family.
|
|
type Reason string
|
|
|
|
const (
|
|
ReasonInvalidURL Reason = "invalid_url"
|
|
ReasonScheme Reason = "scheme"
|
|
ReasonUnresolvable Reason = "unresolvable"
|
|
ReasonPrivateIP Reason = "private_ip"
|
|
ReasonNetworkError Reason = "network_error"
|
|
ReasonTooLarge Reason = "too_large"
|
|
)
|
|
|
|
// Policy is supplied per call.
|
|
type Policy struct {
|
|
Mode Mode
|
|
AllowHosts []string // exact or dotted-suffix match; used in AllowHostsMode
|
|
MaxBytes int64 // 0 means use the config/framework default, never unlimited
|
|
Timeout time.Duration
|
|
|
|
// tlsConfig, if set, is Transport.TLSClientConfig so tests can trust an
|
|
// httptest certificate. Production callers leave it nil.
|
|
tlsConfig *tls.Config
|
|
// skipReservedCheck disables the dial-time private-IP block so tests can
|
|
// exercise a real httptest listener on 127.0.0.1. Production callers
|
|
// leave it false.
|
|
skipReservedCheck bool
|
|
}
|
|
|
|
// Error carries the typed Reason plus the underlying error for logging.
|
|
type Error struct {
|
|
Reason Reason
|
|
Err error
|
|
}
|
|
|
|
func (e *Error) Error() string {
|
|
if e == nil {
|
|
return "fetchguard: error"
|
|
}
|
|
if e.Err != nil {
|
|
return fmt.Sprintf("fetchguard: %s: %v", e.Reason, e.Err)
|
|
}
|
|
return fmt.Sprintf("fetchguard: %s", e.Reason)
|
|
}
|
|
|
|
func (e *Error) Unwrap() error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.Err
|
|
}
|
|
|
|
// Defaults are the framework fallback: 10 MiB, 10s, matching PHP.
|
|
func Defaults() (maxBytes int64, timeout time.Duration) {
|
|
return 10 * 1024 * 1024, 10 * time.Second
|
|
}
|
|
|
|
// DefaultsFromConfig reads http.fetch.max_bytes / http.fetch.timeout_seconds
|
|
// from cfg, falling back to Defaults() for absent keys. An explicitly
|
|
// configured zero or negative value is an error (D-14).
|
|
func DefaultsFromConfig(cfg *compass.Config) (maxBytes int64, timeout time.Duration, err error) {
|
|
maxBytes, timeout = Defaults()
|
|
if cfg == nil {
|
|
return maxBytes, timeout, nil
|
|
}
|
|
if v, ok := cfg.Lookup("http.fetch.max_bytes"); ok {
|
|
n, err := configInt64("http.fetch.max_bytes", v)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
if n <= 0 {
|
|
return 0, 0, fmt.Errorf("fetchguard: http.fetch.max_bytes must be positive, got %d", n)
|
|
}
|
|
maxBytes = n
|
|
}
|
|
if v, ok := cfg.Lookup("http.fetch.timeout_seconds"); ok {
|
|
n, err := configInt64("http.fetch.timeout_seconds", v)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
if n <= 0 {
|
|
return 0, 0, fmt.Errorf("fetchguard: http.fetch.timeout_seconds must be positive, got %d", n)
|
|
}
|
|
timeout = time.Duration(n) * time.Second
|
|
}
|
|
return maxBytes, timeout, nil
|
|
}
|
|
|
|
func configInt64(path string, v any) (int64, error) {
|
|
switch n := v.(type) {
|
|
case int:
|
|
return int64(n), nil
|
|
case int8:
|
|
return int64(n), nil
|
|
case int16:
|
|
return int64(n), nil
|
|
case int32:
|
|
return int64(n), nil
|
|
case int64:
|
|
return n, nil
|
|
case uint:
|
|
return int64(n), nil
|
|
case uint8:
|
|
return int64(n), nil
|
|
case uint16:
|
|
return int64(n), nil
|
|
case uint32:
|
|
return int64(n), nil
|
|
case uint64:
|
|
if n > math.MaxInt64 {
|
|
return 0, fmt.Errorf("fetchguard: %s overflows int64", path)
|
|
}
|
|
return int64(n), nil
|
|
case float64:
|
|
if math.Trunc(n) != n {
|
|
return 0, fmt.Errorf("fetchguard: %s is not an integer", path)
|
|
}
|
|
if n > math.MaxInt64 || n < math.MinInt64 {
|
|
return 0, fmt.Errorf("fetchguard: %s overflows int64", path)
|
|
}
|
|
return int64(n), nil
|
|
case float32:
|
|
return configInt64(path, float64(n))
|
|
default:
|
|
return 0, fmt.Errorf("fetchguard: %s has unexpected type %T", path, v)
|
|
}
|
|
}
|