feat(tools/io): 内置 IO/网络工具(file(路径安全)/http(SSRF防护)/web-search/web-fetch/knowledge)+ 大小截断

This commit is contained in:
Aska committed 2026-09-06 06:29:03 +08:00
1 parent 488cf9c79c
commit 3ce9489f8e
17 files changed
+2223

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
package io
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"strings"
)
// ErrSSRF is returned when a requested URL targets a loopback, private or
// link-local address or a localhost hostname that is not on the allowlist.
// Use errors.Is to test for it.
var ErrSSRF = errors.New("io: disallowed url")
// validateRequestURL parses rawURL and refuses SSRF-prone targets: it returns an
// error wrapping ErrSSRF for loopback/private/link-local/unspecified addresses
// and for localhost hostnames, unless the host (or hostname) is on the explicit
// allowlist. A hostname that resolves to any denied address is also refused.
func validateRequestURL(ctx context.Context, rawURL string, allowed []string) (*url.URL, error) {
if strings.TrimSpace(rawURL) == "" {
return nil, fmt.Errorf("io: url is required")
}
u, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("io: parse url: %w", err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, fmt.Errorf("io: unsupported scheme %q", u.Scheme)
}
host := hostOnly(u.Host)
if host == "" {
return nil, fmt.Errorf("io: url has no host")
}
if isHostAllowed(host, allowed) {
return u, nil
}
if ip := net.ParseIP(host); ip != nil {
if isDeniedIP(ip) {
return nil, fmt.Errorf("%w: %s", ErrSSRF, host)
}
return u, nil
}
addrs, err := net.DefaultResolver.LookupHost(ctx, host)
if err != nil {
return nil, fmt.Errorf("io: resolve %q: %w", host, err)
}
for _, a := range addrs {
if ip := net.ParseIP(a); ip != nil && isDeniedIP(ip) {
return nil, fmt.Errorf("%w: %q resolves to %q", ErrSSRF, host, a)
}
}
return u, nil
}
// isDeniedIP reports whether ip is a loopback, private, link-local, multicast or
// unspecified address — the targets SSRF protection refuses by default.
func isDeniedIP(ip net.IP) bool {
if ip == nil {
return false
}
return ip.IsLoopback() ||
ip.IsPrivate() ||
ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() ||
ip.IsUnspecified()
}
// isHostAllowed reports whether the host (with any port stripped) matches an
// entry on the allowlist.
func isHostAllowed(host string, allowed []string) bool {
h := strings.ToLower(hostOnly(host))
if h == "" {
return false
}
for _, a := range allowed {
if strings.EqualFold(hostOnly(a), h) {
return true
}
}
return false
}
// hostOnly strips the port (and any IPv6 brackets) from a host:port string.
func hostOnly(hostport string) string {
h := strings.TrimSpace(hostport)
if h == "" {
return ""
}
if strings.HasPrefix(h, "[") {
if j := strings.Index(h, "]"); j >= 0 {
return h[1:j]
}
return h
}
if i := strings.LastIndex(h, ":"); i >= 0 {
return h[:i]
}
return h
}