From 5e77ddb03f1c29ee5bc0b595d55764d01ee9bfb3 Mon Sep 17 00:00:00 2001 From: "R.I.Pienaar" Date: Tue, 23 Jan 2018 17:25:32 +0100 Subject: [PATCH] (#3) add ipv4, ipv6 and ipaddress validators --- .gitignore | 1 + example_struct_test.go | 2 ++ ipaddress/ipaddress.go | 27 +++++++++++++++++ ipaddress/ipaddress_test.go | 60 +++++++++++++++++++++++++++++++++++++ ipv4/ipv4.go | 27 +++++++++++++++++ ipv4/ipv4_test.go | 60 +++++++++++++++++++++++++++++++++++++ ipv6/ipv6.go | 31 +++++++++++++++++++ ipv6/ipv6_test.go | 60 +++++++++++++++++++++++++++++++++++++ maxlength/maxlength.go | 2 ++ validator.go | 18 +++++++++++ validator_test.go | 37 +++++++++++++++++++++-- 11 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 ipaddress/ipaddress.go create mode 100644 ipaddress/ipaddress_test.go create mode 100644 ipv4/ipv4.go create mode 100644 ipv4/ipv4_test.go create mode 100644 ipv6/ipv6.go create mode 100644 ipv6/ipv6_test.go diff --git a/.gitignore b/.gitignore index 22d0d82..85d346a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ vendor +.DS_Store diff --git a/example_struct_test.go b/example_struct_test.go index af526ae..22e79c3 100644 --- a/example_struct_test.go +++ b/example_struct_test.go @@ -10,6 +10,7 @@ type Request struct { Command string `validate:"shellsafe"` Flags []string `validate:"enum=debug,verbose"` Args string `validate:"maxlength=128"` + AnyIP string `validate:"ipaddress"` // can also check ipv4 and ipv6 } func Example_struct() { @@ -17,6 +18,7 @@ func Example_struct() { Command: "/bin/some/command", Flags: []string{"debug"}, Args: "hello world", + AnyIP: "2a00:1450:4003:807::200e", } ok, err := validator.ValidateStruct(r) diff --git a/ipaddress/ipaddress.go b/ipaddress/ipaddress.go new file mode 100644 index 0000000..3194ce1 --- /dev/null +++ b/ipaddress/ipaddress.go @@ -0,0 +1,27 @@ +package ipaddress + +import ( + "fmt" + "net" + "reflect" +) + +// ValidateString validates that the given string is either an IPv6 or an IPv4 address +func ValidateString(input string) (bool, error) { + ip := net.ParseIP(input) + + if ip == nil { + return false, fmt.Errorf("%s is not an IP address", input) + } + + return true, nil +} + +// ValidateStructField validates a struct field holds either an IPv6 or an IPv4 address +func ValidateStructField(value reflect.Value, tag string) (bool, error) { + if value.Kind() != reflect.String { + return false, fmt.Errorf("only strings can be IPv6 validated") + } + + return ValidateString(value.String()) +} diff --git a/ipaddress/ipaddress_test.go b/ipaddress/ipaddress_test.go new file mode 100644 index 0000000..7f26531 --- /dev/null +++ b/ipaddress/ipaddress_test.go @@ -0,0 +1,60 @@ +package ipaddress + +import ( + "reflect" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestFileContent(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Validator/IPv4") +} + +var _ = Describe("ValidateString", func() { + It("Should match ipv4 addresses correctly", func() { + ok, err := ValidateString("1.2.3.4") + Expect(err).ToNot(HaveOccurred()) + Expect(ok).To(BeTrue()) + + ok, err = ValidateString("2a00:1450:4003:807::200e") + Expect(err).ToNot(HaveOccurred()) + Expect(ok).To(BeTrue()) + + ok, err = ValidateString("foo") + Expect(err).To(MatchError("foo is not an IP address")) + Expect(ok).To(BeFalse()) + }) +}) + +var _ = Describe("ValidateStructField", func() { + type t struct { + IP string `validate:"ipaddress"` + } + + It("Should validate the struct correctly", func() { + st := t{"1.2.3.4"} + + val := reflect.ValueOf(st) + valueField := val.FieldByName("IP") + typeField, _ := val.Type().FieldByName("IP") + + ok, err := ValidateStructField(valueField, typeField.Tag.Get("validate")) + Expect(err).ToNot(HaveOccurred()) + Expect(ok).To(BeTrue()) + + st.IP = "foo" + valueField = reflect.ValueOf(st).FieldByName("IP") + ok, err = ValidateStructField(valueField, typeField.Tag.Get("validate")) + Expect(err).To(MatchError("foo is not an IP address")) + Expect(ok).To(BeFalse()) + + st.IP = "2a00:1450:4003:807::200e" + valueField = reflect.ValueOf(st).FieldByName("IP") + ok, err = ValidateStructField(valueField, typeField.Tag.Get("validate")) + Expect(err).ToNot(HaveOccurred()) + Expect(ok).To(BeTrue()) + }) +}) diff --git a/ipv4/ipv4.go b/ipv4/ipv4.go new file mode 100644 index 0000000..a44967f --- /dev/null +++ b/ipv4/ipv4.go @@ -0,0 +1,27 @@ +package ipv4 + +import ( + "fmt" + "net" + "reflect" +) + +// ValidateString validates that the given string is an IPv4 address +func ValidateString(input string) (bool, error) { + ip := net.ParseIP(input).To4() + + if ip == nil { + return false, fmt.Errorf("%s is not an IPv4 address", input) + } + + return true, nil +} + +// ValidateStructField validates a struct field holds an IPv4 address +func ValidateStructField(value reflect.Value, tag string) (bool, error) { + if value.Kind() != reflect.String { + return false, fmt.Errorf("only strings can be IPv4 validated") + } + + return ValidateString(value.String()) +} diff --git a/ipv4/ipv4_test.go b/ipv4/ipv4_test.go new file mode 100644 index 0000000..a6d1272 --- /dev/null +++ b/ipv4/ipv4_test.go @@ -0,0 +1,60 @@ +package ipv4 + +import ( + "reflect" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestFileContent(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Validator/IPv4") +} + +var _ = Describe("ValidateString", func() { + It("Should match ipv4 addresses correctly", func() { + ok, err := ValidateString("1.2.3.4") + Expect(err).ToNot(HaveOccurred()) + Expect(ok).To(BeTrue()) + + ok, err = ValidateString("foo") + Expect(err).To(MatchError("foo is not an IPv4 address")) + Expect(ok).To(BeFalse()) + + ok, err = ValidateString("2a00:1450:4003:807::200e") + Expect(err).To(MatchError("2a00:1450:4003:807::200e is not an IPv4 address")) + Expect(ok).To(BeFalse()) + }) +}) + +var _ = Describe("ValidateStructField", func() { + type t struct { + IP string `validate:"ipv4"` + } + + It("Should validate the struct correctly", func() { + st := t{"1.2.3.4"} + + val := reflect.ValueOf(st) + valueField := val.FieldByName("IP") + typeField, _ := val.Type().FieldByName("IP") + + ok, err := ValidateStructField(valueField, typeField.Tag.Get("validate")) + Expect(err).ToNot(HaveOccurred()) + Expect(ok).To(BeTrue()) + + st.IP = "foo" + valueField = reflect.ValueOf(st).FieldByName("IP") + ok, err = ValidateStructField(valueField, typeField.Tag.Get("validate")) + Expect(err).To(MatchError("foo is not an IPv4 address")) + Expect(ok).To(BeFalse()) + + st.IP = "2a00:1450:4003:807::200e" + valueField = reflect.ValueOf(st).FieldByName("IP") + ok, err = ValidateStructField(valueField, typeField.Tag.Get("validate")) + Expect(err).To(MatchError("2a00:1450:4003:807::200e is not an IPv4 address")) + Expect(ok).To(BeFalse()) + }) +}) diff --git a/ipv6/ipv6.go b/ipv6/ipv6.go new file mode 100644 index 0000000..6d5a3f7 --- /dev/null +++ b/ipv6/ipv6.go @@ -0,0 +1,31 @@ +package ipv6 + +import ( + "fmt" + "net" + "reflect" +) + +// ValidateString validates that the given string is an IPv4 address +func ValidateString(input string) (bool, error) { + ip := net.ParseIP(input) + + if ip == nil { + return false, fmt.Errorf("%s is not an IPv6 address", input) + } + + if ip.To4() != nil { + return false, fmt.Errorf("%s is not an IPv6 address", input) + } + + return true, nil +} + +// ValidateStructField validates a struct field holds an IPv4 address +func ValidateStructField(value reflect.Value, tag string) (bool, error) { + if value.Kind() != reflect.String { + return false, fmt.Errorf("only strings can be IPv6 validated") + } + + return ValidateString(value.String()) +} diff --git a/ipv6/ipv6_test.go b/ipv6/ipv6_test.go new file mode 100644 index 0000000..a58a547 --- /dev/null +++ b/ipv6/ipv6_test.go @@ -0,0 +1,60 @@ +package ipv6 + +import ( + "reflect" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestFileContent(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Validator/IPv4") +} + +var _ = Describe("ValidateString", func() { + It("Should match ipv4 addresses correctly", func() { + ok, err := ValidateString("1.2.3.4") + Expect(err).To(MatchError("1.2.3.4 is not an IPv6 address")) + Expect(ok).To(BeFalse()) + + ok, err = ValidateString("foo") + Expect(err).To(MatchError("foo is not an IPv6 address")) + Expect(ok).To(BeFalse()) + + ok, err = ValidateString("2a00:1450:4003:807::200e") + Expect(err).ToNot(HaveOccurred()) + Expect(ok).To(BeTrue()) + }) +}) + +var _ = Describe("ValidateStructField", func() { + type t struct { + IP string `validate:"ipv6"` + } + + It("Should validate the struct correctly", func() { + st := t{"1.2.3.4"} + + val := reflect.ValueOf(st) + valueField := val.FieldByName("IP") + typeField, _ := val.Type().FieldByName("IP") + + ok, err := ValidateStructField(valueField, typeField.Tag.Get("validate")) + Expect(err).To(MatchError("1.2.3.4 is not an IPv6 address")) + Expect(ok).To(BeFalse()) + + st.IP = "foo" + valueField = reflect.ValueOf(st).FieldByName("IP") + ok, err = ValidateStructField(valueField, typeField.Tag.Get("validate")) + Expect(err).To(MatchError("foo is not an IPv6 address")) + Expect(ok).To(BeFalse()) + + st.IP = "2a00:1450:4003:807::200e" + valueField = reflect.ValueOf(st).FieldByName("IP") + ok, err = ValidateStructField(valueField, typeField.Tag.Get("validate")) + Expect(err).ToNot(HaveOccurred()) + Expect(ok).To(BeTrue()) + }) +}) diff --git a/maxlength/maxlength.go b/maxlength/maxlength.go index ac3a16d..15c0272 100644 --- a/maxlength/maxlength.go +++ b/maxlength/maxlength.go @@ -7,6 +7,7 @@ import ( "strconv" ) +// ValidateString validates that input is not longer than max func ValidateString(input string, max int) (bool, error) { if len(input) > max { return false, fmt.Errorf("%d characters, max allowed %d", len(input), max) @@ -15,6 +16,7 @@ func ValidateString(input string, max int) (bool, error) { return true, nil } +// ValidateStructField validates value based on the tag in the form maxlength=10 func ValidateStructField(value reflect.Value, tag string) (bool, error) { re := regexp.MustCompile(`^maxlength=(\d+)$`) parts := re.FindStringSubmatch(tag) diff --git a/validator.go b/validator.go index 3d319fd..be97ab2 100644 --- a/validator.go +++ b/validator.go @@ -13,6 +13,9 @@ import ( "strings" "github.com/choria-io/go-validator/enum" + "github.com/choria-io/go-validator/ipaddress" + "github.com/choria-io/go-validator/ipv4" + "github.com/choria-io/go-validator/ipv6" "github.com/choria-io/go-validator/maxlength" "github.com/choria-io/go-validator/shellsafe" ) @@ -84,6 +87,21 @@ func validateStructField(valueField reflect.Value, typeField reflect.StructField return fmt.Errorf("%s shellsafe validation failed: %s", typeField.Name, err) } + } else if validation == "ipv4" { + if ok, err := ipv4.ValidateStructField(valueField, validation); !ok { + return fmt.Errorf("%s IPv4 validation failed: %s", typeField.Name, err) + } + + } else if validation == "ipv6" { + if ok, err := ipv6.ValidateStructField(valueField, validation); !ok { + return fmt.Errorf("%s IPv6 validation failed: %s", typeField.Name, err) + } + + } else if validation == "ipaddress" { + if ok, err := ipaddress.ValidateStructField(valueField, validation); !ok { + return fmt.Errorf("%s IP address validation failed: %s", typeField.Name, err) + } + } else if ok, _ := regexp.MatchString(`^maxlength=\d+$`, validation); ok { if ok, err := maxlength.ValidateStructField(valueField, validation); !ok { return fmt.Errorf("%s maxlength validation failed: %s", typeField.Name, err) diff --git a/validator_test.go b/validator_test.go index 7d7fdf8..691bd53 100644 --- a/validator_test.go +++ b/validator_test.go @@ -22,6 +22,9 @@ type vdata struct { SS string `validate:"shellsafe"` ML string `validate:"maxlength=3"` Enum []string `validate:"enum=one,two"` + IPv4 string `validate:"ipv4"` + IPv6 string `validate:"ipv6"` + IP string `validate:"ipaddress"` nest } @@ -30,7 +33,11 @@ var s vdata var _ = Describe("ValidateStructField", func() { BeforeEach(func() { - s = vdata{} + s = vdata{ + IPv4: "1.2.3.4", + IPv6: "2a00:1450:4003:807::200e", + IP: "1.2.3.4", + } }) It("Should validate a specific field", func() { @@ -49,7 +56,11 @@ var _ = Describe("ValidateStructField", func() { var _ = Describe("ValidateStruct", func() { BeforeEach(func() { - s = vdata{} + s = vdata{ + IPv4: "1.2.3.4", + IPv6: "2a00:1450:4003:807::200e", + IP: "1.2.3.4", + } }) It("Should support nested structs", func() { @@ -88,4 +99,26 @@ var _ = Describe("ValidateStruct", func() { Expect(err).To(MatchError("Enum enum validation failed: 'four' is not in the allowed list: one, two")) Expect(ok).To(BeFalse()) }) + + It("Should support ipv4", func() { + s.IPv4 = "2a00:1450:4003:807::200e" + ok, err := validator.ValidateStruct(s) + Expect(err).To(MatchError("IPv4 IPv4 validation failed: 2a00:1450:4003:807::200e is not an IPv4 address")) + Expect(ok).To(BeFalse()) + }) + + It("Should support ipv6", func() { + s.IPv6 = "1.2.3.4" + ok, err := validator.ValidateStruct(s) + Expect(err).To(MatchError("IPv6 IPv6 validation failed: 1.2.3.4 is not an IPv6 address")) + Expect(ok).To(BeFalse()) + }) + + It("Should support ipaddress", func() { + s.IP = "foo" + ok, err := validator.ValidateStruct(s) + Expect(err).To(MatchError("IP IP address validation failed: foo is not an IP address")) + Expect(ok).To(BeFalse()) + }) + })