@@ -5,13 +5,31 @@ package url
55
66import (
77 "errors"
8+ "fmt"
89 "net/url"
910 "regexp"
1011 "strings"
1112
1213 "golang.org/x/net/publicsuffix"
1314)
1415
16+ // alphaNumericRegex validates strings containing only letters, numbers, and hyphens
17+ const alphaNumericRegex = "^[a-zA-Z0-9-]+$"
18+
19+ // hostRegex validates hostnames according to RFC 1123
20+ // Rules:
21+ // - Each label must start and end with alphanumeric characters
22+ // - Middle characters can be alphanumeric or hyphens
23+ // - Multiple labels can be joined with dots
24+ const hostRegex = `^([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])*$`
25+
26+ // pathRegex validates URL paths containing letters, numbers, hyphens, underscores and forward slashes
27+ const pathRegex = "^[a-zA-Z0-9_-]+(?:/[a-zA-Z0-9_-]+)*$"
28+
29+ var alphaNumericRe = regexp .MustCompile (alphaNumericRegex )
30+ var hostRe = regexp .MustCompile (hostRegex )
31+ var pathRe = regexp .MustCompile (pathRegex )
32+
1533// BuildURL constructs a URL by combining a scheme, host, path, and query parameters.
1634//
1735// Parameters:
@@ -64,8 +82,7 @@ func BuildURL(scheme, host, path string, query map[string]string) (string, error
6482 errMessage = append (errMessage , "scheme is required" )
6583 }
6684 if host != "" {
67- re := 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])*$` )
68- if ! re .MatchString (host ) {
85+ if ! hostRe .MatchString (host ) {
6986 errMessage = append (errMessage , "the host is not valid" )
7087 }
7188 }
@@ -74,9 +91,8 @@ func BuildURL(scheme, host, path string, query map[string]string) (string, error
7491 }
7592
7693 if path != "" {
77- re := regexp .MustCompile ("^[a-zA-Z]+(\\ /[a-zA-Z]+)*$" )
78- if ! re .MatchString (path ) {
79- errMessage = append (errMessage , "path is permitted with a-z character and multiple path segments" )
94+ if ! pathRe .MatchString (path ) {
95+ errMessage = append (errMessage , "path is permitted with a-z, 0-9, - and _ characters and multiple path segments" )
8096 }
8197 }
8298
@@ -149,25 +165,43 @@ func BuildURL(scheme, host, path string, query map[string]string) (string, error
149165func AddQueryParams (urlStr string , params map [string ]string ) (string , error ) {
150166 parsedURL , err := url .Parse (urlStr )
151167 if err != nil {
152- return "" , errors . New ("URL could not be parsed" )
168+ return "" , fmt . Errorf ("URL %s could not be parsed. err: %w" , urlStr , err )
153169 }
154170 switch parsedURL .Scheme {
155171 case "http" , "https" , "ws" , "wss" , "ftp" :
156172 default :
157- return "" , errors . New ( "invalid URL scheme" )
173+ return "" , fmt . Errorf ( " URL scheme %s is invalid" , parsedURL . Scheme )
158174 }
159175 queryParams := parsedURL .Query ()
176+
160177 for key , value := range params {
161- re := regexp . MustCompile ( "^[a-zA-Z0-9-]+$" )
162- if ! re . MatchString ( value ) || ! re . MatchString ( key ) || value == "" {
163- return "" , errors . New ( "the query parameter is not valid" )
178+ err = validateKeyValue ( key , value )
179+ if err != nil {
180+ return "" , err
164181 }
165182 queryParams .Add (key , value )
166183 }
167184 parsedURL .RawQuery = queryParams .Encode ()
168185 return parsedURL .String (), nil
169186}
170187
188+ func validateKeyValue (key , value string ) error {
189+ if key == "" {
190+ return errors .New ("query parameter key cannot be empty" )
191+ }
192+ if value == "" {
193+ return fmt .Errorf ("query parameter value for key %s cannot be empty" , key )
194+ }
195+ if ! alphaNumericRe .MatchString (key ) {
196+ return fmt .Errorf ("query parameter key %s must be alphanumeric" , key )
197+ }
198+ if ! alphaNumericRe .MatchString (value ) {
199+ return fmt .Errorf ("query parameter value %s for key %s must be alphanumeric" , value , key )
200+ }
201+
202+ return nil
203+ }
204+
171205// IsValidURL checks whether a given URL string is valid and its scheme matches the allowed list.
172206//
173207// Parameters:
@@ -258,16 +292,16 @@ func IsValidURL(urlStr string, allowedReqSchemes []string) bool {
258292func ExtractDomain (urlStr string ) (string , error ) {
259293 parsedURL , err := url .Parse (urlStr )
260294 if err != nil {
261- return "" , errors . New ("URL could not be parsed" )
295+ return "" , fmt . Errorf ("URL %s could not be parsed. err: %w" , urlStr , err )
262296 }
263297
264298 host , err := publicsuffix .EffectiveTLDPlusOne (parsedURL .Hostname ())
265299 if err != nil {
266- return "" , errors . New ("could not extract public suffix" )
300+ return "" , fmt . Errorf ("could not extract public suffix from host %s. err: %w" , parsedURL . Hostname (), err )
267301 }
268302
269303 if host == "" {
270- return "" , errors .New ("parameter not found " )
304+ return "" , errors .New ("public suffix is empty " )
271305 }
272306
273307 return host , nil
@@ -318,7 +352,7 @@ func GetQueryParam(urlStr, param string) (string, error) {
318352 value , exists := queryParams [param ]
319353
320354 if ! exists || len (value ) == 0 {
321- return "" , errors . New ("parameter not found" )
355+ return "" , fmt . Errorf ("parameter %s not found in URL %s" , param , urlStr )
322356 }
323357
324358 return value [0 ], nil
0 commit comments