Test Failed
Push — main ( 9f7a12...05cfc8 )
by Igor
01:08 queued 13s
created

validate.RestrictURLSchemas   A

Complexity

Conditions 4

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
dl 0
loc 9
rs 10
c 0
b 0
f 0
nop 1
1
package validate
2
3
import (
4
	"errors"
5
	"net"
6
	"net/url"
7
	"regexp"
8
	"strings"
9
)
10
11
var (
12
	ErrRestrictedSchema = errors.New("restricted schema")
13
	ErrRestrictedHost   = errors.New("restricted host")
14
)
15
16
// URL is used to validate that value is a valid URL string. You can use a list of restrictions
17
// to additionally check for a restricted set of URLs. By default, if no restrictions are passed,
18
// the function checks for the http:// and https:// schemas.
19
//
20
// Use the RestrictURLSchemas option to configure the list of expected schemas. If an empty string is passed
21
// as a schema, then URL value may be treated as relative (without schema, e.g. "//example.com").
22
//
23
// Use the RestrictURLHosts or RestrictURLHostByPattern option to configure the list of allowed hosts.
24
//
25
// If value is not a valid URL the function will return one of the errors:
26
//	• parsing error from url.Parse method if value cannot be parsed as a URL;
27
//	• ErrRestrictedSchema if schema is not matching one of the listed schemas;
28
//	• ErrRestrictedHost if host is not matching one of the listed hosts;
29
//	• ErrInvalid if value is not matching the regular expression.
30
func URL(value string, restrictions ...URLRestriction) error {
31
	if len(restrictions) == 0 {
32
		restrictions = append(restrictions, RestrictURLSchemas("http", "https"))
33
	}
34
	u, err := url.Parse(value)
35
	if err != nil {
36
		return err
37
	}
38
39
	for _, check := range restrictions {
40
		if err = check(u); err != nil {
41
			return err
42
		}
43
	}
44
45
	if !urlRegex.MatchString(value) {
46
		return ErrInvalid
47
	}
48
49
	return nil
50
}
51
52
// URLRestriction can be used to limit valid URL values.
53
type URLRestriction func(u *url.URL) error
54
55
// RestrictURLSchemas make URL validation accepts only the listed schemas.
56
func RestrictURLSchemas(schemas ...string) URLRestriction {
57
	return func(u *url.URL) error {
58
		for _, schema := range schemas {
59
			if schema == u.Scheme {
60
				return nil
61
			}
62
		}
63
64
		return ErrRestrictedSchema
65
	}
66
}
67
68
// RestrictURLHosts make URL validation accepts only the listed hosts.
69
func RestrictURLHosts(hosts ...string) URLRestriction {
70
	return func(u *url.URL) error {
71
		for _, host := range hosts {
72
			if host == u.Host {
73
				return nil
74
			}
75
		}
76
77
		return ErrRestrictedHost
78
	}
79
}
80
81
// RestrictURLHostByPattern make URL validation accepts only a value with a host matching by pattern.
82
func RestrictURLHostByPattern(pattern *regexp.Regexp) URLRestriction {
83
	return func(u *url.URL) error {
84
		if pattern.MatchString(u.Host) {
85
			return nil
86
		}
87
88
		return ErrRestrictedHost
89
	}
90
}
91
92
// IP validates that a value is a valid IP address (IPv4 or IPv6). You can use a list
93
// of restrictions to additionally check for a restricted range of IPs. For example,
94
// you can deny using private IP addresses using DenyPrivateIP function.
95
//
96
// If value is not valid the function will return one of the errors:
97
//	• ErrInvalid on invalid IP address;
98
//	• ErrProhibited on restricted IP address.
99
func IP(value string, restrictions ...IPRestriction) error {
100
	return validateIP(value, restrictions...)
101
}
102
103
// IPv4 validates that a value is a valid IPv4 address. You can use a list
104
// of restrictions to additionally check for a restricted range of IPs. For example,
105
// you can deny using private IP addresses using DenyPrivateIP function.
106
//
107
// If value is not valid the function will return one of the errors:
108
//	• ErrInvalid on invalid IP address or when using IPv6;
109
//	• ErrProhibited on restricted IP address.
110
func IPv4(value string, restrictions ...IPRestriction) error {
111
	err := validateIP(value, restrictions...)
112
	if err != nil {
113
		return err
114
	}
115
	if !strings.Contains(value, ".") || strings.Contains(value, ":") {
116
		return ErrInvalid
117
	}
118
119
	return nil
120
}
121
122
// IPv6 validates that a value is a valid IPv6 address. You can use a list
123
// of restrictions to additionally check for a restricted range of IPs. For example,
124
// you can deny using private IP addresses using DenyPrivateIP function.
125
//
126
// If value is not valid the function will return one of the errors:
127
//	• ErrInvalid on invalid IP address or when using IPv4;
128
//	• ErrProhibited on restricted IP address.
129
func IPv6(value string, restrictions ...IPRestriction) error {
130
	err := validateIP(value, restrictions...)
131
	if err != nil {
132
		return err
133
	}
134
	if !strings.Contains(value, ":") {
135
		return ErrInvalid
136
	}
137
138
	return nil
139
}
140
141
// IPRestriction can be used to limit valid IP address values.
142
type IPRestriction func(ip net.IP) bool
143
144
// DenyPrivateIP denies using of private IPs according to RFC 1918 (IPv4 addresses)
145
// and RFC 4193 (IPv6 addresses).
146
func DenyPrivateIP() IPRestriction {
147
	return func(ip net.IP) bool {
148
		return ip.IsPrivate()
149
	}
150
}
151
152
func validateIP(value string, restrictions ...IPRestriction) error {
153
	ip := net.ParseIP(value)
154
	if ip == nil {
155
		return ErrInvalid
156
	}
157
	for _, isProhibited := range restrictions {
158
		if isProhibited(ip) {
159
			return ErrProhibited
160
		}
161
	}
162
163
	return nil
164
}
165
166
const (
167
	urlSchema     = `([a-z]*:)?//`
168
	urlBasicAuth  = `(((?:[\_\.\pL\pN-]|%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%[0-9A-Fa-f]{2})+)@)?`
169
	urlDomainName = `([\pL\pN\pS\-\_\.])+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?)`
170
	ipv4          = `\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`
171
	ipv6          = `\[(?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::))))\]`
172
	urlHost       = `(` + urlDomainName + `|` + ipv4 + `|` + ipv6 + `)`
173
	urlPort       = `(:[0-9]+)?`
174
	urlPath       = `(?:/(?:[\pL\pN\-._\~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*`
175
	urlQuery      = `(?:\?(?:[\pL\pN\-._\~!$&\'\[\]()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?`
176
	urlFragment   = `(?:\#(?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?`
177
	urlPattern    = `(?i)^` + urlSchema + urlBasicAuth + urlHost + urlPort + urlPath + urlQuery + urlFragment + `$`
178
)
179
180
var urlRegex = regexp.MustCompile(urlPattern)
181