diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7977b8fd60..108a689b2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -339,7 +339,7 @@ jobs: name: Test I/O on macOS strategy: matrix: - os: [macos-latest] + os: [macos-14] java: [temurin@17] project: [ioJS, ioJVM, ioNative] runs-on: ${{ matrix.os }} diff --git a/build.sbt b/build.sbt index cc3a017d4a..3399baf2b2 100644 --- a/build.sbt +++ b/build.sbt @@ -34,7 +34,9 @@ ThisBuild / githubWorkflowAddedJobs += scalas = Nil, sbtStepPreamble = Nil, javas = List(githubWorkflowJavaVersions.value.head), - oses = List("macos-latest"), + oses = List( + "macos-14" + ), // FIXME: macos-15 breaks sending multicast to local network - https://github.com/actions/runner-images/issues/10924 matrixAdds = Map("project" -> List("ioJS", "ioJVM", "ioNative")), steps = githubWorkflowJobSetup.value.toList ++ List( WorkflowStep.Run(List("brew install s2n"), cond = Some("matrix.project == 'ioNative'")), @@ -272,6 +274,93 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq( ), ProblemFilters.exclude[MissingTypesProblem]( "fs2.interop.flow.StreamSubscriber$State$WaitingOnUpstream$" + ), + // Network refactor: #3563 + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.connect"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.bind"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.bindAndAccept"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Socket.address"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Socket.peerAddress"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.Socket.address"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.Socket.supportedOptions"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.Socket.getOption"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.Socket.setOption"), + ProblemFilters.exclude[DirectMissingMethodProblem]( + "fs2.io.net.SocketCompanionPlatform#AsyncSocket.this" + ), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.SocketGroup$AbstractAsyncSocketGroup"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.SocketGroupCompanionPlatform"), + ProblemFilters.exclude[MissingClassProblem]( + "fs2.io.net.SocketGroupCompanionPlatform$AsyncSocketGroup" + ), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.tls.TLSSocket.address"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]( + "fs2.io.net.tls.TLSSocket.supportedOptions" + ), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.tls.TLSSocket.getOption"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.tls.TLSSocket.setOption"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.JdkUnixSockets"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.JdkUnixSockets$"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.JdkUnixSocketsImpl"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.JnrUnixSockets"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.JnrUnixSockets$"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.JnrUnixSocketsImpl"), + ProblemFilters.exclude[MissingClassProblem]( + "fs2.io.net.unixsocket.UnixSocketsCompanionPlatform$AsyncSocket" + ), + ProblemFilters.exclude[MissingClassProblem]( + "fs2.io.net.unixsocket.UnixSocketsCompanionPlatform$AsyncUnixSockets" + ), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.io.net.SelectingSocket.apply"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.SelectingSocketGroup"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.io.net.Socket.forAsync"), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "fs2.io.net.SocketOptionCompanionPlatform#Key.get" + ), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]( + "fs2.io.net.Network.openDatagramSocket" + ), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.io.net.FdPollingSocket.apply"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.FdPollingSocketGroup"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.FdPollingUnixSockets"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]( + "fs2.io.net.AsynchronousDatagramSocketGroup#WriterDatagram.remote" + ), + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "fs2.io.net.AsynchronousDatagramSocketGroup#WriterDatagram.this" + ), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.address"), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "fs2.io.net.DatagramSocket.supportedOptions" + ), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.getOption"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.setOption"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.readGen"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.connect"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.disconnect"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.write"), + ProblemFilters.exclude[MissingClassProblem]( + "fs2.io.net.DatagramSocketGroupCompanionPlatform$AsyncDatagramSocketGroup" + ), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.bindDatagramSocket"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.SocketGroup$"), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "fs2.io.net.SocketOptionCompanionPlatform#Key.fs2$io$net$SocketOptionCompanionPlatform$Key$$$outer" + ), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "fs2.io.net.DatagramSocketOption#Key.toSocketOption" + ), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.join"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "fs2.io.net.DatagramSocketOption.multicastInterface" + ), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.dns"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.interfaces"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]( + "fs2.io.net.tls.TLSContext#Builder.fromKeyStoreFile" + ), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]( + "fs2.io.net.tls.TLSContext#Builder.fs2$io$net$tls$TLSContextCompanionPlatform$BuilderPlatform$$$outer" ) ) diff --git a/io/js-jvm/src/main/scala/fs2/io/net/Network.scala b/io/js-jvm/src/main/scala/fs2/io/net/Network.scala deleted file mode 100644 index f50c99fb48..0000000000 --- a/io/js-jvm/src/main/scala/fs2/io/net/Network.scala +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2 -package io -package net - -import fs2.io.net.tls.TLSContext - -/** Provides the ability to work with TCP, UDP, and TLS. - * - * @example {{{ - * import fs2.Stream - * import fs2.io.net.{Datagram, Network} - * - * def send[F[_]: Network](datagram: Datagram): F[Unit] = - * Network[F].openDatagramSocket().use { socket => - * socket.write(packet) - * } - * }}} - * - * In this example, the `F[_]` parameter to `send` requires the `Network` constraint instead - * of requiring the much more powerful `Async` constraint. - * - * The `Network` instance has a set of global resources used for managing sockets. Alternatively, - * use the `socketGroup` and `datagramSocketGroup` operations to manage the lifecycle of underlying - * resources. - * - * An instance of `Network` is available for any effect `F` which has an `Async[F]` instance. - */ -sealed trait Network[F[_]] - extends NetworkPlatform[F] - with SocketGroup[F] - with DatagramSocketGroup[F] { - - /** Returns a builder for `TLSContext[F]` values. - * - * For example, `Network[IO].tlsContext.system` returns a `F[TLSContext[F]]`. - */ - def tlsContext: TLSContext.Builder[F] -} - -object Network extends NetworkCompanionPlatform { - private[fs2] trait UnsealedNetwork[F[_]] extends Network[F] - - def apply[F[_]](implicit F: Network[F]): F.type = F -} diff --git a/io/shared/src/main/scala/fs2/io/net/NetworkLowPriority.scala b/io/js-jvm/src/main/scala/fs2/io/net/NetworkLowPriority.scala similarity index 100% rename from io/shared/src/main/scala/fs2/io/net/NetworkLowPriority.scala rename to io/js-jvm/src/main/scala/fs2/io/net/NetworkLowPriority.scala diff --git a/io/js-jvm/src/test/scala/fs2/io/net/udp/UdpSuite.scala b/io/js-jvm/src/test/scala/fs2/io/net/udp/UdpSuite.scala index 19fd4effc2..25993458a5 100644 --- a/io/js-jvm/src/test/scala/fs2/io/net/udp/UdpSuite.scala +++ b/io/js-jvm/src/test/scala/fs2/io/net/udp/UdpSuite.scala @@ -31,25 +31,29 @@ import com.comcast.ip4s._ import scala.concurrent.duration._ -class UdpSuite extends Fs2Suite with UdpSuitePlatform { - def sendAndReceive(socket: DatagramSocket[IO], toSend: Datagram): IO[Datagram] = +class UdpSuite extends Fs2Suite { + private def sendAndReceive(socket: DatagramSocket[IO], toSend: Datagram): IO[Datagram] = socket .write(toSend) >> socket.read.timeoutTo(1.second, IO.defer(sendAndReceive(socket, toSend))) + private def sendAndReceiveBytes(socket: DatagramSocket[IO], bytes: Chunk[Byte]): IO[Datagram] = + socket + .write(bytes) >> socket.read.timeoutTo(1.second, IO.defer(sendAndReceiveBytes(socket, bytes))) + group("udp") { test("echo one") { val msg = Chunk.array("Hello, world!".getBytes) Stream - .resource(Network[IO].openDatagramSocket()) + .resource(Network[IO].bindDatagramSocket(SocketAddress.Wildcard)) .flatMap { serverSocket => - Stream.eval(serverSocket.localAddress).map(_.port).flatMap { serverPort => - val serverAddress = SocketAddress(ip"127.0.0.1", serverPort) - val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) - val client = Stream.resource(Network[IO].openDatagramSocket()).evalMap { clientSocket => - sendAndReceive(clientSocket, Datagram(serverAddress, msg)) + val serverAddress = SocketAddress(ip"127.0.0.1", serverSocket.address.asIpUnsafe.port) + val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) + val client = + Stream.resource(Network[IO].bindDatagramSocket(SocketAddress.Wildcard)).evalMap { + clientSocket => + sendAndReceive(clientSocket, Datagram(serverAddress, msg)) } - client.concurrently(server) - } + client.concurrently(server) } .compile .lastOrError @@ -67,22 +71,20 @@ class UdpSuite extends Fs2Suite with UdpSuitePlatform { .sorted Stream - .resource(Network[IO].openDatagramSocket()) + .resource(Network[IO].bindDatagramSocket()) .flatMap { serverSocket => - Stream.eval(serverSocket.localAddress).map(_.port).flatMap { serverPort => - val serverAddress = SocketAddress(ip"127.0.0.1", serverPort) - val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) - val client = Stream.resource(Network[IO].openDatagramSocket()).flatMap { clientSocket => - Stream - .emits(msgs.map(msg => Datagram(serverAddress, msg))) - .evalMap(msg => sendAndReceive(clientSocket, msg)) - } - val clients = Stream - .constant(client) - .take(numClients.toLong) - .parJoin(numParallelClients) - clients.concurrently(server) + val serverAddress = SocketAddress(ip"127.0.0.1", serverSocket.address.asIpUnsafe.port) + val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) + val client = Stream.resource(Network[IO].bindDatagramSocket()).flatMap { clientSocket => + Stream + .emits(msgs.map(msg => Datagram(serverAddress, msg))) + .evalMap(msg => sendAndReceive(clientSocket, msg)) } + val clients = Stream + .constant(client) + .take(numClients.toLong) + .parJoin(numParallelClients) + clients.concurrently(server) } .compile .toVector @@ -90,32 +92,67 @@ class UdpSuite extends Fs2Suite with UdpSuitePlatform { .assertEquals(expected) } - test("multicast".ignore) { - // Fails often based on routing table of host machine - val group = mip"232.10.10.10" - val groupJoin = MulticastJoin.asm(group) + test("echo connected") { val msg = Chunk.array("Hello, world!".getBytes) Stream - .resource( - Network[IO].openDatagramSocket( - options = List(DatagramSocketOption.multicastTtl(1)), - protocolFamily = Some(v4ProtocolFamily) - ) - ) + .resource(Network[IO].bindDatagramSocket()) .flatMap { serverSocket => - Stream.eval(serverSocket.localAddress).map(_.port).flatMap { serverPort => - val server = Stream - .exec( - v4Interfaces.traverse_(interface => serverSocket.join(groupJoin, interface)) - ) ++ - serverSocket.reads.foreach(packet => serverSocket.write(packet)) - val client = Stream.resource(Network[IO].openDatagramSocket()).flatMap { clientSocket => - Stream(Datagram(SocketAddress(group.address, serverPort), msg)) - .through(clientSocket.writes) - .drain ++ Stream.eval(clientSocket.read) - } - client.concurrently(server) + val serverAddress = serverSocket.address.asIpUnsafe + val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) + val client = Stream.resource(Network[IO].bindDatagramSocket()).evalMap { clientSocket => + clientSocket.connect(serverAddress) >> sendAndReceiveBytes(clientSocket, msg) } + client.concurrently(server) + } + .compile + .lastOrError + .map(_.bytes) + .assertEquals(msg) + } + + test("multicast") { + val group = mip"239.10.10.10" + val groupJoin = MulticastJoin.asm(group) + val msg = Chunk.array("Hello, world!".getBytes) + val outgoingInterface = + // Get first non-loopback interface with an IPv4 address + Network[IO].interfaces.getAll.map { interfaces => + interfaces.values + .filterNot(_.isLoopback) + .flatMap(iface => + iface.addresses.filter(_.address.fold(_ => true, _ => false)).as(iface) + ) + .head + } + Stream + .eval(outgoingInterface) + .flatMap { out => + Stream + .resource( + Network[IO] + .bindDatagramSocket( + options = List(SocketOption.multicastTtl(1), SocketOption.multicastInterface(out)) + ) + .evalMap { serverSocket => + Network[IO].interfaces.getAll.flatMap { interfaces => + interfaces.values.toList + .filter(iface => + iface.addresses.exists(_.address.fold(_ => true, _ => false)) + ) + .traverse_(iface => serverSocket.join(groupJoin, iface)) + .as(serverSocket) + } + } + ) + .flatMap { serverSocket => + val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) + val client = + Stream.resource(Network[IO].bindDatagramSocket()).flatMap { clientSocket => + val to = SocketAddress(group.address, serverSocket.address.asIpUnsafe.port) + Stream.eval(clientSocket.write(msg, to) >> clientSocket.read) + } + client.concurrently(server) + } } .compile .lastOrError diff --git a/io/js/src/main/scala/fs2/io/internal/facade/dgram.scala b/io/js/src/main/scala/fs2/io/internal/facade/dgram.scala index 8b5875b123..7ee4299ca4 100644 --- a/io/js/src/main/scala/fs2/io/internal/facade/dgram.scala +++ b/io/js/src/main/scala/fs2/io/internal/facade/dgram.scala @@ -45,6 +45,11 @@ private[io] object dgram { def bind(options: BindOptions, cb: js.Function0[Unit]): Unit = js.native + def connect(port: Int, address: String, cb: js.Function1[js.UndefOr[js.Error], Unit]): Unit = + js.native + + def disconnect(): Unit = js.native + def addMembership(multicastAddress: String, multicastInterface: String): Unit = js.native def dropMembership(multicastAddress: String, multicastInterface: String): Unit = js.native @@ -63,6 +68,9 @@ private[io] object dgram { def close(cb: js.Function0[Unit]): Unit = js.native + def send(msg: Uint8Array, cb: js.Function1[js.Error, Unit]): Unit = + js.native + def send(msg: Uint8Array, port: Int, address: String, cb: js.Function1[js.Error, Unit]): Unit = js.native @@ -74,8 +82,12 @@ private[io] object dgram { def setMulticastTTL(ttl: Int): Unit = js.native + def getRecvBufferSize: Int = js.native + def setRecvBufferSize(size: Int): Unit = js.native + def getSendBufferSize: Int = js.native + def setSendBufferSize(size: Int): Unit = js.native def setTTL(ttl: Int): Unit = js.native @@ -85,7 +97,7 @@ private[io] object dgram { @js.native trait AddressInfo extends js.Object { def address: String = js.native - def family: Int = js.native + def family: String = js.native def port: Int = js.native } @@ -97,7 +109,7 @@ private[io] object dgram { @js.native trait RemoteInfo extends js.Object { def address: String = js.native - def family: Int = js.native + def family: String = js.native def port: Int = js.native def size: Int = js.native } diff --git a/io/js/src/main/scala/fs2/io/internal/facade/net.scala b/io/js/src/main/scala/fs2/io/internal/facade/net.scala index 3df32775b7..cbb3ce3724 100644 --- a/io/js/src/main/scala/fs2/io/internal/facade/net.scala +++ b/io/js/src/main/scala/fs2/io/internal/facade/net.scala @@ -41,7 +41,7 @@ private[io] object net { @js.native trait Server extends EventEmitter { - def address(): ServerAddress = js.native + def address(): js.UndefOr[ServerAddress] = js.native def listening: Boolean = js.native @@ -110,6 +110,7 @@ private[io] object net { def setTimeout(timeout: Double): Socket = js.native + def timeout: Double = js.native } } diff --git a/io/js/src/main/scala/fs2/io/internal/facade/os.scala b/io/js/src/main/scala/fs2/io/internal/facade/os.scala index 63c5bd0176..e1cb0241ac 100644 --- a/io/js/src/main/scala/fs2/io/internal/facade/os.scala +++ b/io/js/src/main/scala/fs2/io/internal/facade/os.scala @@ -38,17 +38,7 @@ private[io] object os { @JSImport("os", "type") def `type`(): String = js.native - @js.native - @JSImport("os", "networkInterfaces") - def networkInterfaces(): js.Dictionary[js.Array[NetworkInterfaceInfo]] = js.native - @js.native @JSImport("os", "EOL") def EOL: String = js.native - - @js.native - trait NetworkInterfaceInfo extends js.Object { - def family: String = js.native - } - } diff --git a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala new file mode 100644 index 0000000000..4dba59153b --- /dev/null +++ b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.{Async, Resource} +import cats.effect.std.Dispatcher +import cats.effect.syntax.all._ +import cats.syntax.all._ +import com.comcast.ip4s.{Dns, Host, IpAddress, Port, SocketAddress, UnixSocketAddress} +import fs2.concurrent.Channel +import fs2.io.file.Files +import fs2.io.internal.facade + +import scala.scalajs.js + +private[net] final class AsyncSocketsProvider[F[_]](implicit F: Async[F]) + extends IpSocketsProvider[F] + with UnixSocketsProvider[F] + with IpDatagramSocketsProvider[F] + with UnixDatagramSocketsProvider[F] { + + implicit private val dnsInstance: Dns[F] = Dns.forAsync[F] + implicit protected val filesInstance: Files[F] = Files.forAsync[F] + + override def connectIp( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] = + connectIpOrUnix(Left(address), options) + + override def bindIp( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = + bindIpOrUnix(Left(address), options) + + override def connectUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] = + connectIpOrUnix(Right(address), options) + + override def bindUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = { + val (filteredOptions, delete) = SocketOption.extractUnixSocketDeletes(options, address) + delete *> bindIpOrUnix(Right(address), filteredOptions) + } + + override def bindDatagramSocket( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] = + bindDatagramSocketIpOrUnix(Left(address), options) + + override def bindDatagramSocket( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] = { + val (filteredOptions, delete) = SocketOption.extractUnixSocketDeletes(options, address) + delete *> bindDatagramSocketIpOrUnix(Right(address), filteredOptions) + } + + private def setSocketOptions(options: List[SocketOption])(socket: facade.net.Socket): F[Unit] = + options.traverse_(option => option.key.set(socket, option.value)) + + protected def connectIpOrUnix( + to: Either[SocketAddress[Host], UnixSocketAddress], + options: List[SocketOption] + ): Resource[F, Socket[F]] = + (for { + sock <- Resource + .make( + F.delay( + new facade.net.Socket(new facade.net.SocketOptions { allowHalfOpen = true }) + ) + )(sock => + F.delay { + if (!sock.destroyed) + sock.destroy() + } + ) + .evalTap(setSocketOptions(options)) + + _ <- F + .async[Unit] { cb => + sock + .registerOneTimeListener[F, js.Error]("error") { error => + cb(Left(js.JavaScriptException(error))) + } <* F.delay { + to match { + case Left(addr) => + sock.connect(addr.port.value, addr.host.toString, () => cb(Right(()))) + case Right(addr) => + sock.connect(addr.path, () => cb(Right(()))) + } + } + } + .toResource + + address = to match { + case Left(_) => + SocketAddress( + IpAddress.fromString(sock.localAddress.get).get, + Port.fromInt(sock.localPort.get).get + ) + case Right(_) => UnixSocketAddress("") + } + peerAddress = to match { + case Left(_) => + SocketAddress( + IpAddress.fromString(sock.remoteAddress.get).get, + Port.fromInt(sock.remotePort.get).get + ) + case Right(addr) => addr + } + socket <- Socket.forAsync(sock, address, peerAddress) + } yield socket).adaptError { case IOException(ex) => ex } + + protected def bindIpOrUnix( + address: Either[SocketAddress[Host], UnixSocketAddress], + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = + (for { + dispatcher <- Dispatcher.sequential[F] + channel <- Channel.unbounded[F, facade.net.Socket].toResource + server <- Resource.make( + F + .delay( + facade.net.createServer( + new facade.net.ServerOptions { + pauseOnConnect = true + allowHalfOpen = true + }, + sock => dispatcher.unsafeRunAndForget(channel.send(sock)) + ) + ) + )(server => + F.async[Unit] { cb => + if (server.listening) + F.delay(server.close(e => cb(e.toLeft(()).leftMap(js.JavaScriptException)))) *> + channel.close.as(None) + else + F.delay(cb(Right(()))).as(None) + } + ) + _ <- F + .async[Unit] { cb => + server.registerOneTimeListener[F, js.Error]("error") { e => + cb(Left(js.JavaScriptException(e))) + } <* F.delay { + address match { + case Left(addr) => + if ( + addr.host.isInstanceOf[IpAddress] && addr.host.asInstanceOf[IpAddress].isWildcard + ) + server.listen(addr.port.value, () => cb(Right(()))) + else + server.listen(addr.port.value, addr.host.toString, () => cb(Right(()))) + case Right(addr) => + server.listen(addr.path, () => cb(Right(()))) + } + } + } + .toResource + serverSocketAddress = address match { + case Left(_) => + val addr = server.address().get + SocketAddress(IpAddress.fromString(addr.address).get, Port.fromInt(addr.port).get) + case Right(addr) => addr + } + info = new SocketInfo[F] { + val address = serverSocketAddress + private def raiseOptionError[A]: F[A] = + F.raiseError( + new UnsupportedOperationException( + "Node.js server sockets do not support socket options" + ) + ) + def getOption[A](key: SocketOption.Key[A]) = raiseOptionError + def setOption[A](key: SocketOption.Key[A], value: A) = raiseOptionError + def supportedOptions = F.pure(Set.empty) + } + address0 = address + sockets = channel.stream + .evalTap(setSocketOptions(options)) + .flatMap { sock => + val address = address0 match { + case Left(_) => + SocketAddress( + IpAddress.fromString(sock.localAddress.get).get, + Port.fromInt(sock.localPort.get).get + ) + case Right(addr) => addr + } + val peerAddress = address0 match { + case Left(_) => + SocketAddress( + IpAddress.fromString(sock.remoteAddress.get).get, + Port.fromInt(sock.remotePort.get).get + ) + case Right(_) => UnixSocketAddress("") + } + Stream.resource(Socket.forAsync(sock, address, peerAddress)) + } + } yield ServerSocket(info, sockets)).adaptError { case IOException(ex) => ex } + + protected def bindDatagramSocketIpOrUnix( + address: Either[SocketAddress[Host], UnixSocketAddress], + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] = + address match { + case Right(_) => + Resource.eval( + new UnsupportedOperationException( + "Node.js does not support unix datagram sockets" + ).raiseError + ) + case Left(sa) => + for { + ip <- Resource.eval(sa.host.resolve) + protocolFamily = ip.fold(_ => "udp4", _ => "udp6") + sock <- F + .delay(facade.dgram.createSocket(protocolFamily)) + .toResource + _ <- F + .async_[Unit] { cb => + val errorListener: js.Function1[js.Error, Unit] = { error => + cb(Left(js.JavaScriptException(error))) + } + sock.once[js.Error]("error", errorListener) + val options = new facade.dgram.BindOptions {} + if (!ip.isWildcard) options.address = ip.toString + options.port = sa.port.value + sock.bind( + options, + { () => + sock.removeListener("error", errorListener) + cb(Right(())) + } + ) + } + .toResource + _ <- Resource.eval(options.traverse_(option => option.key.set(sock, option.value))) + socket <- DatagramSocket.forAsync[F](sock) + } yield socket + } +} diff --git a/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala b/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala index 176ee90c65..b34f575393 100644 --- a/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala +++ b/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala @@ -24,6 +24,7 @@ package io package net import cats.effect.kernel.Sync +import com.comcast.ip4s.NetworkInterface import fs2.io.internal.facade /** Specifies a socket option on a TCP/UDP socket. @@ -36,11 +37,21 @@ sealed trait DatagramSocketOption { type Value val key: DatagramSocketOption.Key[Value] val value: Value + + private[net] def toSocketOption: SocketOption = + SocketOption(key.toSocketOption, value) } object DatagramSocketOption { sealed trait Key[A] { + private[net] def get[F[_]: Sync](sock: facade.dgram.Socket): F[Option[A]] = { + val _ = sock + Sync[F].raiseError(new UnsupportedOperationException("option does not support get")) + } + private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: A): F[Unit] + + private[net] def toSocketOption: SocketOption.Key[A] } def apply[A](key0: Key[A], value0: A): DatagramSocketOption = new DatagramSocketOption { @@ -49,52 +60,68 @@ object DatagramSocketOption { val value = value0 } - private object Broadcast extends Key[Boolean] { + object Broadcast extends Key[Boolean] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Boolean): F[Unit] = Sync[F].delay(sock.setBroadcast(value)) + override private[net] def toSocketOption: SocketOption.Key[Boolean] = SocketOption.Broadcast } - private object MulticastInterface extends Key[String] { - override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: String): F[Unit] = - Sync[F].delay(sock.setMulticastInterface(value)) + object MulticastInterface extends Key[NetworkInterface] { + override private[net] def set[F[_]: Sync]( + sock: facade.dgram.Socket, + value: NetworkInterface + ): F[Unit] = + SocketOption.MulticastInterface.set(sock, value) + override private[net] def toSocketOption: SocketOption.Key[NetworkInterface] = + SocketOption.MulticastInterface } - private object MulticastLoopback extends Key[Boolean] { + object MulticastLoopback extends Key[Boolean] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Boolean): F[Unit] = Sync[F].delay { sock.setMulticastLoopback(value) () } + override private[net] def toSocketOption: SocketOption.Key[Boolean] = SocketOption.MulticastLoop } - private object MulticastTtl extends Key[Int] { + object MulticastTtl extends Key[Int] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = Sync[F].delay { sock.setMulticastTTL(value) () } + override private[net] def toSocketOption: SocketOption.Key[Int] = SocketOption.MulticastTtl } - private object ReceiveBufferSize extends Key[Int] { + object ReceiveBufferSize extends Key[Int] { + override private[net] def get[F[_]: Sync](sock: facade.dgram.Socket) = + Sync[F].delay(Some(sock.getRecvBufferSize)) override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = Sync[F].delay(sock.setRecvBufferSize(value)) + override private[net] def toSocketOption: SocketOption.Key[Int] = SocketOption.ReceiveBufferSize } - private object SendBufferSize extends Key[Int] { + object SendBufferSize extends Key[Int] { + override private[net] def get[F[_]: Sync](sock: facade.dgram.Socket) = + Sync[F].delay(Some(sock.getSendBufferSize)) override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = Sync[F].delay(sock.setSendBufferSize(value)) + override private[net] def toSocketOption: SocketOption.Key[Int] = SocketOption.SendBufferSize } - private object Ttl extends Key[Int] { + object Ttl extends Key[Int] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = Sync[F].delay { sock.setTTL(value) () } + override private[net] def toSocketOption: SocketOption.Key[Int] = SocketOption.Ttl } def broadcast(value: Boolean): DatagramSocketOption = apply(Broadcast, value) - def multicastInterface(value: String): DatagramSocketOption = apply(MulticastInterface, value) + def multicastInterface(value: NetworkInterface): DatagramSocketOption = + apply(MulticastInterface, value) def multicastLoopback(value: Boolean): DatagramSocketOption = apply(MulticastLoopback, value) def multicastTtl(value: Int): DatagramSocketOption = apply(MulticastTtl, value) def receiveBufferSize(value: Int): DatagramSocketOption = apply(ReceiveBufferSize, value) diff --git a/io/js/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala index c4ebb702bf..8135b6675f 100644 --- a/io/js/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala @@ -31,12 +31,16 @@ import cats.effect.std.Dispatcher import cats.effect.std.Queue import cats.effect.syntax.all._ import cats.syntax.all._ -import com.comcast.ip4s.AnySourceMulticastJoin -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.MulticastJoin -import com.comcast.ip4s.Port -import com.comcast.ip4s.SocketAddress -import com.comcast.ip4s.SourceSpecificMulticastJoin +import com.comcast.ip4s.{ + AnySourceMulticastJoin, + GenSocketAddress, + IpAddress, + MulticastJoin, + NetworkInterface => Ip4sNetworkInterface, + Port, + SocketAddress, + SourceSpecificMulticastJoin +} import fs2.io.internal.facade import scala.scalajs.js @@ -47,6 +51,8 @@ private[net] trait DatagramSocketPlatform[F[_]] { } private[net] trait DatagramSocketCompanionPlatform { + + @deprecated("Use com.comcast.ip4s.NetworkInterface", "3.13.0") type NetworkInterface = String private[net] def forAsync[F[_]]( @@ -73,7 +79,7 @@ private[net] trait DatagramSocketCompanionPlatform { _ <- sock.registerListener[F, js.Error]("error", dispatcher) { e => error.complete(js.JavaScriptException(e)).void } - socket <- Resource.make(F.pure(new AsyncDatagramSocket(sock, queue, error)))(_ => + socket <- Resource.make(F.delay(new AsyncDatagramSocket(sock, queue, error)))(_ => F.async_[Unit](cb => sock.close(() => cb(Right(())))) ) } yield socket @@ -86,34 +92,104 @@ private[net] trait DatagramSocketCompanionPlatform { F: Async[F] ) extends DatagramSocket[F] { + override def connect(address: GenSocketAddress) = + F.async_[Unit] { cb => + val addr = address.asIpUnsafe + sock.connect( + addr.port.value, + addr.host.toString, + err => err.toOption.fold(cb(Right(())))(err => cb(Left(js.JavaScriptException(err)))) + ) + } + + override def disconnect = + F.delay(sock.disconnect()) + override def read: F[Datagram] = EitherT( queue.take.race(error.get.flatMap(F.raiseError[Datagram])) ).merge + override def readGen: F[GenDatagram] = read.map(_.toGenDatagram) + override def reads: Stream[F, Datagram] = Stream .fromQueueUnterminated(queue) .concurrently(Stream.eval(error.get.flatMap(F.raiseError[Datagram]))) - override def write(datagram: Datagram): F[Unit] = F.async_ { cb => + override def write(bytes: Chunk[Byte]) = F.async_ { cb => + sock.send( + bytes.toUint8Array, + err => Option(err).fold(cb(Right(())))(err => cb(Left(js.JavaScriptException(err)))) + ) + } + + override def write(bytes: Chunk[Byte], address: GenSocketAddress) = F.async_ { cb => + val addr = address.asIpUnsafe sock.send( - datagram.bytes.toUint8Array, - datagram.remote.port.value, - datagram.remote.host.toString, + bytes.toUint8Array, + addr.port.value, + addr.host.toString, err => Option(err).fold(cb(Right(())))(err => cb(Left(js.JavaScriptException(err)))) ) } + override def write(datagram: Datagram): F[Unit] = + write(datagram.bytes, datagram.remote) + override def writes: Pipe[F, Datagram, Nothing] = _.foreach(write) + override val address: GenSocketAddress = { + val info = sock.address() + SocketAddress(IpAddress.fromString(info.address).get, Port.fromInt(info.port.toInt).get) + } + override def localAddress: F[SocketAddress[IpAddress]] = - F.delay { - val info = sock.address() - SocketAddress(IpAddress.fromString(info.address).get, Port.fromInt(info.port.toInt).get) + F.pure(address.asIpUnsafe) + + override def join( + join: MulticastJoin[IpAddress], + interface: Ip4sNetworkInterface + ): F[GroupMembership] = F + .delay { + val interfaceAddress = interface.addresses + .collectFirst { case c if c.address.fold(_ => true, _ => false) => c.address } + .getOrElse( + throw new IllegalArgumentException("specified interface does not have ipv4 address") + ) + .toString + join match { + case AnySourceMulticastJoin(group) => + sock.addMembership(group.address.toString, interfaceAddress) + case SourceSpecificMulticastJoin(source, group) => + sock.addSourceSpecificMembership( + source.toString, + group.address.toString, + interfaceAddress + ) + } + interfaceAddress + } + .map { interfaceAddress => + new GroupMembership { + + override def drop: F[Unit] = F.delay { + join match { + case AnySourceMulticastJoin(group) => + sock.dropMembership(group.address.toString, interfaceAddress) + case SourceSpecificMulticastJoin(source, group) => + sock.dropSourceSpecificMembership( + source.toString, + group.address.toString, + interfaceAddress + ) + } + } + } } + @deprecated("Use overload that takes a com.comcast.ip4s.NetworkInterface", "3.13.0") override def join( join: MulticastJoin[IpAddress], - interface: NetworkInterface + interface: DatagramSocket.NetworkInterface ): F[GroupMembership] = F .delay { join match { @@ -135,5 +211,23 @@ private[net] trait DatagramSocketCompanionPlatform { } }) + override def getOption[A](key: SocketOption.Key[A]) = + key.get(sock) + + override def setOption[A](key: SocketOption.Key[A], value: A) = + key.set(sock, value) + + override def supportedOptions = + F.pure( + Set( + SocketOption.MulticastInterface, + SocketOption.MulticastLoop, + SocketOption.MulticastTtl, + SocketOption.Broadcast, + SocketOption.ReceiveBufferSize, + SocketOption.SendBufferSize, + SocketOption.Ttl + ) + ) } } diff --git a/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala index 9fcbf4364d..7966552a78 100644 --- a/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -23,61 +23,24 @@ package fs2 package io package net -import cats.effect.IO -import cats.effect.LiftIO -import cats.effect.kernel.Async -import cats.effect.kernel.Resource -import com.comcast.ip4s.Host -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.Port -import com.comcast.ip4s.SocketAddress -import fs2.io.net.tls.TLSContext +import cats.effect.{Async, LiftIO} private[net] trait NetworkPlatform[F[_]] private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: Network.type => - def forIO: Network[IO] = forLiftIO implicit def forLiftIO[F[_]: Async: LiftIO]: Network[F] = { val _ = LiftIO[F] forAsync } - def forAsync[F[_]](implicit F: Async[F]): Network[F] = - new UnsealedNetwork[F] { - - private lazy val socketGroup = SocketGroup.forAsync[F] - private lazy val datagramSocketGroup = DatagramSocketGroup.forAsync[F] - - override def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = - socketGroup.client(to, options) - - override def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = - socketGroup.server(address, port, options) - - override def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - socketGroup.serverResource(address, port, options) - - override def openDatagramSocket( - address: Option[Host], - port: Option[Port], - options: List[DatagramSocketOption], - protocolFamily: Option[DatagramSocketGroup.ProtocolFamily] - ): Resource[F, DatagramSocket[F]] = - datagramSocketGroup.openDatagramSocket(address, port, options, protocolFamily) - - override def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync - + def forAsync[F[_]](implicit F: Async[F]): Network[F] = { + val omni = new AsyncSocketsProvider[F] + new AsyncProviderBasedNetwork[F] { + protected def mkIpSocketsProvider = omni + protected def mkUnixSocketsProvider = omni + protected def mkIpDatagramSocketsProvider = omni + protected def mkUnixDatagramSocketsProvider = omni } + } } diff --git a/io/js/src/main/scala/fs2/io/net/SocketGroupPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketGroupPlatform.scala deleted file mode 100644 index 632595edf9..0000000000 --- a/io/js/src/main/scala/fs2/io/net/SocketGroupPlatform.scala +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2 -package io -package net - -import cats.effect.kernel.Async -import cats.effect.kernel.Resource -import cats.effect.std.Dispatcher -import cats.effect.syntax.all._ -import cats.syntax.all._ -import com.comcast.ip4s.{Host, IpAddress, Port, SocketAddress} -import fs2.concurrent.Channel -import fs2.io.internal.facade - -import scala.scalajs.js - -private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => - - private[net] def forAsync[F[_]: Async]: SocketGroup[F] = new AsyncSocketGroup[F] - - private[net] final class AsyncSocketGroup[F[_]](implicit F: Async[F]) - extends AbstractAsyncSocketGroup[F] { - - private def setSocketOptions(options: List[SocketOption])(socket: facade.net.Socket): F[Unit] = - options.traverse_(option => option.key.set(socket, option.value)) - - override def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = - (for { - sock <- Resource - .make( - F.delay( - new facade.net.Socket(new facade.net.SocketOptions { allowHalfOpen = true }) - ) - )(sock => - F.delay { - if (!sock.destroyed) - sock.destroy() - } - ) - .evalTap(setSocketOptions(options)) - socket <- Socket.forAsync(sock) - _ <- F - .async[Unit] { cb => - sock - .registerOneTimeListener[F, js.Error]("error") { error => - cb(Left(js.JavaScriptException(error))) - } <* F.delay { - sock.connect(to.port.value, to.host.toString, () => cb(Right(()))) - } - } - .toResource - } yield socket).adaptError { case IOException(ex) => ex } - - override def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - (for { - dispatcher <- Dispatcher.sequential[F] - channel <- Channel.unbounded[F, facade.net.Socket].toResource - server <- Resource.make( - F - .delay( - facade.net.createServer( - new facade.net.ServerOptions { - pauseOnConnect = true - allowHalfOpen = true - }, - sock => dispatcher.unsafeRunAndForget(channel.send(sock)) - ) - ) - )(server => - F.async[Unit] { cb => - if (server.listening) - F.delay(server.close(e => cb(e.toLeft(()).leftMap(js.JavaScriptException)))) *> - channel.close.as(None) - else - F.delay(cb(Right(()))).as(None) - } - ) - _ <- F - .async[Unit] { cb => - server.registerOneTimeListener[F, js.Error]("error") { e => - cb(Left(js.JavaScriptException(e))) - } <* F.delay { - address match { - case Some(host) => - server.listen(port.fold(0)(_.value), host.toString, () => cb(Right(()))) - case None => - server.listen(port.fold(0)(_.value), () => cb(Right(()))) - } - - } - - } - .toResource - ipAddress <- F.delay { - val info = server.address() - SocketAddress(IpAddress.fromString(info.address).get, Port.fromInt(info.port).get) - }.toResource - sockets = channel.stream - .evalTap(setSocketOptions(options)) - .flatMap(sock => Stream.resource(Socket.forAsync(sock))) - } yield (ipAddress, sockets)).adaptError { case IOException(ex) => ex } - - } - -} diff --git a/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala new file mode 100644 index 0000000000..e106b1c34e --- /dev/null +++ b/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +private[net] trait SocketInfoCompanionPlatform diff --git a/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index 570bd6ea36..adafad2b07 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -22,40 +22,62 @@ package fs2.io.net import cats.effect.kernel.Sync +import com.comcast.ip4s.NetworkInterface import fs2.io.internal.facade -import scala.concurrent.duration.FiniteDuration +import scala.annotation.nowarn +import scala.concurrent.duration._ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => sealed trait Key[A] { - private[net] def set[F[_]: Sync](sock: facade.net.Socket, value: A): F[Unit] + @nowarn + private[net] def set[F[_]: Sync](sock: facade.net.Socket, value: A): F[Unit] = + Sync[F].raiseError(new UnsupportedOperationException("option does not support TCP")) + + @nowarn + private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[A]] = + unsupportedGet + + @nowarn + private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: A): F[Unit] = + Sync[F].raiseError(new UnsupportedOperationException("option does not support UDP")) + + @nowarn + private[net] def get[F[_]: Sync](sock: facade.dgram.Socket): F[Option[A]] = + unsupportedGet } - private object Encoding extends Key[String] { + private def unsupportedGet[F[_]: Sync, A]: F[A] = + Sync[F].raiseError(new UnsupportedOperationException("option does not support get")) + + object Encoding extends Key[String] { override private[net] def set[F[_]: Sync](sock: facade.net.Socket, value: String): F[Unit] = Sync[F].delay { sock.setEncoding(value) () } } + def encoding(value: String): SocketOption = apply(Encoding, value) - private object KeepAlive extends Key[Boolean] { + object KeepAlive extends Key[Boolean] { override private[net] def set[F[_]: Sync](sock: facade.net.Socket, value: Boolean): F[Unit] = Sync[F].delay { sock.setKeepAlive(value) () } } + def keepAlive(value: Boolean): SocketOption = apply(KeepAlive, value) - private object NoDelay extends Key[Boolean] { + object NoDelay extends Key[Boolean] { override private[net] def set[F[_]: Sync](sock: facade.net.Socket, value: Boolean): F[Unit] = Sync[F].delay { sock.setNoDelay(value) () } } + def noDelay(value: Boolean): SocketOption = apply(NoDelay, value) - private object Timeout extends Key[FiniteDuration] { + object Timeout extends Key[FiniteDuration] { override private[net] def set[F[_]: Sync]( sock: facade.net.Socket, value: FiniteDuration @@ -64,11 +86,104 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => sock.setTimeout(value.toMillis.toDouble) () } + override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[FiniteDuration]] = + Sync[F].delay { + Some(sock.timeout.toLong.millis) + } } - - def encoding(value: String): SocketOption = apply(Encoding, value) - def keepAlive(value: Boolean): SocketOption = apply(KeepAlive, value) - def noDelay(value: Boolean): SocketOption = apply(NoDelay, value) def timeout(value: FiniteDuration): SocketOption = apply(Timeout, value) + object UnixSocketDeleteIfExists extends Key[Boolean] { + override private[net] def set[F[_]: Sync]( + sock: facade.net.Socket, + value: Boolean + ): F[Unit] = Sync[F].unit + } + def unixSocketDeleteIfExists(value: Boolean): SocketOption = + apply(UnixSocketDeleteIfExists, value) + + object UnixSocketDeleteOnClose extends Key[Boolean] { + override private[net] def set[F[_]: Sync]( + sock: facade.net.Socket, + value: Boolean + ): F[Unit] = Sync[F].unit + } + def unixSocketDeleteOnClose(value: Boolean): SocketOption = + apply(UnixSocketDeleteOnClose, value) + + // Datagram options + + object Broadcast extends Key[Boolean] { + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Boolean): F[Unit] = + Sync[F].delay(sock.setBroadcast(value)) + } + def broadcast(value: Boolean): SocketOption = apply(Broadcast, value) + + object MulticastInterface extends Key[NetworkInterface] { + override private[net] def set[F[_]: Sync]( + sock: facade.dgram.Socket, + value: NetworkInterface + ): F[Unit] = + Sync[F].delay { + val mi = sock.address().family match { + case "IPv4" => + value.addresses + .collectFirst { + case c if c.address.fold(_ => true, _ => false) => c.address.toString + } + .getOrElse( + throw new IllegalArgumentException( + "socket is IPv4 but specified interface does not have an IPv4 address" + ) + ) + case "IPv6" => "::%" + value.name + case other => throw new IllegalStateException(s"unexpected socket family: $other") + } + sock.setMulticastInterface(mi) + } + } + def multicastInterface(value: NetworkInterface): SocketOption = apply(MulticastInterface, value) + + object MulticastLoop extends Key[Boolean] { + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Boolean): F[Unit] = + Sync[F].delay { + sock.setMulticastLoopback(value) + () + } + } + def multicastLoop(value: Boolean): SocketOption = apply(MulticastLoop, value) + + object MulticastTtl extends Key[Int] { + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = + Sync[F].delay { + sock.setMulticastTTL(value) + () + } + } + def multicastTtl(value: Int): SocketOption = apply(MulticastTtl, value) + + object ReceiveBufferSize extends Key[Int] { + override private[net] def get[F[_]: Sync](sock: facade.dgram.Socket) = + Sync[F].delay(Some(sock.getRecvBufferSize)) + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = + Sync[F].delay(sock.setRecvBufferSize(value)) + } + def receiveBufferSize(value: Int): SocketOption = apply(ReceiveBufferSize, value) + + object SendBufferSize extends Key[Int] { + override private[net] def get[F[_]: Sync](sock: facade.dgram.Socket) = + Sync[F].delay(Some(sock.getSendBufferSize)) + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = + Sync[F].delay(sock.setSendBufferSize(value)) + } + def sendBufferSize(value: Int): SocketOption = apply(SendBufferSize, value) + + object Ttl extends Key[Int] { + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = + Sync[F].delay { + sock.setTTL(value) + () + } + } + def ttl(value: Int): SocketOption = apply(Ttl, value) } diff --git a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala index dd7bcfce26..52d7424d18 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -23,28 +23,24 @@ package fs2 package io package net -import cats.data.Kleisli -import cats.data.OptionT -import cats.effect.kernel.Async -import cats.effect.kernel.Resource -import cats.syntax.all._ -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.Port -import com.comcast.ip4s.SocketAddress -import fs2.io.internal.SuspendedStream -import fs2.io.internal.facade +import cats.data.{Kleisli, OptionT} +import cats.effect.{Async, Resource} +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} +import fs2.io.internal.{facade, SuspendedStream} private[net] trait SocketCompanionPlatform { private[net] def forAsync[F[_]]( - sock: facade.net.Socket + sock: facade.net.Socket, + address: GenSocketAddress, + peerAddress: GenSocketAddress )(implicit F: Async[F]): Resource[F, Socket[F]] = suspendReadableAndRead( destroyIfNotEnded = false, destroyIfCanceled = false )(sock.asInstanceOf[Readable]) .flatMap { case (_, stream) => - SuspendedStream(stream).map(new AsyncSocket(sock, _)) + SuspendedStream(stream).map(new AsyncSocket(sock, _, address, peerAddress)) } .onFinalize { F.delay { @@ -55,7 +51,9 @@ private[net] trait SocketCompanionPlatform { private[net] class AsyncSocket[F[_]]( sock: facade.net.Socket, - readStream: SuspendedStream[F, Byte] + readStream: SuspendedStream[F, Byte], + val address: GenSocketAddress, + val peerAddress: GenSocketAddress )(implicit F: Async[F]) extends Socket[F] { @@ -87,17 +85,29 @@ private[net] trait SocketCompanionPlatform { override def isOpen: F[Boolean] = F.delay(sock.readyState == "open") - override def remoteAddress: F[SocketAddress[IpAddress]] = - for { - ip <- F.delay(sock.remoteAddress.toOption.flatMap(IpAddress.fromString).get) - port <- F.delay(sock.remotePort.toOption.map(_.toInt).flatMap(Port.fromInt).get) - } yield SocketAddress(ip, port) - override def localAddress: F[SocketAddress[IpAddress]] = - for { - ip <- F.delay(sock.localAddress.toOption.flatMap(IpAddress.fromString).get) - port <- F.delay(sock.localPort.toOption.map(_.toInt).flatMap(Port.fromInt).get) - } yield SocketAddress(ip, port) + F.delay(address.asIpUnsafe) + + override def remoteAddress: F[SocketAddress[IpAddress]] = + F.delay(peerAddress.asIpUnsafe) + + override def supportedOptions: F[Set[SocketOption.Key[?]]] = + F.pure( + Set( + SocketOption.Encoding, + SocketOption.KeepAlive, + SocketOption.NoDelay, + SocketOption.Timeout, + SocketOption.UnixSocketDeleteIfExists, + SocketOption.UnixSocketDeleteOnClose + ) + ) + + override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = + key.get(sock) + + override def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] = + key.set(sock, value) override def write(bytes: Chunk[Byte]): F[Unit] = Stream.chunk(bytes).through(writes).compile.drain diff --git a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 050f9d8353..9fb45cf2f0 100644 --- a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -68,9 +68,15 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => underlying: Socket[F], val session: F[SSLSession], val applicationProtocol: F[String] - ) extends Socket.AsyncSocket[F](sock, readStream) + ) extends Socket.AsyncSocket[F]( + sock, + readStream, + underlying.address, + underlying.peerAddress + ) with UnsealedTLSSocket[F] { - override def localAddress = underlying.localAddress - override def remoteAddress = underlying.remoteAddress + override def getOption[A](key: SocketOption.Key[A]) = underlying.getOption(key) + override def setOption[A](key: SocketOption.Key[A], value: A) = underlying.setOption(key, value) + override def supportedOptions = underlying.supportedOptions } } diff --git a/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala index 2719d64ccf..2dfad89616 100644 --- a/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -20,103 +20,30 @@ */ package fs2 -package io.net.unixsocket +package io +package net +package unixsocket -import cats.effect.IO -import cats.effect.LiftIO -import cats.effect.kernel.Async -import cats.effect.kernel.Resource -import cats.effect.std.Dispatcher -import cats.syntax.all._ -import fs2.concurrent.Channel +import cats.effect.{Async, IO, LiftIO} import fs2.io.file.Files -import fs2.io.file.Path -import fs2.io.net.Socket -import fs2.io.internal.facade -import scala.scalajs.js - -private[unixsocket] trait UnixSocketsCompanionPlatform { +private[unixsocket] trait UnixSocketsCompanionPlatform { self: UnixSockets.type => + @deprecated("Use Network instead", "3.13.0") def forIO: UnixSockets[IO] = forLiftIO + @deprecated("Use Network instead", "3.13.0") implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = { val _ = LiftIO[F] forAsyncAndFiles } + @deprecated("Use Network instead", "3.13.0") def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = forAsyncAndFiles(Files.forAsync(F), F) - def forAsyncAndFiles[F[_]: Files](implicit F: Async[F]): UnixSockets[F] = - new UnixSockets[F] { - - override def client(address: UnixSocketAddress): Resource[F, Socket[F]] = - Resource - .make( - F.delay( - new facade.net.Socket(new facade.net.SocketOptions { allowHalfOpen = true }) - ) - )(socket => - F.delay { - if (!socket.destroyed) - socket.destroy() - } - ) - .evalTap { socket => - F.async_[Unit] { cb => - socket.connect(address.path, () => cb(Right(()))) - () - } - } - .flatMap(Socket.forAsync[F]) - - override def server( - address: UnixSocketAddress, - deleteIfExists: Boolean, - deleteOnClose: Boolean - ): fs2.Stream[F, Socket[F]] = - for { - dispatcher <- Stream.resource(Dispatcher.sequential[F]) - channel <- Stream.eval(Channel.unbounded[F, facade.net.Socket]) - errored <- Stream.eval(F.deferred[js.JavaScriptException]) - server <- Stream.bracket( - F.delay { - facade.net.createServer( - new facade.net.ServerOptions { - pauseOnConnect = true - allowHalfOpen = true - }, - sock => dispatcher.unsafeRunAndForget(channel.send(sock)) - ) - } - )(server => - F.async_[Unit] { cb => - if (server.listening) { - server.close(e => cb(e.toLeft(()).leftMap(js.JavaScriptException))) - () - } else - cb(Right(())) - } - ) - _ <- Stream - .resource( - server.registerListener[F, js.Error]("error", dispatcher) { e => - errored.complete(js.JavaScriptException(e)).void - } - ) - .concurrently(Stream.eval(errored.get.flatMap(F.raiseError[Unit]))) - _ <- Stream.bracket( - if (deleteIfExists) Files[F].deleteIfExists(Path(address.path)).void else F.unit - )(_ => if (deleteOnClose) Files[F].deleteIfExists(Path(address.path)).void else F.unit) - _ <- Stream.eval( - F.async_[Unit] { cb => - server.listen(address.path, () => cb(Right(()))) - () - } - ) - socket <- channel.stream.flatMap(sock => Stream.resource(Socket.forAsync(sock))) - } yield socket - - } - + @deprecated("Use Network instead", "3.13.0") + def forAsyncAndFiles[F[_]: Files](implicit F: Async[F]): UnixSockets[F] = { + val _ = Files[F] + new AsyncUnixSockets(new AsyncSocketsProvider) + } } diff --git a/io/js/src/test/scala/fs2/io/net/tcp/SocketSuitePlatform.scala b/io/js/src/test/scala/fs2/io/net/SocketSuitePlatform.scala similarity index 98% rename from io/js/src/test/scala/fs2/io/net/tcp/SocketSuitePlatform.scala rename to io/js/src/test/scala/fs2/io/net/SocketSuitePlatform.scala index fe0607a79a..4e753a5bb6 100644 --- a/io/js/src/test/scala/fs2/io/net/tcp/SocketSuitePlatform.scala +++ b/io/js/src/test/scala/fs2/io/net/SocketSuitePlatform.scala @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package fs2.io.net.tcp +package fs2.io.net trait SocketSuitePlatform { diff --git a/io/native/src/test/scala/fs2/io/net/unixsockets/UnixSocketsSuitePlatform.scala b/io/js/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala similarity index 94% rename from io/native/src/test/scala/fs2/io/net/unixsockets/UnixSocketsSuitePlatform.scala rename to io/js/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala index fa9ecc98b9..0d3a307f9e 100644 --- a/io/native/src/test/scala/fs2/io/net/unixsockets/UnixSocketsSuitePlatform.scala +++ b/io/js/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala @@ -20,10 +20,10 @@ */ package fs2 -package io.net.unixsocket +package io.net import cats.effect.IO trait UnixSocketsSuitePlatform { self: UnixSocketsSuite => - testProvider("native")(UnixSockets.forLiftIO[IO]) + testProvider("node.js", new AsyncSocketsProvider[IO]) } diff --git a/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index ca642a1cb3..cfc2241f42 100644 --- a/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -45,7 +45,7 @@ class TLSSocketSuite extends TLSSuite { SecureContext(minVersion = protocol.some, maxVersion = protocol.some) ) ) - socket <- Network[IO].client(SocketAddress(host"google.com", port"443")) + socket <- Network[IO].connect(SocketAddress(host"google.com", port"443")) tlsSocket <- tlsContext .clientBuilder(socket) .withParameters( @@ -110,10 +110,9 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(testTlsContext(true)) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) @@ -124,7 +123,7 @@ class TLSSocketSuite extends TLSSuite { ) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -150,10 +149,9 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(Network[IO].tlsContext.system) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) @@ -164,7 +162,7 @@ class TLSSocketSuite extends TLSSuite { ) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -191,10 +189,9 @@ class TLSSocketSuite extends TLSSuite { val setup = for { serverContext <- Resource.eval(testTlsContext(true)) clientContext <- Resource.eval(testTlsContext(false)) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap( clientContext .clientBuilder(_) @@ -205,7 +202,7 @@ class TLSSocketSuite extends TLSSuite { ) .build ) - } yield server.flatMap(s => + } yield serverSocket.accept.flatMap(s => Stream.resource( serverContext .serverBuilder(s) @@ -239,10 +236,9 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(testTlsContext(true, Some(protocol))) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) @@ -255,7 +251,7 @@ class TLSSocketSuite extends TLSSuite { ) .build ) - } yield server.flatMap(s => + } yield serverSocket.accept.flatMap(s => Stream.resource( tlsContext .serverBuilder(s) @@ -295,16 +291,15 @@ class TLSSocketSuite extends TLSSuite { val setup = for { clientContext <- Resource.eval(Network[IO].tlsContext.insecure) tlsContext <- Resource.eval(testTlsContext(true)) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap(s => clientContext .clientBuilder(s) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -331,16 +326,15 @@ class TLSSocketSuite extends TLSSuite { val setup = for { clientContext <- Resource.eval(Network[IO].tlsContext.system) tlsContext <- Resource.eval(testTlsContext(true)) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap(s => clientContext .clientBuilder(s) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -364,10 +358,9 @@ class TLSSocketSuite extends TLSSuite { test("get local and remote address") { val setup = for { tlsContext <- Resource.eval(testTlsContext(true)) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) @@ -378,18 +371,18 @@ class TLSSocketSuite extends TLSSuite { ) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) .flatMap { case (server, clientSocket) => - val serverSocketAddresses = server.evalMap { socket => - socket.localAddress.product(socket.remoteAddress) + val serverSocketAddresses = server.map { socket => + socket.address -> socket.peerAddress } val clientSocketAddresses = - Stream.resource(clientSocket).evalMap { socket => - socket.localAddress.product(socket.remoteAddress) + Stream.resource(clientSocket).map { socket => + socket.address -> socket.peerAddress } serverSocketAddresses.parZip(clientSocketAddresses).map { diff --git a/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala new file mode 100644 index 0000000000..87dde719a5 --- /dev/null +++ b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import java.net.InetSocketAddress +import java.nio.channels.{ + AsynchronousCloseException, + AsynchronousServerSocketChannel, + AsynchronousSocketChannel, + CompletionHandler +} +import java.nio.channels.AsynchronousChannelGroup + +import cats.syntax.all._ +import cats.effect.{Async, Resource} +import com.comcast.ip4s.{Dns, Host, SocketAddress} + +private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( + channelGroup: AsynchronousChannelGroup +)(implicit F: Async[F], F2: Dns[F]) + extends IpSocketsProvider[F] { + + override def connectIp( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] = { + + def setup: Resource[F, AsynchronousSocketChannel] = + Resource + .make( + F.delay( + AsynchronousSocketChannel.open(channelGroup) + ) + )(ch => F.delay(if (ch.isOpen) ch.close else ())) + .evalTap(ch => F.delay(options.foreach(opt => ch.setOption(opt.key, opt.value)))) + + def connect(ch: AsynchronousSocketChannel): F[AsynchronousSocketChannel] = + address.resolve[F].flatMap { ip => + F.async[AsynchronousSocketChannel] { cb => + F.delay { + ch.connect( + ip.toInetSocketAddress, + null, + new CompletionHandler[Void, Void] { + def completed(result: Void, attachment: Void): Unit = + cb(Right(ch)) + def failed(rsn: Throwable, attachment: Void): Unit = + cb(Left(rsn)) + } + ) + }.as(Some(F.delay(ch.close()))) + } + } + + setup.evalMap(ch => connect(ch) *> Socket.forAsync(ch)) + } + + override def bindIp( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = { + + val setup: Resource[F, AsynchronousServerSocketChannel] = + Resource.eval(address.host.resolve[F]).flatMap { addr => + Resource + .make( + F.delay( + AsynchronousServerSocketChannel.open(channelGroup) + ) + )(sch => F.delay(if (sch.isOpen) sch.close())) + .evalTap(ch => + F.delay( + ch.bind( + new InetSocketAddress( + if (addr.isWildcard) null else addr.toInetAddress, + address.port.value + ) + ) + ) + ) + } + + def acceptIncoming( + sch: AsynchronousServerSocketChannel + ): Stream[F, Socket[F]] = { + def go: Stream[F, Socket[F]] = { + def acceptChannel = Resource.makeFull[F, AsynchronousSocketChannel] { poll => + poll { + F.async[AsynchronousSocketChannel] { cb => + F.delay { + sch.accept( + null, + new CompletionHandler[AsynchronousSocketChannel, Void] { + def completed(ch: AsynchronousSocketChannel, attachment: Void): Unit = + cb(Right(ch)) + def failed(rsn: Throwable, attachment: Void): Unit = + cb(Left(rsn)) + } + ) + }.as(Some(F.delay(sch.close()))) + } + } + }(ch => F.delay(if (ch.isOpen) ch.close else ())) + + def setOpts(ch: AsynchronousSocketChannel) = + F.delay { + options.foreach(o => ch.setOption(o.key, o.value)) + } + + Stream.resource(acceptChannel.attempt).flatMap { + case Left(_) => Stream.empty[F] + case Right(accepted) => Stream.eval(setOpts(accepted) *> Socket.forAsync(accepted)) + } ++ go + } + + go.handleErrorWith { + case err: AsynchronousCloseException => + Stream.eval(F.delay(sch.isOpen)).flatMap { isOpen => + if (isOpen) Stream.raiseError[F](err) + else Stream.empty + } + case err => Stream.raiseError[F](err) + } + } + + setup.map(sch => ServerSocket(SocketInfo.forAsync(sch), acceptIncoming(sch))) + } + +} + +private[net] object AsynchronousChannelGroupIpSocketsProvider { + + def forAsync[F[_]: Async]: AsynchronousChannelGroupIpSocketsProvider[F] = { + implicit val dnsInstance: Dns[F] = Dns.forAsync[F] + new AsynchronousChannelGroupIpSocketsProvider[F](null) + } +} diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala deleted file mode 100644 index 0e3729a706..0000000000 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2 -package io -package net - -import java.net.InetSocketAddress -import java.nio.channels.{ - AsynchronousCloseException, - AsynchronousServerSocketChannel, - AsynchronousSocketChannel, - CompletionHandler -} -import java.nio.channels.AsynchronousChannelGroup -import cats.syntax.all._ -import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{Dns, Host, IpAddress, Port, SocketAddress} - -private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => - private[fs2] def unsafe[F[_]: Async: Dns]( - channelGroup: AsynchronousChannelGroup - ): SocketGroup[F] = - new AsyncSocketGroup[F](channelGroup) - - private final class AsyncSocketGroup[F[_]: Async: Dns](channelGroup: AsynchronousChannelGroup) - extends AbstractAsyncSocketGroup[F] { - - def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = { - def setup: Resource[F, AsynchronousSocketChannel] = - Resource - .make( - Async[F].delay( - AsynchronousSocketChannel.open(channelGroup) - ) - )(ch => Async[F].delay(if (ch.isOpen) ch.close else ())) - .evalTap(ch => Async[F].delay(options.foreach(opt => ch.setOption(opt.key, opt.value)))) - - def connect(ch: AsynchronousSocketChannel): F[AsynchronousSocketChannel] = - to.resolve[F].flatMap { ip => - Async[F].async[AsynchronousSocketChannel] { cb => - Async[F] - .delay { - ch.connect( - ip.toInetSocketAddress, - null, - new CompletionHandler[Void, Void] { - def completed(result: Void, attachment: Void): Unit = - cb(Right(ch)) - def failed(rsn: Throwable, attachment: Void): Unit = - cb(Left(rsn)) - } - ) - } - .as(Some(Async[F].delay(ch.close()))) - } - } - - setup.evalMap(ch => connect(ch) *> Socket.forAsync(ch)) - } - - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = { - - val setup: Resource[F, AsynchronousServerSocketChannel] = - Resource.eval(address.traverse(_.resolve[F])).flatMap { addr => - Resource - .make( - Async[F].delay( - AsynchronousServerSocketChannel.open(channelGroup) - ) - )(sch => Async[F].delay(if (sch.isOpen) sch.close())) - .evalTap(ch => - Async[F].delay( - ch.bind( - new InetSocketAddress( - addr.map(_.toInetAddress).orNull, - port.map(_.value).getOrElse(0) - ) - ) - ) - ) - } - - def acceptIncoming( - sch: AsynchronousServerSocketChannel - ): Stream[F, Socket[F]] = { - def go: Stream[F, Socket[F]] = { - def acceptChannel = Resource.makeFull[F, AsynchronousSocketChannel] { poll => - poll { - Async[F].async[AsynchronousSocketChannel] { cb => - Async[F] - .delay { - sch.accept( - null, - new CompletionHandler[AsynchronousSocketChannel, Void] { - def completed(ch: AsynchronousSocketChannel, attachment: Void): Unit = - cb(Right(ch)) - def failed(rsn: Throwable, attachment: Void): Unit = - cb(Left(rsn)) - } - ) - } - .as(Some(Async[F].delay(sch.close()))) - } - } - }(ch => Async[F].delay(if (ch.isOpen) ch.close else ())) - - def setOpts(ch: AsynchronousSocketChannel) = - Async[F].delay { - options.foreach(o => ch.setOption(o.key, o.value)) - } - - Stream.resource(acceptChannel.attempt).flatMap { - case Left(_) => Stream.empty[F] - case Right(accepted) => Stream.eval(setOpts(accepted) *> Socket.forAsync(accepted)) - } ++ go - } - - go.handleErrorWith { - case err: AsynchronousCloseException => - Stream.eval(Async[F].delay(sch.isOpen)).flatMap { isOpen => - if (isOpen) Stream.raiseError[F](err) - else Stream.empty - } - case err => Stream.raiseError[F](err) - } - } - - setup.map { sch => - val jLocalAddress = sch.getLocalAddress.asInstanceOf[java.net.InetSocketAddress] - val localAddress = SocketAddress.fromInetSocketAddress(jLocalAddress) - (localAddress, acceptIncoming(sch)) - } - } - } - -} diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala new file mode 100644 index 0000000000..7535c52d31 --- /dev/null +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import com.comcast.ip4s.GenSocketAddress +import cats.effect.Async + +import java.nio.channels.NetworkChannel + +import CollectionCompat.* + +private[net] trait SocketInfoCompanionPlatform { + private[net] def forAsync[F[_]](ch: NetworkChannel)(implicit F: Async[F]): SocketInfo[F] = + new AsyncSocketInfo[F] { + def asyncInstance = F + def channel = ch + } + + private[net] trait OptionsSupport[F[_]] extends SocketInfo[F] { + + implicit protected def asyncInstance: Async[F] + protected def channel: NetworkChannel + + override def supportedOptions: F[Set[SocketOption.Key[?]]] = + asyncInstance.delay { + channel.supportedOptions.asScala.toSet + } + + override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = + asyncInstance.delay { + try + Some(channel.getOption(key)) + catch { + case _: UnsupportedOperationException => None + } + } + + override def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] = + asyncInstance.delay { + channel.setOption(key, value) + () + } + } + + private[net] trait AsyncSocketInfo[F[_]] extends OptionsSupport[F] { + override val address: GenSocketAddress = + SocketAddressHelpers.toGenSocketAddress(channel.getLocalAddress) + } +} diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index cdb050fcb7..d188bf0937 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -25,7 +25,9 @@ import java.net.{SocketOption => JSocketOption} import java.net.StandardSocketOptions import java.lang.{Boolean => JBoolean, Integer => JInt} -import java.net.NetworkInterface +import java.net.{NetworkInterface => JNetworkInterface} + +import com.comcast.ip4s.NetworkInterface private[net] trait SocketOptionCompanionPlatform { type Key[A] = JSocketOption[A] @@ -36,39 +38,69 @@ private[net] trait SocketOptionCompanionPlatform { def integer(key: JSocketOption[JInt], value: Int): SocketOption = SocketOption[JInt](key, value) + val MulticastInterface = StandardSocketOptions.IP_MULTICAST_IF def multicastInterface(value: NetworkInterface): SocketOption = - SocketOption(StandardSocketOptions.IP_MULTICAST_IF, value) + SocketOption(MulticastInterface, JNetworkInterface.getByName(value.name)) + + def multicastInterface(value: JNetworkInterface): SocketOption = + SocketOption(MulticastInterface, value) + val MulticastLoop = StandardSocketOptions.IP_MULTICAST_LOOP def multicastLoop(value: Boolean): SocketOption = - boolean(StandardSocketOptions.IP_MULTICAST_LOOP, value) + boolean(MulticastLoop, value) + val MulticastTtl = StandardSocketOptions.IP_MULTICAST_TTL def multicastTtl(value: Int): SocketOption = - integer(StandardSocketOptions.IP_MULTICAST_TTL, value) + integer(MulticastTtl, value) + val TypeOfService = StandardSocketOptions.IP_TOS def typeOfService(value: Int): SocketOption = - integer(StandardSocketOptions.IP_TOS, value) + integer(TypeOfService, value) + val Broadcast = StandardSocketOptions.SO_BROADCAST def broadcast(value: Boolean): SocketOption = - boolean(StandardSocketOptions.SO_BROADCAST, value) + boolean(Broadcast, value) + val KeepAlive = StandardSocketOptions.SO_KEEPALIVE def keepAlive(value: Boolean): SocketOption = - boolean(StandardSocketOptions.SO_KEEPALIVE, value) + boolean(KeepAlive, value) + val Linger = StandardSocketOptions.SO_LINGER def linger(value: Int): SocketOption = - integer(StandardSocketOptions.SO_LINGER, value) + integer(Linger, value) + val ReceiveBufferSize = StandardSocketOptions.SO_RCVBUF def receiveBufferSize(value: Int): SocketOption = - integer(StandardSocketOptions.SO_RCVBUF, value) + integer(ReceiveBufferSize, value) + val ReuseAddress = StandardSocketOptions.SO_REUSEADDR def reuseAddress(value: Boolean): SocketOption = - boolean(StandardSocketOptions.SO_REUSEADDR, value) + boolean(ReuseAddress, value) + // Note: this option was added in Java 9 so lazily load it to avoid failure on Java 8 + lazy val ReusePort = StandardSocketOptions.SO_REUSEPORT def reusePort(value: Boolean): SocketOption = - boolean(StandardSocketOptions.SO_REUSEPORT, value) + boolean(ReusePort, value) + val SendBufferSize = StandardSocketOptions.SO_SNDBUF def sendBufferSize(value: Int): SocketOption = - integer(StandardSocketOptions.SO_SNDBUF, value) + integer(SendBufferSize, value) + val NoDelay = StandardSocketOptions.TCP_NODELAY def noDelay(value: Boolean): SocketOption = - boolean(StandardSocketOptions.TCP_NODELAY, value) + boolean(NoDelay, value) + + val UnixSocketDeleteIfExists: Key[JBoolean] = new Key[JBoolean] { + def name() = "FS2_UNIX_DELETE_IF_EXISTS" + def `type`() = classOf[JBoolean] + } + def unixSocketDeleteIfExists(value: JBoolean): SocketOption = + boolean(UnixSocketDeleteIfExists, value) + + val UnixSocketDeleteOnClose: Key[JBoolean] = new Key[JBoolean] { + def name() = "FS2_UNIX_DELETE_ON_CLOSE" + def `type`() = classOf[JBoolean] + } + def unixSocketDeleteOnClose(value: Boolean): SocketOption = + boolean(UnixSocketDeleteOnClose, value) } diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala index 257a435fea..f42cfd7a2d 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -23,12 +23,11 @@ package fs2 package io package net -import com.comcast.ip4s.{IpAddress, SocketAddress} +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} import cats.effect.Async import cats.effect.std.Mutex import cats.syntax.all._ -import java.net.InetSocketAddress import java.nio.channels.{AsynchronousSocketChannel, CompletionHandler} import java.nio.{Buffer, ByteBuffer} @@ -37,7 +36,12 @@ private[net] trait SocketCompanionPlatform { ch: AsynchronousSocketChannel ): F[Socket[F]] = (Mutex[F], Mutex[F]).mapN { (readMutex, writeMutex) => - new AsyncSocket[F](ch, readMutex, writeMutex) + new AsyncSocket[F]( + ch, + readMutex, + writeMutex, + SocketAddressHelpers.toGenSocketAddress(ch.getRemoteAddress) + ) } private[net] abstract class BufferedReads[F[_]]( @@ -106,9 +110,14 @@ private[net] trait SocketCompanionPlatform { private final class AsyncSocket[F[_]]( ch: AsynchronousSocketChannel, readMutex: Mutex[F], - writeMutex: Mutex[F] + writeMutex: Mutex[F], + val peerAddress: GenSocketAddress )(implicit F: Async[F]) - extends BufferedReads[F](readMutex) { + extends BufferedReads[F](readMutex) + with SocketInfo.AsyncSocketInfo[F] { + + protected def asyncInstance = F + protected def channel = ch protected def readChunk(buffer: ByteBuffer): F[Int] = F.async[Int] { cb => @@ -120,7 +129,7 @@ private[net] trait SocketCompanionPlatform { F.delay(Some(endOfInput.voidError)) } - def write(bytes: Chunk[Byte]): F[Unit] = { + override def write(bytes: Chunk[Byte]): F[Unit] = { def go(buff: ByteBuffer): F[Unit] = F.async[Int] { cb => ch.write( @@ -139,28 +148,20 @@ private[net] trait SocketCompanionPlatform { } } - def localAddress: F[SocketAddress[IpAddress]] = - F.delay( - SocketAddress.fromInetSocketAddress( - ch.getLocalAddress.asInstanceOf[InetSocketAddress] - ) - ) + override def localAddress: F[SocketAddress[IpAddress]] = + asyncInstance.pure(address.asIpUnsafe) - def remoteAddress: F[SocketAddress[IpAddress]] = - F.delay( - SocketAddress.fromInetSocketAddress( - ch.getRemoteAddress.asInstanceOf[InetSocketAddress] - ) - ) + override def remoteAddress: F[SocketAddress[IpAddress]] = + asyncInstance.pure(peerAddress.asIpUnsafe) - def isOpen: F[Boolean] = F.delay(ch.isOpen) + override def isOpen: F[Boolean] = F.delay(ch.isOpen) - def endOfOutput: F[Unit] = + override def endOfOutput: F[Unit] = F.delay { ch.shutdownOutput(); () } - def endOfInput: F[Unit] = + override def endOfInput: F[Unit] = F.delay { ch.shutdownInput(); () } diff --git a/io/jvm-native/src/main/scala/fs2/io/net/net.scala b/io/jvm-native/src/main/scala/fs2/io/net/net.scala index 1e5a6f98b7..ebcd6ecba4 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/net.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/net.scala @@ -32,4 +32,9 @@ package object net { type UnknownHostException = com.comcast.ip4s.UnknownHostException type DatagramSocketOption = SocketOption val DatagramSocketOption = SocketOption + + private[net] implicit final class DatagramSocketOptionOps(private val self: DatagramSocketOption) + extends AnyVal { + def toSocketOption: SocketOption = self + } } diff --git a/io/jvm-native/src/test/scala/fs2/io/net/tcp/SocketSuitePlatform.scala b/io/jvm-native/src/test/scala/fs2/io/net/SocketSuitePlatform.scala similarity index 96% rename from io/jvm-native/src/test/scala/fs2/io/net/tcp/SocketSuitePlatform.scala rename to io/jvm-native/src/test/scala/fs2/io/net/SocketSuitePlatform.scala index e78ecc048f..e7645d86e2 100644 --- a/io/jvm-native/src/test/scala/fs2/io/net/tcp/SocketSuitePlatform.scala +++ b/io/jvm-native/src/test/scala/fs2/io/net/SocketSuitePlatform.scala @@ -19,9 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package fs2.io.net.tcp - -import fs2.io.net.SocketOption +package fs2.io.net trait SocketSuitePlatform { diff --git a/io/jvm/src/main/scala/fs2/io/net/AsyncIpDatagramSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/AsyncIpDatagramSocketsProvider.scala new file mode 100644 index 0000000000..b510e958d4 --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/AsyncIpDatagramSocketsProvider.scala @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import java.net.InetSocketAddress + +import cats.syntax.all._ +import cats.effect.{Async, Resource} +import com.comcast.ip4s.{Dns, Host, NetworkInterface, SocketAddress} + +import fs2.internal.ThreadFactories +import java.net.StandardProtocolFamily +import java.nio.channels.DatagramChannel +import com.comcast.ip4s.* +import java.net.{NetworkInterface => JNetworkInterface} +import CollectionCompat.* + +private[net] object AsyncIpDatagramSocketsProvider { + + private[net] def forAsync[F[_]: Async]: IpDatagramSocketsProvider[F] = + new IpDatagramSocketsProvider[F] { + + private implicit val dnsInstance: Dns[F] = Dns.forAsync[F] + + private lazy val globalAdsg = + AsynchronousDatagramSocketGroup.unsafe(ThreadFactories.named("fs2-global-udp", true)) + + override def bindDatagramSocket( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] = + Resource.eval(address.resolve[F]).flatMap { addr => + val mkChannel = Async[F].delay { + val pf = + addr.host.fold(_ => StandardProtocolFamily.INET, _ => StandardProtocolFamily.INET6) + val channel = DatagramChannel.open(pf) + options.foreach(o => channel.setOption[o.Value](o.key, o.value)) + channel.bind( + new InetSocketAddress( + if (addr.host.isWildcard) null else addr.host.toInetAddress, + addr.port.value + ) + ) + channel + } + Resource + .make(mkChannel)(c => Async[F].delay(c.close())) + .flatMap(mkDatagramSocket(_, globalAdsg)) + } + + private def mkDatagramSocket( + channel: DatagramChannel, + adsg: AsynchronousDatagramSocketGroup + ): Resource[F, DatagramSocket[F]] = + Resource(Async[F].delay { + val ctx0 = adsg.register(channel) + + val socket = new DatagramSocket[F] { + private val ctx = ctx0 + + override val address: GenSocketAddress = + SocketAddress.fromInetSocketAddress( + channel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress] + ) + + override def localAddress: F[SocketAddress[IpAddress]] = + Async[F].pure(address.asIpUnsafe) + + override def connect(address: GenSocketAddress) = + Async[F].blocking(channel.connect(address.asIpUnsafe.toInetSocketAddress)).void + + override def disconnect = + Async[F].blocking(channel.disconnect()).void + + override def readGen: F[GenDatagram] = read.map(_.toGenDatagram) + + override def read: F[Datagram] = + Async[F].async[Datagram] { cb => + Async[F].delay { + val cancel = adsg.read(ctx, result => cb(result)) + Some(Async[F].delay(cancel())) + } + } + + override def reads: Stream[F, Datagram] = + Stream.repeatEval(read) + + override def write(bytes: Chunk[Byte]) = + write(bytes, None) + + override def write(bytes: Chunk[Byte], address: GenSocketAddress) = + write(bytes, Some(address.asIpUnsafe.toInetSocketAddress)) + + private def write(bytes: Chunk[Byte], address: Option[InetSocketAddress]) = + Async[F].async[Unit] { cb => + Async[F].delay { + val cancel = adsg.write(ctx, bytes, address, t => cb(t.toLeft(()))) + Some(Async[F].delay(cancel())) + } + } + + override def write(datagram: Datagram): F[Unit] = + write(datagram.bytes, datagram.remote) + + override def writes: Pipe[F, Datagram, Nothing] = + _.foreach(write) + + override def join( + join: MulticastJoin[IpAddress], + interface: NetworkInterface + ): F[GroupMembership] = + Async[F].delay { + val jinterface = JNetworkInterface.getByName(interface.name) + val membership = join.fold( + j => channel.join(j.group.address.toInetAddress, jinterface), + j => + channel.join(j.group.address.toInetAddress, jinterface, j.source.toInetAddress) + ) + new GroupMembership { + def drop = Async[F].delay(membership.drop) + def block(source: IpAddress) = + Async[F].delay { membership.block(source.toInetAddress); () } + def unblock(source: IpAddress) = + Async[F].delay { membership.unblock(source.toInetAddress); () } + override def toString = "GroupMembership" + } + } + + override def join( + j: MulticastJoin[IpAddress], + interface: JNetworkInterface + ): F[GroupMembership] = + join(j, NetworkInterface.fromJava(interface)) + + override def supportedOptions: F[Set[SocketOption.Key[?]]] = + Async[F].delay { + channel.supportedOptions.asScala.toSet + } + + override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = + Async[F].delay { + try + Some(channel.getOption(key)) + catch { + case _: UnsupportedOperationException => None + } + } + + override def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] = + Async[F].delay { + channel.setOption(key, value) + () + } + + override def toString = + s"DatagramSocket($address)" + } + + (socket, Async[F].delay(adsg.close(ctx0))) + }) + + } +} diff --git a/io/jvm/src/main/scala/fs2/io/net/AsyncUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/AsyncUnixSocketsProvider.scala new file mode 100644 index 0000000000..11aa99ead6 --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/AsyncUnixSocketsProvider.scala @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.{Async, Resource} +import cats.effect.std.Mutex +import cats.effect.syntax.all._ +import cats.syntax.all._ + +import com.comcast.ip4s.{IpAddress, SocketAddress, UnixSocketAddress} + +import fs2.io.file.{Files, FileHandle, SyncFileHandle} + +import java.nio.ByteBuffer +import java.nio.channels.SocketChannel + +private[net] abstract class AsyncUnixSocketsProvider[F[_]: Files](implicit F: Async[F]) + extends UnixSocketsProvider[F] { + + protected def openChannel( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, SocketChannel] + + protected def openServerChannel( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, (SocketInfo[F], Resource[F, SocketChannel])] + + override def connectUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] = + openChannel(address, options).evalMap( + AsyncUnixSocketsProvider.makeSocket[F](_, UnixSocketAddress(""), address) + ) + + override def bindUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = { + val (filteredOptions, delete) = SocketOption.extractUnixSocketDeletes(options, address) + + (delete *> openServerChannel(address, filteredOptions)).map { case (info, accept) => + val acceptIncoming = + Stream + .resource(accept.attempt) + .flatMap { + case Left(_) => Stream.empty[F] + case Right(accepted) => + Stream.eval( + AsyncUnixSocketsProvider.makeSocket(accepted, address, UnixSocketAddress("")) + ) + } + .repeat + ServerSocket(info, acceptIncoming) + } + } +} + +private[net] object AsyncUnixSocketsProvider { + + private def makeSocket[F[_]: Async]( + ch: SocketChannel, + localAddress: UnixSocketAddress, + remoteAddress: UnixSocketAddress + ): F[Socket[F]] = + (Mutex[F], Mutex[F]).mapN { (readMutex, writeMutex) => + new AsyncSocket[F](ch, readMutex, writeMutex, localAddress, remoteAddress) + } + + private final class AsyncSocket[F[_]]( + ch: SocketChannel, + readMutex: Mutex[F], + writeMutex: Mutex[F], + override val address: UnixSocketAddress, + val peerAddress: UnixSocketAddress + )(implicit F: Async[F]) + extends Socket.BufferedReads[F](readMutex) + with SocketInfo.OptionsSupport[F] { + + protected def asyncInstance = F + protected def channel = ch + + def readChunk(buff: ByteBuffer): F[Int] = + evalOnVirtualThreadIfAvailable(F.blocking(ch.read(buff))) + .cancelable(close) + + def write(bytes: Chunk[Byte]): F[Unit] = { + def go(buff: ByteBuffer): F[Unit] = + F.blocking(ch.write(buff)).cancelable(close) *> + F.delay(buff.remaining <= 0).ifM(F.unit, go(buff)) + + writeMutex.lock.surround { + F.delay(bytes.toByteBuffer).flatMap(buffer => evalOnVirtualThreadIfAvailable(go(buffer))) + } + } + + private def raiseIpAddressError[A]: F[A] = + F.raiseError(new UnsupportedOperationException("Unix sockets do not use IP addressing")) + + override def localAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError + + override def remoteAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError + + def isOpen: F[Boolean] = evalOnVirtualThreadIfAvailable(F.blocking(ch.isOpen())) + def close: F[Unit] = evalOnVirtualThreadIfAvailable(F.blocking(ch.close())) + def endOfOutput: F[Unit] = + evalOnVirtualThreadIfAvailable( + F.blocking { + ch.shutdownOutput(); () + } + ) + def endOfInput: F[Unit] = + evalOnVirtualThreadIfAvailable( + F.blocking { + ch.shutdownInput(); () + } + ) + override def sendFile( + file: FileHandle[F], + offset: Long, + count: Long, + chunkSize: Int + ): Stream[F, Nothing] = file match { + case syncFileHandle: SyncFileHandle[F] => + val fileChannel = syncFileHandle.chan + + def go(currOffset: Long, remaining: Long): F[Unit] = + if (remaining <= 0) F.unit + else { + F.blocking(fileChannel.transferTo(currOffset, remaining, ch)).flatMap { written => + if (written == 0) F.unit + else { + go(currOffset + written, remaining - written) + } + } + } + + Stream.exec(writeMutex.lock.surround(go(offset, count))) + + case _ => + super.sendFile(file, offset, count, chunkSize) + } + } +} diff --git a/io/jvm/src/main/scala/fs2/io/net/AsynchronousDatagramSocketGroup.scala b/io/jvm/src/main/scala/fs2/io/net/AsynchronousDatagramSocketGroup.scala index 8ccfa506d1..18c3d73e3c 100644 --- a/io/jvm/src/main/scala/fs2/io/net/AsynchronousDatagramSocketGroup.scala +++ b/io/jvm/src/main/scala/fs2/io/net/AsynchronousDatagramSocketGroup.scala @@ -48,7 +48,8 @@ private[net] trait AsynchronousDatagramSocketGroup { ): () => Unit def write( ctx: Context, - datagram: Datagram, + bytes: Chunk[Byte], + address: Option[InetSocketAddress], cb: Option[Throwable] => Unit ): () => Unit def close(ctx: Context): Unit @@ -60,7 +61,7 @@ private[net] object AsynchronousDatagramSocketGroup { * Used to avoid copying between Chunk[Byte] and ByteBuffer during writes within the selector thread, * as it can be expensive depending on particular implementation of Chunk. */ - private class WriterDatagram(val remote: InetSocketAddress, val bytes: ByteBuffer) + private class WriterDatagram(val remote: Option[InetSocketAddress], val bytes: ByteBuffer) def unsafe(threadFactory: ThreadFactory): AsynchronousDatagramSocketGroup = new AsynchronousDatagramSocketGroup { @@ -218,13 +219,14 @@ private[net] object AsynchronousDatagramSocketGroup { override def write( key: SelectionKey, - datagram: Datagram, + bytes: Chunk[Byte], + remote: Option[InetSocketAddress], cb: Option[Throwable] => Unit ): () => Unit = { val writerId = ids.getAndIncrement() val writerDatagram = { - val bytes = { - val srcBytes = datagram.bytes.toArraySlice + val bytes0 = { + val srcBytes = bytes.toArraySlice if (srcBytes.size == srcBytes.values.size) srcBytes.values else { val destBytes = new Array[Byte](srcBytes.size) @@ -232,7 +234,7 @@ private[net] object AsynchronousDatagramSocketGroup { destBytes } } - new WriterDatagram(datagram.remote.toInetSocketAddress, ByteBuffer.wrap(bytes)) + new WriterDatagram(remote, ByteBuffer.wrap(bytes0)) } val attachment = key.attachment.asInstanceOf[Attachment] onSelectorThread { @@ -259,7 +261,11 @@ private[net] object AsynchronousDatagramSocketGroup { cb: Option[Throwable] => Unit ): Boolean = try { - val sent = channel.send(datagram.bytes, datagram.remote) + val remote = datagram.remote + val sent = remote match { + case Some(addr) => channel.send(datagram.bytes, addr) + case None => channel.write(datagram.bytes) + } if (sent > 0) { cb(None) true diff --git a/io/jvm/src/main/scala/fs2/io/net/AutoDetectingUnixDatagramSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/AutoDetectingUnixDatagramSocketsProvider.scala new file mode 100644 index 0000000000..db7e0d4df7 --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/AutoDetectingUnixDatagramSocketsProvider.scala @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.{Async, IO, LiftIO} +import fs2.io.file.Files + +private[net] object AutoDetectingUnixDatagramSocketsProvider { + def forIO: UnixDatagramSocketsProvider[IO] = forLiftIO + + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixDatagramSocketsProvider[F] = { + val _ = LiftIO[F] + forAsyncAndFiles + } + + def forAsyncAndFiles[F[_]: Async: Files]: UnixDatagramSocketsProvider[F] = + if (JnrUnixSocketsProvider.supported) JnrUnixDatagramSocketsProvider.forAsyncAndFiles + else + throw new UnsupportedOperationException( + """Unix datagram sockets only supported by having "com.github.jnr" % "jnr-unixsocket" % on the classpath""" + ) + + def forAsync[F[_]](implicit F: Async[F]): UnixDatagramSocketsProvider[F] = + forAsyncAndFiles(F, Files.forAsync(F)) +} diff --git a/io/jvm/src/main/scala/fs2/io/net/AutoDetectingUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/AutoDetectingUnixSocketsProvider.scala new file mode 100644 index 0000000000..b69c91e6c9 --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/AutoDetectingUnixSocketsProvider.scala @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.{Async, IO, LiftIO} + +import fs2.io.file.Files + +private[net] object AutoDetectingUnixSocketsProvider { + def forIO: UnixSocketsProvider[IO] = forLiftIO + + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSocketsProvider[F] = { + val _ = LiftIO[F] + forAsyncAndFiles + } + + def forAsyncAndFiles[F[_]: Async: Files]: UnixSocketsProvider[F] = + if (JdkUnixSocketsProvider.supported) JdkUnixSocketsProvider.forAsyncAndFiles + else if (JnrUnixSocketsProvider.supported) JnrUnixSocketsProvider.forAsyncAndFiles + else + throw new UnsupportedOperationException( + """Must either run on JDK 16+ or have "com.github.jnr" % "jnr-unixsocket" % on the classpath""" + ) + + def forAsync[F[_]](implicit F: Async[F]): UnixSocketsProvider[F] = + forAsyncAndFiles(F, Files.forAsync(F)) +} diff --git a/io/jvm/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala index 987f29a519..71ed8d5291 100644 --- a/io/jvm/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala @@ -23,111 +23,6 @@ package fs2 package io package net -import java.net.{InetSocketAddress, NetworkInterface} -import java.nio.channels.DatagramChannel - -import cats.effect.kernel.{Async, Resource} -import cats.syntax.all._ - -import com.comcast.ip4s._ - private[net] trait DatagramSocketGroupCompanionPlatform { type ProtocolFamily = java.net.ProtocolFamily - - def unsafe[F[_]: Async: Dns](adsg: AsynchronousDatagramSocketGroup): DatagramSocketGroup[F] = - new AsyncDatagramSocketGroup(adsg) - - private final class AsyncDatagramSocketGroup[F[_]: Async: Dns]( - adsg: AsynchronousDatagramSocketGroup - ) extends DatagramSocketGroup[F] { - def openDatagramSocket( - address: Option[Host], - port: Option[Port], - options: List[SocketOption], - protocolFamily: Option[ProtocolFamily] - ): Resource[F, DatagramSocket[F]] = - Resource.eval(address.traverse(_.resolve[F])).flatMap { addr => - val mkChannel = Async[F].delay { - val channel = protocolFamily - .map(pf => DatagramChannel.open(pf)) - .getOrElse(DatagramChannel.open()) - options.foreach(o => channel.setOption[o.Value](o.key, o.value)) - channel.bind( - new InetSocketAddress(addr.map(_.toInetAddress).orNull, port.map(_.value).getOrElse(0)) - ) - channel - } - Resource.make(mkChannel)(c => Async[F].delay(c.close())).flatMap(mkSocket) - } - - private def mkSocket( - channel: DatagramChannel - ): Resource[F, DatagramSocket[F]] = - Resource(Async[F].delay { - val ctx0 = adsg.register(channel) - - val socket = new DatagramSocket[F] { - private val ctx = ctx0 - - def localAddress: F[SocketAddress[IpAddress]] = - Async[F].delay { - val addr = - Option(channel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress]) - .getOrElse(throw new ClosedChannelException) - SocketAddress.fromInetSocketAddress(addr) - } - - def read: F[Datagram] = - Async[F].async[Datagram] { cb => - Async[F].delay { - val cancel = adsg.read(ctx, result => cb(result)) - Some(Async[F].delay(cancel())) - } - } - - def reads: Stream[F, Datagram] = - Stream.repeatEval(read) - - def write(datagram: Datagram): F[Unit] = - Async[F].async[Unit] { cb => - Async[F].delay { - val cancel = adsg.write(ctx, datagram, t => cb(t.toLeft(()))) - Some(Async[F].delay(cancel())) - } - } - - def writes: Pipe[F, Datagram, Nothing] = - _.foreach(write) - - def close: F[Unit] = Async[F].delay(adsg.close(ctx)) - - def join( - join: MulticastJoin[IpAddress], - interface: NetworkInterface - ): F[GroupMembership] = - Async[F].delay { - val membership = join.fold( - j => channel.join(j.group.address.toInetAddress, interface), - j => channel.join(j.group.address.toInetAddress, interface, j.source.toInetAddress) - ) - new GroupMembership { - def drop = Async[F].delay(membership.drop) - def block(source: IpAddress) = - Async[F].delay { membership.block(source.toInetAddress); () } - def unblock(source: IpAddress) = - Async[F].delay { membership.unblock(source.toInetAddress); () } - override def toString = "GroupMembership" - } - } - - override def toString = - s"Socket(${Option( - channel.socket.getLocalSocketAddress - ).getOrElse("")})" - } - - (socket, Async[F].delay(adsg.close(ctx0))) - }) - - } } diff --git a/io/jvm/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala index a25fc1ea0b..f572640888 100644 --- a/io/jvm/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala @@ -37,5 +37,6 @@ private[net] trait DatagramSocketPlatform[F[_]] { } private[net] trait DatagramSocketCompanionPlatform { + @deprecated("Use com.comcast.ip4s.NetworkInterface", "3.13.0") type NetworkInterface = java.net.NetworkInterface } diff --git a/io/jvm/src/main/scala/fs2/io/net/unixsocket/JdkUnixSockets.scala b/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala similarity index 65% rename from io/jvm/src/main/scala/fs2/io/net/unixsocket/JdkUnixSockets.scala rename to io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala index d1e476568a..53f91fe47c 100644 --- a/io/jvm/src/main/scala/fs2/io/net/unixsocket/JdkUnixSockets.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala @@ -19,29 +19,35 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package fs2.io.net.unixsocket +package fs2 +package io +package net -import cats.effect.kernel.{Async, Resource} +import cats.syntax.all._ +import cats.effect.{Async, Resource} import cats.effect.syntax.all._ + +import com.comcast.ip4s.UnixSocketAddress + import fs2.io.file.Files -import fs2.io.evalOnVirtualThreadIfAvailable + import java.net.{StandardProtocolFamily, UnixDomainSocketAddress} import java.nio.channels.{ServerSocketChannel, SocketChannel} -object JdkUnixSockets { +private[net] object JdkUnixSocketsProvider { def supported: Boolean = StandardProtocolFamily.values.size > 2 - def forAsyncAndFiles[F[_]: Async: Files]: UnixSockets[F] = - new JdkUnixSocketsImpl[F] + def forAsyncAndFiles[F[_]: Async: Files]: UnixSocketsProvider[F] = + new JdkUnixSocketsProvider[F] - def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = + def forAsync[F[_]](implicit F: Async[F]): UnixSocketsProvider[F] = forAsyncAndFiles(F, Files.forAsync[F]) } -private[unixsocket] class JdkUnixSocketsImpl[F[_]: Files](implicit F: Async[F]) - extends UnixSockets.AsyncUnixSockets[F] { - protected def openChannel(address: UnixSocketAddress) = +private[net] class JdkUnixSocketsProvider[F[_]: Files](implicit F: Async[F]) + extends AsyncUnixSocketsProvider[F] { + protected def openChannel(address: UnixSocketAddress, options: List[SocketOption]) = evalOnVirtualThreadIfAvailable( Resource .make( @@ -49,11 +55,12 @@ private[unixsocket] class JdkUnixSocketsImpl[F[_]: Files](implicit F: Async[F]) )(ch => F.blocking(ch.close())) .evalTap { ch => F.blocking(ch.connect(UnixDomainSocketAddress.of(address.path))) - .cancelable(F.blocking(ch.close())) + .cancelable(F.blocking(ch.close())) *> + F.delay(options.foreach(o => ch.setOption(o.key, o.value))) } ) - protected def openServerChannel(address: UnixSocketAddress) = + protected def openServerChannel(address: UnixSocketAddress, options: List[SocketOption]) = evalOnVirtualThreadIfAvailable( Resource .make( @@ -64,10 +71,12 @@ private[unixsocket] class JdkUnixSocketsImpl[F[_]: Files](implicit F: Async[F]) .cancelable(F.blocking(sch.close())) } .map { sch => - Resource.makeFull[F, SocketChannel] { poll => - poll(F.blocking(sch.accept).cancelable(F.blocking(sch.close()))) - }(ch => F.blocking(ch.close())) + SocketInfo.forAsync(sch) -> + Resource + .makeFull[F, SocketChannel] { poll => + poll(F.blocking(sch.accept).cancelable(F.blocking(sch.close()))) + }(ch => F.blocking(ch.close())) + .evalTap(ch => F.delay(options.foreach(o => ch.setOption(o.key, o.value)))) } ) - } diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala new file mode 100644 index 0000000000..879491f213 --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.{Async, Resource} +import cats.effect.std.Mutex +import cats.effect.syntax.all._ +import cats.syntax.all._ + +import com.comcast.ip4s.{ + GenSocketAddress, + IpAddress, + MulticastJoin, + NetworkInterface, + UnixSocketAddress +} + +import fs2.io.file.Files + +import java.nio.{Buffer, ByteBuffer} +import jnr.unixsocket.{UnixDatagramChannel, UnixSocketAddress => JnrUnixSocketAddress} + +import CollectionCompat.* + +private[net] object JnrUnixDatagramSocketsProvider { + + lazy val supported: Boolean = + try { + Class.forName("jnr.unixsocket.UnixSocketChannel") + true + } catch { + case _: ClassNotFoundException => false + } + + def forAsyncAndFiles[F[_]: Async: Files]: UnixDatagramSocketsProvider[F] = + new JnrUnixDatagramSocketsProvider[F] + + def forAsync[F[_]](implicit F: Async[F]): UnixDatagramSocketsProvider[F] = + forAsyncAndFiles(F, Files.forAsync[F]) +} + +private[net] class JnrUnixDatagramSocketsProvider[F[_]](implicit F: Async[F], F2: Files[F]) + extends UnixDatagramSocketsProvider[F] { + + override def bindDatagramSocket( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] = { + val (filteredOptions, delete) = SocketOption.extractUnixSocketDeletes(options, address) + + delete *> Resource + .make(F.blocking(UnixDatagramChannel.open()))(ch => F.blocking(ch.close())) + .evalTap { ch => + F.blocking(ch.bind(new JnrUnixSocketAddress(address.path))) + .cancelable(F.blocking(ch.close())) + } + .evalTap(ch => F.delay(filteredOptions.foreach(o => ch.setOption(o.key, o.value)))) + .evalMap { ch => + Mutex[F].map(ch -> _) + } + .map { case (ch, readMutex) => + val address0 = address + new DatagramSocket[F] { + private val readBuffer = ByteBuffer.allocate(1 << 16) + + override val address = address0 + override def localAddress = F.delay(address.asIpUnsafe) + + override def supportedOptions = + F.delay(ch.supportedOptions.asScala.toSet) + + override def getOption[A](key: SocketOption.Key[A]) = + F.delay { + try + Some(ch.getOption(key)) + catch { + case _: UnsupportedOperationException => None + } + } + + override def setOption[A](key: SocketOption.Key[A], value: A) = + F.delay(ch.setOption(key, value)).void + + private def withUnixSocketAddress[A]( + address: GenSocketAddress + )(f: UnixSocketAddress => A): A = + address match { + case u: UnixSocketAddress => f(u) + case _ => + throw new IllegalArgumentException( + s"Unsupported address type $address; must pass a UnixSocketAddress" + ) + } + + private def blockingAndCloseIfCanceled[A](a: => A): F[A] = + F.blocking(a).cancelable(F.blocking(ch.close())) + + override def connect(address: GenSocketAddress) = + blockingAndCloseIfCanceled { + withUnixSocketAddress(address) { u => + ch.connect(new JnrUnixSocketAddress(u.path)) + () + } + } + + override def disconnect = + blockingAndCloseIfCanceled { + ch.disconnect() + () + } + + override def read = readGen.map(_.toDatagram) + + override def readGen = readMutex.lock.surround { + blockingAndCloseIfCanceled { + val clientAddress = ch.receive(readBuffer) + val read = readBuffer.position() + val result = + if (read == 0) Chunk.empty + else { + val dest = new Array[Byte](read) + (readBuffer: Buffer).flip() + readBuffer.get(dest) + Chunk.array(dest) + } + (readBuffer: Buffer).clear() + GenDatagram(UnixSocketAddress(clientAddress.path), result) + } + } + + override def reads = Stream.repeatEval(read) + + override def write(bytes: Chunk[Byte]) = + blockingAndCloseIfCanceled { + ch.send( + bytes.toByteBuffer, + ch.getRemoteSocketAddress + ) // note: shouldn't be necessary to pass remote socket address but jnr throws an error otherwise + () + } + + override def write(bytes: Chunk[Byte], address: GenSocketAddress) = + blockingAndCloseIfCanceled { + withUnixSocketAddress(address) { u => + val buffer = bytes.toByteBuffer + val target = new JnrUnixSocketAddress(u.path) + ch.send(buffer, target) + () + } + } + + override def write(datagram: Datagram) = + F.raiseError( + new UnsupportedOperationException( + "Unix datagram socket does not support variant of write that takes a Datagram. Use variant that takes a GenSocketAddress or use connect & write(bytes)" + ) + ) + + override def writes = _.foreach(write) + + override def join( + join: MulticastJoin[IpAddress], + interface: NetworkInterface + ) = + F.raiseError( + new UnsupportedOperationException("Multicast not supported on unix datagram sockets") + ) + + @deprecated("Use overload that takes a com.comcast.ip4s.NetworkInterface", "3.13.0") + override def join( + join: MulticastJoin[IpAddress], + interface: DatagramSocket.NetworkInterface + ) = + F.raiseError( + new UnsupportedOperationException("Multicast not supported on unix datagram sockets") + ) + } + } + } +} diff --git a/io/jvm/src/main/scala/fs2/io/net/unixsocket/JnrUnixSockets.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala similarity index 51% rename from io/jvm/src/main/scala/fs2/io/net/unixsocket/JnrUnixSockets.scala rename to io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala index 787b721412..9c220322f3 100644 --- a/io/jvm/src/main/scala/fs2/io/net/unixsocket/JnrUnixSockets.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala @@ -19,11 +19,17 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package fs2.io.net.unixsocket +package fs2 +package io +package net -import cats.effect.kernel.{Async, Resource} +import cats.effect.{Async, Resource} import cats.effect.syntax.all._ + +import com.comcast.ip4s.UnixSocketAddress + import fs2.io.file.Files + import java.nio.channels.SocketChannel import jnr.unixsocket.{ UnixServerSocketChannel, @@ -31,7 +37,7 @@ import jnr.unixsocket.{ UnixSocketChannel } -object JnrUnixSockets { +private[net] object JnrUnixSocketsProvider { lazy val supported: Boolean = try { @@ -41,21 +47,21 @@ object JnrUnixSockets { case _: ClassNotFoundException => false } - def forAsyncAndFiles[F[_]: Async: Files]: UnixSockets[F] = - new JnrUnixSocketsImpl[F] - - def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = - forAsyncAndFiles(F, Files.forAsync[F]) + def forAsyncAndFiles[F[_]](implicit F: Async[F], F2: Files[F]): UnixSocketsProvider[F] = + new JnrUnixSocketsProvider[F] } -private[unixsocket] class JnrUnixSocketsImpl[F[_]: Files](implicit F: Async[F]) - extends UnixSockets.AsyncUnixSockets[F] { - protected def openChannel(address: UnixSocketAddress) = - Resource.make(F.blocking(UnixSocketChannel.open(new JnrUnixSocketAddress(address.path))))(ch => - F.blocking(ch.close()) - ) +private[net] class JnrUnixSocketsProvider[F[_]](implicit F: Async[F], F2: Files[F]) + extends AsyncUnixSocketsProvider[F] { - protected def openServerChannel(address: UnixSocketAddress) = + protected def openChannel(address: UnixSocketAddress, options: List[SocketOption]) = + Resource + .make(F.blocking(UnixSocketChannel.open(new JnrUnixSocketAddress(address.path))))(ch => + F.blocking(ch.close()) + ) + .evalTap(ch => F.delay(options.foreach(o => ch.setOption(o.key, o.value)))) + + protected def openServerChannel(address: UnixSocketAddress, options: List[SocketOption]) = Resource .make(F.blocking(UnixServerSocketChannel.open()))(ch => F.blocking(ch.close())) .evalTap { sch => @@ -63,9 +69,24 @@ private[unixsocket] class JnrUnixSocketsImpl[F[_]: Files](implicit F: Async[F]) .cancelable(F.blocking(sch.close())) } .map { sch => - Resource.makeFull[F, SocketChannel] { poll => - F.widen(poll(F.blocking(sch.accept).cancelable(F.blocking(sch.close())))) - }(ch => F.blocking(ch.close())) + def raiseOptionError[A]: F[A] = + F.raiseError( + new UnsupportedOperationException( + "JNR unix server sockets do not support socket options" + ) + ) + val address0 = address + val info: SocketInfo[F] = new SocketInfo[F] { + override def address = address0 + override def supportedOptions = F.pure(Set.empty) + override def getOption[A](key: SocketOption.Key[A]) = raiseOptionError + override def setOption[A](key: SocketOption.Key[A], value: A) = raiseOptionError + } + info -> + Resource + .makeFull[F, SocketChannel] { poll => + F.widen(poll(F.blocking(sch.accept).cancelable(F.blocking(sch.close())))) + }(ch => F.blocking(ch.close())) + .evalTap(ch => F.delay(options.foreach(o => ch.setOption(o.key, o.value)))) } - } diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index 4c258c4021..f7ff010860 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -28,13 +28,10 @@ import cats.effect.LiftIO import cats.effect.Selector import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{Dns, Host, IpAddress, Port, SocketAddress} +import com.comcast.ip4s.{Dns, GenSocketAddress} import fs2.internal.ThreadFactories -import fs2.io.net.tls.TLSContext -import java.net.ProtocolFamily -import java.nio.channels.AsynchronousChannelGroup import java.util.concurrent.ThreadFactory private[net] trait NetworkPlatform[F[_]] { @@ -49,6 +46,10 @@ private[net] trait NetworkPlatform[F[_]] { * @param threadCount number of threads to allocate in the fixed thread pool backing the NIO channel group * @param threadFactory factory used to create fixed threads */ + @deprecated( + "3.13.0", + "Explicitly managed socket groups are no longer supported; use connect and bind operations on Network instead" + ) def socketGroup( threadCount: Int = 1, threadFactory: ThreadFactory = ThreadFactories.named("fs2-tcp", true) @@ -63,6 +64,10 @@ private[net] trait NetworkPlatform[F[_]] { * * @param threadFactory factory used to create selector thread */ + @deprecated( + "3.13.0", + "Explicitly managed socket groups are no longer supported; use bindDatagramSocket operation on Network instead" + ) def datagramSocketGroup( threadFactory: ThreadFactory = ThreadFactories.named("fs2-udp", true) ): Resource[F, DatagramSocketGroup[F]] @@ -70,123 +75,85 @@ private[net] trait NetworkPlatform[F[_]] { } private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: Network.type => - private lazy val globalAcg = AsynchronousChannelGroup.withFixedThreadPool( - 1, - ThreadFactories.named("fs2-global-tcp", true) - ) - private lazy val globalAdsg = - AsynchronousDatagramSocketGroup.unsafe(ThreadFactories.named("fs2-global-udp", true)) - - def forIO: Network[IO] = forLiftIO implicit def forLiftIO[F[_]: Async: LiftIO]: Network[F] = - new UnsealedNetwork[F] { + new AsyncNetwork[F] { private lazy val fallback = forAsync[F] private def tryGetSelector = IO.pollers.map(_.collectFirst { case selector: Selector => selector }).to[F] - private implicit def dns: Dns[F] = Dns.forAsync[F] + private implicit def dnsInstance: Dns[F] = dns - def socketGroup(threadCount: Int, threadFactory: ThreadFactory): Resource[F, SocketGroup[F]] = + private def selecting[A]( + ifSelecting: SelectingIpSocketsProvider[F] => Resource[F, A], + orElse: => Resource[F, A] + ): Resource[F, A] = Resource.eval(tryGetSelector).flatMap { - case Some(selector) => Resource.pure(new SelectingSocketGroup[F](selector)) - case None => fallback.socketGroup(threadCount, threadFactory) + case Some(selector) => ifSelecting(new SelectingIpSocketsProvider(selector)) + case None => orElse } - def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = - fallback.datagramSocketGroup(threadFactory) - - def client( - to: SocketAddress[Host], + override def connect( + address: GenSocketAddress, options: List[SocketOption] - ): Resource[F, Socket[F]] = Resource.eval(tryGetSelector).flatMap { - case Some(selector) => new SelectingSocketGroup(selector).client(to, options) - case None => fallback.client(to, options) - } - - def server( - address: Option[Host], - port: Option[Port], + ): Resource[F, Socket[F]] = + matchAddress( + address, + sa => selecting(_.connectIp(sa, options), fallback.connect(sa, options)), + ua => fallback.connect(ua, options) + ) + + override def bind( + address: GenSocketAddress, options: List[SocketOption] - ): Stream[F, Socket[F]] = Stream.eval(tryGetSelector).flatMap { - case Some(selector) => new SelectingSocketGroup(selector).server(address, port, options) - case None => fallback.server(address, port, options) - } - - def serverResource( - address: Option[Host], - port: Option[Port], + ): Resource[F, ServerSocket[F]] = + matchAddress( + address, + sa => selecting(_.bindIp(sa, options), fallback.bind(sa, options)), + ua => fallback.bind(ua, options) + ) + + override def bindDatagramSocket( + address: GenSocketAddress, options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - Resource.eval(tryGetSelector).flatMap { - case Some(selector) => - new SelectingSocketGroup(selector).serverResource(address, port, options) - case None => fallback.serverResource(address, port, options) - } - - def openDatagramSocket( - address: Option[Host], - port: Option[Port], - options: List[SocketOption], - protocolFamily: Option[ProtocolFamily] ): Resource[F, DatagramSocket[F]] = - fallback.openDatagramSocket(address, port, options, protocolFamily) + fallback.bindDatagramSocket(address, options) + + // Implementations of deprecated operations - def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync[F] + override def datagramSocketGroup( + threadFactory: ThreadFactory + ): Resource[F, DatagramSocketGroup[F]] = + Resource.pure(this) + + override def socketGroup( + threadCount: Int, + threadFactory: ThreadFactory + ): Resource[F, SocketGroup[F]] = + Resource.pure(this) } + def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = { + val _ = dns + forAsync + } + def forAsync[F[_]](implicit F: Async[F]): Network[F] = - forAsyncAndDns(F, Dns.forAsync(F)) + new AsyncProviderBasedNetwork[F] { + protected def mkIpSocketsProvider = AsynchronousChannelGroupIpSocketsProvider.forAsync[F] + protected def mkUnixSocketsProvider = AutoDetectingUnixSocketsProvider.forAsync[F] - def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = - new UnsealedNetwork[F] { - private lazy val globalSocketGroup = SocketGroup.unsafe[F](globalAcg) - private lazy val globalDatagramSocketGroup = DatagramSocketGroup.unsafe[F](globalAdsg) + protected def mkIpDatagramSocketsProvider = AsyncIpDatagramSocketsProvider.forAsync[F] + protected def mkUnixDatagramSocketsProvider = + AutoDetectingUnixDatagramSocketsProvider.forAsync[F] - def socketGroup(threadCount: Int, threadFactory: ThreadFactory): Resource[F, SocketGroup[F]] = - Resource - .make( - F.delay( - AsynchronousChannelGroup.withFixedThreadPool(threadCount, threadFactory) - ) - )(acg => F.delay(acg.shutdown())) - .map(SocketGroup.unsafe[F](_)) + // Implementations of deprecated operations def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = - Resource - .make(F.delay(AsynchronousDatagramSocketGroup.unsafe(threadFactory)))(adsg => - F.delay(adsg.close()) - ) - .map(DatagramSocketGroup.unsafe[F](_)) - - def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = globalSocketGroup.client(to, options) - - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = globalSocketGroup.server(address, port, options) + Resource.pure(this) - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - globalSocketGroup.serverResource(address, port, options) - - def openDatagramSocket( - address: Option[Host], - port: Option[Port], - options: List[SocketOption], - protocolFamily: Option[ProtocolFamily] - ): Resource[F, DatagramSocket[F]] = - globalDatagramSocketGroup.openDatagramSocket(address, port, options, protocolFamily) - - def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync[F] + def socketGroup(threadCount: Int, threadFactory: ThreadFactory): Resource[F, SocketGroup[F]] = + Resource.pure(this) } - } diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala similarity index 55% rename from io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala rename to io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala index b6a384f56f..a517d689ca 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala @@ -24,29 +24,25 @@ package io.net import scala.concurrent.duration._ -import cats.effect.LiftIO -import cats.effect.Selector -import cats.effect.kernel.Async -import cats.effect.kernel.Resource +import cats.effect.{Async, LiftIO, Resource, Selector} import cats.syntax.all._ -import com.comcast.ip4s.Dns -import com.comcast.ip4s.Host -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.Port -import com.comcast.ip4s.SocketAddress +import com.comcast.ip4s.{Dns, Host, IpAddress, SocketAddress} import java.net.InetSocketAddress -import java.nio.channels.AsynchronousCloseException -import java.nio.channels.ClosedChannelException -import java.nio.channels.SelectionKey.OP_ACCEPT -import java.nio.channels.SelectionKey.OP_CONNECT -import java.nio.channels.SocketChannel +import java.nio.channels.{ + AsynchronousCloseException, + ClosedChannelException, + SelectionKey, + SocketChannel +} -private final class SelectingSocketGroup[F[_]: LiftIO: Dns](selector: Selector)(implicit - F: Async[F] -) extends SocketGroup[F] { +private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implicit + F: Async[F], + F2: LiftIO[F], + F3: Dns[F] +) extends IpSocketsProvider[F] { - def client( + override def connectIp( to: SocketAddress[Host], options: List[SocketOption] ): Resource[F, Socket[F]] = @@ -63,43 +59,31 @@ private final class SelectingSocketGroup[F[_]: LiftIO: Dns](selector: Selector)( val connect = to.resolve.flatMap { ip => F.delay(ch.connect(ip.toInetSocketAddress)).flatMap { connected => selector - .select(ch, OP_CONNECT) + .select(ch, SelectionKey.OP_CONNECT) .to .untilM_(F.delay(ch.finishConnect())) - .unlessA(connected) + .unlessA(connected) *> F + .delay { + localAddress(ch) -> remoteAddress(ch) + } + .flatMap { case (addr, peerAddr) => + SelectingSocket[F]( + selector, + ch, + addr, + peerAddr + ) + } } } - val make = SelectingSocket[F]( - selector, - ch, - localAddress(ch), - remoteAddress(ch) - ) - - configure *> connect *> make + configure *> connect } - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = - Stream - .resource( - serverResource( - address, - port, - options - ) - ) - .flatMap { case (_, clients) => clients } - - def serverResource( - address: Option[Host], - port: Option[Port], + override def bindIp( + address: SocketAddress[Host], options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = + ): Resource[F, ServerSocket[F]] = Resource .make(F.delay(selector.provider.openServerSocketChannel())) { ch => def waitForDeregistration: F[Unit] = @@ -108,14 +92,14 @@ private final class SelectingSocketGroup[F[_]: LiftIO: Dns](selector: Selector)( F.delay(ch.isRegistered()).ifM(F.sleep(2.millis) >> waitForDeregistration, F.unit) F.delay(ch.close()) >> waitForDeregistration } - .evalMap { serverCh => - val configure = address.traverse(_.resolve).flatMap { ip => + .evalMap { sch => + val configure = address.host.resolve.flatMap { addr => F.delay { - serverCh.configureBlocking(false) - serverCh.bind( + sch.configureBlocking(false) + sch.bind( new InetSocketAddress( - ip.map(_.toInetAddress).orNull, - port.map(_.value).getOrElse(0) + if (addr.isWildcard) null else addr.toInetAddress, + address.port.value ) ) } @@ -124,8 +108,8 @@ private final class SelectingSocketGroup[F[_]: LiftIO: Dns](selector: Selector)( def acceptLoop: Stream[F, SocketChannel] = Stream .bracketFull[F, SocketChannel] { poll => def go: F[SocketChannel] = - F.delay(serverCh.accept()).flatMap { - case null => poll(selector.select(serverCh, OP_ACCEPT).to) *> go + F.delay(sch.accept()).flatMap { + case null => poll(selector.select(sch, SelectionKey.OP_ACCEPT).to) *> go case ch => F.pure(ch) } go @@ -136,39 +120,31 @@ private final class SelectingSocketGroup[F[_]: LiftIO: Dns](selector: Selector)( case ex => Stream.raiseError(ex) } - val clients = acceptLoop.evalMap { ch => + val accept = acceptLoop.evalMap { ch => F.delay { ch.configureBlocking(false) options.foreach(opt => ch.setOption(opt.key, opt.value)) - } *> SelectingSocket[F]( - selector, - ch, - localAddress(ch), - remoteAddress(ch) - ) - } - - val socketAddress = F.delay { - SocketAddress.fromInetSocketAddress( - serverCh.getLocalAddress.asInstanceOf[InetSocketAddress] - ) + localAddress(ch) -> remoteAddress(ch) + }.flatMap { case (addr, peerAddr) => + SelectingSocket[F]( + selector, + ch, + addr, + peerAddr + ) + } } - configure *> socketAddress.tupleRight(clients) + configure *> F.delay(ServerSocket(SocketInfo.forAsync(sch), accept)) } - private def localAddress(ch: SocketChannel) = - F.delay { - SocketAddress.fromInetSocketAddress( - ch.getLocalAddress.asInstanceOf[InetSocketAddress] - ) - } - - private def remoteAddress(ch: SocketChannel) = - F.delay { - SocketAddress.fromInetSocketAddress( - ch.getRemoteAddress.asInstanceOf[InetSocketAddress] - ) - } + private def localAddress(ch: SocketChannel): SocketAddress[IpAddress] = + SocketAddress.fromInetSocketAddress( + ch.getLocalAddress.asInstanceOf[InetSocketAddress] + ) + private def remoteAddress(ch: SocketChannel): SocketAddress[IpAddress] = + SocketAddress.fromInetSocketAddress( + ch.getRemoteAddress.asInstanceOf[InetSocketAddress] + ) } diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala index 3c1b424912..2b7cf81031 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala @@ -29,8 +29,7 @@ import cats.effect.Selector import cats.effect.kernel.Async import cats.effect.std.Mutex import cats.syntax.all._ -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.SocketAddress +import com.comcast.ip4s.{IpAddress, SocketAddress} import java.nio.ByteBuffer import java.nio.channels.SelectionKey.OP_READ @@ -42,10 +41,20 @@ private final class SelectingSocket[F[_]: LiftIO] private ( ch: SocketChannel, readMutex: Mutex[F], writeMutex: Mutex[F], - val localAddress: F[SocketAddress[IpAddress]], - val remoteAddress: F[SocketAddress[IpAddress]] + override val address: SocketAddress[IpAddress], + val peerAddress: SocketAddress[IpAddress] )(implicit F: Async[F]) - extends Socket.BufferedReads(readMutex) { + extends Socket.BufferedReads(readMutex) + with SocketInfo.AsyncSocketInfo[F] { + + protected def asyncInstance = F + protected def channel = ch + + override def localAddress: F[SocketAddress[IpAddress]] = + asyncInstance.pure(address) + + override def remoteAddress: F[SocketAddress[IpAddress]] = + asyncInstance.pure(peerAddress) protected def readChunk(buf: ByteBuffer): F[Int] = F.delay(ch.read(buf)).flatMap { readed => @@ -79,6 +88,7 @@ private final class SelectingSocket[F[_]: LiftIO] private ( F.delay { ch.shutdownInput(); () } + override def sendFile( file: FileHandle[F], offset: Long, @@ -111,8 +121,8 @@ private object SelectingSocket { def apply[F[_]: LiftIO]( selector: Selector, ch: SocketChannel, - localAddress: F[SocketAddress[IpAddress]], - remoteAddress: F[SocketAddress[IpAddress]] + address: SocketAddress[IpAddress], + remoteAddress: SocketAddress[IpAddress] )(implicit F: Async[F]): F[Socket[F]] = (Mutex[F], Mutex[F]).flatMapN { (readMutex, writeMutex) => F.delay { @@ -121,7 +131,7 @@ private object SelectingSocket { ch, readMutex, writeMutex, - localAddress, + address, remoteAddress ) } diff --git a/io/shared/src/test/scala/fs2/io/net/unixsocket/UnixSocketsSuite.scala b/io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala similarity index 54% rename from io/shared/src/test/scala/fs2/io/net/unixsocket/UnixSocketsSuite.scala rename to io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala index 09fe206d5b..cdb81d82d1 100644 --- a/io/shared/src/test/scala/fs2/io/net/unixsocket/UnixSocketsSuite.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala @@ -20,36 +20,23 @@ */ package fs2 -package io.net.unixsocket - -import scala.concurrent.duration._ - -import cats.effect.IO - -class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { - - def testProvider(provider: String)(implicit sockets: UnixSockets[IO]) = - test(s"echoes - $provider") { - val address = UnixSocketAddress("fs2-unix-sockets-test.sock") - - val server = UnixSockets[IO] - .server(address) - .map { client => - client.reads.through(client.writes) - } - .parJoinUnbounded - - def client(msg: Chunk[Byte]) = UnixSockets[IO].client(address).use { server => - server.write(msg) *> server.endOfOutput *> server.reads.compile - .to(Chunk) - .map(read => assertEquals(read, msg)) - } - - val clients = (0 until 100).map(b => client(Chunk.singleton(b.toByte))) - - (Stream.sleep_[IO](1.second) ++ Stream.emits(clients).evalMap(identity)) - .concurrently(server) - .compile - .drain +package io +package net + +import com.comcast.ip4s.{GenSocketAddress, SocketAddress, UnixSocketAddress} +import java.net.{SocketAddress => JSocketAddress, InetSocketAddress, UnixDomainSocketAddress} +import jnr.unixsocket.{UnixSocketAddress => JnrUnixSocketAddress} + +private[net] object SocketAddressHelpers { + + def toGenSocketAddress(address: JSocketAddress): GenSocketAddress = + address match { + case addr: InetSocketAddress => SocketAddress.fromInetSocketAddress(addr) + case _ => + if (JdkUnixSocketsProvider.supported && address.isInstanceOf[UnixDomainSocketAddress]) { + UnixSocketAddress(address.asInstanceOf[UnixDomainSocketAddress].getPath.toString) + } else if (JnrUnixSocketsProvider.supported && address.isInstanceOf[JnrUnixSocketAddress]) { + UnixSocketAddress(address.asInstanceOf[JnrUnixSocketAddress].path) + } else throw new IllegalArgumentException("Unsupported address type: " + address) } } diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/DTLSSocket.scala b/io/jvm/src/main/scala/fs2/io/net/tls/DTLSSocket.scala index 649c1b5a6f..b75167ed5a 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/DTLSSocket.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/DTLSSocket.scala @@ -24,7 +24,7 @@ package io package net package tls -import java.net.NetworkInterface +import java.net.{NetworkInterface => JNetworkInterface} import cats.Applicative import cats.effect.kernel.{Async, Resource, Sync} @@ -63,25 +63,65 @@ object DTLSSocket { ): F[DTLSSocket[F]] = Applicative[F].pure { new DTLSSocket[F] { + override def connect(address: GenSocketAddress) = + socket.connect(address) - def read: F[Datagram] = + override def disconnect = + socket.disconnect + + override def readGen: F[GenDatagram] = read.map(_.toGenDatagram) + + override def read: F[Datagram] = engine.read(Int.MaxValue).flatMap { case Some(bytes) => Applicative[F].pure(Datagram(remoteAddress, bytes)) case None => read } - def reads: Stream[F, Datagram] = + override def reads: Stream[F, Datagram] = Stream.repeatEval(read) - def write(datagram: Datagram): F[Unit] = + + override def write(bytes: Chunk[Byte]): F[Unit] = + engine.write(bytes) + + override def write(bytes: Chunk[Byte], address: GenSocketAddress): F[Unit] = + if (address == remoteAddress) engine.write(bytes) + else + new IOException( + "Cannot write to a DTLSSocket with an address that differs from the connected address" + ).raiseError + + override def write(datagram: Datagram): F[Unit] = engine.write(datagram.bytes) - def writes: Pipe[F, Datagram, Nothing] = + override def writes: Pipe[F, Datagram, Nothing] = _.foreach(write) - def localAddress: F[SocketAddress[IpAddress]] = socket.localAddress - def join(join: MulticastJoin[IpAddress], interface: NetworkInterface): F[GroupMembership] = + + override def address = socket.address + + @deprecated( + "3.13.0", + "Use address instead, which returns GenSocketAddress instead of F[SocketAddress[IpAddress]]. If ip and port are needed, call .asIpUnsafe" + ) + override def localAddress: F[SocketAddress[IpAddress]] = socket.localAddress + + override def join( + join: MulticastJoin[IpAddress], + interface: NetworkInterface + ): F[GroupMembership] = + Sync[F].raiseError(new RuntimeException("DTLSSocket does not support multicast")) + + override def join( + join: MulticastJoin[IpAddress], + interface: JNetworkInterface + ): F[GroupMembership] = Sync[F].raiseError(new RuntimeException("DTLSSocket does not support multicast")) - def beginHandshake: F[Unit] = engine.beginHandshake - def session: F[SSLSession] = engine.session + + override def supportedOptions = socket.supportedOptions + override def getOption[A](key: SocketOption.Key[A]) = socket.getOption(key) + override def setOption[A](key: SocketOption.Key[A], value: A) = socket.setOption(key, value) + + override def beginHandshake: F[Unit] = engine.beginHandshake + override def session: F[SSLSession] = engine.session } } } diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala index 681f7793ef..8d301331e9 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala @@ -40,7 +40,9 @@ import cats.syntax.all._ import com.comcast.ip4s.{IpAddress, SocketAddress} import java.util.function.BiFunction -import java.nio.file.Path +import java.nio.file.{Path => JPath} + +import fs2.io.file.Path private[tls] trait TLSContextPlatform[F[_]] { @@ -104,6 +106,14 @@ private[tls] trait TLSContextCompanionPlatform { self: TLSContext.type => def insecure: F[TLSContext[F]] /** Creates a `TLSContext` from the specified key store file. */ + @deprecated("Use overload that takes an fs2.io.file.Path instead", "3.13.0") + def fromKeyStoreFile( + file: JPath, + storePassword: Array[Char], + keyPassword: Array[Char] + ): F[TLSContext[F]] = + fromKeyStoreFile(Path.fromNioPath(file), storePassword, keyPassword) + def fromKeyStoreFile( file: Path, storePassword: Array[Char], @@ -186,7 +196,7 @@ private[tls] trait TLSContextCompanionPlatform { self: TLSContext.type => new TLSEngine.Binding[F] { def write(data: Chunk[Byte]): F[Unit] = if (data.isEmpty) Applicative[F].unit - else socket.write(Datagram(remoteAddress, data)) + else socket.write(data, remoteAddress) def read(maxBytes: Int): F[Option[Chunk[Byte]]] = socket.read.map(p => Some(p.bytes)) }, @@ -275,7 +285,7 @@ private[tls] trait TLSContextCompanionPlatform { self: TLSContext.type => storePassword: Array[Char], keyPassword: Array[Char] ): F[TLSContext[F]] = { - val load = Async[F].blocking(new FileInputStream(file.toFile): InputStream) + val load = Async[F].blocking(new FileInputStream(file.toNioPath.toFile): InputStream) val stream = Resource.make(load)(s => Async[F].blocking(s.close)) fromKeyStoreStream(stream, storePassword, keyPassword) } @@ -285,7 +295,11 @@ private[tls] trait TLSContextCompanionPlatform { self: TLSContext.type => storePassword: Array[Char], keyPassword: Array[Char] ): F[TLSContext[F]] = { - val load = Async[F].blocking(getClass.getClassLoader.getResourceAsStream(resource)) + val load = Async[F].blocking { + val s = getClass.getClassLoader.getResourceAsStream(resource) + if (s eq null) throw new IOException(s"Classpath resource not found [$resource]") + else s + } val stream = Resource.make(load)(s => Async[F].blocking(s.close)) fromKeyStoreStream(stream, storePassword, keyPassword) } diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/TLSEngine.scala b/io/jvm/src/main/scala/fs2/io/net/tls/TLSEngine.scala index 8829f56e69..cb7a4bdf23 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/TLSEngine.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/TLSEngine.scala @@ -215,9 +215,7 @@ private[tls] object TLSEngine { else binding.read(engine.getSession.getPacketBufferSize).flatMap { case Some(c) => unwrapBuffer.input(c) >> unwrapHandshake - case None => - unwrapBuffer.inputRemains - .flatMap(x => if (x > 0) Applicative[F].unit else stopUnwrap) + case None => stopUnwrap } } case SSLEngineResult.HandshakeStatus.NEED_UNWRAP_AGAIN => diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 66ecb83612..ae31cce0db 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -29,7 +29,7 @@ import cats.effect.std.Mutex import cats.effect.kernel._ import cats.syntax.all._ -import com.comcast.ip4s.{IpAddress, SocketAddress} +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} private[tls] trait TLSSocketPlatform[F[_]] { @@ -88,12 +88,29 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => def endOfInput: F[Unit] = socket.endOfInput + @deprecated("3.13.0", "Use address instead") def localAddress: F[SocketAddress[IpAddress]] = socket.localAddress + @deprecated("3.13.0", "Use peerAddress instead") def remoteAddress: F[SocketAddress[IpAddress]] = socket.remoteAddress + def address: GenSocketAddress = + socket.address + + def peerAddress: GenSocketAddress = + socket.peerAddress + + def supportedOptions: F[Set[SocketOption.Key[?]]] = + socket.supportedOptions + + def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = + socket.getOption(key) + + def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] = + socket.setOption(key, value) + def beginHandshake: F[Unit] = engine.beginHandshake @@ -103,6 +120,7 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => def applicationProtocol: F[String] = engine.applicationProtocol + @deprecated("3.13.0", "No replacement; sockets are open until they are finalized") def isOpen: F[Boolean] = socket.isOpen } } diff --git a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala index c12348984e..adcbc1ff30 100644 --- a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -19,146 +19,31 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package fs2.io.net.unixsocket -import fs2.io.file.SyncFileHandle -import cats.effect.IO -import cats.effect.LiftIO -import cats.effect.kernel.{Async, Resource} -import cats.effect.std.Mutex -import cats.effect.syntax.all._ -import cats.syntax.all._ -import com.comcast.ip4s.{IpAddress, SocketAddress} -import fs2.{Chunk, Stream} -import fs2.io.file.{Files, Path} -import fs2.io.net.Socket -import fs2.io.evalOnVirtualThreadIfAvailable -import java.nio.ByteBuffer -import java.nio.channels.SocketChannel -import fs2.io.file.FileHandle +package fs2 +package io +package net +package unixsocket -private[unixsocket] trait UnixSocketsCompanionPlatform { +import cats.effect.{Async, IO, LiftIO} +import fs2.io.file.Files + +private[unixsocket] trait UnixSocketsCompanionPlatform { self: UnixSockets.type => + @deprecated("Use Network instead", "3.13.0") def forIO: UnixSockets[IO] = forLiftIO + @deprecated("Use Network instead", "3.13.0") implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = { val _ = LiftIO[F] forAsyncAndFiles } - def forAsyncAndFiles[F[_]: Async: Files]: UnixSockets[F] = - if (JdkUnixSockets.supported) JdkUnixSockets.forAsyncAndFiles - else if (JnrUnixSockets.supported) JnrUnixSockets.forAsyncAndFiles - else - throw new UnsupportedOperationException( - """Must either run on JDK 16+ or have "com.github.jnr" % "jnr-unixsocket" % on the classpath""" - ) + @deprecated("Use Network instead", "3.13.0") + def forAsyncAndFiles[F[_]: Async: Files]: UnixSockets[F] = { + val _ = Files[F] + new AsyncUnixSockets(AutoDetectingUnixSocketsProvider.forAsync) + } + @deprecated("Use Network instead", "3.13.0") def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = forAsyncAndFiles(F, Files.forAsync(F)) - - private[unixsocket] abstract class AsyncUnixSockets[F[_]: Files](implicit F: Async[F]) - extends UnixSockets[F] { - protected def openChannel(address: UnixSocketAddress): Resource[F, SocketChannel] - protected def openServerChannel( - address: UnixSocketAddress - ): Resource[F, Resource[F, SocketChannel]] - - def client(address: UnixSocketAddress): Resource[F, Socket[F]] = - openChannel(address).evalMap(makeSocket[F](_)) - - def server( - address: UnixSocketAddress, - deleteIfExists: Boolean, - deleteOnClose: Boolean - ): Stream[F, Socket[F]] = { - val delete = Resource.make { - Files[F].deleteIfExists(Path(address.path)).whenA(deleteIfExists) - } { _ => - Files[F].deleteIfExists(Path(address.path)).whenA(deleteOnClose) - } - - Stream.resource(delete *> openServerChannel(address)).flatMap { accept => - Stream - .resource(accept.attempt) - .flatMap { - case Left(_) => Stream.empty[F] - case Right(accepted) => Stream.eval(makeSocket(accepted)) - } - .repeat - } - } - } - - private def makeSocket[F[_]: Async]( - ch: SocketChannel - ): F[Socket[F]] = - (Mutex[F], Mutex[F]).mapN { (readMutex, writeMutex) => - new AsyncSocket[F](ch, readMutex, writeMutex) - } - - private final class AsyncSocket[F[_]]( - ch: SocketChannel, - readMutex: Mutex[F], - writeMutex: Mutex[F] - )(implicit F: Async[F]) - extends Socket.BufferedReads[F](readMutex) { - - def readChunk(buff: ByteBuffer): F[Int] = - evalOnVirtualThreadIfAvailable(F.blocking(ch.read(buff))) - .cancelable(close) - - def write(bytes: Chunk[Byte]): F[Unit] = { - def go(buff: ByteBuffer): F[Unit] = - F.blocking(ch.write(buff)).cancelable(close) *> - F.delay(buff.remaining <= 0).ifM(F.unit, go(buff)) - - writeMutex.lock.surround { - F.delay(bytes.toByteBuffer).flatMap(buffer => evalOnVirtualThreadIfAvailable(go(buffer))) - } - } - - def localAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError - def remoteAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError - private def raiseIpAddressError[A]: F[A] = - F.raiseError(new UnsupportedOperationException("UnixSockets do not use IP addressing")) - - def isOpen: F[Boolean] = evalOnVirtualThreadIfAvailable(F.blocking(ch.isOpen())) - def close: F[Unit] = evalOnVirtualThreadIfAvailable(F.blocking(ch.close())) - def endOfOutput: F[Unit] = - evalOnVirtualThreadIfAvailable( - F.blocking { - ch.shutdownOutput(); () - } - ) - def endOfInput: F[Unit] = - evalOnVirtualThreadIfAvailable( - F.blocking { - ch.shutdownInput(); () - } - ) - override def sendFile( - file: FileHandle[F], - offset: Long, - count: Long, - chunkSize: Int - ): Stream[F, Nothing] = file match { - case syncFileHandle: SyncFileHandle[F] => - val fileChannel = syncFileHandle.chan - - def go(currOffset: Long, remaining: Long): F[Unit] = - if (remaining <= 0) F.unit - else { - F.blocking(fileChannel.transferTo(currOffset, remaining, ch)).flatMap { written => - if (written == 0) F.unit - else { - go(currOffset + written, remaining - written) - } - } - } - - Stream.exec(writeMutex.lock.surround(go(offset, count))) - - case _ => - super.sendFile(file, offset, count, chunkSize) - } - } } diff --git a/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala new file mode 100644 index 0000000000..271d711c12 --- /dev/null +++ b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.IO +import cats.syntax.all._ + +import com.comcast.ip4s._ + +import scala.concurrent.duration._ +import scala.util.Properties + +import fs2.io.file.Files + +class UnixDatagramSuite extends Fs2Suite { + + private def sendAndReceive( + socket: DatagramSocket[IO], + bytes: Chunk[Byte], + address: UnixSocketAddress + ): IO[Datagram] = + socket + .write(bytes, address) >> socket.read.timeoutTo( + 1.second, + IO.defer(sendAndReceive(socket, bytes, address)) + ) + + private def sendAndReceiveBytes(socket: DatagramSocket[IO], bytes: Chunk[Byte]): IO[Datagram] = + socket + .write(bytes) >> socket.read.timeoutTo(1.second, IO.defer(sendAndReceiveBytes(socket, bytes))) + + val opts = List(SocketOption.unixSocketDeleteIfExists(true)) + val tempUnixSocketAddress = Files[IO].tempFile.map(f => UnixSocketAddress(f.toString)) + + test("echo one") { + assume(!Properties.isLinux) // https://github.com/jnr/jnr-unixsocket/pull/107 + val msg = Chunk.array("Hello, world!".getBytes) + Stream + .resource((tempUnixSocketAddress, tempUnixSocketAddress).tupled) + .flatMap { case (serverAddress, clientAddress) => + + Stream + .resource(Network[IO].bindDatagramSocket(serverAddress, opts)) + .flatMap { serverSocket => + val server = Stream.repeatEval(serverSocket.readGen).foreach(serverSocket.write) + val client = + Stream.resource(Network[IO].bindDatagramSocket(clientAddress, opts)).evalMap { + clientSocket => + sendAndReceive(clientSocket, msg, serverAddress) + } + client.concurrently(server) + } + } + .compile + .lastOrError + .map(_.bytes) + .assertEquals(msg) + } + + test("echo connected") { + assume(!Properties.isLinux) // https://github.com/jnr/jnr-unixsocket/pull/107 + val msg = Chunk.array("Hello, world!".getBytes) + Stream + .resource((tempUnixSocketAddress, tempUnixSocketAddress).tupled) + .flatMap { case (serverAddress, clientAddress) => + Stream + .resource(Network[IO].bindDatagramSocket(serverAddress, opts)) + .flatMap { serverSocket => + val server = Stream.repeatEval(serverSocket.readGen).foreach(serverSocket.write) + val client = + Stream.resource(Network[IO].bindDatagramSocket(clientAddress, opts)).evalMap { + clientSocket => + clientSocket.connect(serverAddress) >> sendAndReceiveBytes(clientSocket, msg) + } + client.concurrently(server) + } + } + .compile + .lastOrError + .map(_.bytes) + .assertEquals(msg) + } +} diff --git a/io/jvm/src/test/scala/fs2/io/net/unixsocket/UnixSocketsSuitePlatform.scala b/io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala similarity index 84% rename from io/jvm/src/test/scala/fs2/io/net/unixsocket/UnixSocketsSuitePlatform.scala rename to io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala index 0fffed5479..4408b2a29b 100644 --- a/io/jvm/src/test/scala/fs2/io/net/unixsocket/UnixSocketsSuitePlatform.scala +++ b/io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala @@ -20,11 +20,13 @@ */ package fs2 -package io.net.unixsocket +package io.net import cats.effect.IO trait UnixSocketsSuitePlatform { self: UnixSocketsSuite => - if (JdkUnixSockets.supported) testProvider("jdk")(JdkUnixSockets.forAsync[IO]) - if (JnrUnixSockets.supported) testProvider("jnr")(JnrUnixSockets.forAsync[IO]) + if (JdkUnixSocketsProvider.supported) + testProvider("jdk", JdkUnixSocketsProvider.forAsyncAndFiles[IO]) + if (JnrUnixSocketsProvider.supported) + testProvider("jnr", JnrUnixSocketsProvider.forAsyncAndFiles[IO]) } diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/DTLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/DTLSSocketSuite.scala index 83ef0ac961..715dc418ff 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/DTLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/DTLSSocketSuite.scala @@ -35,16 +35,14 @@ class DTLSSocketSuite extends TLSSuite { val msg = Chunk.array("Hello, world!".getBytes) def address(s: DatagramSocket[IO]) = - Resource - .eval(s.localAddress) - .map(a => SocketAddress(ip"127.0.0.1", a.port)) + SocketAddress(ip"127.0.0.1", s.address.asIpUnsafe.port) val setup = for { tlsContext <- Resource.eval(testTlsContext) - serverSocket <- Network[IO].openDatagramSocket() - serverAddress <- address(serverSocket) - clientSocket <- Network[IO].openDatagramSocket() - clientAddress <- address(clientSocket) + serverSocket <- Network[IO].bindDatagramSocket() + serverAddress = address(serverSocket) + clientSocket <- Network[IO].bindDatagramSocket() + clientAddress = address(clientSocket) tlsServerSocket <- tlsContext .dtlsServerBuilder(serverSocket, clientAddress) .withLogger(logger) diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSDebugExample.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSDebugExample.scala index 3068d85912..92a4a886ef 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSDebugExample.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSDebugExample.scala @@ -37,7 +37,7 @@ object TLSDebug { host: SocketAddress[Hostname] ): F[String] = host.resolve.flatMap { socketAddress => - Network[F].client(socketAddress).use { rawSocket => + Network[F].connect(socketAddress).use { rawSocket => tlsContext .clientBuilder(rawSocket) .withParameters( diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index fd514a7814..459f2d0adf 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -32,9 +32,12 @@ import cats.effect.{IO, Resource} import cats.syntax.all._ import com.comcast.ip4s._ +import java.io.FileNotFoundException import java.security.Security import javax.net.ssl.SSLHandshakeException +import fs2.io.file.Path + class TLSSocketSuite extends TLSSuite { val size = 8192 @@ -43,7 +46,7 @@ class TLSSocketSuite extends TLSSuite { def googleSetup(protocol: String) = for { tlsContext <- Resource.eval(Network[IO].tlsContext.system) - socket <- Network[IO].client(SocketAddress(host"google.com", port"443")) + socket <- Network[IO].connect(SocketAddress(host"google.com", port"443")) tlsSocket <- tlsContext .clientBuilder(socket) .withParameters( @@ -130,10 +133,9 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(testTlsContext) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections - client <- Network[IO].client(serverAddress).flatMap(tlsContext.client(_)) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + client <- Network[IO].connect(serverSocket.address).flatMap(tlsContext.client(_)) + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -158,10 +160,9 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(Network[IO].tlsContext.system) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections - client <- Network[IO].client(serverAddress).flatMap(tlsContext.client(_)) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + client <- Network[IO].connect(serverSocket.address).flatMap(tlsContext.client(_)) + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -188,10 +189,9 @@ class TLSSocketSuite extends TLSSuite { val setup = for { clientContext <- Resource.eval(TLSContext.Builder.forAsync[IO].insecure) tlsContext <- Resource.eval(testTlsContext) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client <- Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap(s => clientContext .clientBuilder(s) @@ -200,7 +200,7 @@ class TLSSocketSuite extends TLSSuite { ) // makes test fail if using X509TrustManager, passes if using X509ExtendedTrustManager .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -219,5 +219,82 @@ class TLSSocketSuite extends TLSSuite { .to(Chunk) .assertEquals(msg) } + + test("endOfOutput during handshake results in termination") { + val msg = Chunk.array(("Hello, world! " * 20000).getBytes) + + def limitWrites(raw: Socket[IO], limit: Int): Socket[IO] = new Socket[IO] { + def endOfInput = raw.endOfInput + def endOfOutput = raw.endOfOutput + @deprecated("", "") + def isOpen = raw.isOpen + @deprecated("", "") + def localAddress = raw.localAddress + def peerAddress = raw.peerAddress + def read(maxBytes: Int) = raw.read(maxBytes) + def readN(numBytes: Int) = raw.readN(numBytes) + def reads = raw.reads + @deprecated("", "") + def remoteAddress = raw.remoteAddress + def writes = raw.writes + + def address = raw.address + def getOption[A](key: SocketOption.Key[A]) = raw.getOption(key) + def setOption[A](key: SocketOption.Key[A], value: A) = raw.setOption(key, value) + def supportedOptions = raw.supportedOptions + + private var totalWritten: Int = 0 + def write(bytes: Chunk[Byte]) = + if (totalWritten >= limit) endOfOutput + else { + val b = bytes.take(limit - totalWritten) + raw.write(b) >> IO(totalWritten += b.size) >> IO(totalWritten >= limit) + .ifM(endOfOutput, IO.unit) + } + } + + // Setup an HTTPS echo server & a client that starts a TLS handshake but only sends the first few bytes and then signals no more output + // Doing so should not cause the server to peg a CPU + val setup = for { + tlsContext <- Resource.eval(testTlsContext) + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + echoServer = + serverSocket.accept + .flatMap(s => Stream.resource(tlsContext.serverBuilder(s).withLogger(logger).build)) + .map { socket => + socket.reads.chunks.foreach(socket.write) + } + .parJoinUnbounded + client <- Network[IO].connect(serverSocket.address).flatMap { rawClient => + tlsContext.client(limitWrites(rawClient, 10)) + } + } yield echoServer -> client + + Stream + .resource(setup) + .flatMap { case (echoServer, clientSocket) => + val client = + Stream.exec(clientSocket.write(msg)).onFinalize(clientSocket.endOfOutput) ++ + clientSocket.reads.take(msg.size.toLong) + + client.concurrently(echoServer) + } + .compile + .drain + } + } + + group("TLSContextBuilder") { + test("fromKeyStoreResource - not found") { + Network[IO].tlsContext + .fromKeyStoreResource("does-not-exist.jks", Array.empty[Char], Array.empty[Char]) + .intercept[IOException] + } + + test("fromKeyStoreFile - not found") { + Network[IO].tlsContext + .fromKeyStoreFile(Path("does-not-exist.jks"), Array.empty[Char], Array.empty[Char]) + .intercept[FileNotFoundException] + } } } diff --git a/io/native/src/main/scala/fs2/io/internal/NativeUtil.scala b/io/native/src/main/scala/fs2/io/internal/NativeUtil.scala index 726ab36f0a..ebd53d946f 100644 --- a/io/native/src/main/scala/fs2/io/internal/NativeUtil.scala +++ b/io/native/src/main/scala/fs2/io/internal/NativeUtil.scala @@ -75,7 +75,7 @@ private[io] object NativeUtil { else if (e == ECONNREFUSED) new ConnectException(msg) else - new IOException(msg) + new IOException(s"errno($e) $msg") } def setNonBlocking[F[_]](fd: CInt)(implicit F: Sync[F]): F[Unit] = F.delay { diff --git a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala index b4685113e9..84156e4d82 100644 --- a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala +++ b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala @@ -21,19 +21,23 @@ package fs2.io.internal -import cats.effect.kernel.Resource -import cats.effect.kernel.Sync +import cats.effect.{Resource, Sync} import cats.syntax.all._ -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.Ipv4Address -import com.comcast.ip4s.Ipv6Address -import com.comcast.ip4s.Port -import com.comcast.ip4s.SocketAddress +import com.comcast.ip4s.{ + GenSocketAddress, + IpAddress, + Ipv4Address, + Ipv6Address, + Port, + SocketAddress, + UnixSocketAddress +} import java.net.SocketOption import java.net.StandardSocketOptions import scala.scalanative.meta.LinktimeInfo import scala.scalanative.posix.arpa.inet._ +import scala.scalanative.posix.errno.ENOPROTOOPT import scala.scalanative.posix.netinet.in.IPPROTO_TCP import scala.scalanative.posix.netinet.tcp._ import scala.scalanative.posix.string._ @@ -46,6 +50,8 @@ import NativeUtil._ import netinetin._ import netinetinOps._ import syssocket._ +import sysun._ +import sysunOps._ private[io] object SocketHelpers { @@ -66,6 +72,67 @@ private[io] object SocketHelpers { (if (LinktimeInfo.isMac) setNoSigPipe(fd) else F.unit) } + // TODO: Support other options (e.g. extended options?) + + def supportedOptions[F[_]: Sync]: F[Set[SocketOption[?]]] = + Sync[F].pure( + Set( + StandardSocketOptions.SO_SNDBUF, + StandardSocketOptions.SO_RCVBUF, + StandardSocketOptions.SO_REUSEADDR, + StandardSocketOptions.SO_REUSEPORT, + StandardSocketOptions.SO_KEEPALIVE, + StandardSocketOptions.TCP_NODELAY + ) + ) + + def getOption[F[_]: Sync, A](fd: CInt, name: SocketOption[A]): F[Option[A]] = (name match { + case StandardSocketOptions.SO_SNDBUF => + getOptionInt(fd, SO_SNDBUF) + case StandardSocketOptions.SO_RCVBUF => + getOptionInt(fd, SO_RCVBUF) + case StandardSocketOptions.SO_REUSEADDR => + getOptionInt(fd, SO_REUSEADDR) + case StandardSocketOptions.SO_REUSEPORT => + getOptionBool(fd, SO_REUSEPORT) + case StandardSocketOptions.SO_KEEPALIVE => + getOptionBool(fd, SO_KEEPALIVE) + case StandardSocketOptions.TCP_NODELAY => + getTcpOptionBool(fd, TCP_NODELAY) + case _ => Sync[F].pure(None) + }).asInstanceOf[F[Option[A]]] + + def getOptionBool[F[_]: Sync](fd: CInt, option: CInt): F[Option[Boolean]] = + getOptionInt(fd, option).map(_.map(v => if (v == 0) false else true)) + + def getOptionInt[F[_]: Sync](fd: CInt, option: CInt): F[Option[Int]] = + getOptionImpl(fd, SOL_SOCKET, option) + + def getTcpOptionBool[F[_]: Sync](fd: CInt, option: CInt): F[Option[Boolean]] = + getTcpOptionInt(fd, option).map(_.map(v => if (v == 0) false else true)) + + def getTcpOptionInt[F[_]: Sync](fd: CInt, option: CInt): F[Option[Int]] = + getOptionImpl(fd, IPPROTO_TCP /* aka SOL_TCP */, option) + + def getOptionImpl[F[_]](fd: CInt, level: CInt, option: CInt)(implicit + F: Sync[F] + ): F[Option[Int]] = + F.delay { + val ptr = stackalloc[CInt]() + val szPtr = stackalloc[UInt]() + !szPtr = sizeof[CInt].toUInt + val ret = guardMask( + getsockopt( + fd, + level, + option, + ptr.asInstanceOf[Ptr[Byte]], + szPtr + ) + )(_ == ENOPROTOOPT) + if (ret == ENOPROTOOPT) None else Some(!ptr) + } + // macOS-only def setNoSigPipe[F[_]: Sync](fd: CInt): F[Unit] = setOption(fd, SO_NOSIGPIPE, true) @@ -158,13 +225,14 @@ private[io] object SocketHelpers { throw errnoToThrowable(!optval) } - def getLocalAddress[F[_]](fd: Int, ipv4: Boolean)(implicit - F: Sync[F] - ): F[SocketAddress[IpAddress]] = - F.delay { - SocketHelpers.toSocketAddress(ipv4) { (addr, len) => - guard_(getsockname(fd, addr, len)) - } + def getAddress(fd: Int, domain: CInt): GenSocketAddress = + SocketHelpers.toSocketAddress(domain) { (addr, len) => + guard_(getsockname(fd, addr, len)) + } + + def getPeerAddress(fd: Int, domain: CInt): GenSocketAddress = + SocketHelpers.toSocketAddress(domain) { (addr, len) => + guard_(getpeername(fd, addr, len)) } def toSockaddr[A]( @@ -252,29 +320,33 @@ private[io] object SocketHelpers { } } - def allocateSockaddr[A]( + def allocateSockaddr[A](domain: CInt)( f: (Ptr[sockaddr], Ptr[socklen_t]) => A ): A = { - val addr = // allocate enough for an IPv6 - stackalloc[sockaddr_in6]().asInstanceOf[Ptr[sockaddr]] - val len = stackalloc[socklen_t]() - !len = sizeof[sockaddr_in6].toUInt + // FIXME: Scala Native 0.4 doesn't support getsockname for unix sockets; after upgrading to 0.5, + // this can be simplified to just allocate a sockaddr_un + val (addr, lenValue) = // allocate enough for unix socket address + if (domain == AF_UNIX) + (stackalloc[sockaddr_un]().asInstanceOf[Ptr[sockaddr]], sizeof[sockaddr_un].toUInt) + else (stackalloc[sockaddr_in6]().asInstanceOf[Ptr[sockaddr]], sizeof[sockaddr_in6].toUInt) + val len = stackalloc[socklen_t]() + !len = lenValue f(addr, len) } - def toSocketAddress[A](ipv4: Boolean)( + def toSocketAddress[A](domain: CInt)( f: (Ptr[sockaddr], Ptr[socklen_t]) => Unit - ): SocketAddress[IpAddress] = allocateSockaddr { (addr, len) => + ): GenSocketAddress = allocateSockaddr(domain) { (addr, len) => f(addr, len) - toSocketAddress(addr, ipv4) + toSocketAddress(addr, domain) } - def toSocketAddress(addr: Ptr[sockaddr], ipv4: Boolean): SocketAddress[IpAddress] = - if (ipv4) - toIpv4SocketAddress(addr.asInstanceOf[Ptr[sockaddr_in]]) - else - toIpv6SocketAddress(addr.asInstanceOf[Ptr[sockaddr_in6]]) + def toSocketAddress(addr: Ptr[sockaddr], domain: CInt): GenSocketAddress = + if (domain == AF_INET) toIpv4SocketAddress(addr.asInstanceOf[Ptr[sockaddr_in]]) + else if (domain == AF_INET6) toIpv6SocketAddress(addr.asInstanceOf[Ptr[sockaddr_in6]]) + else if (domain == AF_UNIX) toUnixSocketAddress(addr.asInstanceOf[Ptr[sockaddr_un]]) + else throw new UnsupportedOperationException private[this] def toIpv4SocketAddress(addr: Ptr[sockaddr_in]): SocketAddress[Ipv4Address] = { val port = Port.fromInt(ntohs(addr.sin_port).toInt).get @@ -298,4 +370,9 @@ private[io] object SocketHelpers { }.get SocketAddress(host, port) } + + private[this] def toUnixSocketAddress(addr: Ptr[sockaddr_un]): UnixSocketAddress = { + val path = fromCString(addr.sun_path.asInstanceOf[CString]) + UnixSocketAddress(path) + } } diff --git a/io/native/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala b/io/native/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala new file mode 100644 index 0000000000..71ed8d5291 --- /dev/null +++ b/io/native/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +private[net] trait DatagramSocketGroupCompanionPlatform { + type ProtocolFamily = java.net.ProtocolFamily +} diff --git a/io/native/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala b/io/native/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala new file mode 100644 index 0000000000..f572640888 --- /dev/null +++ b/io/native/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import com.comcast.ip4s.IpAddress + +private[net] trait DatagramSocketPlatform[F[_]] { + private[net] trait GroupMembershipPlatform { + + /** Blocks datagrams from the specified source address. */ + def block(source: IpAddress): F[Unit] + + /** Unblocks datagrams from the specified source address. */ + def unblock(source: IpAddress): F[Unit] + } +} + +private[net] trait DatagramSocketCompanionPlatform { + @deprecated("Use com.comcast.ip4s.NetworkInterface", "3.13.0") + type NetworkInterface = java.net.NetworkInterface +} diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingSocketGroup.scala b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala similarity index 72% rename from io/native/src/main/scala/fs2/io/net/FdPollingSocketGroup.scala rename to io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala index fb058c7870..c04278fcd3 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingSocketGroup.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala @@ -31,17 +31,20 @@ import cats.syntax.all._ import com.comcast.ip4s._ import fs2.io.internal.NativeUtil._ import fs2.io.internal.SocketHelpers -import fs2.io.internal.syssocket._ +import fs2.io.internal.syssocket.{connect => sconnect, bind => sbind, _} import scala.scalanative.meta.LinktimeInfo import scala.scalanative.posix.errno._ import scala.scalanative.posix.sys.socket.{bind => _, connect => _, accept => _, _} import scala.scalanative.posix.unistd._ -private final class FdPollingSocketGroup[F[_]: Dns: LiftIO](implicit F: Async[F]) - extends SocketGroup[F] { +private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: Async[F]) + extends IpSocketsProvider[F] { - def client(to: SocketAddress[Host], options: List[SocketOption]): Resource[F, Socket[F]] = for { + override def connectIp( + to: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] = for { poller <- Resource.eval(fileDescriptorPoller[F]) address <- Resource.eval(to.resolve) ipv4 = address.host.isInstanceOf[Ipv4Address] @@ -55,7 +58,7 @@ private final class FdPollingSocketGroup[F[_]: Dns: LiftIO](implicit F: Async[F] else IO { SocketHelpers.toSockaddr(address) { (addr, len) => - if (connect(fd, addr, len) < 0) { + if (sconnect(fd, addr, len) < 0) { val e = errno if (e == EINPROGRESS) Left(true) // we will be connected when we unblock @@ -71,34 +74,26 @@ private final class FdPollingSocketGroup[F[_]: Dns: LiftIO](implicit F: Async[F] socket <- FdPollingSocket[F]( fd, handle, - SocketHelpers.getLocalAddress(fd, ipv4), - F.pure(address) + SocketHelpers.getAddress(fd, if (ipv4) AF_INET else AF_INET6), + SocketHelpers.getPeerAddress(fd, if (ipv4) AF_INET else AF_INET6) ) } yield socket - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = - Stream.resource(serverResource(address, port, options)).flatMap(_._2) - - def serverResource( - address: Option[Host], - port: Option[Port], + override def bindIp( + address: SocketAddress[Host], options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = for { + ): Resource[F, ServerSocket[F]] = for { poller <- Resource.eval(fileDescriptorPoller[F]) - address <- Resource.eval(address.fold(IpAddress.loopback)(_.resolve)) - ipv4 = address.isInstanceOf[Ipv4Address] + resolvedHost <- Resource.eval(address.host.resolve) + ipv4 = resolvedHost.isInstanceOf[Ipv4Address] fd <- SocketHelpers.openNonBlocking(if (ipv4) AF_INET else AF_INET6, SOCK_STREAM) handle <- poller.registerFileDescriptor(fd, true, false).mapK(LiftIO.liftK) _ <- Resource.eval { val bindF = F.delay { - val socketAddress = SocketAddress(address, port.getOrElse(port"0")) - SocketHelpers.toSockaddr(socketAddress) { (addr, len) => - guard_(bind(fd, addr, len)) + val resolvedAddress = SocketAddress(resolvedHost, address.port) + SocketHelpers.toSockaddr(resolvedAddress) { (addr, len) => + guard_(sbind(fd, addr, len)) } } @@ -113,7 +108,7 @@ private final class FdPollingSocketGroup[F[_]: Dns: LiftIO](implicit F: Async[F] handle .pollReadRec(()) { _ => IO { - SocketHelpers.allocateSockaddr { (addr, len) => + SocketHelpers.allocateSockaddr(if (ipv4) AF_INET else AF_INET6) { (addr, len) => val clientFd = if (LinktimeInfo.isLinux) guard(accept4(fd, addr, len, SOCK_NONBLOCK)) @@ -121,7 +116,9 @@ private final class FdPollingSocketGroup[F[_]: Dns: LiftIO](implicit F: Async[F] guard(accept(fd, addr, len)) if (clientFd >= 0) { - val address = SocketHelpers.toSocketAddress(addr, ipv4) + val address = SocketHelpers + .toSocketAddress(addr, if (ipv4) AF_INET else AF_INET6) + .asInstanceOf[SocketAddress[IpAddress]] Right((address, clientFd)) } else Left(()) @@ -140,8 +137,8 @@ private final class FdPollingSocketGroup[F[_]: Dns: LiftIO](implicit F: Async[F] socket <- FdPollingSocket[F]( fd, handle, - SocketHelpers.getLocalAddress(fd, ipv4), - F.pure(address) + SocketHelpers.getAddress(fd, if (ipv4) AF_INET else AF_INET6), + SocketHelpers.getPeerAddress(fd, if (ipv4) AF_INET else AF_INET6) ) } yield socket @@ -150,7 +147,12 @@ private final class FdPollingSocketGroup[F[_]: Dns: LiftIO](implicit F: Async[F] .repeat .unNone - serverAddress <- Resource.eval(SocketHelpers.getLocalAddress(fd, ipv4)) - } yield (serverAddress, sockets) + info = new SocketInfo[F] { + def getOption[A](key: SocketOption.Key[A]) = SocketHelpers.getOption(fd, key) + def setOption[A](key: SocketOption.Key[A], value: A) = SocketHelpers.setOption(fd, key, value) + def supportedOptions = SocketHelpers.supportedOptions + val address = SocketHelpers.getAddress(fd, if (ipv4) AF_INET else AF_INET6) + } + } yield ServerSocket(info, sockets) } diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala index 4878c82403..939a936a10 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala @@ -22,16 +22,11 @@ package fs2 package io.net -import cats.effect.FileDescriptorPollHandle -import cats.effect.IO -import cats.effect.LiftIO -import cats.effect.kernel.Async -import cats.effect.kernel.Resource +import cats.effect.{Async, FileDescriptorPollHandle, IO, LiftIO, Resource} import cats.syntax.all._ -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.SocketAddress +import com.comcast.ip4s.GenSocketAddress import fs2.io.internal.NativeUtil._ -import fs2.io.internal.ResizableBuffer +import fs2.io.internal.{ResizableBuffer, SocketHelpers} import scala.scalanative.meta.LinktimeInfo import scala.scalanative.posix.errno._ @@ -47,11 +42,14 @@ private final class FdPollingSocket[F[_]: LiftIO] private ( handle: FileDescriptorPollHandle, readBuffer: ResizableBuffer[F], val isOpen: F[Boolean], - val localAddress: F[SocketAddress[IpAddress]], - val remoteAddress: F[SocketAddress[IpAddress]] + val address: GenSocketAddress, + val peerAddress: GenSocketAddress )(implicit F: Async[F]) extends Socket[F] { + def localAddress = F.pure(address.asIpUnsafe) + def remoteAddress = F.pure(peerAddress.asIpUnsafe) + def endOfInput: F[Unit] = shutdownF(0) def endOfOutput: F[Unit] = shutdownF(1) private[this] def shutdownF(how: Int): F[Unit] = F.delay { @@ -119,6 +117,13 @@ private final class FdPollingSocket[F[_]: LiftIO] private ( def writes: Pipe[F, Byte, Nothing] = _.chunks.foreach(write(_)) + def getOption[A](key: SocketOption.Key[A]) = + SocketHelpers.getOption[F, A](fd, key) + + def setOption[A](key: SocketOption.Key[A], value: A) = + SocketHelpers.setOption(fd, key, value) + + def supportedOptions = SocketHelpers.supportedOptions } private object FdPollingSocket { @@ -127,10 +132,10 @@ private object FdPollingSocket { def apply[F[_]: LiftIO]( fd: Int, handle: FileDescriptorPollHandle, - localAddress: F[SocketAddress[IpAddress]], - remoteAddress: F[SocketAddress[IpAddress]] + address: GenSocketAddress, + peerAddress: GenSocketAddress )(implicit F: Async[F]): Resource[F, Socket[F]] = for { buffer <- ResizableBuffer(DefaultReadSize) isOpen <- Resource.make(F.ref(true))(_.set(false)) - } yield new FdPollingSocket(fd, handle, buffer, isOpen.get, localAddress, remoteAddress) + } yield new FdPollingSocket(fd, handle, buffer, isOpen.get, address, peerAddress) } diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala new file mode 100644 index 0000000000..62f5ee2a63 --- /dev/null +++ b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.{Async, IO, LiftIO, Resource} +import cats.syntax.all._ + +import com.comcast.ip4s.UnixSocketAddress + +import fs2.io.file.Files +import fs2.io.internal.NativeUtil._ +import fs2.io.internal.SocketHelpers +import fs2.io.internal.syssocket.{connect => uconnect, bind => ubind, _} +import fs2.io.internal.sysun._ +import fs2.io.internal.sysunOps._ + +import scala.scalanative.meta.LinktimeInfo +import scala.scalanative.posix.string._ +import scala.scalanative.posix.sys.socket.{bind => _, connect => _, accept => _, _} +import scala.scalanative.posix.unistd._ +import scala.scalanative.unsafe._ +import scala.scalanative.unsigned._ + +private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F: Async[F]) + extends UnixSocketsProvider[F] { + + override def connectUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] = + for { + poller <- Resource.eval(fileDescriptorPoller[F]) + fd <- SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM) + _ <- Resource.eval(options.traverse(so => SocketHelpers.setOption(fd, so.key, so.value))) + handle <- poller.registerFileDescriptor(fd, true, true).mapK(LiftIO.liftK) + _ <- Resource.eval { + handle + .pollWriteRec(false) { connected => + if (connected) SocketHelpers.checkSocketError[IO](fd).as(Either.unit) + else + IO { + toSockaddrUn(address.path) { addr => + if (guard(uconnect(fd, addr, sizeof[sockaddr_un].toUInt)) < 0) + Left(true) // we will be connected when unblocked + else + Either.unit[Boolean] + } + } + } + .to + } + socket <- FdPollingSocket[F]( + fd, + handle, + UnixSocketAddress(""), + address + ) + } yield socket + + override def bindUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = { + + val (filteredOptions, delete) = SocketOption.extractUnixSocketDeletes(options, address) + + for { + poller <- Resource.eval(fileDescriptorPoller[F]) + + _ <- delete + + fd <- SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM) + + handle <- poller.registerFileDescriptor(fd, true, false).mapK(LiftIO.liftK) + _ <- Resource.eval { + F.delay { + toSockaddrUn(address.path)(addr => guard_(ubind(fd, addr, sizeof[sockaddr_un].toUInt))) + } *> F.delay(guard_(listen(fd, 0))) + } + + address0 = address + info = new SocketInfo[F] { + def getOption[A](key: SocketOption.Key[A]) = SocketHelpers.getOption(fd, key) + def setOption[A](key: SocketOption.Key[A], value: A) = + SocketHelpers.setOption(fd, key, value) + def supportedOptions = SocketHelpers.supportedOptions + val address = address0 + } + + clients = Stream + .resource { + val accepted = for { + fd <- Resource.makeFull[F, Int] { poll => + poll { + handle + .pollReadRec(()) { _ => + IO { + val clientFd = + if (LinktimeInfo.isLinux) + guard(accept4(fd, null, null, SOCK_NONBLOCK)) + else + guard(accept(fd, null, null)) + + if (clientFd >= 0) + Right(clientFd) + else + Left(()) + } + } + .to + } + }(fd => F.delay(guard_(close(fd)))) + _ <- + if (!LinktimeInfo.isLinux) + Resource.eval(setNonBlocking(fd)) + else Resource.unit[F] + + _ <- Resource.eval( + filteredOptions.traverse(so => SocketHelpers.setOption(fd, so.key, so.value)) + ) + handle <- poller.registerFileDescriptor(fd, true, true).mapK(LiftIO.liftK) + socket <- FdPollingSocket[F]( + fd, + handle, + address, + UnixSocketAddress("") + ) + } yield socket + + accepted.attempt.map(_.toOption) + } + .repeat + .unNone + + } yield ServerSocket(info, clients) + } + + private def toSockaddrUn[A](path: String)(f: Ptr[sockaddr] => A): A = { + val pathBytes = path.getBytes + if (pathBytes.length > 107) + throw new IllegalArgumentException(s"Path too long: $path") + + val addr = stackalloc[sockaddr_un]() + addr.sun_family = AF_UNIX.toUShort + memcpy(addr.sun_path.at(0), pathBytes.atUnsafe(0), pathBytes.length.toUSize) + + f(addr.asInstanceOf[Ptr[sockaddr]]) + } +} diff --git a/io/native/src/main/scala/fs2/io/net/NetworkLowPriority.scala b/io/native/src/main/scala/fs2/io/net/NetworkLowPriority.scala new file mode 100644 index 0000000000..19a791e954 --- /dev/null +++ b/io/native/src/main/scala/fs2/io/net/NetworkLowPriority.scala @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2.io.net + +import cats.effect.{Async, LiftIO} + +private[fs2] trait NetworkLowPriority { this: Network.type => + @deprecated("Add Network constraint or use forAsync", "3.7.0") + implicit def implicitForAsync[F[_]: Async]: Network[F] = + Async[F] match { + case l: LiftIO[?] => forLiftIO(Async[F], l.asInstanceOf[LiftIO[F]]) + case _ => throw new UnsupportedOperationException + } +} diff --git a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala index 1d45570f75..af583a624e 100644 --- a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -23,72 +23,34 @@ package fs2 package io package net -import cats.effect.IO -import cats.effect.LiftIO -import cats.effect.kernel.{Async, Resource} +import cats.effect.{Async, LiftIO} -import com.comcast.ip4s.{Dns, Host, IpAddress, Port, SocketAddress} - -import fs2.io.net.tls.TLSContext +import com.comcast.ip4s.Dns private[net] trait NetworkPlatform[F[_]] private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: Network.type => - def forIO: Network[IO] = forLiftIO - implicit def forLiftIO[F[_]: Async: LiftIO]: Network[F] = - new UnsealedNetwork[F] { - private lazy val globalSocketGroup = - new FdPollingSocketGroup[F]()(Dns.forAsync, implicitly, implicitly) - - def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = globalSocketGroup.client(to, options) - - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = globalSocketGroup.server(address, port, options) - - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - globalSocketGroup.serverResource(address, port, options) - - def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync + new AsyncProviderBasedNetwork[F] { + protected def mkIpSocketsProvider = + new FdPollingIpSocketsProvider[F]()(Dns.forAsync, implicitly, implicitly) + protected def mkUnixSocketsProvider = new FdPollingUnixSocketsProvider[F] + protected def mkIpDatagramSocketsProvider = + throw new UnsupportedOperationException( + "Datagram sockets not currently supported on Native" + ) + protected def mkUnixDatagramSocketsProvider = + throw new UnsupportedOperationException( + "Unix datagram sockets not currently supported on Native" + ) } def forAsync[F[_]](implicit F: Async[F]): Network[F] = forAsyncAndDns(F, Dns.forAsync(F)) - def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = - new UnsealedNetwork[F] { - private lazy val globalSocketGroup = SocketGroup.unsafe[F](null) - - def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = globalSocketGroup.client(to, options) - - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = globalSocketGroup.server(address, port, options) - - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - globalSocketGroup.serverResource(address, port, options) - - def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync(F) - } - + def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = { + val _ = (F, dns) + throw new UnsupportedOperationException("must use forLiftIO instead of forAsync/forAsyncAndDns") + } } diff --git a/io/native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala b/io/native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala new file mode 100644 index 0000000000..7453f1a6b7 --- /dev/null +++ b/io/native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import com.comcast.ip4s.{GenSocketAddress, SocketAddress} +import java.net.{SocketAddress => JSocketAddress, InetSocketAddress} + +private[net] object SocketAddressHelpers { + + def toGenSocketAddress(address: JSocketAddress): GenSocketAddress = + address match { + case addr: InetSocketAddress => SocketAddress.fromInetSocketAddress(addr) + case _ => throw new IllegalArgumentException("Unsupported address type: " + address) + } +} diff --git a/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 2aa0be334d..43ddc7daee 100644 --- a/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -24,12 +24,10 @@ package io package net package tls -import cats.effect.kernel.Async -import cats.effect.kernel.Resource +import cats.effect.{Async, Resource} import cats.effect.std.Mutex import cats.syntax.all._ -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.SocketAddress +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} private[tls] trait TLSSocketPlatform[F[_]] @@ -87,16 +85,34 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => def endOfInput: F[Unit] = socket.endOfInput + @deprecated("3.13.0", "Use address instead") def localAddress: F[SocketAddress[IpAddress]] = socket.localAddress + @deprecated("3.13.0", "Use peerAddress instead") def remoteAddress: F[SocketAddress[IpAddress]] = socket.remoteAddress + def address: GenSocketAddress = + socket.address + + def peerAddress: GenSocketAddress = + socket.peerAddress + + def supportedOptions: F[Set[SocketOption.Key[?]]] = + socket.supportedOptions + + def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = + socket.getOption(key) + + def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] = + socket.setOption(key, value) + def session: F[SSLSession] = connection.session def applicationProtocol: F[String] = connection.applicationProtocol + @deprecated("3.13.0", "No replacement; sockets are open until they are finalized") def isOpen: F[Boolean] = socket.isOpen } } diff --git a/io/native/src/main/scala/fs2/io/net/unixsocket/FdPollingUnixSockets.scala b/io/native/src/main/scala/fs2/io/net/unixsocket/FdPollingUnixSockets.scala deleted file mode 100644 index 73729acdbd..0000000000 --- a/io/native/src/main/scala/fs2/io/net/unixsocket/FdPollingUnixSockets.scala +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2 -package io -package net -package unixsocket - -import cats.effect.IO -import cats.effect.LiftIO -import cats.effect.kernel.Async -import cats.effect.kernel.Resource -import cats.syntax.all._ -import fs2.io.file.Files -import fs2.io.file.Path -import fs2.io.internal.NativeUtil._ -import fs2.io.internal.SocketHelpers -import fs2.io.internal.syssocket._ -import fs2.io.internal.sysun._ -import fs2.io.internal.sysunOps._ - -import scala.scalanative.meta.LinktimeInfo -import scala.scalanative.posix.string._ -import scala.scalanative.posix.sys.socket.{bind => _, connect => _, accept => _, _} -import scala.scalanative.posix.unistd._ -import scala.scalanative.unsafe._ -import scala.scalanative.unsigned._ - -private final class FdPollingUnixSockets[F[_]: Files: LiftIO](implicit F: Async[F]) - extends UnixSockets[F] { - - def client(address: UnixSocketAddress): Resource[F, Socket[F]] = for { - poller <- Resource.eval(fileDescriptorPoller[F]) - fd <- SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM) - handle <- poller.registerFileDescriptor(fd, true, true).mapK(LiftIO.liftK) - _ <- Resource.eval { - handle - .pollWriteRec(false) { connected => - if (connected) SocketHelpers.checkSocketError[IO](fd).as(Either.unit) - else - IO { - toSockaddrUn(address.path) { addr => - if (guard(connect(fd, addr, sizeof[sockaddr_un].toUInt)) < 0) - Left(true) // we will be connected when unblocked - else - Either.unit[Boolean] - } - } - } - .to - } - socket <- FdPollingSocket[F](fd, handle, raiseIpAddressError, raiseIpAddressError) - } yield socket - - def server( - address: UnixSocketAddress, - deleteIfExists: Boolean, - deleteOnClose: Boolean - ): Stream[F, Socket[F]] = for { - poller <- Stream.eval(fileDescriptorPoller[F]) - - _ <- Stream.bracket(Files[F].deleteIfExists(Path(address.path)).whenA(deleteIfExists)) { _ => - Files[F].deleteIfExists(Path(address.path)).whenA(deleteOnClose) - } - - fd <- Stream.resource(SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM)) - handle <- Stream.resource(poller.registerFileDescriptor(fd, true, false).mapK(LiftIO.liftK)) - - _ <- Stream.eval { - F.delay { - toSockaddrUn(address.path)(addr => guard_(bind(fd, addr, sizeof[sockaddr_un].toUInt))) - } *> F.delay(guard_(listen(fd, 0))) - } - - socket <- Stream - .resource { - val accepted = for { - fd <- Resource.makeFull[F, Int] { poll => - poll { - handle - .pollReadRec(()) { _ => - IO { - val clientFd = - if (LinktimeInfo.isLinux) - guard(accept4(fd, null, null, SOCK_NONBLOCK)) - else - guard(accept(fd, null, null)) - - if (clientFd >= 0) - Right(clientFd) - else - Left(()) - } - } - .to - } - }(fd => F.delay(guard_(close(fd)))) - _ <- - if (!LinktimeInfo.isLinux) - Resource.eval(setNonBlocking(fd)) - else Resource.unit[F] - handle <- poller.registerFileDescriptor(fd, true, true).mapK(LiftIO.liftK) - socket <- FdPollingSocket[F](fd, handle, raiseIpAddressError, raiseIpAddressError) - } yield socket - - accepted.attempt - .map(_.toOption) - } - .repeat - .unNone - - } yield socket - - private def toSockaddrUn[A](path: String)(f: Ptr[sockaddr] => A): A = { - val pathBytes = path.getBytes - if (pathBytes.length > 107) - throw new IllegalArgumentException(s"Path too long: $path") - - val addr = stackalloc[sockaddr_un]() - addr.sun_family = AF_UNIX.toUShort - memcpy(addr.sun_path.at(0), pathBytes.atUnsafe(0), pathBytes.length.toUSize) - - f(addr.asInstanceOf[Ptr[sockaddr]]) - } - - private def raiseIpAddressError[A]: F[A] = - F.raiseError(new UnsupportedOperationException("UnixSockets do not use IP addressing")) - -} diff --git a/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala index d35bd8014e..ea75751cff 100644 --- a/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -19,12 +19,15 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package fs2.io.net.unixsocket +package fs2 +package io +package net +package unixsocket -import cats.effect.LiftIO -import cats.effect.kernel.Async +import cats.effect.{Async, LiftIO} -private[unixsocket] trait UnixSocketsCompanionPlatform { +private[unixsocket] trait UnixSocketsCompanionPlatform { self: UnixSockets.type => + @deprecated("Use Network instead", "3.13.0") implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = - new FdPollingUnixSockets[F] + new AsyncUnixSockets(new FdPollingUnixSocketsProvider[F]) } diff --git a/io/js/src/test/scala/fs2/io/net/unixsockets/UnixSocketsSuitePlatform.scala b/io/native/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala similarity index 94% rename from io/js/src/test/scala/fs2/io/net/unixsockets/UnixSocketsSuitePlatform.scala rename to io/native/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala index b39cd42c62..b8ed8e270b 100644 --- a/io/js/src/test/scala/fs2/io/net/unixsockets/UnixSocketsSuitePlatform.scala +++ b/io/native/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala @@ -20,10 +20,10 @@ */ package fs2 -package io.net.unixsocket +package io.net import cats.effect.IO trait UnixSocketsSuitePlatform { self: UnixSocketsSuite => - testProvider("node.js")(UnixSockets.forAsync[IO]) + testProvider("native", new FdPollingUnixSocketsProvider[IO]) } diff --git a/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index 81d6e594ed..07883b8404 100644 --- a/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -38,7 +38,7 @@ class TLSSocketSuite extends TLSSuite { def googleSetup(version: String) = for { tlsContext <- Network[IO].tlsContext.systemResource - socket <- Network[IO].client(SocketAddress(host"google.com", port"443")) + socket <- Network[IO].connect(SocketAddress(host"google.com", port"443")) tlsSocket <- tlsContext .clientBuilder(socket) .withParameters( @@ -107,17 +107,16 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- testTlsContext - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) .withParameters(TLSParameters(serverName = Some("Unknown"))) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -141,17 +140,16 @@ class TLSSocketSuite extends TLSSuite { test("empty write") { val setup = for { tlsContext <- testTlsContext - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) .withParameters(TLSParameters(serverName = Some("Unknown"))) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -175,17 +173,16 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Network[IO].tlsContext.systemResource - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) .withParameters(TLSParameters(serverName = Some("Unknown"))) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -211,17 +208,16 @@ class TLSSocketSuite extends TLSSuite { val setup = for { serverContext <- testTlsContext clientContext <- testClientTlsContext - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap( clientContext .clientBuilder(_) .withParameters(TLSParameters(serverName = Some("Unknown"))) .build ) - } yield server.flatMap(s => + } yield serverSocket.accept.flatMap(s => Stream.resource( serverContext .serverBuilder(s) @@ -255,17 +251,16 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- testTlsContext - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .client(serverAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) .withParameters(TLSParameters(serverName = Some("Unknown"))) .build ) - } yield server.flatMap(s => + } yield serverSocket.accept.flatMap(s => Stream.resource( tlsContext .serverBuilder(s) @@ -298,10 +293,9 @@ class TLSSocketSuite extends TLSSuite { val setup = for { clientContext <- Network[IO].tlsContext.insecureResource tlsContext <- testTlsContext - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections - client = Network[IO].client(serverAddress).flatMap(clientContext.client(_)) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + client = Network[IO].connect(serverSocket.address).flatMap(clientContext.client(_)) + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -327,10 +321,9 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- testTlsContext - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections - client = Network[IO].client(serverAddress).flatMap(tlsContext.client(_)) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + client = Network[IO].connect(serverSocket.address).flatMap(tlsContext.client(_)) + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client val echo = Stream .resource(setup) diff --git a/io/js-jvm/src/main/scala/fs2/io/net/Datagram.scala b/io/shared/src/main/scala/fs2/io/net/Datagram.scala similarity index 80% rename from io/js-jvm/src/main/scala/fs2/io/net/Datagram.scala rename to io/shared/src/main/scala/fs2/io/net/Datagram.scala index ea84525c26..72d0696f5e 100644 --- a/io/js-jvm/src/main/scala/fs2/io/net/Datagram.scala +++ b/io/shared/src/main/scala/fs2/io/net/Datagram.scala @@ -23,11 +23,17 @@ package fs2 package io package net -import com.comcast.ip4s.{IpAddress, SocketAddress} +import com.comcast.ip4s.{GenSocketAddress, IpAddress, Ipv4Address, Port, SocketAddress} /** A single datagram to send to the specified remote address or received from the specified address. * * @param remote remote party to send/receive datagram to/from * @param bytes data to send/receive */ -final case class Datagram(remote: SocketAddress[IpAddress], bytes: Chunk[Byte]) +final case class Datagram(remote: SocketAddress[IpAddress], bytes: Chunk[Byte]) { + def toGenDatagram: GenDatagram = GenDatagram(remote, bytes) +} + +final case class GenDatagram(remote: GenSocketAddress, bytes: Chunk[Byte]) { + def toDatagram: Datagram = Datagram(SocketAddress(Ipv4Address.Wildcard, Port.Wildcard), bytes) +} diff --git a/io/js-jvm/src/main/scala/fs2/io/net/DatagramSocket.scala b/io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala similarity index 64% rename from io/js-jvm/src/main/scala/fs2/io/net/DatagramSocket.scala rename to io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala index 8c637c84a2..e3161e5f4f 100644 --- a/io/js-jvm/src/main/scala/fs2/io/net/DatagramSocket.scala +++ b/io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala @@ -29,6 +29,23 @@ import com.comcast.ip4s._ */ trait DatagramSocket[F[_]] extends DatagramSocketPlatform[F] { + /** Local address of this socket. */ + def address: GenSocketAddress + + /** Gets the set of options that may be used with `setOption`. Note some options may not support `getOption`. */ + def supportedOptions: F[Set[SocketOption.Key[?]]] + + /** Gets the value of the specified option, if defined. */ + def getOption[A](key: SocketOption.Key[A]): F[Option[A]] + + /** Sets the specified option to the supplied value. */ + def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] + + def readGen: F[GenDatagram] + + def connect(address: GenSocketAddress): F[Unit] + def disconnect: F[Unit] + /** Reads a single datagram from this udp socket. */ def read: F[Datagram] @@ -46,13 +63,25 @@ trait DatagramSocket[F[_]] extends DatagramSocketPlatform[F] { * * @param datagram datagram to write */ - def write(datagram: Datagram): F[Unit] + def write(datagram: Datagram): F[Unit] = + write(datagram.bytes, datagram.remote) + + def write(datagram: GenDatagram): F[Unit] = + write(datagram.bytes, datagram.remote) + + def write(bytes: Chunk[Byte]): F[Unit] + + def write(bytes: Chunk[Byte], address: GenSocketAddress): F[Unit] /** Writes supplied datagrams to this udp socket. */ def writes: Pipe[F, Datagram, Nothing] /** Returns the local address of this udp socket. */ + @deprecated( + "3.13.0", + "Use address instead, which returns GenSocketAddress instead of F[SocketAddress[IpAddress]]. If ip and port are needed, call .asIpUnsafe" + ) def localAddress: F[SocketAddress[IpAddress]] /** Joins a multicast group on a specific network interface. @@ -60,6 +89,17 @@ trait DatagramSocket[F[_]] extends DatagramSocketPlatform[F] { * @param join group to join * @param interface network interface upon which to listen for datagrams */ + def join( + join: MulticastJoin[IpAddress], + interface: NetworkInterface + ): F[GroupMembership] + + /** Joins a multicast group on a specific network interface. + * + * @param join group to join + * @param interface network interface upon which to listen for datagrams + */ + @deprecated("Use overload that takes a com.comcast.ip4s.NetworkInterface", "3.13.0") def join( join: MulticastJoin[IpAddress], interface: DatagramSocket.NetworkInterface diff --git a/io/js-jvm/src/main/scala/fs2/io/net/DatagramSocketGroup.scala b/io/shared/src/main/scala/fs2/io/net/DatagramSocketGroup.scala similarity index 96% rename from io/js-jvm/src/main/scala/fs2/io/net/DatagramSocketGroup.scala rename to io/shared/src/main/scala/fs2/io/net/DatagramSocketGroup.scala index b843881819..75d2cbc31e 100644 --- a/io/js-jvm/src/main/scala/fs2/io/net/DatagramSocketGroup.scala +++ b/io/shared/src/main/scala/fs2/io/net/DatagramSocketGroup.scala @@ -35,6 +35,7 @@ trait DatagramSocketGroup[F[_]] { * @param options socket options to apply to the underlying socket * @param protocolFamily protocol family to use when opening the supporting `DatagramChannel` */ + @deprecated("3.13.0", "Use Network[F].bindDatagramSocket instead") def openDatagramSocket( address: Option[Host] = None, port: Option[Port] = None, diff --git a/io/js/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala b/io/shared/src/main/scala/fs2/io/net/IpDatagramSocketsProvider.scala similarity index 81% rename from io/js/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala rename to io/shared/src/main/scala/fs2/io/net/IpDatagramSocketsProvider.scala index 0d92a8ea8e..fcff0d9bf1 100644 --- a/io/js/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala +++ b/io/shared/src/main/scala/fs2/io/net/IpDatagramSocketsProvider.scala @@ -22,20 +22,14 @@ package fs2 package io package net -package udp -import fs2.io.internal.facade +import cats.effect.kernel.Resource +import com.comcast.ip4s.{Host, SocketAddress} -trait UdpSuitePlatform extends Fs2Suite { - - val v4Interfaces = facade.os - .networkInterfaces() - .toMap - .collect { - case (k, v) if v.exists(_.family == "IPv4") => k - } - .toList - - val v4ProtocolFamily = "udp4" +private[net] trait IpDatagramSocketsProvider[F[_]] { + def bindDatagramSocket( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] } diff --git a/io/jvm/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala b/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala similarity index 74% rename from io/jvm/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala rename to io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala index 3bc6df08ea..f0489bf21f 100644 --- a/io/jvm/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala +++ b/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala @@ -22,19 +22,19 @@ package fs2 package io package net -package udp -import java.net.{Inet4Address, NetworkInterface, StandardProtocolFamily} +import cats.effect.kernel.Resource +import com.comcast.ip4s.{Host, SocketAddress} -import CollectionCompat._ +private[net] trait IpSocketsProvider[F[_]] { -trait UdpSuitePlatform extends Fs2Suite { - - val v4Interfaces = - NetworkInterface.getNetworkInterfaces.asScala.toList.filter { interface => - interface.getInetAddresses.asScala.exists(_.isInstanceOf[Inet4Address]) - } - - val v4ProtocolFamily = StandardProtocolFamily.INET + def connectIp( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] + def bindIp( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] } diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala new file mode 100644 index 0000000000..7116cd89da --- /dev/null +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.ApplicativeThrow +import cats.effect.{Async, IO, Resource} +import com.comcast.ip4s.{ + Dns, + GenSocketAddress, + Host, + IpAddress, + Ipv4Address, + NetworkInterfaces, + Port, + SocketAddress, + UnixSocketAddress +} +import fs2.io.net.tls.TLSContext + +/** Provides the ability to work with stream sockets (e.g. TCP), datagram sockets (e.g. UDP), and TLS. + * + * Both IP and unix sockets are supported, though unix socket support depends on the underlying platform support. + * The socket type is derived from addresses supplied to operations that open new sockets. + * + * @example {{{ + * import fs2.Stream + * import fs2.io.net.{Datagram, Network} + * + * def send[F[_]: Network](datagram: Datagram): F[Unit] = + * Network[F].bindDatagramSocket().use { socket => + * socket.write(datagram) + * } + * }}} + * + * In this example, the `F[_]` parameter to `send` requires the `Network` constraint instead + * of requiring the much more powerful `Async` constraint. + * + * An instance of `Network` is available for any effect `F` which has a `LiftIO[F]` instance. + */ +sealed trait Network[F[_]] + extends NetworkPlatform[F] + with SocketGroup[F] + with DatagramSocketGroup[F] { + + /** Opens a stream socket and connects it to the supplied address. + * + * TCP is used when the supplied address contains an IP address or hostname. Unix sockets are also + * supported (when the supplied address contains a unix socket address). + * + * @param address address to connect to + * @param options socket options to apply to the socket + */ + def connect(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, Socket[F]] + + /** Opens and binds a stream server socket to the supplied address. + * + * TCP is used when the supplied address contains an IP address or hostname. Unix sockets are also + * supported (when the supplied address contains a unix socket address). + * + * @param address address to bind to + * @param options socket options to apply to each accepted socket + */ + def bind( + address: GenSocketAddress = SocketAddress.Wildcard, + options: List[SocketOption] = Nil + ): Resource[F, ServerSocket[F]] + + /** Opens and binds a stream server socket to the supplied address and returns a stream of + * client sockets, each representing a client connection to the server. + * + * @param address address to bind to + * @param options socket options to apply to each accepted socket + */ + def bindAndAccept( + address: GenSocketAddress = SocketAddress.Wildcard, + options: List[SocketOption] = Nil + ): Stream[F, Socket[F]] + + /** Opens and binds a datagram socket bound to the specified address. + * + * @param address address to bind to + * @param options socket options to apply to the socket + */ + def bindDatagramSocket( + address: GenSocketAddress = SocketAddress.Wildcard, + options: List[SocketOption] = Nil + ): Resource[F, DatagramSocket[F]] + + /** Returns a builder for `TLSContext[F]` values. + * + * For example, `Network[IO].tlsContext.system` returns a `F[TLSContext[F]]`. + */ + def tlsContext: TLSContext.Builder[F] + + /** Explicit instance of `Dns[F]`. */ + def dns: Dns[F] + + /** Explicit instance of `NetworkInterfaces[F]`. */ + def interfaces: NetworkInterfaces[F] +} + +object Network extends NetworkCompanionPlatform { + def forIO: Network[IO] = forLiftIO + + private[fs2] trait UnsealedNetwork[F[_]] extends Network[F] + + private[fs2] abstract class AsyncNetwork[F[_]](implicit F: Async[F]) extends Network[F] { + + def dns: Dns[F] = Dns.forAsync(F) + def interfaces: NetworkInterfaces[F] = NetworkInterfaces.forSync(F) + + override def bindAndAccept( + address: GenSocketAddress, + options: List[SocketOption] + ): Stream[F, Socket[F]] = + Stream.resource(bind(address, options)).flatMap(_.accept) + + override def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync[F] + + protected def matchAddress[G[_]: ApplicativeThrow, A]( + address: GenSocketAddress, + ifIp: SocketAddress[Host] => G[A], + ifUnix: UnixSocketAddress => G[A] + ): G[A] = + address match { + case sa: SocketAddress[Host] => ifIp(sa) + case ua: UnixSocketAddress => ifUnix(ua) + case other => + ApplicativeThrow[G].raiseError( + new UnsupportedOperationException(s"Unsupported address type: $other") + ) + } + + // Implementations of deprecated operations + + override def client( + to: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] = connect(to, options) + + override def server( + address: Option[Host], + port: Option[Port], + options: List[SocketOption] + ): Stream[F, Socket[F]] = Stream.resource(serverResource(address, port, options)).flatMap(_._2) + + override def serverResource( + address: Option[Host], + port: Option[Port], + options: List[SocketOption] + ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = + bind( + SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), + options + ) + .map(ss => ss.address.asIpUnsafe -> ss.accept) + + override def openDatagramSocket( + address: Option[Host], + port: Option[Port], + options: List[DatagramSocketOption], + protocolFamily: Option[DatagramSocketGroup.ProtocolFamily] + ): Resource[F, DatagramSocket[F]] = + bindDatagramSocket( + SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), + options.map(_.toSocketOption) + ) + } + + private[fs2] abstract class AsyncProviderBasedNetwork[F[_]](implicit F: Async[F]) + extends AsyncNetwork[F] { + protected def mkIpSocketsProvider: IpSocketsProvider[F] + protected def mkUnixSocketsProvider: UnixSocketsProvider[F] + protected def mkIpDatagramSocketsProvider: IpDatagramSocketsProvider[F] + protected def mkUnixDatagramSocketsProvider: UnixDatagramSocketsProvider[F] + + protected lazy val ipSockets: IpSocketsProvider[F] = mkIpSocketsProvider + protected lazy val unixSockets: UnixSocketsProvider[F] = mkUnixSocketsProvider + protected lazy val ipDatagramSockets: IpDatagramSocketsProvider[F] = mkIpDatagramSocketsProvider + protected lazy val unixDatagramSockets: UnixDatagramSocketsProvider[F] = + mkUnixDatagramSocketsProvider + + override def connect( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] = + matchAddress( + address, + sa => ipSockets.connectIp(sa, options), + ua => unixSockets.connectUnix(ua, options) + ) + + override def bind( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = + matchAddress( + address, + sa => ipSockets.bindIp(sa, options), + ua => unixSockets.bindUnix(ua, options) + ) + + override def bindDatagramSocket( + address: GenSocketAddress, + options: List[SocketOption] = Nil + ): Resource[F, DatagramSocket[F]] = + matchAddress( + address, + sa => ipDatagramSockets.bindDatagramSocket(sa, options), + ua => unixDatagramSockets.bindDatagramSocket(ua, options) + ) + } + + def apply[F[_]](implicit F: Network[F]): F.type = F +} diff --git a/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala new file mode 100644 index 0000000000..9ea66c223d --- /dev/null +++ b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import com.comcast.ip4s.GenSocketAddress + +/** Represents a bound TCP server socket. + * + * Note some platforms do not support getting and setting socket options on server sockets + * so take care when using `getOption` and `setOption`. + * + * Client sockets can be accepted by pulling on the `accept` stream. A concurrent server + * that is limited to `maxAccept` concurrent clients is accomplished by + * `b.accept.map(handleClientSocket).parJoin(maxAccept)`. + */ +sealed trait ServerSocket[F[_]] extends SocketInfo[F] { + + def address: GenSocketAddress + + /** Stream of client sockets; typically processed concurrently to allow concurrent clients. */ + def accept: Stream[F, Socket[F]] +} + +object ServerSocket { + + private[net] def apply[F[_]]( + info: SocketInfo[F], + accept: Stream[F, Socket[F]] + ): ServerSocket[F] = { + val accept0 = accept + new ServerSocket[F] { + override def accept: Stream[F, Socket[F]] = accept0 + override def address = info.address + override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = info.getOption(key) + override def setOption[A](key: SocketOption.Key[A], value: A) = info.setOption(key, value) + override def supportedOptions = info.supportedOptions + } + } +} diff --git a/io/shared/src/main/scala/fs2/io/net/Socket.scala b/io/shared/src/main/scala/fs2/io/net/Socket.scala index 736317c781..50b540f7b4 100644 --- a/io/shared/src/main/scala/fs2/io/net/Socket.scala +++ b/io/shared/src/main/scala/fs2/io/net/Socket.scala @@ -23,12 +23,15 @@ package fs2 package io package net -import com.comcast.ip4s.{IpAddress, SocketAddress} +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} import fs2.io.file.FileHandle /** Provides the ability to read/write from a TCP socket in the effect `F`. */ -trait Socket[F[_]] { +trait Socket[F[_]] extends SocketInfo[F] { + + /** Gets the remote address of this socket. */ + def peerAddress: GenSocketAddress /** Reads up to `maxBytes` from the peer. * @@ -50,17 +53,9 @@ trait Socket[F[_]] { */ def endOfInput: F[Unit] - /** Indicates to peer, we are done writing. * */ + /** Indicates to peer that we are done writing. * */ def endOfOutput: F[Unit] - def isOpen: F[Boolean] - - /** Asks for the remote address of the peer. */ - def remoteAddress: F[SocketAddress[IpAddress]] - - /** Asks for the local address of the socket. */ - def localAddress: F[SocketAddress[IpAddress]] - /** Writes `bytes` to the peer. * * Completes when the bytes are written to the socket. @@ -98,6 +93,23 @@ trait Socket[F[_]] { go(offset, count).through(writes) } + + // Deprecated members + + @deprecated("3.13.0", "No replacement; sockets are open until they are finalized") + def isOpen: F[Boolean] + + @deprecated( + "3.13.0", + "Use address instead, which returns GenSocketAddress instead of F[SocketAddress[IpAddress]]. If ip and port are needed, call .asIpUnsafe" + ) + def localAddress: F[SocketAddress[IpAddress]] + + @deprecated( + "3.13.0", + "Use peerAddress instead, which returns GenSocketAddress instead of F[SocketAddress[IpAddress]]. If ip and port are needed, call .asIpUnsafe" + ) + def remoteAddress: F[SocketAddress[IpAddress]] } object Socket extends SocketCompanionPlatform diff --git a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala index 016609a129..48c5c3ab27 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala @@ -25,7 +25,6 @@ package net import cats.effect.kernel.Resource import com.comcast.ip4s.{Host, IpAddress, Port, SocketAddress} -import cats.effect.kernel.Async /** Supports creation of client and server TCP sockets that all share * an underlying non-blocking channel group. @@ -39,6 +38,7 @@ trait SocketGroup[F[_]] { * @param to address of remote server * @param options socket options to apply to the underlying socket */ + @deprecated("3.13.0", "Use Network[F].connect instead") def client( to: SocketAddress[Host], options: List[SocketOption] = List.empty @@ -54,6 +54,7 @@ trait SocketGroup[F[_]] { * @param port port to bind * @param options socket options to apply to the underlying socket */ + @deprecated("3.13.0", "Use Network[F].bindAndAccept instead") def server( address: Option[Host] = None, port: Option[Port] = None, @@ -64,30 +65,10 @@ trait SocketGroup[F[_]] { * * Make sure to handle errors in the client socket Streams. */ + @deprecated("3.13.0", "Use Network[F].bind instead") def serverResource( address: Option[Host] = None, port: Option[Port] = None, options: List[SocketOption] = List.empty ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] } - -private[net] object SocketGroup extends SocketGroupCompanionPlatform { - - private[net] abstract class AbstractAsyncSocketGroup[F[_]: Async] extends SocketGroup[F] { - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = - Stream - .resource( - serverResource( - address, - port, - options - ) - ) - .flatMap { case (_, clients) => clients } - } - -} diff --git a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala new file mode 100644 index 0000000000..2e6621b1cd --- /dev/null +++ b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import com.comcast.ip4s.GenSocketAddress + +/** Information about a connected socket. Super trait of both [[ServerSocket]] and [[Socket]]. */ +trait SocketInfo[F[_]] { + + /** Local address of this socket. */ + def address: GenSocketAddress + + /** Gets the set of options that may be used with `setOption`. Note some options may not support `getOption`. */ + def supportedOptions: F[Set[SocketOption.Key[?]]] + + /** Gets the value of the specified option, if defined. */ + def getOption[A](key: SocketOption.Key[A]): F[Option[A]] + + /** Sets the specified option to the supplied value. */ + def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] +} + +object SocketInfo extends SocketInfoCompanionPlatform diff --git a/io/shared/src/main/scala/fs2/io/net/SocketOption.scala b/io/shared/src/main/scala/fs2/io/net/SocketOption.scala index 859bc5fe3a..e48f5bbef8 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketOption.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketOption.scala @@ -21,11 +21,15 @@ package fs2.io.net -/** Specifies a socket option on a TCP/UDP socket. +import cats.Applicative +import cats.effect.Resource +import cats.syntax.all.* +import com.comcast.ip4s.UnixSocketAddress +import fs2.io.file.{Files, Path} + +/** Specifies a socket option on a socket. * - * The companion provides methods for creating a socket option from each of the - * JDK `java.net.StandardSocketOptions` as well as the ability to construct arbitrary - * additional options. See the docs on `StandardSocketOptions` for details on each. + * The companion provides methods for creating various socket options. */ sealed trait SocketOption { type Value @@ -39,4 +43,32 @@ object SocketOption extends SocketOptionCompanionPlatform { val key = key0 val value = value0 } + + private[net] def extractUnixSocketDeletes[F[_]: Applicative: Files]( + options: List[SocketOption], + address: UnixSocketAddress + ): (List[SocketOption], Resource[F, Unit]) = { + var deleteIfExists: Boolean = false + var deleteOnClose: Boolean = true + + val filteredOptions = options.filter { opt => + if (opt.key == SocketOption.UnixSocketDeleteIfExists) { + deleteIfExists = opt.value.asInstanceOf[Boolean] + false + } else if (opt.key == SocketOption.UnixSocketDeleteOnClose) { + deleteOnClose = opt.value.asInstanceOf[Boolean] + false + } else { + true + } + } + + val delete = Resource.make { + Files[F].deleteIfExists(Path(address.path)).whenA(deleteIfExists) + } { _ => + Files[F].deleteIfExists(Path(address.path)).whenA(deleteOnClose) + } + + (filteredOptions, delete) + } } diff --git a/io/shared/src/main/scala/fs2/io/net/UnixDatagramSocketsProvider.scala b/io/shared/src/main/scala/fs2/io/net/UnixDatagramSocketsProvider.scala new file mode 100644 index 0000000000..1b7c99d583 --- /dev/null +++ b/io/shared/src/main/scala/fs2/io/net/UnixDatagramSocketsProvider.scala @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.Resource +import com.comcast.ip4s.UnixSocketAddress + +private[net] trait UnixDatagramSocketsProvider[F[_]] { + + def bindDatagramSocket( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] +} diff --git a/io/native/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala similarity index 77% rename from io/native/src/main/scala/fs2/io/net/Network.scala rename to io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala index 4733608bf0..5b9a81c964 100644 --- a/io/native/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala @@ -23,14 +23,18 @@ package fs2 package io package net -import fs2.io.net.tls.TLSContext +import cats.effect.Resource +import com.comcast.ip4s.UnixSocketAddress -sealed trait Network[F[_]] extends NetworkPlatform[F] with SocketGroup[F] { - def tlsContext: TLSContext.Builder[F] -} +private[net] trait UnixSocketsProvider[F[_]] { -object Network extends NetworkCompanionPlatform { - private[fs2] trait UnsealedNetwork[F[_]] extends Network[F] + def connectUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] - def apply[F[_]](implicit F: Network[F]): F.type = F + def bindUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] } diff --git a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala index 5cdf9bb83c..8b4abf59dc 100644 --- a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala +++ b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala @@ -21,15 +21,20 @@ package fs2.io.net.unixsocket -import cats.effect.kernel.Resource +import cats.effect.{Async, Resource} + +import com.comcast.ip4s.{UnixSocketAddress => Ip4sUnixSocketAddress} + import fs2.Stream -import fs2.io.net.Socket +import fs2.io.net.{Socket, SocketOption, UnixSocketsProvider} /** Capability of working with AF_UNIX sockets. */ +@deprecated("Use client, bind, bindAndAccept on Network[F] instead", "3.13.0") trait UnixSockets[F[_]] { /** Returns a resource which opens a unix socket to the specified path. */ + @deprecated("Use Network[F].connect(address) instead", "3.13.0") def client(address: UnixSocketAddress): Resource[F, Socket[F]] /** Listens to the specified path for connections and emits a `Socket` for each connection. @@ -40,6 +45,7 @@ trait UnixSockets[F[_]] { * * By default, the path is deleted when the server closes. To override this, pass `deleteOnClose = false`. */ + @deprecated("Use Network[F].bind(address) instead", "3.13.0") def server( address: UnixSocketAddress, deleteIfExists: Boolean = false, @@ -48,5 +54,31 @@ trait UnixSockets[F[_]] { } object UnixSockets extends UnixSocketsCompanionPlatform { + @deprecated("Use Network[F] instead", "3.13.0") def apply[F[_]](implicit F: UnixSockets[F]): UnixSockets[F] = F + + @deprecated("Use Network[F] instead", "3.13.0") + protected class AsyncUnixSockets[F[_]: Async](delegate: UnixSocketsProvider[F]) + extends UnixSockets[F] { + + def client(address: UnixSocketAddress): Resource[F, Socket[F]] = + delegate.connectUnix(Ip4sUnixSocketAddress(address.path), Nil) + + def server( + address: UnixSocketAddress, + deleteIfExists: Boolean, + deleteOnClose: Boolean + ): Stream[F, Socket[F]] = + Stream + .resource( + delegate.bindUnix( + Ip4sUnixSocketAddress(address.path), + List( + SocketOption.unixSocketDeleteIfExists(deleteIfExists), + SocketOption.unixSocketDeleteOnClose(deleteOnClose) + ) + ) + ) + .flatMap(_.accept) + } } diff --git a/io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala similarity index 75% rename from io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala rename to io/shared/src/test/scala/fs2/io/net/SocketSuite.scala index 58c72b1ff5..3ff14b3b07 100644 --- a/io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala @@ -22,10 +22,9 @@ package fs2 package io package net -package tcp import cats.effect.IO -import com.comcast.ip4s._ +import com.comcast.ip4s.{UnknownHostException => Ip4sUnknownHostException, _} import scala.concurrent.duration._ import scala.concurrent.TimeoutException @@ -36,14 +35,14 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { val timeout = 30.seconds val setup = for { - serverSetup <- Network[IO].serverResource(address = Some(ip"127.0.0.1")) - (bindAddress, server) = serverSetup - clients = Stream - .resource( - Network[IO].client(bindAddress, options = setupOptionsPlatform) - ) - .repeat - } yield server -> clients + serverSocket <- Network[IO].bind(address = SocketAddress(ip"127.0.0.1", Port.Wildcard)) + clients = + Stream + .resource( + Network[IO].connect(serverSocket.address, options = setupOptionsPlatform) + ) + .repeat + } yield serverSocket.accept -> clients group("tcp") { test("echo requests - each concurrent client gets back what it sent") { @@ -140,14 +139,14 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { .resource(setup) .flatMap { case (server, clients) => val serverSocketAddresses = server.evalMap { socket => - socket.endOfOutput *> socket.localAddress.product(socket.remoteAddress) + socket.endOfOutput.as(socket.address -> socket.peerAddress) } val clientSocketAddresses = clients .take(1) .evalMap { socket => - socket.endOfOutput *> socket.localAddress.product(socket.remoteAddress) + socket.endOfOutput.as(socket.address -> socket.peerAddress) } serverSocketAddresses.parZip(clientSocketAddresses).map { @@ -155,7 +154,6 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { assertEquals(clientRemote, serverLocal) assertEquals(clientLocal, serverRemote) } - } .compile .drain @@ -163,27 +161,29 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("errors - should be captured in the effect") { val connectionRefused = for { - port <- Network[IO].serverResource(Some(ip"127.0.0.1")).use(s => IO.pure(s._1.port)) + port <- Network[IO] + .bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + .use(serverSocket => IO.pure(serverSocket.address.asIpUnsafe.port)) _ <- Network[IO] - .client(SocketAddress(host"localhost", port)) + .connect(SocketAddress(host"localhost", port)) .use_ .interceptMessage[ConnectException]("Connection refused") } yield () val addressAlreadyInUse = - Network[IO].serverResource(Some(ip"127.0.0.1")).map(_._1).use { bindAddress => + Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)).use { serverSocket => Network[IO] - .serverResource(Some(bindAddress.host), Some(bindAddress.port)) + .bind(serverSocket.address) .use_ .interceptMessage[BindException]("Address already in use") } val unknownHost = Network[IO] - .client(SocketAddress.fromString("not.example.com:80").get) + .connect(SocketAddress.fromString("not.example.com:80").get) .use_ .attempt .map { - case Left(ex: UnknownHostException) => + case Left(ex: Ip4sUnknownHostException) => assert( ex.getMessage == "not.example.com: Name or service not known" || ex.getMessage == "not.example.com: nodename nor servname provided, or not known" ) @@ -199,10 +199,9 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { SocketOption.noDelay(true) ) ++ optionsPlatform val setup = for { - serverSetup <- Network[IO].serverResource(Some(ip"127.0.0.1"), None, opts) - (bindAddress, server) = serverSetup - client <- Network[IO].client(bindAddress, opts) - } yield (server, client) + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard), opts) + client <- Network[IO].connect(serverSocket.address, opts) + } yield (serverSocket.accept, client) val msg = "hello" @@ -228,10 +227,9 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("read after timed out read") { val setup = for { - serverSetup <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (bindAddress, server) = serverSetup - client <- Network[IO].client(bindAddress) - } yield (server, client) + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + client <- Network[IO].connect(serverSocket.address) + } yield (serverSocket.accept, client) Stream .resource(setup) .flatMap { case (server, client) => @@ -251,9 +249,9 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { } test("can shutdown a socket that's pending a read") { - Network[IO].serverResource().use { case (bindAddress, clients) => - Network[IO].client(bindAddress).use { _ => - clients.head.flatMap(_.reads).compile.drain.timeout(2.seconds).recover { + Network[IO].bind(SocketAddress.Wildcard).use { serverSocket => + Network[IO].connect(serverSocket.address).use { _ => + serverSocket.accept.head.flatMap(_.reads).compile.drain.timeout(2.seconds).recover { case _: TimeoutException => () } } @@ -261,9 +259,9 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { } test("accepted socket closes timely") { - Network[IO].serverResource().use { case (bindAddress, clients) => - clients.foreach(_ => IO.sleep(1.second)).compile.drain.background.surround { - Network[IO].client(bindAddress).use { client => + Network[IO].bind(SocketAddress.Wildcard).use { serverSocket => + serverSocket.accept.foreach(_ => IO.sleep(1.second)).compile.drain.background.surround { + Network[IO].connect(serverSocket.address).use { client => client.read(1).assertEquals(None) } } @@ -272,9 +270,9 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("sockets are released at the end of the resource scope") { val f = - Network[IO].serverResource(port = Some(port"9071")).use { case (bindAddress, clients) => - clients.foreach(_ => IO.sleep(1.second)).compile.drain.background.surround { - Network[IO].client(bindAddress).use { client => + Network[IO].bind(SocketAddress.port(port"9071")).use { serverSocket => + serverSocket.accept.foreach(_ => IO.sleep(1.second)).compile.drain.background.surround { + Network[IO].connect(serverSocket.address).use { client => client.read(1).assertEquals(None) } } @@ -283,20 +281,25 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { } test("endOfOutput / endOfInput ignores ENOTCONN") { - Network[IO].serverResource().use { case (bindAddress, clients) => - Network[IO].client(bindAddress).surround(IO.sleep(100.millis)).background.surround { - clients - .take(1) - .foreach { socket => - socket.write(Chunk.array("fs2.rocks".getBytes)) *> - IO.sleep(1.second) *> - socket.endOfOutput *> socket.endOfInput - } - .compile - .drain - } + Network[IO].bind(SocketAddress.Wildcard).use { serverSocket => + Network[IO] + .connect(serverSocket.address) + .surround(IO.sleep(100.millis)) + .background + .surround { + serverSocket.accept + .take(1) + .foreach { socket => + socket.write(Chunk.array("fs2.rocks".getBytes)) *> + IO.sleep(1.second) *> + socket.endOfOutput *> socket.endOfInput + } + .compile + .drain + } } } + test("sendFile - sends data from file to socket from the offset") { val content = "Hello, world!" val offset = 7L @@ -313,10 +316,9 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { .compile .drain .toResource - serverSetup <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (bindAddress, server) = serverSetup - client <- Network[IO].client(bindAddress) - } yield (tempFile, server, client) + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + client <- Network[IO].connect(serverSocket.address) + } yield (tempFile, serverSocket.accept, client) Stream .resource(setup) diff --git a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala new file mode 100644 index 0000000000..43c493ce67 --- /dev/null +++ b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io.net + +import scala.concurrent.duration._ + +import cats.effect.IO +import com.comcast.ip4s.UnixSocketAddress + +class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { + + protected def testProvider(provider: String, sockets: UnixSocketsProvider[IO]) = { + test(s"echoes - $provider") { + val address = UnixSocketAddress("fs2-unix-sockets-test.sock") + + val server = Stream + .resource(sockets.bindUnix(address, Nil)) + .flatMap(_.accept) + .map { socket => + socket.reads.through(socket.writes) + } + .parJoinUnbounded + + def client(msg: Chunk[Byte]) = sockets.connectUnix(address, Nil).use { socket => + socket.write(msg) *> socket.endOfOutput *> socket.reads.compile + .to(Chunk) + .map(read => assertEquals(read, msg)) + } + + val clients = (0 until 100).map(b => client(Chunk.singleton(b.toByte))) + + (Stream.sleep_[IO](1.second) ++ Stream.emits(clients).evalMap(identity)) + .concurrently(server) + .compile + .drain + } + + test(s"addresses - $provider") { + val address = UnixSocketAddress("fs2-unix-sockets-test.sock") + + val server = Stream + .resource(sockets.bindUnix(address, Nil)) + .flatMap { ss => + assertEquals(ss.address, address) + ss.accept + } + .map { socket => + assertEquals(socket.address, address) + assertEquals(socket.peerAddress, UnixSocketAddress("")) + socket.reads.through(socket.writes) + } + .parJoinUnbounded + + val msg = Chunk.array("Hello, world".getBytes) + val client = Stream.resource(sockets.connectUnix(address, Nil).evalMap { socket => + assertEquals(socket.address, UnixSocketAddress("")) + assertEquals(socket.peerAddress, address) + socket.write(msg) *> socket.endOfOutput *> socket.reads.compile + .to(Chunk) + .map(read => assertEquals(read, msg)) + }) + + (Stream.sleep_[IO](1.second) ++ client) + .concurrently(server) + .compile + .drain + } + } +} diff --git a/site/concurrency-primitives.md b/site/concurrency-primitives.md index 9efc080e72..09146e6325 100644 --- a/site/concurrency-primitives.md +++ b/site/concurrency-primitives.md @@ -147,7 +147,6 @@ The program ends after 15 seconds when the signal interrupts the publishing of m ```scala mdoc:silent import scala.concurrent.duration._ -import scala.language.higherKinds import cats.effect.std.Console import cats.effect.{Clock, IO, IOApp, Temporal} import cats.syntax.all._ diff --git a/site/io.md b/site/io.md index 7b04b5ed51..b8514d2b17 100644 --- a/site/io.md +++ b/site/io.md @@ -25,7 +25,7 @@ The `fs2.io.net.Socket` trait provides mechanisms for reading and writing data - To get started, let's write a client program that connects to a server, sends a message, and reads a response. ```scala mdoc -import fs2.{Chunk, Stream} +import fs2.Chunk import fs2.io.net.Network import cats.effect.MonadCancelThrow import cats.effect.std.Console @@ -33,7 +33,7 @@ import cats.syntax.all._ import com.comcast.ip4s._ def client[F[_]: MonadCancelThrow: Console: Network]: F[Unit] = - Network[F].client(SocketAddress(host"localhost", port"5555")).use { socket => + Network[F].connect(SocketAddress(host"localhost", port"5555")).use { socket => socket.write(Chunk.array("Hello, world!".getBytes)) >> socket.read(8192).flatMap { response => Console[F].println(s"Response: $response") @@ -41,22 +41,21 @@ def client[F[_]: MonadCancelThrow: Console: Network]: F[Unit] = } ``` -To open a socket that's connected to `localhost:5555`, we use the `client` method on the `Network` capability. The `Network` capability provides the runtime environment for the sockets it creates. +To open a socket that's connected to `localhost:5555`, we use the `connect` method on the `Network` capability. The `Network` capability provides the runtime environment for the sockets it creates. -The `Network[F].client` method returns a `Resource[F, Socket[F]]` which automatically closes the socket after the resource has been used. To write data to the socket, we call `socket.write`, which takes a `Chunk[Byte]` and returns an `F[Unit]`. Once the write completes, we do a single read from the socket via `socket.read`, passing the maximum amount of bytes we want to read. The returns an `F[Option[Chunk[Byte]]]` -- `None` if the socket reaches end of input and `Some` if the read produced a chunk. Finally, we print the response to the console. +The `Network[F].connect` method returns a `Resource[F, Socket[F]]` which automatically closes the socket after the resource has been used. To write data to the socket, we call `socket.write`, which takes a `Chunk[Byte]` and returns an `F[Unit]`. Once the write completes, we do a single read from the socket via `socket.read`, passing the maximum amount of bytes we want to read. The returns an `F[Option[Chunk[Byte]]]` -- `None` if the socket reaches end of input and `Some` if the read produced a chunk. Finally, we print the response to the console. Note we aren't doing any binary message framing or packetization in this example. Hence, it's very possible for the single read to only receive a portion of the original message -- perhaps just the bytes for `"Hello, w"`. We can use FS2 streams to simplify this. The `Socket` trait defines stream operations -- `writes` and `reads`. We could rewrite this example using the stream operations like so: ```scala mdoc:reset -import fs2.{Chunk, Stream, text} +import fs2.{Stream, text} import fs2.io.net.Network import cats.effect.MonadCancelThrow import cats.effect.std.Console -import cats.syntax.all._ import com.comcast.ip4s._ def client[F[_]: MonadCancelThrow: Console: Network]: Stream[F, Unit] = - Stream.resource(Network[F].client(SocketAddress(host"localhost", port"5555"))).flatMap { socket => + Stream.resource(Network[F].connect(SocketAddress(host"localhost", port"5555"))).flatMap { socket => Stream("Hello, world!") .through(text.utf8.encode) .through(socket.writes) ++ @@ -76,7 +75,7 @@ This program won't end until the server side closes the socket or indicates ther ```scala mdoc:nest def client[F[_]: MonadCancelThrow: Console: Network]: Stream[F, Unit] = - Stream.resource(Network[F].client(SocketAddress(host"localhost", port"5555"))).flatMap { socket => + Stream.resource(Network[F].connect(SocketAddress(host"localhost", port"5555"))).flatMap { socket => Stream("Hello, world!") .interleave(Stream.constant("\n")) .through(text.utf8.encode) @@ -95,23 +94,23 @@ To update the write side, we added `.interleave(Stream.constant("\n"))` before d #### Handling Connection Errors -If a TCP connection cannot be established, `socketGroup.client` fails with a `java.net.ConnectException`. To automatically attempt a reconnection, we can handle the `ConnectException` and try connecting again. +If a TCP connection cannot be established, `connect` fails with a `fs2.io.net.ConnectException`. To automatically attempt a reconnection, we can handle the `ConnectException` and try connecting again. ```scala mdoc:nest import scala.concurrent.duration._ import cats.effect.Temporal -import fs2.io.net.Socket -import java.net.ConnectException +import fs2.io.net.{ConnectException, Socket} -def connect[F[_]: Temporal: Network](address: SocketAddress[Host]): Stream[F, Socket[F]] = - Stream.resource(Network[F].client(address)) +def retryingConnect[F[_]: Temporal: Network](address: SocketAddress[Host]): Stream[F, Socket[F]] = + Stream.resource(Network[F].connect(address)) .handleErrorWith { case _: ConnectException => - connect(address).delayBy(5.seconds) + retryingConnect(address).delayBy(5.seconds) + case other => Stream.raiseError(other) } def client[F[_]: Temporal: Console: Network]: Stream[F, Unit] = - connect(SocketAddress(host"localhost", port"5555")).flatMap { socket => + retryingConnect(SocketAddress(host"localhost", port"5555")).flatMap { socket => Stream("Hello, world!") .interleave(Stream.constant("\n")) .through(text.utf8.encode) @@ -126,7 +125,7 @@ def client[F[_]: Temporal: Console: Network]: Stream[F, Unit] = } ``` -We've extracted the `Network[IO].client` call in to a new method called `connect`. The connect method attempts to create a client and handles the `ConnectException`. Upon encountering the exception, we call `connect` recursively after a 5 second delay. Because we are using `delayBy`, we needed to add a `Temporal` constraint to `F`. This same pattern could be used for more advanced retry strategies -- e.g., exponential delays and failing after a fixed number of attempts. Streams that call methods on `Socket` can fail with exceptions due to loss of the underlying TCP connection. Such exceptions can be handled in a similar manner. +We've extracted the `Network[IO].connect` call in to a new method called `retryingConnect`. The `retryingConnect` method attempts to create a client and handles the `ConnectException`. Upon encountering the exception, we call `retryingConnect` recursively after a 5 second delay. Because we are using `delayBy`, we needed to add a `Temporal` constraint to `F`. This same pattern could be used for more advanced retry strategies -- e.g., exponential delays and failing after a fixed number of attempts. Streams that call methods on `Socket` can fail with exceptions due to loss of the underlying TCP connection. Such exceptions can be handled in a similar manner. ### Servers @@ -136,18 +135,20 @@ Now let's implement a server application that communicates with the client app w import cats.effect.Concurrent def echoServer[F[_]: Concurrent: Network]: F[Unit] = - Network[F].server(port = Some(port"5555")).map { client => - client.reads - .through(text.utf8.decode) - .through(text.lines) - .interleave(Stream.constant("\n")) - .through(text.utf8.encode) - .through(client.writes) - .handleErrorWith(_ => Stream.empty) // handle errors of client sockets + Stream.resource(Network[F].bind(SocketAddress.port(port"5555"))).map { serverSocket => + serverSocket.accept.map { clientSocket => + clientSocket.reads + .through(text.utf8.decode) + .through(text.lines) + .interleave(Stream.constant("\n")) + .through(text.utf8.encode) + .through(clientSocket.writes) + .handleErrorWith(_ => Stream.empty) // handle errors of client sockets + } }.parJoin(100).compile.drain ``` -We start with a call to `Network[IO].server` which returns a value of an interesting type -- `Stream[F, Socket[F]]`. This is an infinite stream of client sockets -- each time a client connects to the server, a `Socket[F]` is emitted, allowing interaction with that client. The lifetime of the client socket is managed by the overall stream -- e.g. flat mapping over a socket will keep that socket open until the returned inner stream completes, at which point, the client socket is closed and any underlying resources are returned to the runtime environment. +We start with a call to `Network[IO].bind` which returns a value of type `Resource[F, ServerSocket[F]]`. The `ServerSocket` type defines an `accept` method of type `Stream[F, Socket[F]]`. This is an infinite stream of client sockets -- each time a client connects to the server, a `Socket[F]` is emitted, allowing interaction with that client. The lifetime of the client socket is managed by the overall stream -- e.g. flat mapping over a socket will keep that socket open until the returned inner stream completes, at which point, the client socket is closed and any underlying resources are returned to the runtime environment. We map over this infinite stream of clients and provide the logic for handling an individual client. In this case, we read from the client socket, UTF-8 decode the received bytes, extract individual lines, and then write each line back to the client. This logic is implemented as a single `Stream[F, Unit]`. @@ -156,7 +157,7 @@ Since we mapped over the infinite client stream, we end up with a `Stream[F, Str In joining all these streams together, be prudent to handle errors in the client streams. -The pattern of `Network[F].server(address).map(handleClient).parJoin(maxConcurrentClients)` is very common when working with server sockets. +The pattern of `Network[F].bind(address).map(ss => handleClient(ss.accept)).parJoin(maxConcurrentClients)` is very common when working with server sockets. A simpler echo server could be implemented with this core logic: @@ -170,26 +171,24 @@ The [fs2-chat](https://github.com/functional-streams-for-scala/fs2-chat) sample ## UDP -UDP support works much the same way as TCP. The `fs2.io.net.DatagramSocket` trait provides mechanisms for reading and writing UDP datagrams. UDP sockets are created via the `openDatagramSocket` method on `fs2.io.net.Network`. Unlike TCP, there's no differentiation between client and server sockets. Additionally, since UDP is a packet based protocol, read and write operations use `fs2.io.net.Datagram` values, which consist of a `Chunk[Byte]` and a `SocketAddress[IpAddress]`. +UDP support works much the same way as TCP. The `fs2.io.net.DatagramSocket` trait provides mechanisms for reading and writing UDP datagrams. UDP sockets are created via the `bindDatagramSocket` method on `fs2.io.net.Network`. Unlike TCP, there's no differentiation between client and server sockets. Adapting the TCP client example for UDP gives us the following: ```scala mdoc:reset import fs2.{Stream, text} -import fs2.io.net.{Datagram, Network} +import fs2.io.net.Network import cats.effect.Concurrent import cats.effect.std.Console import com.comcast.ip4s._ def client[F[_]: Concurrent: Console: Network]: F[Unit] = { val address = SocketAddress(ip"127.0.0.1", port"5555") - Stream.resource(Network[F].openDatagramSocket()).flatMap { socket => + Stream.resource(Network[F].bindDatagramSocket()).flatMap { socket => Stream("Hello, world!") .through(text.utf8.encode) .chunks - .map(data => Datagram(address, data)) - .through(socket.writes) - .drain ++ + .evalTap(data => socket.write(data, address)) ++ socket.reads .flatMap(datagram => Stream.chunk(datagram.bytes)) .through(text.utf8.decode) @@ -200,11 +199,11 @@ def client[F[_]: Concurrent: Console: Network]: F[Unit] = { } ``` -When writing, we map each chunk of bytes to a `Datagram`, which includes the destination address of the packet. When reading, we convert the `Stream[F, Datagram]` to a `Stream[F, Byte]` via `flatMap(datagram => Stream.chunk(datagram.bytes))`. Otherwise, the example is unchanged. +We call `socket.write`, supplying a chunk of bytes and the destination address. When reading, we convert the `Stream[F, Datagram]` to a `Stream[F, Byte]` via `flatMap(datagram => Stream.chunk(datagram.bytes))`. Otherwise, the example is unchanged. ```scala mdoc def echoServer[F[_]: Concurrent: Network]: F[Unit] = - Stream.resource(Network[F].openDatagramSocket(port = Some(port"5555"))).flatMap { socket => + Stream.resource(Network[F].bindDatagramSocket(SocketAddress.port(port"5555"))).flatMap { socket => socket.reads.through(socket.writes) }.compile.drain ``` @@ -234,7 +233,7 @@ import com.comcast.ip4s._ def client[F[_]: MonadCancelThrow: Console: Network]( tlsContext: TLSContext[F]): Stream[F, Unit] = { - Stream.resource(Network[F].client(SocketAddress(host"localhost", port"5555"))).flatMap { underlyingSocket => + Stream.resource(Network[F].connect(SocketAddress(host"localhost", port"5555"))).flatMap { underlyingSocket => Stream.resource(tlsContext.client(underlyingSocket)).flatMap { socket => Stream("Hello, world!") .interleave(Stream.constant("\n")) @@ -263,10 +262,10 @@ import fs2.io.net.tls.{TLSParameters, TLSSocket} import cats.effect.Resource import javax.net.ssl.SNIHostName -def tlsClientWithSni[F[_]: MonadCancelThrow: Network]( +def tlsClientWithSni[F[_]: Network]( tlsContext: TLSContext[F], address: SocketAddress[Host]): Resource[F, TLSSocket[F]] = - Network[F].client(address).flatMap { underlyingSocket => + Network[F].connect(address).flatMap { underlyingSocket => tlsContext.clientBuilder( underlyingSocket ).withParameters( @@ -291,7 +290,7 @@ def debug[F[_]: MonadCancelThrow: Network]( tlsContext: TLSContext[F], address: SocketAddress[Host] ): F[String] = - Network[F].client(address).use { underlyingSocket => + Network[F].connect(address).use { underlyingSocket => tlsContext .clientBuilder(underlyingSocket) .withParameters( @@ -316,13 +315,15 @@ The `fs2.io.file` package provides support for working with files. The README ex ```scala mdoc import cats.effect.Concurrent -import fs2.{hash, text} +import fs2.text +import fs2.hashing.{Hashing, HashAlgorithm} import fs2.io.file.{Files, Path} -def writeDigest[F[_]: Files: Concurrent](path: Path): F[Path] = { +def writeDigest[F[_]: Files: Hashing: Concurrent](path: Path): F[Path] = { val target = Path(path.toString + ".sha256") Files[F].readAll(path) - .through(hash.sha256) + .through(Hashing[F].hash(HashAlgorithm.SHA256)) + .flatMap(h => Stream.chunk(h.bytes)) .through(text.hex.encode) .through(text.utf8.encode) .through(Files[F].writeAll(target)) @@ -436,4 +437,4 @@ The `fs2.io.writeOutputStream` method provides a pipe that writes the bytes emit The `fs2.io.readOutputStream` method creates a `Stream[F, Byte]` from a function which writes to an `OutputStream`. [s2n-tls]: https://github.com/aws/s2n-tls -[`node:tls` module]: https://nodejs.org/api/tls.html \ No newline at end of file +[`node:tls` module]: https://nodejs.org/api/tls.html diff --git a/site/timeseries.md b/site/timeseries.md index ffe6cb119e..2ac60c6761 100644 --- a/site/timeseries.md +++ b/site/timeseries.md @@ -25,7 +25,7 @@ Our `withBitrate` combinator requires a `Stream[F, TimeStamped[ByteVector]]` arg ```scala mdoc def withReceivedBitrate[F[_]](input: Stream[F, Byte]): Stream[F, TimeStamped[Either[Long, ByteVector]]] = - input.chunks.map(c => TimeStamped.unsafeNow(c.toByteVector)).through(withBitrate) + input.chunks.map(c => TimeStamped.unsafeMonotonic(c.toByteVector)).through(withBitrate) ``` Each emitted sample is the sum of bits received during each one second period. Let's compute an average of that value over the last 10 seconds. We can do this via `mapAccumulate` along with a `scala.collection.immutable.Queue`: