From e029d305e4af6d0571a1910ce02448a3cf928f1c Mon Sep 17 00:00:00 2001 From: Tim Steinbach Date: Thu, 16 Nov 2017 20:23:19 -0500 Subject: [PATCH 1/2] Add IPv4 string validation --- .../scala/eu/timepit/refined/string.scala | 25 ++++++++++++++++++- .../timepit/refined/StringValidateSpec.scala | 12 +++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/modules/core/shared/src/main/scala/eu/timepit/refined/string.scala b/modules/core/shared/src/main/scala/eu/timepit/refined/string.scala index 15f69cde0..d6578d05b 100644 --- a/modules/core/shared/src/main/scala/eu/timepit/refined/string.scala +++ b/modules/core/shared/src/main/scala/eu/timepit/refined/string.scala @@ -15,6 +15,9 @@ object string extends StringValidate with StringInference { /** Predicate that checks if a `String` ends with the suffix `S`. */ final case class EndsWith[S](s: S) + /** Predicate that checks if a `String` is a valid IPv4 */ + final case class IPv4() + /** Predicate that checks if a `String` matches the regular expression `S`. */ final case class MatchesRegex[S](s: S) @@ -40,14 +43,34 @@ object string extends StringValidate with StringInference { final case class XPath() } -private[refined] trait StringValidate { +object Predicates { + object IPv4 { + val regex = "^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$".r.pattern + val maxOctet = 255 + val predicate: String => Boolean = s => { + val matcher = regex.matcher(s) + matcher.find() && matcher.matches() && { + val octet1 = matcher.group(1).toInt + val octet2 = matcher.group(2).toInt + val octet3 = matcher.group(3).toInt + val octet4 = matcher.group(4).toInt + + (octet1 <= maxOctet) && (octet2 <= maxOctet) && (octet3 <= maxOctet) && (octet4 <= maxOctet) + } + } + } +} +private[refined] trait StringValidate { implicit def endsWithValidate[S <: String]( implicit ws: Witness.Aux[S]): Validate.Plain[String, EndsWith[S]] = Validate.fromPredicate(_.endsWith(ws.value), t => s""""$t".endsWith("${ws.value}")""", EndsWith(ws.value)) + implicit def ipv4Validate[S <: String]: Validate.Plain[String, IPv4] = + Validate.fromPredicate(Predicates.IPv4.predicate, t => s"${t} is a valid IPv4", IPv4()) + implicit def matchesRegexValidate[S <: String]( implicit ws: Witness.Aux[S]): Validate.Plain[String, MatchesRegex[S]] = Validate.fromPredicate(_.matches(ws.value), diff --git a/modules/core/shared/src/test/scala/eu/timepit/refined/StringValidateSpec.scala b/modules/core/shared/src/test/scala/eu/timepit/refined/StringValidateSpec.scala index eadc89589..9180efba7 100644 --- a/modules/core/shared/src/test/scala/eu/timepit/refined/StringValidateSpec.scala +++ b/modules/core/shared/src/test/scala/eu/timepit/refined/StringValidateSpec.scala @@ -69,4 +69,16 @@ class StringValidateSpec extends Properties("StringValidate") { property("Uuid.showResult.Failed") = secure { showResult[Uuid]("whops") ?= "Uuid predicate failed: Invalid UUID string: whops" } + + property("IPv4.isValid") = secure { + isValid[IPv4]("10.0.0.1") + } + + property("IPv4.showResult.InvalidOctet") = secure { + showResult[IPv4]("10.0.256.1") ?= "Predicate failed: 10.0.256.1 is a valid IPv4." + } + + property("IPv4.showResult.Failed") = secure { + showResult[IPv4]("::1") ?= "Predicate failed: ::1 is a valid IPv4." + } } From f8e23af006a76f39764d4e8fe02fbfe3369b9bc9 Mon Sep 17 00:00:00 2001 From: Tim Steinbach Date: Thu, 16 Nov 2017 20:23:43 -0500 Subject: [PATCH 2/2] Add types.net for private IPv4 networks --- build.sbt | 6 +- .../scala/eu/timepit/refined/string.scala | 35 ++--- .../scala/eu/timepit/refined/types/net.scala | 56 +++++++ .../eu/timepit/refined/types/NetSpec.scala | 137 ++++++++++++++++++ 4 files changed, 214 insertions(+), 20 deletions(-) create mode 100644 modules/core/shared/src/test/scala/eu/timepit/refined/types/NetSpec.scala diff --git a/build.sbt b/build.sbt index 9e7a315e0..54bd3e0f3 100644 --- a/build.sbt +++ b/build.sbt @@ -264,7 +264,11 @@ lazy val moduleJvmSettings = Def.settings( ProblemFilters.exclude[ReversedMissingMethodProblem]( "eu.timepit.refined.NumericValidate.moduloValidateNat"), ProblemFilters.exclude[MissingClassProblem]("eu.timepit.refined.scalacheck.util.OurMath"), - ProblemFilters.exclude[MissingClassProblem]("eu.timepit.refined.scalacheck.util.OurMath$") + ProblemFilters.exclude[MissingClassProblem]("eu.timepit.refined.scalacheck.util.OurMath$"), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "eu.timepit.refined.StringValidate.ipv4Validate"), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "eu.timepit.refined.types.NetTypes.PrivateNetworks") ) } ) diff --git a/modules/core/shared/src/main/scala/eu/timepit/refined/string.scala b/modules/core/shared/src/main/scala/eu/timepit/refined/string.scala index d6578d05b..77b56851d 100644 --- a/modules/core/shared/src/main/scala/eu/timepit/refined/string.scala +++ b/modules/core/shared/src/main/scala/eu/timepit/refined/string.scala @@ -17,6 +17,21 @@ object string extends StringValidate with StringInference { /** Predicate that checks if a `String` is a valid IPv4 */ final case class IPv4() + object IPv4 { + val regex = "^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$".r.pattern + val maxOctet = 255 + val predicate: String => Boolean = s => { + val matcher = regex.matcher(s) + matcher.find() && matcher.matches() && { + val octet1 = matcher.group(1).toInt + val octet2 = matcher.group(2).toInt + val octet3 = matcher.group(3).toInt + val octet4 = matcher.group(4).toInt + + (octet1 <= maxOctet) && (octet2 <= maxOctet) && (octet3 <= maxOctet) && (octet4 <= maxOctet) + } + } + } /** Predicate that checks if a `String` matches the regular expression `S`. */ final case class MatchesRegex[S](s: S) @@ -43,24 +58,6 @@ object string extends StringValidate with StringInference { final case class XPath() } -object Predicates { - object IPv4 { - val regex = "^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$".r.pattern - val maxOctet = 255 - val predicate: String => Boolean = s => { - val matcher = regex.matcher(s) - matcher.find() && matcher.matches() && { - val octet1 = matcher.group(1).toInt - val octet2 = matcher.group(2).toInt - val octet3 = matcher.group(3).toInt - val octet4 = matcher.group(4).toInt - - (octet1 <= maxOctet) && (octet2 <= maxOctet) && (octet3 <= maxOctet) && (octet4 <= maxOctet) - } - } - } -} - private[refined] trait StringValidate { implicit def endsWithValidate[S <: String]( implicit ws: Witness.Aux[S]): Validate.Plain[String, EndsWith[S]] = @@ -69,7 +66,7 @@ private[refined] trait StringValidate { EndsWith(ws.value)) implicit def ipv4Validate[S <: String]: Validate.Plain[String, IPv4] = - Validate.fromPredicate(Predicates.IPv4.predicate, t => s"${t} is a valid IPv4", IPv4()) + Validate.fromPredicate(IPv4.predicate, t => s"${t} is a valid IPv4", IPv4()) implicit def matchesRegexValidate[S <: String]( implicit ws: Witness.Aux[S]): Validate.Plain[String, MatchesRegex[S]] = diff --git a/modules/core/shared/src/main/scala/eu/timepit/refined/types/net.scala b/modules/core/shared/src/main/scala/eu/timepit/refined/types/net.scala index 185b2c8e0..f392fc248 100644 --- a/modules/core/shared/src/main/scala/eu/timepit/refined/types/net.scala +++ b/modules/core/shared/src/main/scala/eu/timepit/refined/types/net.scala @@ -2,7 +2,9 @@ package eu.timepit.refined.types import eu.timepit.refined.W import eu.timepit.refined.api.Refined +import eu.timepit.refined.boolean.{And, Or} import eu.timepit.refined.numeric.Interval +import eu.timepit.refined.string.{IPv4, MatchesRegex, StartsWith} /** Module for refined types that are related to the Internet protocol suite. */ object net extends NetTypes @@ -23,4 +25,58 @@ trait NetTypes { /** An `Int` in the range from 1024 to 65535 representing a port number. */ type NonSystemPortNumber = Int Refined Interval.Closed[W.`1024`.T, W.`65535`.T] + + import PrivateNetworks._ + + /** A `String` representing a valid IPv4 in the private network 10.0.0.0/8 (RFC1918) */ + type Rfc1918ClassAPrivate = String Refined Rfc1918ClassAPrivateSpec + + /** A `String` representing a valid IPv4 in the private network 172.15.0.0/12 (RFC1918) */ + type Rfc1918ClassBPrivate = String Refined Rfc1918ClassBPrivateSpec + + /** A `String` representing a valid IPv4 in the private network 192.168.0.0/16 (RFC1918) */ + type Rfc1918ClassCPrivate = String Refined Rfc1918ClassCPrivateSpec + + /** A `String` representing a valid IPv4 in a private network according to RFC1918 */ + type Rfc1918Private = String Refined Rfc1918PrivateSpec + + /** A `String` representing a valid IPv4 in the private network 192.0.2.0/24 (RFC5737) */ + type Rfc5737Testnet1 = String Refined Rfc5737Testnet1Spec + + /** A `String` representing a valid IPv4 in the private network 198.51.100.0/24 (RFC5737) */ + type Rfc5737Testnet2 = String Refined Rfc5737Testnet2Spec + + /** A `String` representing a valid IPv4 in the private network 203.0.113.0/24 (RFC5737) */ + type Rfc5737Testnet3 = String Refined Rfc5737Testnet3Spec + + /** A `String` representing a valid IPv4 in a private network according to RFC5737 */ + type Rfc5737Testnet = String Refined Rfc5737TestnetSpec + + /** A `String` representing a valid IPv4 in the local link network 169.254.0.0/16 (RFC3927) */ + type Rfc3927LocalLink = String Refined Rfc3927LocalLinkSpec + + /** A `String` representing a valid IPv4 in the benchmarking network 198.18.0.0/15 (RFC2544) */ + type Rfc2544Benchmark = String Refined Rfc2544BenchmarkSpec + + /** A `String` representing a valid IPv4 in a private network according to RFC1918, RFC5737, RFC3927 or RFC2544 */ + type PrivateNetwork = String Refined Rfc1918PrivateSpec Or + Rfc5737TestnetSpec Or + Rfc3927LocalLinkSpec Or + Rfc2544BenchmarkSpec + + object PrivateNetworks { + type Rfc1918ClassAPrivateSpec = IPv4 And StartsWith[W.`"10."`.T] + type Rfc1918ClassBPrivateSpec = + IPv4 And MatchesRegex[W.`"^172\\\\.(15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31).+"`.T] + type Rfc1918ClassCPrivateSpec = IPv4 And StartsWith[W.`"192.168."`.T] + type Rfc1918PrivateSpec = + Rfc1918ClassAPrivateSpec Or Rfc1918ClassBPrivateSpec Or Rfc1918ClassCPrivateSpec + type Rfc5737Testnet1Spec = IPv4 And StartsWith[W.`"192.0.2."`.T] + type Rfc5737Testnet2Spec = IPv4 And StartsWith[W.`"198.51.100."`.T] + type Rfc5737Testnet3Spec = IPv4 And StartsWith[W.`"203.0.113."`.T] + type Rfc5737TestnetSpec = Rfc5737Testnet1Spec Or Rfc5737Testnet2Spec Or Rfc5737Testnet3Spec + type Rfc3927LocalLinkSpec = IPv4 And StartsWith[W.`"169.254."`.T] + type Rfc2544BenchmarkSpec = + IPv4 And Or[StartsWith[W.`"198.18."`.T], StartsWith[W.`"198.19."`.T]] + } } diff --git a/modules/core/shared/src/test/scala/eu/timepit/refined/types/NetSpec.scala b/modules/core/shared/src/test/scala/eu/timepit/refined/types/NetSpec.scala new file mode 100644 index 000000000..8d0668e0b --- /dev/null +++ b/modules/core/shared/src/test/scala/eu/timepit/refined/types/NetSpec.scala @@ -0,0 +1,137 @@ +package eu.timepit.refined.types + +import eu.timepit.refined.TestUtils._ +import org.scalacheck.Prop._ +import org.scalacheck.Properties + +class NetSpec extends Properties("NetTypes") { + import net.PrivateNetworks._ + + property("Rfc1918ClassAPrivateSpec.before") = secure { + showResult[Rfc1918ClassAPrivateSpec]("9.255.255.255") ?= """Right predicate of (9.255.255.255 is a valid IPv4 && "9.255.255.255".startsWith("10.")) failed: Predicate failed: "9.255.255.255".startsWith("10.").""" + } + property("Rfc1918ClassAPrivateSpec.isValid.first") = secure { + isValid[Rfc1918ClassAPrivateSpec]("10.0.0.0") + } + property("Rfc1918ClassAPrivateSpec.isValid.inside") = secure { + isValid[Rfc1918ClassAPrivateSpec]("10.200.18.255") + } + property("Rfc1918ClassAPrivateSpec.isValid.last") = secure { + isValid[Rfc1918ClassAPrivateSpec]("10.255.255.255") + } + property("Rfc1918ClassAPrivateSpec.after") = secure { + showResult[Rfc1918ClassAPrivateSpec]("11.0.0.0") ?= """Right predicate of (11.0.0.0 is a valid IPv4 && "11.0.0.0".startsWith("10.")) failed: Predicate failed: "11.0.0.0".startsWith("10.").""" + } + + property("Rfc1918ClassBPrivateSpec.before") = secure { + showResult[Rfc1918ClassBPrivateSpec]("172.14.255.255") ?= """Right predicate of (172.14.255.255 is a valid IPv4 && "172.14.255.255".matches("^172\.(15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31).+")) failed: Predicate failed: "172.14.255.255".matches("^172\.(15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31).+").""" + } + property("Rfc1918ClassBPrivateSpec.isValid.first") = secure { + isValid[Rfc1918ClassBPrivateSpec]("172.15.0.0") + } + property("Rfc1918ClassBPrivateSpec.isValid.inside") = secure { + isValid[Rfc1918ClassBPrivateSpec]("172.23.17.172") + } + property("Rfc1918ClassBPrivateSpec.isValid.last") = secure { + isValid[Rfc1918ClassBPrivateSpec]("172.31.255.255") + } + property("Rfc1918ClassBPrivateSpec.after") = secure { + showResult[Rfc1918ClassBPrivateSpec]("172.32.0.0") ?= """Right predicate of (172.32.0.0 is a valid IPv4 && "172.32.0.0".matches("^172\.(15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31).+")) failed: Predicate failed: "172.32.0.0".matches("^172\.(15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31).+").""" + } + + property("Rfc1918ClassCPrivateSpec.before") = secure { + showResult[Rfc1918ClassCPrivateSpec]("192.167.255.255") ?= """Right predicate of (192.167.255.255 is a valid IPv4 && "192.167.255.255".startsWith("192.168.")) failed: Predicate failed: "192.167.255.255".startsWith("192.168.").""" + } + property("Rfc1918ClassCPrivateSpec.isValid.first") = secure { + isValid[Rfc1918ClassCPrivateSpec]("192.168.0.0") + } + property("Rfc1918ClassCPrivateSpec.isValid.inside") = secure { + isValid[Rfc1918ClassCPrivateSpec]("192.168.100.100") + } + property("Rfc1918ClassCPrivateSpec.isValid.last") = secure { + isValid[Rfc1918ClassCPrivateSpec]("192.168.255.255") + } + property("Rfc1918ClassCPrivateSpec.after") = secure { + showResult[Rfc1918ClassCPrivateSpec]("192.169.0.0") ?= """Right predicate of (192.169.0.0 is a valid IPv4 && "192.169.0.0".startsWith("192.168.")) failed: Predicate failed: "192.169.0.0".startsWith("192.168.").""" + } + + property("Rfc5737Testnet1Spec.before") = secure { + showResult[Rfc5737Testnet1Spec]("192.0.1.255") ?= """Right predicate of (192.0.1.255 is a valid IPv4 && "192.0.1.255".startsWith("192.0.2.")) failed: Predicate failed: "192.0.1.255".startsWith("192.0.2.").""" + } + property("Rfc5737Testnet1Spec.isValid.first") = secure { + isValid[Rfc5737Testnet1Spec]("192.0.2.0") + } + property("Rfc5737Testnet1Spec.isValid.inside") = secure { + isValid[Rfc5737Testnet1Spec]("192.0.2.150") + } + property("Rfc5737Testnet1Spec.isValid.last") = secure { + isValid[Rfc5737Testnet1Spec]("192.0.2.255") + } + property("Rfc5737Testnet1Spec.after") = secure { + showResult[Rfc5737Testnet1Spec]("192.0.3.0") ?= """Right predicate of (192.0.3.0 is a valid IPv4 && "192.0.3.0".startsWith("192.0.2.")) failed: Predicate failed: "192.0.3.0".startsWith("192.0.2.").""" + } + + property("Rfc5737Testnet2Spec.before") = secure { + showResult[Rfc5737Testnet2Spec]("198.51.99.255") ?= """Right predicate of (198.51.99.255 is a valid IPv4 && "198.51.99.255".startsWith("198.51.100.")) failed: Predicate failed: "198.51.99.255".startsWith("198.51.100.").""" + } + property("Rfc5737Testnet2Spec.isValid.first") = secure { + isValid[Rfc5737Testnet2Spec]("198.51.100.0") + } + property("Rfc5737Testnet2Spec.isValid.inside") = secure { + isValid[Rfc5737Testnet2Spec]("198.51.100.200") + } + property("Rfc5737Testnet2Spec.isValid.last") = secure { + isValid[Rfc5737Testnet2Spec]("198.51.100.255") + } + property("Rfc5737Testnet2Spec.after") = secure { + showResult[Rfc5737Testnet2Spec]("198.51.101.0") ?= """Right predicate of (198.51.101.0 is a valid IPv4 && "198.51.101.0".startsWith("198.51.100.")) failed: Predicate failed: "198.51.101.0".startsWith("198.51.100.").""" + } + + property("Rfc5737Testnet3Spec.before") = secure { + showResult[Rfc5737Testnet3Spec]("203.0.112.255") ?= """Right predicate of (203.0.112.255 is a valid IPv4 && "203.0.112.255".startsWith("203.0.113.")) failed: Predicate failed: "203.0.112.255".startsWith("203.0.113.").""" + } + property("Rfc5737Testnet3Spec.isValid.first") = secure { + isValid[Rfc5737Testnet3Spec]("203.0.113.0") + } + property("Rfc5737Testnet3Spec.isValid.inside") = secure { + isValid[Rfc5737Testnet3Spec]("203.0.113.123") + } + property("Rfc5737Testnet3Spec.isValid.last") = secure { + isValid[Rfc5737Testnet3Spec]("203.0.113.255") + } + property("Rfc5737Testnet3Spec.after") = secure { + showResult[Rfc5737Testnet3Spec]("203.0.114.0") ?= """Right predicate of (203.0.114.0 is a valid IPv4 && "203.0.114.0".startsWith("203.0.113.")) failed: Predicate failed: "203.0.114.0".startsWith("203.0.113.").""" + } + + property("Rfc3927LocalLinkSpec.before") = secure { + showResult[Rfc3927LocalLinkSpec]("169.253.255.255") ?= """Right predicate of (169.253.255.255 is a valid IPv4 && "169.253.255.255".startsWith("169.254.")) failed: Predicate failed: "169.253.255.255".startsWith("169.254.").""" + } + property("Rfc3927LocalLinkSpec.isValid.first") = secure { + isValid[Rfc3927LocalLinkSpec]("169.254.0.0") + } + property("Rfc3927LocalLinkSpec.isValid.inside") = secure { + isValid[Rfc3927LocalLinkSpec]("169.254.213.65") + } + property("Rfc3927LocalLinkSpec.isValid.last") = secure { + isValid[Rfc3927LocalLinkSpec]("169.254.255.255") + } + property("Rfc3927LocalLinkSpec.after") = secure { + showResult[Rfc3927LocalLinkSpec]("169.255.0.0") ?= """Right predicate of (169.255.0.0 is a valid IPv4 && "169.255.0.0".startsWith("169.254.")) failed: Predicate failed: "169.255.0.0".startsWith("169.254.").""" + } + + property("Rfc2544BenchmarkSpec.before") = secure { + showResult[Rfc2544BenchmarkSpec]("198.17.255.255") ?= """Right predicate of (198.17.255.255 is a valid IPv4 && ("198.17.255.255".startsWith("198.18.") || "198.17.255.255".startsWith("198.19."))) failed: Both predicates of ("198.17.255.255".startsWith("198.18.") || "198.17.255.255".startsWith("198.19.")) failed. Left: Predicate failed: "198.17.255.255".startsWith("198.18."). Right: Predicate failed: "198.17.255.255".startsWith("198.19.").""" + } + property("Rfc2544BenchmarkSpec.isValid.first") = secure { + isValid[Rfc2544BenchmarkSpec]("198.18.0.0") + } + property("Rfc2544BenchmarkSpec.isValid.inside") = secure { + isValid[Rfc2544BenchmarkSpec]("198.19.12.2") + } + property("Rfc2544BenchmarkSpec.isValid.last") = secure { + isValid[Rfc2544BenchmarkSpec]("198.19.255.255") + } + property("Rfc2544BenchmarkSpec.after") = secure { + showResult[Rfc2544BenchmarkSpec]("198.20.0.0") ?= """Right predicate of (198.20.0.0 is a valid IPv4 && ("198.20.0.0".startsWith("198.18.") || "198.20.0.0".startsWith("198.19."))) failed: Both predicates of ("198.20.0.0".startsWith("198.18.") || "198.20.0.0".startsWith("198.19.")) failed. Left: Predicate failed: "198.20.0.0".startsWith("198.18."). Right: Predicate failed: "198.20.0.0".startsWith("198.19.").""" + } +}