feat(tools/io): 内置 IO/网络工具(file(路径安全)/http(SSRF防护)/web-search/web-fetch/knowledge)+ 大小截断
This commit is contained in:
1 parent
488cf9c79c
commit
3ce9489f8e
17 files changed
+2223
No files matched your search
@@ -0,0 +1,189 @@
|
||||
package io
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
)
|
||||
|
||||
// httpToolForTest builds a tools set with opts and returns the http tool.
|
||||
func httpToolForTest(t *testing.T, opts ...Option) *httpTool {
|
||||
t.Helper()
|
||||
return findTool(t, buildTools(t, &config.Config{}, opts...), "http").(*httpTool)
|
||||
}
|
||||
|
||||
func TestHTTPTool_Get(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = io.WriteString(w, `{"ok":true,"path":"`+r.URL.Path+`"}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
ht := httpToolForTest(t, WithAllowedHosts(serverHost(srv)), WithHTTPClient(srv.Client()))
|
||||
|
||||
got, err := ht.Execute(context.Background(), map[string]any{"method": "GET", "url": srv.URL + "/hello"})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
m := got.(map[string]any)
|
||||
if m["status"] != http.StatusOK {
|
||||
t.Fatalf("status = %v", m["status"])
|
||||
}
|
||||
body := m["body"].(string)
|
||||
if !strings.Contains(body, `"ok":true`) || !strings.Contains(body, "/hello") {
|
||||
t.Fatalf("body = %q", body)
|
||||
}
|
||||
if m["truncated"].(bool) {
|
||||
t.Fatal("unexpected truncation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTool_Post(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Fatalf("method = %s", r.Method)
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write(body)
|
||||
}))
|
||||
defer srv.Close()
|
||||
ht := httpToolForTest(t, WithAllowedHosts(serverHost(srv)), WithHTTPClient(srv.Client()))
|
||||
|
||||
got, err := ht.Execute(context.Background(), map[string]any{
|
||||
"method": "POST",
|
||||
"url": srv.URL,
|
||||
"body": "name=cyrene",
|
||||
"headers": map[string]any{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
if body := got.(map[string]any)["body"].(string); body != "name=cyrene" {
|
||||
t.Fatalf("echoed body = %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTool_UnsupportedMethod(t *testing.T) {
|
||||
t.Parallel()
|
||||
ht := httpToolForTest(t)
|
||||
_, err := ht.Execute(context.Background(), map[string]any{"method": "DELETE", "url": "http://example.com"})
|
||||
if err == nil || !strings.Contains(err.Error(), "unsupported method") {
|
||||
t.Fatalf("expected unsupported-method error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTool_Timeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
_, _ = w.Write([]byte("late"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
ht := httpToolForTest(t, WithAllowedHosts(serverHost(srv)), WithHTTPTimeout(30*time.Millisecond))
|
||||
|
||||
_, err := ht.Execute(context.Background(), map[string]any{"method": "GET", "url": srv.URL})
|
||||
if err == nil {
|
||||
t.Fatal("expected timeout error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTool_RejectsBadContentType(t *testing.T) {
|
||||
t.Parallel()
|
||||
ht := httpToolForTest(t)
|
||||
_, err := ht.Execute(context.Background(), map[string]any{
|
||||
"method": "POST",
|
||||
"url": "http://93.184.216.34/",
|
||||
"body": "x",
|
||||
"headers": map[string]any{"Content-Type": "not a real content type {{"},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid content-type") {
|
||||
t.Fatalf("expected invalid content-type error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTool_ResponseTruncation(t *testing.T) {
|
||||
t.Parallel()
|
||||
payload := strings.Repeat("a", 5000)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, payload)
|
||||
}))
|
||||
defer srv.Close()
|
||||
ht := httpToolForTest(t, WithAllowedHosts(serverHost(srv)), WithHTTPClient(srv.Client()), WithHTTPResponseLimit(1000))
|
||||
|
||||
got, err := ht.Execute(context.Background(), map[string]any{"method": "GET", "url": srv.URL})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
m := got.(map[string]any)
|
||||
if !m["truncated"].(bool) {
|
||||
t.Fatal("expected truncated=true")
|
||||
}
|
||||
if body := m["body"].(string); len(body) != 1000 {
|
||||
t.Fatalf("body len = %d", len(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTool_SSRFDenied(t *testing.T) {
|
||||
t.Parallel()
|
||||
ht := httpToolForTest(t) // no whitelist
|
||||
cases := []string{
|
||||
"http://127.0.0.1:9/",
|
||||
"http://localhost:9/",
|
||||
"http://10.0.0.1/",
|
||||
"http://172.16.0.1/",
|
||||
"http://192.168.1.1/",
|
||||
"http://169.254.1.1/",
|
||||
"http://0.0.0.0/",
|
||||
"http://[::1]:9/",
|
||||
}
|
||||
for _, u := range cases {
|
||||
_, err := ht.Execute(context.Background(), map[string]any{"method": "GET", "url": u})
|
||||
if !errors.Is(err, ErrSSRF) {
|
||||
t.Fatalf("url %q: expected ErrSSRF, got %v", u, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTool_SSRFAllowedViaWhitelist(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, "ok")
|
||||
}))
|
||||
defer srv.Close()
|
||||
ht := httpToolForTest(t, WithAllowedHosts(serverHost(srv)), WithHTTPClient(srv.Client()))
|
||||
|
||||
got, err := ht.Execute(context.Background(), map[string]any{"method": "GET", "url": srv.URL})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute (whitelisted): %v", err)
|
||||
}
|
||||
if body := got.(map[string]any)["body"].(string); body != "ok" {
|
||||
t.Fatalf("body = %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTool_UnsupportedScheme(t *testing.T) {
|
||||
t.Parallel()
|
||||
ht := httpToolForTest(t)
|
||||
_, err := ht.Execute(context.Background(), map[string]any{"method": "GET", "url": "ftp://example.com/x"})
|
||||
if err == nil || !strings.Contains(err.Error(), "unsupported scheme") {
|
||||
t.Fatalf("expected unsupported-scheme error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTool_MissingURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
ht := httpToolForTest(t)
|
||||
_, err := ht.Execute(context.Background(), map[string]any{"method": "GET"})
|
||||
if err == nil || !strings.Contains(err.Error(), "url is required") {
|
||||
t.Fatalf("expected url-required error, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user