Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Dns cross-platform, add reverse methods #325

Merged
merged 4 commits into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ ThisBuild / initialCommands := "import com.comcast.ip4s._"
ThisBuild / fatalWarningsInCI := false

ThisBuild / mimaBinaryIssueFilters ++= Seq(
ProblemFilters.exclude[DirectMissingMethodProblem]("com.comcast.ip4s.Ipv6Address.toInetAddress")
ProblemFilters.exclude[DirectMissingMethodProblem]("com.comcast.ip4s.Ipv6Address.toInetAddress"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.Dns.*") // sealed trait
)

lazy val root = project
Expand All @@ -64,7 +65,9 @@ lazy val testKit = crossProject(JVMPlatform, JSPlatform)
.settings(
libraryDependencies ++= Seq(
"org.scalacheck" %%% "scalacheck" % "1.15.4",
"org.scalameta" %%% "munit-scalacheck" % "0.7.29" % Test
"org.scalameta" %%% "munit-scalacheck" % "0.7.29" % Test,
"org.typelevel" %%% "cats-effect" % "3.2.9" % Test,
"org.typelevel" %%% "munit-cats-effect-3" % "1.0.6" % Test,
)
)
.jvmSettings(
Expand Down Expand Up @@ -107,16 +110,11 @@ lazy val core = crossProject(JVMPlatform, JSPlatform)
)
}
)
.settings(
libraryDependencies += "org.typelevel" %%% "literally" % "1.0.2"
)
.jvmSettings(
libraryDependencies += "org.typelevel" %%% "cats-effect" % "3.2.9"
)
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %%% "literally" % "1.0.2",
"org.typelevel" %%% "cats-core" % "2.6.1",
"org.scalacheck" %%% "scalacheck" % "1.15.4" % Test
"org.typelevel" %%% "cats-effect-kernel" % "3.2.9",
)
)

Expand Down
104 changes: 104 additions & 0 deletions js/src/main/scala/com/comcast/ip4s/DnsPlatform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2018 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.comcast.ip4s

import cats.effect.kernel.Async
import cats.syntax.all._

import scala.scalajs.js
import scala.scalajs.js.|
import scala.scalajs.js.annotation.JSImport

private[ip4s] trait DnsCompanionPlatform {
implicit def forAsync[F[_]](implicit F: Async[F]): Dns[F] = new UnsealedDns[F] {
def resolve(hostname: Hostname): F[IpAddress] =
F.fromPromise(F.delay(dnsPromises.lookup(hostname.toString, LookupOptions(all = false))))
.flatMap { address =>
IpAddress
.fromString(address.asInstanceOf[LookupResult].address)
.liftTo[F](new RuntimeException("Node.js returned invalid IP address"))
}
.adaptError {
case ex @ js.JavaScriptException(error: js.Error) if error.message.contains("ENOTFOUND") =>
new JavaScriptUnknownHostException(hostname.toString, ex)
}

def resolveOption(hostname: Hostname): F[Option[IpAddress]] =
resolve(hostname).map(_.some).recover { case _: UnknownHostException => None }

def resolveAll(hostname: Hostname): F[List[IpAddress]] =
F.fromPromise(F.delay(dnsPromises.lookup(hostname.toString, LookupOptions(all = true))))
.flatMap { addresses =>
addresses
.asInstanceOf[js.Array[LookupResult]]
.toList
.traverse { address =>
IpAddress
.fromString(address.address)
.liftTo[F](new RuntimeException("Node.js returned invalid IP address"))
}
}
.recover {
case js.JavaScriptException(error: js.Error) if error.message.contains("ENOTFOUND") =>
Nil
}

def reverse(address: IpAddress): F[Hostname] =
reverseAllOrError(address).flatMap(_.headOption.liftTo(new UnknownHostException(address.toString)))

def reverseOption(address: IpAddress): F[Option[Hostname]] = reverseAll(address).map(_.headOption)

def reverseAll(address: IpAddress): F[List[Hostname]] =
reverseAllOrError(address).recover { case _: UnknownHostException => Nil }

private def reverseAllOrError(address: IpAddress): F[List[Hostname]] =
F.fromPromise(F.delay(dnsPromises.reverse(address.toString)))
.flatMap { hostnames =>
hostnames.toList.traverse { hostname =>
Hostname
.fromString(hostname)
.liftTo[F](new RuntimeException("Node.js returned invalid hostname"))
}
}
.adaptError {
case ex @ js.JavaScriptException(error: js.Error) if error.message.contains("ENOTFOUND") =>
new JavaScriptUnknownHostException(address.toString, ex)
}

def loopback: F[IpAddress] = resolve(Hostname.fromString("localhost").get)
}
}

