diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..24295c8 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.POSIX: + +GO=go + +check: + $(GO) vet ./... + gocyclo --over 10 . + staticcheck --checks=all ./cache/ ./errors/ ./log/ ./netutil/ ./stringutil/ + $(GO) test --count 1 --race ./... diff --git a/file/file.go b/file/file.go index 241ba35..a06b639 100644 --- a/file/file.go +++ b/file/file.go @@ -1,4 +1,7 @@ // Package file provides helper functions for working with files +// +// Deprecated: This package is deprecated and will be removed in v0.10.0. Use +// functions and methods from other packages of module golibs. package file import ( @@ -9,6 +12,9 @@ import ( ) // SafeWrite writes data to a temporary file and then renames it to what's specified in path +// +// Deprecated: Use renameio.WriteFile or maybe.WriteFile from module +// github.com/google/renameio. func SafeWrite(path string, data []byte) error { dir := filepath.Dir(path) diff --git a/netutil/addr.go b/netutil/addr.go index 288426a..4c1b5c8 100644 --- a/netutil/addr.go +++ b/netutil/addr.go @@ -2,9 +2,6 @@ // network addresses. // // TODO(a.garipov): Add more examples. -// -// TODO(a.garipov): Add HostPort and IPPort structs with decoding and encoding, -// fmt.Srtinger implementations, etc. package netutil import ( @@ -63,9 +60,9 @@ func CloneURL(u *url.URL) (clone *url.URL) { return &cloneVal } -// IPPortFromAddr returns the IP address and the port from addr. If addr is +// IPAndPortFromAddr returns the IP address and the port from addr. If addr is // neither a *net.TCPAddr nor a *net.UDPAddr, it returns nil and 0. -func IPPortFromAddr(addr net.Addr) (ip net.IP, port int) { +func IPAndPortFromAddr(addr net.Addr) (ip net.IP, port int) { switch addr := addr.(type) { case *net.TCPAddr: return addr.IP, addr.Port @@ -98,11 +95,14 @@ func JoinHostPort(host string, port int) (hostport string) { // ParseIP is a wrapper around net.ParseIP that returns a useful error. // -// Any error returned will have the underlying type of *BadIPError. +// Any error returned will have the underlying type of *AddrError. func ParseIP(s string) (ip net.IP, err error) { ip = net.ParseIP(s) if ip == nil { - return nil, &BadIPError{IP: s} + return nil, &AddrError{ + Kind: AddrKindIP, + Addr: s, + } } return ip, nil @@ -111,16 +111,20 @@ func ParseIP(s string) (ip net.IP, err error) { // ParseIPv4 is a wrapper around net.ParseIP that makes sure that the parsed IP // is an IPv4 address and returns a useful error. // -// Any error returned will have the underlying type of either *BadIPError or -// *BadIPv4Error, +// Any error returned will have the underlying type of either *AddrError. func ParseIPv4(s string) (ip net.IP, err error) { ip, err = ParseIP(s) if err != nil { + err.(*AddrError).Kind = AddrKindIPv4 + return nil, err } if ip = ip.To4(); ip == nil { - return nil, &BadIPv4Error{IP: s} + return nil, &AddrError{ + Kind: AddrKindIPv4, + Addr: s, + } } return ip, nil @@ -168,27 +172,18 @@ func SplitHost(hostport string) (host string, err error) { // ValidateMAC returns an error if hwa is not a valid EUI-48, EUI-64, or // 20-octet InfiniBand link-layer address. // -// Any error returned will have the underlying type of *BadMACError. +// Any error returned will have the underlying type of *AddrError. func ValidateMAC(mac net.HardwareAddr) (err error) { - defer func() { - if err != nil { - err = &BadMACError{ - Err: err, - MAC: mac, - } - } - }() - - const kind = "mac address" + defer makeAddrError(&err, mac.String(), AddrKindMAC) switch l := len(mac); l { case 0: - return &EmptyError{Kind: kind} + return ErrAddrIsEmpty case 6, 8, 20: return nil default: - return &BadLengthError{ - Kind: kind, + return &LengthError{ + Kind: AddrKindMAC, Allowed: []int{6, 8, 20}, Length: l, } @@ -208,26 +203,24 @@ const MaxDomainNameLen = 253 // ValidateDomainNameLabel returns an error if label is not a valid label of // a domain name. An empty label is considered invalid. // -// Any error returned will have the underlying type of *BadLabelError. +// Any error returned will have the underlying type of *AddrError. func ValidateDomainNameLabel(label string) (err error) { - defer func() { - if err != nil { - err = &BadLabelError{Err: err, Label: label} - } - }() - - const kind = "domain name label" + defer makeAddrError(&err, label, AddrKindLabel) l := len(label) if l == 0 { - return &EmptyError{Kind: kind} + return ErrLabelIsEmpty } else if l > MaxDomainLabelLen { - return &TooLongError{Kind: kind, Max: MaxDomainLabelLen} + return &LengthError{ + Kind: AddrKindLabel, + Max: MaxDomainLabelLen, + Length: l, + } } if r := rune(label[0]); !IsValidHostOuterRune(r) { - return &BadRuneError{ - Kind: kind, + return &RuneError{ + Kind: AddrKindLabel, Rune: r, } } else if l == 1 { @@ -236,16 +229,16 @@ func ValidateDomainNameLabel(label string) (err error) { for _, r := range label[1 : l-1] { if !IsValidHostInnerRune(r) { - return &BadRuneError{ - Kind: kind, + return &RuneError{ + Kind: AddrKindLabel, Rune: r, } } } if r := rune(label[l-1]); !IsValidHostOuterRune(r) { - return &BadRuneError{ - Kind: kind, + return &RuneError{ + Kind: AddrKindLabel, Rune: r, } } @@ -258,19 +251,9 @@ func ValidateDomainNameLabel(label string) (err error) { // doesn't validate against two or more hyphens to allow punycode and // internationalized domains. // -// Any error returned will have the underlying type of *BadDomainError. +// Any error returned will have the underlying type of *AddrError. func ValidateDomainName(name string) (err error) { - const kind = "domain name" - - defer func() { - if err != nil { - err = &BadDomainError{ - Err: err, - Kind: kind, - Name: name, - } - } - }() + defer makeAddrError(&err, name, AddrKindName) name, err = idna.ToASCII(name) if err != nil { @@ -279,9 +262,13 @@ func ValidateDomainName(name string) (err error) { l := len(name) if l == 0 { - return &EmptyError{Kind: kind} + return ErrAddrIsEmpty } else if l > MaxDomainNameLen { - return &TooLongError{Kind: kind, Max: MaxDomainNameLen} + return &LengthError{ + Kind: AddrKindName, + Max: MaxDomainNameLen, + Length: l, + } } labels := strings.Split(name, ".") @@ -294,153 +281,3 @@ func ValidateDomainName(name string) (err error) { return nil } - -// fromHexByte converts a single hexadecimal ASCII digit character into an -// integer from 0 to 15. For all other characters it returns 0xff. -func fromHexByte(c byte) (n byte) { - switch { - case c >= '0' && c <= '9': - return c - '0' - case c >= 'a' && c <= 'f': - return c - 'a' + 10 - case c >= 'A' && c <= 'F': - return c - 'A' + 10 - default: - return 0xff - } -} - -// ARPA reverse address domains. -const ( - arpaV4Suffix = ".in-addr.arpa" - arpaV6Suffix = ".ip6.arpa" -) - -// The maximum lengths of the ARPA-formatted reverse addresses. -// -// An example of IPv4 with a maximum length: -// -// 49.91.20.104.in-addr.arpa -// -// An example of IPv6 with a maximum length: -// -// 1.3.b.5.4.1.8.6.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.7.4.6.0.6.2.ip6.arpa -// -const ( - arpaV4MaxIPLen = len("000.000.000.000") - arpaV6MaxIPLen = len("0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0") - - arpaV4MaxLen = arpaV4MaxIPLen + len(arpaV4Suffix) - arpaV6MaxLen = arpaV6MaxIPLen + len(arpaV6Suffix) -) - -// reverseIP inverts the order of bytes in an IP address in-place. -func reverseIP(ip net.IP) { - l := len(ip) - for i := range ip[:l/2] { - ip[i], ip[l-i-1] = ip[l-i-1], ip[i] - } -} - -// ipv6FromReversedAddr parses an IPv6 reverse address. It assumes that arpa is -// a valid domain name. -func ipv6FromReversedAddr(arpa string) (ip net.IP, err error) { - const kind = "arpa domain name" - - ip = make(net.IP, net.IPv6len) - - const addrStep = len("0.0.") - for i := range ip { - // Get the two half-byte and merge them together. Validate the - // dots between them since while arpa is assumed to be a valid - // domain name, those labels can still be invalid on their own. - sIdx := i * addrStep - - c := arpa[sIdx] - lo := fromHexByte(c) - if lo == 0xff { - return nil, &BadRuneError{ - Kind: kind, - Rune: rune(c), - } - } - - c = arpa[sIdx+2] - hi := fromHexByte(c) - if hi == 0xff { - return nil, &BadRuneError{ - Kind: kind, - Rune: rune(c), - } - } - - if arpa[sIdx+1] != '.' || arpa[sIdx+3] != '.' { - return nil, ErrNotAReversedIP - } - - ip[net.IPv6len-i-1] = hi<<4 | lo - } - - return ip, nil -} - -// IPFromReversedAddr tries to convert a full reversed ARPA address to a normal -// IP address. arpa can be domain name or an FQDN. -// -// Any error returned will have the underlying type of *BadDomainError. -func IPFromReversedAddr(arpa string) (ip net.IP, err error) { - const kind = "arpa domain name" - - arpa = strings.TrimSuffix(arpa, ".") - err = ValidateDomainName(arpa) - if err != nil { - bdErr := err.(*BadDomainError) - bdErr.Kind = kind - - return nil, bdErr - } - - defer func() { - if err != nil { - err = &BadDomainError{ - Err: err, - Kind: kind, - Name: arpa, - } - } - }() - - // TODO(a.garipov): Add stringutil.HasSuffixFold and remove this. - arpa = strings.ToLower(arpa) - - if strings.HasSuffix(arpa, arpaV4Suffix) { - ipStr := arpa[:len(arpa)-len(arpaV4Suffix)] - ip, err = ParseIPv4(ipStr) - if err != nil { - return nil, err - } - - reverseIP(ip) - - return ip, nil - } - - if strings.HasSuffix(arpa, arpaV6Suffix) { - if l := len(arpa); l != arpaV6MaxLen { - return nil, &BadLengthError{ - Kind: kind, - Allowed: []int{arpaV6MaxLen}, - Length: l, - } - } - - ip, err = ipv6FromReversedAddr(arpa) - if err != nil { - return nil, err - } - - return ip, nil - } - - return nil, ErrNotAReversedIP -} diff --git a/netutil/addr_example_test.go b/netutil/addr_example_test.go index d48dc1d..d746d3c 100644 --- a/netutil/addr_example_test.go +++ b/netutil/addr_example_test.go @@ -45,7 +45,7 @@ func ExampleParseIPv4() { // // 1.2.3.4 // bad ipv4 address "1234::cdef" - // bad ip address "!!!" + // bad ipv4 address "!!!" } func ExampleSplitHostPort() { diff --git a/netutil/addr_test.go b/netutil/addr_test.go index 822548f..b4f9793 100644 --- a/netutil/addr_test.go +++ b/netutil/addr_test.go @@ -66,7 +66,7 @@ func TestCloneURL(t *testing.T) { assert.NotSame(t, u, clone) } -func TestIPPortFromAddr(t *testing.T) { +func TestIPAndPortFromAddr(t *testing.T) { ip := net.IP{1, 2, 3, 4} testCases := []struct { @@ -98,7 +98,7 @@ func TestIPPortFromAddr(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - gotIP, gotPort := netutil.IPPortFromAddr(tc.in) + gotIP, gotPort := netutil.IPAndPortFromAddr(tc.in) assert.Equal(t, tc.wantIP, gotIP) assert.Equal(t, tc.wantPort, gotPort) }) @@ -132,19 +132,19 @@ func TestValidateMAC(t *testing.T) { }, }, { name: "error_nil", - wantErrMsg: `bad mac address "": mac address is empty`, - wantErrAs: new(*netutil.EmptyError), + wantErrMsg: `bad mac address "": address is empty`, + wantErrAs: new(errors.Error), in: nil, }, { name: "error_empty", - wantErrMsg: `bad mac address "": mac address is empty`, - wantErrAs: new(*netutil.EmptyError), + wantErrMsg: `bad mac address "": address is empty`, + wantErrAs: new(errors.Error), in: net.HardwareAddr{}, }, { name: "error_bad", wantErrMsg: `bad mac address "00:01:02:03": ` + `bad mac address length 4, allowed: [6 8 20]`, - wantErrAs: new(*netutil.BadLengthError), + wantErrAs: new(*netutil.LengthError), in: net.HardwareAddr{0x00, 0x01, 0x02, 0x03}, }} @@ -273,47 +273,49 @@ func TestValidateDomainName(t *testing.T) { }, { name: "empty", in: "", - wantErrAs: new(*netutil.EmptyError), - wantErrMsg: `bad domain name "": domain name is empty`, + wantErrAs: new(errors.Error), + wantErrMsg: `bad domain name "": address is empty`, }, { - name: "bad_symbol", - in: "!!!", - wantErrAs: new(*netutil.BadRuneError), - wantErrMsg: `bad domain name "!!!": bad domain name label "!!!": bad domain name label rune '!'`, + name: "bad_symbol", + in: "!!!", + wantErrAs: new(*netutil.RuneError), + wantErrMsg: `bad domain name "!!!": ` + + `bad domain name label "!!!": bad domain name label rune '!'`, }, { - name: "bad_length", - in: longDomainName, - wantErrAs: new(*netutil.TooLongError), - wantErrMsg: `bad domain name "` + longDomainName + `": domain name is too long, max: 253`, + name: "bad_length", + in: longDomainName, + wantErrAs: new(*netutil.LengthError), + wantErrMsg: `bad domain name "` + longDomainName + `": ` + + `domain name is too long: got 255, max 253`, }, { name: "bad_label_length", in: longLabelDomainName, - wantErrAs: new(*netutil.TooLongError), + wantErrAs: new(*netutil.LengthError), wantErrMsg: `bad domain name "` + longLabelDomainName + `": ` + `bad domain name label "` + longLabel + `": ` + - `domain name label is too long, max: 63`, + `domain name label is too long: got 64, max 63`, }, { name: "bad_label_empty", in: "example..com", - wantErrAs: new(*netutil.EmptyError), + wantErrAs: new(errors.Error), wantErrMsg: `bad domain name "example..com": ` + - `bad domain name label "": domain name label is empty`, + `bad domain name label "": label is empty`, }, { name: "bad_label_first_symbol", in: "example.-aa.com", - wantErrAs: new(*netutil.BadRuneError), + wantErrAs: new(*netutil.RuneError), wantErrMsg: `bad domain name "example.-aa.com": ` + `bad domain name label "-aa": bad domain name label rune '-'`, }, { name: "bad_label_last_symbol", in: "example-.aa.com", - wantErrAs: new(*netutil.BadRuneError), + wantErrAs: new(*netutil.RuneError), wantErrMsg: `bad domain name "example-.aa.com": ` + `bad domain name label "example-": bad domain name label rune '-'`, }, { name: "bad_label_symbol", in: "example.a!!!.com", - wantErrAs: new(*netutil.BadRuneError), + wantErrAs: new(*netutil.RuneError), wantErrMsg: `bad domain name "example.a!!!.com": ` + `bad domain name label "a!!!": bad domain name label rune '!'`, }} @@ -327,162 +329,7 @@ func TestValidateDomainName(t *testing.T) { require.Error(t, err) assert.Equal(t, tc.wantErrMsg, err.Error()) - assert.ErrorAs(t, err, new(*netutil.BadDomainError)) - assert.ErrorAs(t, err, tc.wantErrAs) - } - }) - } -} - -func TestUnreverseAddr(t *testing.T) { - const ( - ipv4Good = `1.0.0.127.in-addr.arpa` - ipv4GoodUppercase = `1.0.0.127.In-Addr.Arpa` - - ipv4Missing = `.0.0.127.in-addr.arpa` - ipv4Char = `1.0.z.127.in-addr.arpa` - ) - - const ( - ipv6Zeroes = `0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0` - ipv6Suffix = ipv6Zeroes + `.ip6.arpa` - - ipv6Good = `4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.` + ipv6Suffix - ipv6GoodUppercase = `4.3.2.1.D.C.B.A.0.0.0.0.0.0.0.0.` + ipv6Suffix - - ipv6CharHi = `4.3.2.1.d.c.b.a.0.z.0.0.0.0.0.0.` + ipv6Suffix - ipv6CharLo = `4.3.2.1.d.c.b.a.z.0.0.0.0.0.0.0.` + ipv6Suffix - ipv6Dots = `4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.` + ipv6Zeroes + `..ip6.arpa` - ipv6Len = `3.2.1.d.c.b.a.z.0.0.0.0.0.0.0.` + ipv6Suffix - ipv6Many = `4.3.2.1.dbc.b.a.0.0.0.0.0.0.0.0.` + ipv6Suffix - ipv6Missing = `.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.` + ipv6Suffix - ipv6Space = `4.3.2.1.d.c.b.a. .0.0.0.0.0.0.0.` + ipv6Suffix - ) - - testCases := []struct { - name string - in string - wantErrMsg string - wantErrAs interface{} - want net.IP - }{{ - name: "good_ipv4", - in: ipv4Good, - wantErrMsg: "", - wantErrAs: nil, - want: net.IP{127, 0, 0, 1}, - }, { - name: "good_ipv4_fqdn", - in: ipv4Good + ".", - wantErrMsg: "", - wantErrAs: nil, - want: net.IP{127, 0, 0, 1}, - }, { - name: "good_ipv4_case", - in: ipv4GoodUppercase, - wantErrMsg: "", - wantErrAs: nil, - want: net.IP{127, 0, 0, 1}, - }, { - name: "bad_ipv4_missing", - in: ipv4Missing, - wantErrMsg: `bad arpa domain name "` + ipv4Missing + `": ` + - `bad domain name label "": domain name label is empty`, - wantErrAs: new(*netutil.EmptyError), - want: nil, - }, { - name: "bad_ipv4_char", - in: ipv4Char, - wantErrMsg: `bad arpa domain name "` + ipv4Char + `": ` + - `bad ip address "1.0.z.127"`, - wantErrAs: new(*netutil.BadIPError), - want: nil, - }, { - name: "good_ipv6", - in: ipv6Good, - wantErrMsg: "", - wantErrAs: nil, - want: net.ParseIP("::abcd:1234"), - }, { - name: "good_ipv6_fqdn", - in: ipv6Good + ".", - wantErrMsg: "", - wantErrAs: nil, - want: net.ParseIP("::abcd:1234"), - }, { - name: "good_ipv6_case", - in: ipv6GoodUppercase, - wantErrMsg: "", - wantErrAs: nil, - want: net.ParseIP("::abcd:1234"), - }, { - name: "bad_ipv6_many", - in: ipv6Many, - wantErrMsg: `bad arpa domain name "` + ipv6Many + `": ` + - `not a full reversed ip address`, - wantErrAs: new(*netutil.BadDomainError), - want: nil, - }, { - name: "bad_ipv6_missing", - in: ipv6Missing, - wantErrMsg: `bad arpa domain name "` + ipv6Missing + `": ` + - `bad domain name label "": domain name label is empty`, - wantErrAs: new(*netutil.EmptyError), - want: nil, - }, { - name: "bad_ipv6_char_lo", - in: ipv6CharLo, - wantErrMsg: `bad arpa domain name "` + ipv6CharLo + `": ` + - `bad arpa domain name rune 'z'`, - wantErrAs: new(*netutil.BadRuneError), - want: nil, - }, { - name: "bad_ipv6_char_hi", - in: ipv6CharHi, - wantErrMsg: `bad arpa domain name "` + ipv6CharHi + `": ` + - `bad arpa domain name rune 'z'`, - wantErrAs: new(*netutil.BadRuneError), - want: nil, - }, { - name: "bad_ipv6_dots", - in: ipv6Dots, - wantErrMsg: `bad arpa domain name "` + ipv6Dots + `": ` + - `bad domain name label "": domain name label is empty`, - wantErrAs: new(*netutil.EmptyError), - want: nil, - }, { - name: "bad_ipv6_len", - in: ipv6Len, - wantErrMsg: `bad arpa domain name "` + ipv6Len + `": ` + - `bad arpa domain name length 70, allowed: [72]`, - wantErrAs: new(*netutil.BadLengthError), - want: nil, - }, { - name: "bad_ipv6_space", - in: ipv6Space, - wantErrMsg: `bad arpa domain name "` + ipv6Space + `": ` + - `bad domain name label " ": bad domain name label rune ' '`, - wantErrAs: new(*netutil.BadRuneError), - want: nil, - }, { - name: "not_a_reversed_ip", - in: "1.2.3.4", - wantErrMsg: `bad arpa domain name "1.2.3.4": not a full reversed ip address`, - wantErrAs: new(errors.Error), - want: nil, - }} - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ip, err := netutil.IPFromReversedAddr(tc.in) - if tc.wantErrMsg == "" { - assert.NoError(t, err) - assert.Equal(t, tc.want.To16(), ip.To16()) - } else { - require.Error(t, err) - - assert.Equal(t, tc.wantErrMsg, err.Error()) - assert.ErrorAs(t, err, new(*netutil.BadDomainError)) + assert.ErrorAs(t, err, new(*netutil.AddrError)) assert.ErrorAs(t, err, tc.wantErrAs) } }) diff --git a/netutil/error.go b/netutil/error.go index ba59956..c16c5f6 100644 --- a/netutil/error.go +++ b/netutil/error.go @@ -2,172 +2,122 @@ package netutil import ( "fmt" - "net" "github.com/AdguardTeam/golibs/errors" ) const ( + // ErrAddrIsEmpty is the underlying error returned from validation + // functions when an address is empty. + ErrAddrIsEmpty errors.Error = "address is empty" + + // ErrLabelIsEmpty is the underlying error returned from validation + // functions when a domain name label is empty. + ErrLabelIsEmpty errors.Error = "label is empty" + // ErrNotAReversedIP is the underlying error returned from validation // functions when a domain name is not a full reversed IP address. ErrNotAReversedIP errors.Error = "not a full reversed ip address" ) -// BadDomainError is the underlying type of errors returned from validation -// functions when a domain name is invalid. -type BadDomainError struct { - // Err is the underlying error. The type of the underlying error is - // one of the following: - // - // *BadIPError - // *BadIPv4Error - // *BadLabelError - // *BadLengthError - // *BadRuneError - // *EmptyError - // *TooLongError - // any error returned by the Punicode validation. - // - // It can also be ErrNotAReversedIP. - Err error - // Kind is either "arpa domain name" or "domain name". - Kind string - // Name is the text of the invalid domain name. - Name string -} - -// Error implements the error interface for *BadDomainError. -func (err *BadDomainError) Error() (msg string) { - return fmt.Sprintf("bad %s %q: %s", err.Kind, err.Name, err.Err) -} - -// Unwrap implements the errors.Wrapper interface for *BadDomainError. It -// returns err.Err. -func (err *BadDomainError) Unwrap() (unwrapped error) { - return err.Err -} +// AddrKind is the kind of address or address part used for error reporting. +type AddrKind string -// BadIPError is the underlying type of errors returned from validation -// functions when an IP address is invalid. -type BadIPError struct { - // IP is the text of the invalid IP address. - IP string -} +// Kinds of addresses for AddrError. +const ( + AddrKindARPA AddrKind = "arpa domain name" + AddrKindHostPort AddrKind = "hostport address" + AddrKindIP AddrKind = "ip address" + AddrKindIPPort AddrKind = "ipport address" + AddrKindIPv4 AddrKind = "ipv4 address" + AddrKindLabel AddrKind = "domain name label" + AddrKindMAC AddrKind = "mac address" + AddrKindName AddrKind = "domain name" +) -// Error implements the error interface for *BadIPError. -func (err *BadIPError) Error() (msg string) { - return fmt.Sprintf("bad ip address %q", err.IP) +// AddrError is the underlying type of errors returned from validation +// functions when a domain name is invalid. +type AddrError struct { + // Err is the underlying error, if any. + Err error + // Kind is the kind of address or address part. + Kind AddrKind + // Addr is the text of the invalid address. + Addr string } -// BadIPv4Error is the underlying type of errors returned from validation -// functions when an IP address is not a valid IPv4 address. -type BadIPv4Error struct { - // IP is the text of the invalid IP address. - IP string -} +// Error implements the error interface for *AddrError. +func (err *AddrError) Error() (msg string) { + if err.Err != nil { + return fmt.Sprintf("bad %s %q: %s", err.Kind, err.Addr, err.Err) + } -// Error implements the error interface for *BadIPv4Error. -func (err *BadIPv4Error) Error() (msg string) { - return fmt.Sprintf("bad ipv4 address %q", err.IP) + return fmt.Sprintf("bad %s %q", err.Kind, err.Addr) } -// BadLabelError is the underlying type of errors returned from validation -// functions when a domain name label is invalid. -type BadLabelError struct { - // Err is the underlying error. The type of the underlying error is - // either *BadRuneError, or *EmptyError, or *TooLongError. - Err error - // Label is the text of the label. - Label string +// Unwrap implements the errors.Wrapper interface for *AddrError. It returns +// err.Err. +func (err *AddrError) Unwrap() (unwrapped error) { + return err.Err } -// Error implements the error interface for *BadLabelError. -func (err *BadLabelError) Error() (msg string) { - return fmt.Sprintf("bad domain name label %q: %s", err.Label, err.Err) -} +// makeAddrError is a deferrable helper for functions that return *AddrError. +// errPtr must be non-nil. Usage example: +// +// defer makeAddrError(&err, addr, AddrKindARPA) +// +func makeAddrError(errPtr *error, addr string, k AddrKind) { + err := *errPtr + if err == nil { + return + } -// Unwrap implements the errors.Wrapper interface for *BadLabelError. It -// returns err.Err. -func (err *BadLabelError) Unwrap() (unwrapped error) { - return err.Err + *errPtr = &AddrError{ + Err: err, + Kind: k, + Addr: addr, + } } -// BadLengthError is the underlying type of errors returned from validation +// LengthError is the underlying type of errors returned from validation // functions when an address or a part of an address has a bad length. -type BadLengthError struct { - // Kind is either "arpa domain name" or "mac address". - Kind string - // Allowed are the allowed lengths for this kind of address. +type LengthError struct { + // Kind is the kind of address or address part. + Kind AddrKind + // Allowed are the allowed lengths for this kind of address. If allowed + // is empty, Max should be non-zero. Allowed []int + // Max is the maximum length for this part or address kind. If Max is + // zero, Allowed should be non-empty. + Max int // Length is the length of the provided address. Length int } -// Error implements the error interface for *BadLengthError. -func (err *BadLengthError) Error() (msg string) { - return fmt.Sprintf("bad %s length %d, allowed: %v", err.Kind, err.Length, err.Allowed) -} +// Error implements the error interface for *LengthError. +func (err *LengthError) Error() (msg string) { + if err.Max > 0 { + return fmt.Sprintf("%s is too long: got %d, max %d", err.Kind, err.Length, err.Max) + } -// BadMACError is the underlying type of errors returned from validation -// functions when a MAC address is invalid. -type BadMACError struct { - // Err is the underlying error. The type of the underlying error is - // either *EmptyError, or *BadLengthError. - Err error - // MAC is the text of the MAC address. - MAC net.HardwareAddr -} - -// Error implements the error interface for *BadMACError. -func (err *BadMACError) Error() (msg string) { - return fmt.Sprintf("bad mac address %q: %s", err.MAC, err.Err) -} + format := "bad %s length %d, allowed: %v" + if len(err.Allowed) == 1 { + return fmt.Sprintf(format, err.Kind, err.Length, err.Allowed[0]) + } -// Unwrap implements the errors.Wrapper interface for *BadMACError. It -// returns err.Err. -func (err *BadMACError) Unwrap() (unwrapped error) { - return err.Err + return fmt.Sprintf(format, err.Kind, err.Length, err.Allowed) } -// BadRuneError is the underlying type of errors returned from validation -// functions when a rune in the address is invalid. -type BadRuneError struct { - // Kind is either "arpa domain name", or "domain name label", or "mac - // address". - Kind string +// RuneError is the underlying type of errors returned from validation functions +// when a rune in the address is invalid. +type RuneError struct { + // Kind is the kind of address or address part. + Kind AddrKind // Rune is the invalid rune. Rune rune } -// Error implements the error interface for *BadRuneError. -func (err *BadRuneError) Error() (msg string) { +// Error implements the error interface for *RuneError. +func (err *RuneError) Error() (msg string) { return fmt.Sprintf("bad %s rune %q", err.Kind, err.Rune) } - -// EmptyError is the underlying type of errors returned from validation -// functions when an address or a part of an address is missing. -type EmptyError struct { - // Kind is either "domain name", or "domain name label", or "mac - // address". - Kind string -} - -// Error implements the error interface for *EmptyError. -func (err *EmptyError) Error() (msg string) { - return fmt.Sprintf("%s is empty", err.Kind) -} - -// TooLongError is the underlying type of errors returned from validation -// functions when an address or a part of an address is too long. -type TooLongError struct { - // Kind is either "domain name", or "domain name label", or "mac - // address". - Kind string - // Max is the maximum length for this part or address kind. - Max int -} - -// Error implements the error interface for *TooLongError. -func (err *TooLongError) Error() (msg string) { - return fmt.Sprintf("%s is too long, max: %d", err.Kind, err.Max) -} diff --git a/netutil/hostport.go b/netutil/hostport.go new file mode 100644 index 0000000..fd23d3e --- /dev/null +++ b/netutil/hostport.go @@ -0,0 +1,78 @@ +package netutil + +// HostPort And Utilities + +// HostPort is a convenient type for addresses that contain a hostname and +// a port, like "example.com:12345", "1.2.3.4:56789", or "[1234::cdef]:12345". +type HostPort struct { + Host string + Port int +} + +// ParseHostPort parses a HostPort from addr. Any error returned will have the +// underlying type of *AddrError. +func ParseHostPort(addr string) (hp *HostPort, err error) { + defer makeAddrError(&err, addr, AddrKindHostPort) + + var host string + var port int + host, port, err = SplitHostPort(addr) + if err != nil { + return nil, err + } + + return &HostPort{ + Host: host, + Port: port, + }, nil +} + +// CloneHostPorts returns a deep copy of hps. +func CloneHostPorts(hps []*HostPort) (clone []*HostPort) { + if hps == nil { + return nil + } + + clone = make([]*HostPort, len(hps)) + for i, hp := range hps { + clone[i] = hp.Clone() + } + + return clone +} + +// Clone returns a clone of hp. +func (hp *HostPort) Clone() (clone *HostPort) { + if hp == nil { + return nil + } + + return &HostPort{ + Host: hp.Host, + Port: hp.Port, + } +} + +// MarshalText implements the encoding.TextMarshaler interface for HostPort. +func (hp HostPort) MarshalText() (b []byte, err error) { + return []byte(hp.String()), nil +} + +// String implements the fmt.Stringer interface for *HostPort. +func (hp HostPort) String() (s string) { + return JoinHostPort(hp.Host, hp.Port) +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for +// *HostPort. Any error returned will have the underlying type of *AddrError. +func (hp *HostPort) UnmarshalText(b []byte) (err error) { + var newHP *HostPort + newHP, err = ParseHostPort(string(b)) + if err != nil { + return err + } + + *hp = *newHP + + return nil +} diff --git a/netutil/hostport_example_test.go b/netutil/hostport_example_test.go new file mode 100644 index 0000000..b49a323 --- /dev/null +++ b/netutil/hostport_example_test.go @@ -0,0 +1,111 @@ +package netutil_test + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/AdguardTeam/golibs/netutil" +) + +func ExampleHostPort_MarshalText() { + resp := struct { + Hosts []netutil.HostPort `json:"hosts"` + }{ + Hosts: []netutil.HostPort{{ + Host: "example.com", + Port: 12345, + }, { + Host: "example.org", + Port: 23456, + }}, + } + + err := json.NewEncoder(os.Stdout).Encode(resp) + if err != nil { + panic(err) + } + + respPtrs := struct { + HostPtrs []*netutil.HostPort `json:"host_ptrs"` + }{ + HostPtrs: []*netutil.HostPort{{ + Host: "example.com", + Port: 12345, + }, { + Host: "example.org", + Port: 23456, + }}, + } + + err = json.NewEncoder(os.Stdout).Encode(respPtrs) + if err != nil { + panic(err) + } + + // Output: + // + // {"hosts":["example.com:12345","example.org:23456"]} + // {"host_ptrs":["example.com:12345","example.org:23456"]} +} + +func ExampleHostPort_String() { + hp := &netutil.HostPort{ + Host: "example.com", + Port: 12345, + } + + fmt.Println(hp) + + hp.Host = "1234::cdef" + fmt.Println(hp) + + hp.Port = 0 + fmt.Println(hp) + + hp.Host = "" + fmt.Println(hp) + + // Output: + // + // example.com:12345 + // [1234::cdef]:12345 + // [1234::cdef]:0 + // :0 +} + +func ExampleHostPort_UnmarshalText() { + resp := &struct { + Hosts []netutil.HostPort `json:"hosts"` + }{} + + r := strings.NewReader(`{"hosts":["example.com:12345","example.org:23456"]}`) + err := json.NewDecoder(r).Decode(resp) + if err != nil { + panic(err) + } + + fmt.Printf("%#v\n", resp.Hosts[0]) + fmt.Printf("%#v\n", resp.Hosts[1]) + + respPtrs := &struct { + HostPtrs []*netutil.HostPort `json:"host_ptrs"` + }{} + + r = strings.NewReader(`{"host_ptrs":["example.com:12345","example.org:23456"]}`) + err = json.NewDecoder(r).Decode(respPtrs) + if err != nil { + panic(err) + } + + fmt.Printf("%#v\n", respPtrs.HostPtrs[0]) + fmt.Printf("%#v\n", respPtrs.HostPtrs[1]) + + // Output: + // + // netutil.HostPort{Host:"example.com", Port:12345} + // netutil.HostPort{Host:"example.org", Port:23456} + // &netutil.HostPort{Host:"example.com", Port:12345} + // &netutil.HostPort{Host:"example.org", Port:23456} +} diff --git a/netutil/hostport_test.go b/netutil/hostport_test.go new file mode 100644 index 0000000..23fe9b9 --- /dev/null +++ b/netutil/hostport_test.go @@ -0,0 +1,30 @@ +package netutil_test + +import ( + "testing" + + "github.com/AdguardTeam/golibs/netutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCloneHostPort(t *testing.T) { + assert.Equal(t, (*netutil.HostPort)(nil), (*netutil.HostPort)(nil).Clone()) + assert.Equal(t, &netutil.HostPort{}, (&netutil.HostPort{}).Clone()) + + hp := &netutil.HostPort{Host: "example.com", Port: 12345} + clone := hp.Clone() + assert.Equal(t, hp, clone) +} + +func TestCloneHostPorts(t *testing.T) { + assert.Equal(t, []*netutil.HostPort(nil), netutil.CloneHostPorts(nil)) + assert.Equal(t, []*netutil.HostPort{}, netutil.CloneHostPorts([]*netutil.HostPort{})) + + hps := []*netutil.HostPort{{Host: "example.com", Port: 12345}} + clone := netutil.CloneHostPorts(hps) + assert.Equal(t, hps, clone) + + require.Len(t, clone, len(hps)) + require.Len(t, clone[0].Host, len(hps[0].Host)) +} diff --git a/netutil/ipport.go b/netutil/ipport.go new file mode 100644 index 0000000..e124134 --- /dev/null +++ b/netutil/ipport.go @@ -0,0 +1,121 @@ +package netutil + +import "net" + +// IPPort And Utilities + +// IPPort is a convenient type for network addresses that contain an IP address +// and a port, like "1.2.3.4:56789" or "[1234::cdef]:12345". +type IPPort struct { + IP net.IP + Port int +} + +// IPPortFromAddr returns an *IPPort from a if its underlying type is either +// *net.TCPAddr or *net.UDPAddr. Otherwise, it returns nil. +func IPPortFromAddr(a net.Addr) (ipp *IPPort) { + ip, port := IPAndPortFromAddr(a) + if ip == nil { + return nil + } + + return &IPPort{ + IP: CloneIP(ip), + Port: port, + } +} + +// ParseIPPort parses an *IPPort from addr. Any error returned will have the +// underlying type of *AddrError. +func ParseIPPort(addr string) (ipp *IPPort, err error) { + defer makeAddrError(&err, addr, AddrKindIPPort) + + var host string + var port int + host, port, err = SplitHostPort(addr) + if err != nil { + return nil, err + } + + var ip net.IP + ip, err = ParseIP(host) + if err != nil { + return nil, err + } + + return &IPPort{ + IP: ip, + Port: port, + }, nil +} + +// CloneIPPorts returns a deep copy of ipps. +func CloneIPPorts(ipps []*IPPort) (clone []*IPPort) { + if ipps == nil { + return nil + } + + clone = make([]*IPPort, len(ipps)) + for i, hp := range ipps { + clone[i] = hp.Clone() + } + + return clone +} + +// Clone returns a clone of ipp. +func (ipp *IPPort) Clone() (clone *IPPort) { + if ipp == nil { + return nil + } + + return &IPPort{ + IP: CloneIP(ipp.IP), + Port: ipp.Port, + } +} + +// MarshalText implements the encoding.TextMarshaler interface for IPPort. +func (ipp IPPort) MarshalText() (b []byte, err error) { + return []byte(ipp.String()), nil +} + +// String implements the fmt.Stringer interface for *IPPort. +func (ipp IPPort) String() (s string) { + var ipStr string + if ipp.IP != nil { + ipStr = ipp.IP.String() + } + + return JoinHostPort(ipStr, ipp.Port) +} + +// TCP returns a *net.TCPAddr with a clone of ipp's IP address and its port. +func (ipp *IPPort) TCP() (a *net.TCPAddr) { + return &net.TCPAddr{ + IP: CloneIP(ipp.IP), + Port: ipp.Port, + } +} + +// UDP returns a *net.UDPAddr with a clone of ipp's IP address and its port. +func (ipp *IPPort) UDP() (a *net.UDPAddr) { + return &net.UDPAddr{ + IP: CloneIP(ipp.IP), + Port: ipp.Port, + } +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for *IPPort. +// Any error returned will have the underlying type of *AddrError. +func (ipp *IPPort) UnmarshalText(b []byte) (err error) { + var newIPP *IPPort + newIPP, err = ParseIPPort(string(b)) + if err != nil { + return err + } + + *ipp = *newIPP + + return nil +} diff --git a/netutil/ipport_example_test.go b/netutil/ipport_example_test.go new file mode 100644 index 0000000..a5ffa5a --- /dev/null +++ b/netutil/ipport_example_test.go @@ -0,0 +1,140 @@ +package netutil_test + +import ( + "encoding/json" + "fmt" + "net" + "os" + "strings" + + "github.com/AdguardTeam/golibs/netutil" +) + +func ExampleIPPort_MarshalText() { + ip4 := net.ParseIP("1.2.3.4") + ip6 := net.ParseIP("1234::cdef") + + resp := struct { + IPs []netutil.IPPort `json:"ips"` + }{ + IPs: []netutil.IPPort{{ + IP: ip4, + Port: 12345, + }, { + IP: ip6, + Port: 23456, + }}, + } + + err := json.NewEncoder(os.Stdout).Encode(resp) + if err != nil { + panic(err) + } + + respPtrs := struct { + IPPtrs []*netutil.IPPort `json:"ip_ptrs"` + }{ + IPPtrs: []*netutil.IPPort{{ + IP: ip4, + Port: 12345, + }, { + IP: ip6, + Port: 23456, + }}, + } + + err = json.NewEncoder(os.Stdout).Encode(respPtrs) + if err != nil { + panic(err) + } + + // Output: + // + // {"ips":["1.2.3.4:12345","[1234::cdef]:23456"]} + // {"ip_ptrs":["1.2.3.4:12345","[1234::cdef]:23456"]} +} + +func ExampleIPPort_String() { + ip4 := net.ParseIP("1.2.3.4") + ip6 := net.ParseIP("1234::cdef") + + ipp := &netutil.IPPort{ + IP: ip4, + Port: 12345, + } + + fmt.Println(ipp) + + ipp.IP = ip6 + fmt.Println(ipp) + + ipp.Port = 0 + fmt.Println(ipp) + + ipp.IP = nil + fmt.Println(ipp) + + // Output: + // + // 1.2.3.4:12345 + // [1234::cdef]:12345 + // [1234::cdef]:0 + // :0 +} + +func ExampleIPPort_TCP() { + ipp := &netutil.IPPort{ + IP: net.IP{1, 2, 3, 4}, + Port: 12345, + } + + fmt.Printf("%#v\n", ipp.TCP()) + + // Output: + // + // &net.TCPAddr{IP:net.IP{0x1, 0x2, 0x3, 0x4}, Port:12345, Zone:""} +} + +func ExampleIPPort_UDP() { + ipp := &netutil.IPPort{ + IP: net.IP{1, 2, 3, 4}, + Port: 12345, + } + + fmt.Printf("%#v\n", ipp.UDP()) + + // Output: + // + // &net.UDPAddr{IP:net.IP{0x1, 0x2, 0x3, 0x4}, Port:12345, Zone:""} +} + +func ExampleIPPort_UnmarshalText() { + resp := &struct { + IPs []netutil.IPPort `json:"ips"` + }{} + + r := strings.NewReader(`{"ips":["1.2.3.4:12345","[1234::cdef]:23456"]}`) + err := json.NewDecoder(r).Decode(resp) + if err != nil { + panic(err) + } + + fmt.Printf("%v\n", resp.IPs) + + respPtrs := &struct { + IPPtrs []*netutil.IPPort `json:"ip_ptrs"` + }{} + + r = strings.NewReader(`{"ip_ptrs":["1.2.3.4:12345","[1234::cdef]:23456"]}`) + err = json.NewDecoder(r).Decode(respPtrs) + if err != nil { + panic(err) + } + + fmt.Printf("%v\n", respPtrs.IPPtrs) + + // Output: + // + // [1.2.3.4:12345 [1234::cdef]:23456] + // [1.2.3.4:12345 [1234::cdef]:23456] +} diff --git a/netutil/ipport_test.go b/netutil/ipport_test.go new file mode 100644 index 0000000..ed04fe9 --- /dev/null +++ b/netutil/ipport_test.go @@ -0,0 +1,72 @@ +package netutil_test + +import ( + "net" + "testing" + + "github.com/AdguardTeam/golibs/netutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIPPortFromAddr(t *testing.T) { + ip4 := net.IP{1, 2, 3, 4} + ipp := &netutil.IPPort{IP: ip4, Port: 12345} + + testCases := []struct { + in net.Addr + wantIPPort *netutil.IPPort + name string + }{{ + in: nil, + wantIPPort: nil, + name: "nil", + }, { + in: &net.TCPAddr{IP: ip4, Port: 12345}, + wantIPPort: ipp, + name: "tcp", + }, { + in: &net.UDPAddr{IP: ip4, Port: 12345}, + wantIPPort: ipp, + name: "udp", + }, { + in: struct{ net.Addr }{}, + wantIPPort: nil, + name: "custom", + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotIPPort := netutil.IPPortFromAddr(tc.in) + assert.Equal(t, tc.wantIPPort, gotIPPort) + }) + } +} + +func TestCloneIPPort(t *testing.T) { + assert.Equal(t, (*netutil.IPPort)(nil), (*netutil.IPPort)(nil).Clone()) + assert.Equal(t, &netutil.IPPort{}, (&netutil.IPPort{}).Clone()) + + ipp := &netutil.IPPort{IP: net.IP{1, 2, 3, 4}, Port: 12345} + clone := ipp.Clone() + assert.Equal(t, ipp, clone) + + require.Len(t, clone.IP, len(ipp.IP)) + + assert.NotSame(t, &ipp.IP[0], &clone.IP[0]) +} + +func TestCloneIPPorts(t *testing.T) { + assert.Equal(t, []*netutil.IPPort(nil), netutil.CloneIPPorts(nil)) + assert.Equal(t, []*netutil.IPPort{}, netutil.CloneIPPorts([]*netutil.IPPort{})) + + ipps := []*netutil.IPPort{{IP: net.IP{1, 2, 3, 4}, Port: 12345}} + clone := netutil.CloneIPPorts(ipps) + assert.Equal(t, ipps, clone) + + require.Len(t, clone, len(ipps)) + require.Len(t, clone[0].IP, len(ipps[0].IP)) + + assert.NotSame(t, &ipps[0], &clone[0]) + assert.NotSame(t, &ipps[0].IP[0], &clone[0].IP[0]) +} diff --git a/netutil/reversed.go b/netutil/reversed.go new file mode 100644 index 0000000..e1b7c0b --- /dev/null +++ b/netutil/reversed.go @@ -0,0 +1,198 @@ +package netutil + +import ( + "net" + "strconv" + "strings" + + "github.com/AdguardTeam/golibs/stringutil" +) + +// Reversed ARPA Addresses + +// fromHexByte converts a single hexadecimal ASCII digit character into an +// integer from 0 to 15. For all other characters it returns 0xff. +func fromHexByte(c byte) (n byte) { + switch { + case c >= '0' && c <= '9': + return c - '0' + case c >= 'a' && c <= 'f': + return c - 'a' + 10 + case c >= 'A' && c <= 'F': + return c - 'A' + 10 + default: + return 0xff + } +} + +// ARPA reverse address domains. +const ( + arpaV4Suffix = ".in-addr.arpa" + arpaV6Suffix = ".ip6.arpa" +) + +// The maximum lengths of the ARPA-formatted reverse addresses. +// +// An example of IPv4 with a maximum length: +// +// 49.91.20.104.in-addr.arpa +// +// An example of IPv6 with a maximum length: +// +// 1.3.b.5.4.1.8.6.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.7.4.6.0.6.2.ip6.arpa +// +const ( + arpaV4MaxIPLen = len("000.000.000.000") + arpaV6MaxIPLen = len("0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0") + + arpaV4MaxLen = arpaV4MaxIPLen + len(arpaV4Suffix) + arpaV6MaxLen = arpaV6MaxIPLen + len(arpaV6Suffix) +) + +// reverseIP inverts the order of bytes in an IP address in-place. +func reverseIP(ip net.IP) { + l := len(ip) + for i := range ip[:l/2] { + ip[i], ip[l-i-1] = ip[l-i-1], ip[i] + } +} + +// ipv6FromReversedAddr parses an IPv6 reverse address. It assumes that arpa is +// a valid domain name. +func ipv6FromReversedAddr(arpa string) (ip net.IP, err error) { + const kind = "arpa domain name" + + ip = make(net.IP, net.IPv6len) + + const addrStep = len("0.0.") + for i := range ip { + // Get the two half-byte and merge them together. Validate the + // dots between them since while arpa is assumed to be a valid + // domain name, those labels can still be invalid on their own. + sIdx := i * addrStep + + c := arpa[sIdx] + lo := fromHexByte(c) + if lo == 0xff { + return nil, &RuneError{ + Kind: kind, + Rune: rune(c), + } + } + + c = arpa[sIdx+2] + hi := fromHexByte(c) + if hi == 0xff { + return nil, &RuneError{ + Kind: kind, + Rune: rune(c), + } + } + + if arpa[sIdx+1] != '.' || arpa[sIdx+3] != '.' { + return nil, ErrNotAReversedIP + } + + ip[net.IPv6len-i-1] = hi<<4 | lo + } + + return ip, nil +} + +// IPFromReversedAddr tries to convert a full reversed ARPA address to a normal +// IP address. arpa can be domain name or an FQDN. +// +// Any error returned will have the underlying type of *AddrError. +func IPFromReversedAddr(arpa string) (ip net.IP, err error) { + arpa = strings.TrimSuffix(arpa, ".") + err = ValidateDomainName(arpa) + if err != nil { + bdErr := err.(*AddrError) + bdErr.Kind = AddrKindARPA + + return nil, bdErr + } + + defer makeAddrError(&err, arpa, AddrKindARPA) + + // TODO(a.garipov): Add stringutil.HasSuffixFold and remove this. + arpa = strings.ToLower(arpa) + + if strings.HasSuffix(arpa, arpaV4Suffix) { + ipStr := arpa[:len(arpa)-len(arpaV4Suffix)] + ip, err = ParseIPv4(ipStr) + if err != nil { + return nil, err + } + + reverseIP(ip) + + return ip, nil + } + + if strings.HasSuffix(arpa, arpaV6Suffix) { + if l := len(arpa); l != arpaV6MaxLen { + return nil, &LengthError{ + Kind: AddrKindARPA, + Allowed: []int{arpaV6MaxLen}, + Length: l, + } + } + + ip, err = ipv6FromReversedAddr(arpa) + if err != nil { + return nil, err + } + + return ip, nil + } + + return nil, ErrNotAReversedIP +} + +// IPToReversedAddr returns the reversed ARPA address of ip suitable for reverse +// DNS (PTR) record lookups. This is a modified version of function ReverseAddr +// from package github.com/miekg/dns package that accepts an IP. +// +// Any error returned will have the underlying type of *AddrError. +func IPToReversedAddr(ip net.IP) (arpa string, err error) { + const dot = "." + + var l int + var suffix string + var writeByte func(val byte) + b := &strings.Builder{} + if ip4 := ip.To4(); ip4 != nil { + l, suffix = arpaV4MaxLen, arpaV4Suffix[1:] + ip = ip4 + writeByte = func(val byte) { + stringutil.WriteToBuilder(b, strconv.Itoa(int(val)), dot) + } + } else if ip6 := ip.To16(); ip6 != nil { + l, suffix = arpaV6MaxLen, arpaV6Suffix[1:] + ip = ip6 + writeByte = func(val byte) { + stringutil.WriteToBuilder( + b, + strconv.FormatUint(uint64(val&0x0f), 16), + dot, + strconv.FormatUint(uint64(val>>4), 16), + dot, + ) + } + } else { + return "", &AddrError{ + Kind: AddrKindIP, + Addr: ip.String(), + } + } + + b.Grow(l) + for i := len(ip) - 1; i >= 0; i-- { + writeByte(ip[i]) + } + + stringutil.WriteToBuilder(b, suffix) + + return b.String(), nil +} diff --git a/netutil/reversed_example_test.go b/netutil/reversed_example_test.go new file mode 100644 index 0000000..6a57586 --- /dev/null +++ b/netutil/reversed_example_test.go @@ -0,0 +1,62 @@ +package netutil_test + +import ( + "fmt" + "net" + + "github.com/AdguardTeam/golibs/netutil" +) + +func ExampleIPFromReversedAddr() { + ip, err := netutil.IPFromReversedAddr("4.3.2.1.in-addr.arpa") + if err != nil { + panic(err) + } + + fmt.Println(ip) + + // Output: + // + // 1.2.3.4 +} + +func ExampleIPFromReversedAddr_ipv6() { + a := `4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa` + ip, err := netutil.IPFromReversedAddr(a) + if err != nil { + panic(err) + } + + fmt.Println(ip) + + // Output: + // + // ::abcd:1234 +} + +func ExampleIPToReversedAddr() { + arpa, err := netutil.IPToReversedAddr(net.IP{1, 2, 3, 4}) + if err != nil { + panic(err) + } + + fmt.Println(arpa) + + // Output: + // + // 4.3.2.1.in-addr.arpa +} + +func ExampleIPToReversedAddr_ipv6() { + ip := net.ParseIP("::abcd:1234") + arpa, err := netutil.IPToReversedAddr(ip) + if err != nil { + panic(err) + } + + fmt.Println(arpa) + + // Output: + // + // 4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa +} diff --git a/netutil/reversed_test.go b/netutil/reversed_test.go new file mode 100644 index 0000000..28b470f --- /dev/null +++ b/netutil/reversed_test.go @@ -0,0 +1,253 @@ +package netutil_test + +import ( + "net" + "testing" + + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/netutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + ipv4RevGood = `1.0.0.127.in-addr.arpa` + ipv4RevGoodUp = `1.0.0.127.In-Addr.Arpa` + + ipv4RevGoodUnspecified = `0.0.0.0.in-addr.arpa` + + ipv4Missing = `.0.0.127.in-addr.arpa` + ipv4Char = `1.0.z.127.in-addr.arpa` +) + +const ( + ipv6RevZeroes = `0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0` + ipv6Suffix = ipv6RevZeroes + `.ip6.arpa` + + ipv6RevGood = `4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.` + ipv6Suffix + ipv6RevGoodUp = `4.3.2.1.D.C.B.A.0.0.0.0.0.0.0.0.` + ipv6Suffix + + ipv6RevGoodUnspecified = `0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.` + ipv6Suffix + + ipv6RevCharHi = `4.3.2.1.d.c.b.a.0.z.0.0.0.0.0.0.` + ipv6Suffix + ipv6RevCharLo = `4.3.2.1.d.c.b.a.z.0.0.0.0.0.0.0.` + ipv6Suffix + ipv6RevDots = `4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.` + ipv6RevZeroes + `..ip6.arpa` + ipv6RevLen = `3.2.1.d.c.b.a.z.0.0.0.0.0.0.0.` + ipv6Suffix + ipv6RevMany = `4.3.2.1.dbc.b.a.0.0.0.0.0.0.0.0.` + ipv6Suffix + ipv6RevMissing = `.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.` + ipv6Suffix + ipv6RevSpace = `4.3.2.1.d.c.b.a. .0.0.0.0.0.0.0.` + ipv6Suffix +) + +func TestUnreverseAddr(t *testing.T) { + ip4 := net.IP{127, 0, 0, 1} + ip6 := net.IP{ + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0xab, 0xcd, 0x12, 0x34, + } + + testCases := []struct { + name string + in string + wantErrMsg string + wantErrAs interface{} + want net.IP + }{{ + name: "good_ipv4", + in: ipv4RevGood, + wantErrMsg: "", + wantErrAs: nil, + want: ip4, + }, { + name: "good_ipv4_fqdn", + in: ipv4RevGood + ".", + wantErrMsg: "", + wantErrAs: nil, + want: ip4, + }, { + name: "good_ipv4_case", + in: ipv4RevGoodUp, + wantErrMsg: "", + wantErrAs: nil, + want: ip4, + }, { + name: "bad_ipv4_missing", + in: ipv4Missing, + wantErrMsg: `bad arpa domain name "` + ipv4Missing + `": ` + + `bad domain name label "": label is empty`, + wantErrAs: new(errors.Error), + want: nil, + }, { + name: "bad_ipv4_char", + in: ipv4Char, + wantErrMsg: `bad arpa domain name "` + ipv4Char + `": ` + + `bad ipv4 address "1.0.z.127"`, + wantErrAs: new(*netutil.AddrError), + want: nil, + }, { + name: "good_ipv6", + in: ipv6RevGood, + wantErrMsg: "", + wantErrAs: nil, + want: ip6, + }, { + name: "good_ipv6_fqdn", + in: ipv6RevGood + ".", + wantErrMsg: "", + wantErrAs: nil, + want: ip6, + }, { + name: "good_ipv6_case", + in: ipv6RevGoodUp, + wantErrMsg: "", + wantErrAs: nil, + want: ip6, + }, { + name: "bad_ipv6_many", + in: ipv6RevMany, + wantErrMsg: `bad arpa domain name "` + ipv6RevMany + `": ` + + `not a full reversed ip address`, + wantErrAs: new(*netutil.AddrError), + want: nil, + }, { + name: "bad_ipv6_missing", + in: ipv6RevMissing, + wantErrMsg: `bad arpa domain name "` + ipv6RevMissing + `": ` + + `bad domain name label "": label is empty`, + wantErrAs: new(errors.Error), + want: nil, + }, { + name: "bad_ipv6_char_lo", + in: ipv6RevCharLo, + wantErrMsg: `bad arpa domain name "` + ipv6RevCharLo + `": ` + + `bad arpa domain name rune 'z'`, + wantErrAs: new(*netutil.RuneError), + want: nil, + }, { + name: "bad_ipv6_char_hi", + in: ipv6RevCharHi, + wantErrMsg: `bad arpa domain name "` + ipv6RevCharHi + `": ` + + `bad arpa domain name rune 'z'`, + wantErrAs: new(*netutil.RuneError), + want: nil, + }, { + name: "bad_ipv6_dots", + in: ipv6RevDots, + wantErrMsg: `bad arpa domain name "` + ipv6RevDots + `": ` + + `bad domain name label "": label is empty`, + wantErrAs: new(errors.Error), + want: nil, + }, { + name: "bad_ipv6_len", + in: ipv6RevLen, + wantErrMsg: `bad arpa domain name "` + ipv6RevLen + `": ` + + `bad arpa domain name length 70, allowed: 72`, + wantErrAs: new(*netutil.LengthError), + want: nil, + }, { + name: "bad_ipv6_space", + in: ipv6RevSpace, + wantErrMsg: `bad arpa domain name "` + ipv6RevSpace + `": ` + + `bad domain name label " ": bad domain name label rune ' '`, + wantErrAs: new(*netutil.RuneError), + want: nil, + }, { + name: "not_a_reversed_ip", + in: "1.2.3.4", + wantErrMsg: `bad arpa domain name "1.2.3.4": not a full reversed ip address`, + wantErrAs: new(errors.Error), + want: nil, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ip, err := netutil.IPFromReversedAddr(tc.in) + if tc.wantErrMsg == "" { + assert.NoError(t, err) + assert.Equal(t, tc.want.To16(), ip.To16()) + } else { + require.Error(t, err) + + assert.Equal(t, tc.wantErrMsg, err.Error()) + assert.ErrorAs(t, err, new(*netutil.AddrError)) + assert.ErrorAs(t, err, tc.wantErrAs) + } + }) + } +} + +func TestIPToReversedAddr(t *testing.T) { + ip4 := net.IP{127, 0, 0, 1} + ip6 := net.IP{ + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0xab, 0xcd, 0x12, 0x34, + } + + testCases := []struct { + name string + want string + wantErrMsg string + wantErrAs interface{} + in net.IP + }{{ + name: "good_ipv4", + want: ipv4RevGood, + wantErrMsg: "", + wantErrAs: nil, + in: ip4, + }, { + name: "good_ipv6", + want: ipv6RevGood, + wantErrMsg: "", + wantErrAs: nil, + in: ip6, + }, { + name: "nil_ip", + want: "", + wantErrMsg: `bad ip address ""`, + wantErrAs: new(*netutil.AddrError), + in: nil, + }, { + name: "empty_ip", + want: "", + wantErrMsg: `bad ip address ""`, + wantErrAs: new(*netutil.AddrError), + in: net.IP{}, + }, { + name: "unspecified_ipv4", + want: ipv4RevGoodUnspecified, + wantErrMsg: "", + wantErrAs: nil, + in: net.IPv4zero, + }, { + name: "unspecified_ipv6", + want: ipv6RevGoodUnspecified, + wantErrMsg: "", + wantErrAs: nil, + in: net.IPv6unspecified, + }, { + name: "wrong_length_ip", + want: "", + wantErrMsg: `bad ip address "?0102030405"`, + wantErrAs: new(*netutil.AddrError), + in: net.IP{1, 2, 3, 4, 5}, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + arpa, err := netutil.IPToReversedAddr(tc.in) + if tc.wantErrMsg == "" { + assert.NoError(t, err) + assert.Equal(t, tc.want, arpa) + } else { + require.Error(t, err) + + assert.Equal(t, tc.wantErrMsg, err.Error()) + assert.ErrorAs(t, err, tc.wantErrAs) + } + }) + } +} diff --git a/utils/utils.go b/utils/utils.go index a9cc553..8898e7a 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,4 +1,7 @@ // Package utils provides simple helper functions that are used in AdGuard projects +// +// Deprecated: This package is deprecated and will be removed in v0.10.0. Use +// functions and methods from other packages of module golibs. package utils import ( @@ -8,10 +11,14 @@ import ( "sync" ) -var hostnameRegexp *regexp.Regexp -var hostnameRegexpLock sync.Mutex // to silence Go race detector +var ( + hostnameRegexp *regexp.Regexp + hostnameRegexpLock sync.Mutex // to silence Go race detector +) // IsValidHostname returns an error if hostname is invalid +// +// Deprecated: Use netutil.ValidateDomainName instead. func IsValidHostname(hostname string) error { hostnameRegexpLock.Lock() if hostnameRegexp == nil {