From bf859387f4691fc4340fe4df92c1cfe2e931bc69 Mon Sep 17 00:00:00 2001 From: dudaodong Date: Sat, 15 Mar 2025 14:25:03 +0800 Subject: [PATCH] feat: add BuildUrl --- netutil/net.go | 61 +++++++++++++++++++++++++++++++++++++++++++++ netutil/net_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/netutil/net.go b/netutil/net.go index 767b5d1..5d41c7f 100644 --- a/netutil/net.go +++ b/netutil/net.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "io" "mime/multipart" "net" @@ -11,6 +12,7 @@ import ( "net/url" "os" "os/exec" + "regexp" "runtime" "strings" "time" @@ -305,3 +307,62 @@ func IsTelnetConnected(host string, port string) bool { return true } + +// BuildUrl builds a URL from the given params. +// Play: todo +func BuildUrl(scheme, host, path string, query map[string]string) (string, error) { + if err := validateScheme(scheme); err != nil { + return "", err + } + + if path != "" { + if !hostRegex.MatchString(host) { + return "", fmt.Errorf("invalid host: '%s' is not a valid host", host) + } + } + + parsedUrl := &url.URL{ + Scheme: scheme, + Host: host, + } + + if path == "" { + parsedUrl.Path = "/" + } else if !strings.HasPrefix(path, "/") { + path = "/" + path + } else { + parsedUrl.Path = path + } + + queryParams := parsedUrl.Query() + + for key, value := range query { + queryParams.Add(key, value) + } + + parsedUrl.RawQuery = queryParams.Encode() + + return parsedUrl.String(), nil +} + +// 支持的 Scheme 列表 +var supportedSchemes = map[string]bool{ + "http": true, + "https": true, + "ftp": true, + "file": true, + "mailto": true, + "ws": true, // WebSocket + "wss": true, // WebSocket Secure + "data": true, // Data URL +} + +func validateScheme(scheme string) error { + if _, exists := supportedSchemes[scheme]; !exists { + return fmt.Errorf("invalid scheme: '%s' is not supported", scheme) + } + return nil +} + +var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])(\.[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])*$`) +var pathRegex = regexp.MustCompile(`^\/([a-zA-Z0-9%_-]+(?:\/[a-zA-Z0-9%_-]+)*)$`) diff --git a/netutil/net_test.go b/netutil/net_test.go index 2acea07..5912289 100644 --- a/netutil/net_test.go +++ b/netutil/net_test.go @@ -142,3 +142,63 @@ func TestTelnetConnected(t *testing.T) { result2 := IsTelnetConnected("www.baidu.com", "123") assert.Equal(false, result2) } + +func TestBuildUrl(t *testing.T) { + t.Parallel() + + assert := internal.NewAssert(t, "TestBuildUrl") + + tests := []struct { + scheme string + host string + path string + query map[string]string + want string + wantErr bool + }{ + { + scheme: "http", + host: "www.test.com", + path: "/path/subpath", + query: map[string]string{"a": "1", "b": "2"}, + want: "http://www.test.com/path/subpath?a=1&b=2", + wantErr: false, + }, + { + scheme: "http", + host: "www.test.com", + path: "/simple-path", + query: map[string]string{"a": "1", "b": "2"}, + want: "http://www.test.com/simple-path?a=1&b=2", + wantErr: false, + }, + { + scheme: "https", + host: "www.test. com", + path: "/path", + query: nil, + want: "", + wantErr: true, + }, + { + scheme: "https", + host: "www.test.com", + path: "/path with spaces", + query: nil, + want: "https://www.test.com/path%20with%20spaces", + wantErr: false, + }, + } + + for _, tt := range tests { + got, err := BuildUrl(tt.scheme, tt.host, tt.path, tt.query) + // if (err != nil) != tt.wantErr { + // t.Errorf("BuildUrl() error = %v, wantErr %v", err, tt.wantErr) + // return + // } + + assert.Equal(tt.want, got) + assert.Equal(tt.wantErr, err != nil) + } + +}