Skip to content

Commit

Permalink
Merge pull request #325 from armanbilge/feature/dns-js
Browse files Browse the repository at this point in the history
Make `Dns` cross-platform, add `reverse` methods
  • Loading branch information
mpilquist authored Oct 7, 2021
2 parents fe22bb9 + 6e494f9 commit 3b94c49
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 44 deletions.
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`.
*/
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

0 comments on commit 3b94c49

Please sign in to comment.