@js.native
@JSImport("dns", "promises")
private[ip4s] object dnsPromises extends js.Any {

def lookup(hostname: String, options: LookupOptions): js.Promise[LookupResult | js.Array[LookupResult]] = js.native

def reverse(ip: String): js.Promise[js.Array[String]] = js.native
}

private[ip4s] sealed trait LookupOptions extends js.Object
object LookupOptions {
def apply(all: Boolean): LookupOptions = js.Dynamic.literal(all = all).asInstanceOf[LookupOptions]
}

@js.native
private[ip4s] sealed trait LookupResult extends js.Object {
def address: String = js.native
def family: Int = js.native
}
27 changes: 27 additions & 0 deletions js/src/main/scala/com/comcast/ip4s/UnknownHostException.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2018 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.comcast.ip4s

import java.io.IOException
import scala.scalajs.js
import scala.util.control.NoStackTrace

class UnknownHostException(message: String = null, cause: Throwable = null) extends IOException(message, cause)

private[ip4s] class JavaScriptUnknownHostException(message: String, cause: js.JavaScriptException)
extends UnknownHostException(message, cause)
with NoStackTrace
19 changes: 19 additions & 0 deletions js/src/main/scala/com/comcast/ip4s/ip4splatform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2018 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.comcast

private[comcast] trait ip4splatform
62 changes: 62 additions & 0 deletions jvm/src/main/scala/com/comcast/ip4s/DnsPlatform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2018 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.comcast.ip4s

import cats.effect.kernel.Sync
import cats.syntax.all._

import java.net.InetAddress

private[ip4s] trait DnsCompanionPlatform {
implicit def forSync[F[_]](implicit F: Sync[F]): Dns[F] = new UnsealedDns[F] {
def resolve(hostname: Hostname): F[IpAddress] =
F.blocking {
val addr = InetAddress.getByName(hostname.toString)
IpAddress.fromBytes(addr.getAddress).get
}

def resolveOption(hostname: Hostname): F[Option[IpAddress]] =
resolve(hostname).map(_.some).recover { case _: UnknownHostException => None }

def resolveAll(hostname: Hostname): F[List[IpAddress]] =
F.blocking {
try {
val addrs = InetAddress.getAllByName(hostname.toString)
addrs.toList.flatMap(addr => IpAddress.fromBytes(addr.getAddress))
} catch {
case _: UnknownHostException => Nil
}
}

def reverse(address: IpAddress): F[Hostname] =
F.blocking {
address.toInetAddress.getCanonicalHostName
} flatMap { hn =>
// getCanonicalHostName returns the IP address as a string on failure
Hostname.fromString(hn).liftTo[F](new UnknownHostException(address.toString))
}

def reverseOption(address: IpAddress): F[Option[Hostname]] =
reverse(address).map(_.some).recover { case _: UnknownHostException => None }

def reverseAll(address: IpAddress): F[List[Hostname]] =
reverseOption(address).map(_.toList)

def loopback: F[IpAddress] =
F.blocking(IpAddress.fromInetAddress(InetAddress.getByName(null)))
}
}
21 changes: 21 additions & 0 deletions jvm/src/main/scala/com/comcast/ip4s/ip4splatform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2018 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.comcast

