validate.RestrictURLSchemas   A
last analyzed

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 callable 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 [net/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 ...func(u *url.URL) error) 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
// RestrictURLSchemas make [URL] validation accepts only the listed schemas.
53
func RestrictURLSchemas(schemas ...string) func(u *url.URL) error {
54
	return func(u *url.URL) error {
55
		for _, schema := range schemas {
56
			if schema == u.Scheme {
57
				return nil
58
			}
59
		}
60
61
		return ErrRestrictedSchema
62
	}
63
}
64
65
// RestrictURLHosts make [URL] validation accepts only the listed hosts.
66
func RestrictURLHosts(hosts ...string) func(u *url.URL) error {
67
	return func(u *url.URL) error {
68
		for _, host := range hosts {
69
			if host == u.Host {
70
				return nil
71
			}
72
		}
73
74
		return ErrRestrictedHost
75
	}
76
}
77
78
// RestrictURLHostByPattern make [URL] validation accepts only a value with a host matching by pattern.
79
func RestrictURLHostByPattern(pattern *regexp.Regexp) func(u *url.URL) error {
80
	return func(u *url.URL) error {
81
		if pattern.MatchString(u.Host) {
82
			return nil
83
		}
84
85
		return ErrRestrictedHost
86
	}
87
}
88
89
// IP validates that a value is a valid IP address (IPv4 or IPv6). You can use a list
90
// of restrictions to additionally check for a restricted range of IPs. For example,
91
// you can deny using private IP addresses using [DenyPrivateIP] function.
92
//
93
// If value is not valid the function will return one of the errors:
94
//   - [ErrInvalid] on invalid IP address;
95
//   - [ErrProhibited] on restricted IP address.
96
func IP(value string, restrictions ...func(ip net.IP) error) error {
97
	return validateIP(value, restrictions...)
98
}
99
100
// IPv4 validates that a value is a valid IPv4 address. You can use a list
101
// of restrictions to additionally check for a restricted range of IPs. For example,
102
// you can deny using private IP addresses using [DenyPrivateIP] function.
103
//
104
// If value is not valid the function will return one of the errors:
105
//   - [ErrInvalid] on invalid IP address or when using IPv6;
106
//   - [ErrProhibited] on restricted IP address.
107
func IPv4(value string, restrictions ...func(ip net.IP) error) error {
108
	err := validateIP(value, restrictions...)
109
	if err != nil {
110
		return err
111
	}
112
	if !strings.Contains(value, ".") || strings.Contains(value, ":") {
113
		return ErrInvalid
114
	}
115
116
	return nil
117
}
118
119
// IPv6 validates that a value is a valid IPv6 address. You can use a list
120
// of restrictions to additionally check for a restricted range of IPs. For example,
121
// you can deny using private IP addresses using [DenyPrivateIP] function.
122
//
123
// If value is not valid the function will return one of the errors:
124
//   - [ErrInvalid] on invalid IP address or when using IPv4;
125
//   - [ErrProhibited] on restricted IP address.
126
func IPv6(value string, restrictions ...func(ip net.IP) error) error {
127
	err := validateIP(value, restrictions...)
128
	if err != nil {
129
		return err
130
	}
131
	if !strings.Contains(value, ":") {
132
		return ErrInvalid
133
	}
134
135
	return nil
136
}
137
138
// DenyPrivateIP denies using of private IPs according to RFC 1918 (IPv4 addresses)
139
// and RFC 4193 (IPv6 addresses).
140
func DenyPrivateIP() func(ip net.IP) error {
141
	return func(ip net.IP) error {
142
		if ip.IsPrivate() {
143
			return ErrProhibited
144
		}
145
146
		return nil
147
	}
148
}
149
150
func validateIP(value string, restrictions ...func(ip net.IP) error) error {
151
	ip := net.ParseIP(value)
152
	if ip == nil {
153
		return ErrInvalid
154
	}
155
	for _, check := range restrictions {
156
		if err := check(ip); err != nil {
157
			return err
158
		}
159
	}
160
161
	return nil
162
}
163
164
const (
165
	urlSchema     = `([a-z]*:)?//`
166
	urlBasicAuth  = `(((?:[\_\.\pL\pN-]|%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%[0-9A-Fa-f]{2})+)@)?`
167
	urlDomainName = `([\pL\pN\pS\-\_\.])+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?)`
168
	ipv4          = `\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`
169
	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})))?::))))\]`
170
	urlHost       = `(` + urlDomainName + `|` + ipv4 + `|` + ipv6 + `)`
171
	urlPort       = `(:[0-9]+)?`
172
	urlPath       = `(?:/(?:[\pL\pN\-._\~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*`
173
	urlQuery      = `(?:\?(?:[\pL\pN\-._\~!$&\'\[\]()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?`
174
	urlFragment   = `(?:\#(?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?`
175
	urlPattern    = `(?i)^` + urlSchema + urlBasicAuth + urlHost + urlPort + urlPath + urlQuery + urlFragment + `$`
176
)
177
178
var urlRegex = regexp.MustCompile(urlPattern)
179