private[comcast] trait ip4splatform {
type UnknownHostException = java.net.UnknownHostException
}
2 changes: 1 addition & 1 deletion shared/src/main/scala-2/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package com.comcast

package object ip4s {
package object ip4s extends ip4splatform {
final implicit class IpLiteralSyntax(val sc: StringContext) extends AnyVal {
def ip(args: Any*): IpAddress = macro Literals.ip.make
def ipv4(args: Any*): Ipv4Address =
Expand Down
2 changes: 1 addition & 1 deletion shared/src/main/scala-3/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@

package com.comcast

package object ip4s
package object ip4s extends ip4splatform
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,15 @@

package com.comcast.ip4s

import cats.effect.Sync
import cats.syntax.all._

import java.net.{InetAddress, UnknownHostException}

/** Capability for an effect `F[_]` which can do DNS lookups.
*
* An instance is available for any effect which has a `Sync` instance.
* An instance is available for any effect which has a `Sync` instance on JVM and `Async` on Node.js.
*/
trait Dns[F[_]] {
sealed trait Dns[F[_]] {

/** Resolves the supplied hostname to an ip address using the platform DNS resolver.
*
* If the hostname cannot be resolved, the effect fails with a `java.net.UnknownHostException`.
* If the hostname cannot be resolved, the effect fails with an `UnknownHostException`.
*/
def resolve(hostname: Hostname): F[IpAddress]

Expand All @@ -45,34 +40,30 @@ trait Dns[F[_]] {
*/
def resolveAll(hostname: Hostname): F[List[IpAddress]]

/** Gets an IP address representing the loopback interface. */
def loopback: F[IpAddress]
}
/** Reverses the supplied address to a hostname using the platform DNS resolver.
*
* If the address cannot be reversed, the effect fails with an `UnknownHostException`.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure about this since its a slight abuse, but I don't see why UnknownHostException couldn't apply here as well.

For precedence, Node.js returns the same ENOTFOUND error for both resolve and reverse.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, JVM instead of returning an error for reverse just returns the IP address as a String ...

*/
def reverse(address: IpAddress): F[Hostname]

object Dns {
def apply[F[_]](implicit F: Dns[F]): F.type = F
/** Reverses the supplied address to a hostname using the platform DNS resolver.
*
* If the address cannot be reversed, a `None` is returned.
*/
def reverseOption(address: IpAddress): F[Option[Hostname]]

implicit def forSync[F[_]](implicit F: Sync[F]): Dns[F] = new Dns[F] {
def resolve(hostname: Hostname): F[IpAddress] =
F.blocking {
val addr = InetAddress.getByName(hostname.toString)
IpAddress.fromBytes(addr.getAddress).get
}
/** Reverses the supplied address to all hostnames known to the platform DNS resolver.
*
* If the address cannot be reversed, an empty list is returned.
*/
def reverseAll(address: IpAddress): F[List[Hostname]]

def resolveOption(hostname: Hostname): F[Option[IpAddress]] =
resolve(hostname).map(Some(_): Option[IpAddress]).recover { case _: UnknownHostException => None }
/** Gets an IP address representing the loopback interface. */
def loopback: F[IpAddress]
}

def resolveAll(hostname: Hostname): F[List[IpAddress]] =
F.blocking {
try {
val addrs = InetAddress.getAllByName(hostname.toString)
addrs.toList.flatMap(addr => IpAddress.fromBytes(addr.getAddress))
} catch {
case _: UnknownHostException => Nil
}
}
private[ip4s] trait UnsealedDns[F[_]] extends Dns[F]

def loopback: F[IpAddress] =
F.blocking(IpAddress.fromInetAddress(InetAddress.getByName(null)))
}
object Dns extends DnsCompanionPlatform {
def apply[F[_]](implicit F: Dns[F]): F.type = F
}
Loading