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 eebb3c8982..869f39c4c6 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 @@ -71,10 +71,10 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => tlsSock, readStream, sessionRef.discrete.unNone.head.compile.lastOrError, - F.delay[Any](tlsSock.alpnProtocol).flatMap { - case false => "".pure // mimicking JVM - case protocol: String => protocol.pure - case _ => F.raiseError(new NoSuchElementException) + F.delay[Any](tlsSock.alpnProtocol).map { + case false => Some("") // mimicking JVM + case protocol: String => Some(protocol) + case _ => None } ) @@ -82,7 +82,17 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => sock: facade.tls.TLSSocket, readStream: SuspendedStream[F, Byte], val session: F[SSLSession], - val applicationProtocol: F[String] + val applicationProtocolOption: F[Option[String]] ) extends Socket.AsyncSocket[F](sock, readStream) - with UnsealedTLSSocket[F] + with UnsealedTLSSocket[F] { + override def applicationProtocol: F[String] = applicationProtocolOption.flatMap { + case None => + Async[F].raiseError( + new NoSuchElementException( + "`tlsSock.alpnProtocol` returned neither false, nor a String" + ) + ) + case Some(protocol) => Async[F].pure(protocol) + } + } } 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 9be231c4ba..ccdabb6bfa 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 @@ -37,7 +37,15 @@ import cats.syntax.all._ */ private[tls] trait TLSEngine[F[_]] { def beginHandshake: F[Unit] - def applicationProtocol: F[String] + + /** Returns [[None]] if it has not yet been determined if application protocols might be used for this connection, + * [[Some]] with an empty String if application protocols values will not be used, or a non-empty application + * protocol String if a value was successfully negotiated. + * + * @see https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/SSLEngine.html#getApplicationProtocol-- + */ + def applicationProtocol: F[Option[String]] + def session: F[SSLSession] def stopWrap: F[Unit] def stopUnwrap: F[Unit] @@ -80,7 +88,7 @@ private[tls] object TLSEngine { def beginHandshake = Sync[F].delay(engine.beginHandshake()) def session = Sync[F].delay(engine.getSession()) - def applicationProtocol = Sync[F].delay(Option(engine.getApplicationProtocol()).get) + def applicationProtocol = Sync[F].delay(Option(engine.getApplicationProtocol())) def stopWrap = Sync[F].delay(engine.closeOutbound()) def stopUnwrap = Sync[F].delay(engine.closeInbound()).attempt.void 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 d541f927c4..6f8b61a572 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 @@ -103,6 +103,17 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => engine.session def applicationProtocol: F[String] = + engine.applicationProtocol.flatMap { + case Some(protocol) => Applicative[F].pure(protocol) + case None => + Async[F].raiseError( + new NoSuchElementException( + "It has not yet been determined if application protocols might be used for this connection." + ) + ) + } + + def applicationProtocolOption: F[Option[String]] = engine.applicationProtocol def isOpen: F[Boolean] = socket.isOpen diff --git a/io/native/src/main/scala/fs2/io/net/tls/S2nConnection.scala b/io/native/src/main/scala/fs2/io/net/tls/S2nConnection.scala index 03868cf04b..37df08ecad 100644 --- a/io/native/src/main/scala/fs2/io/net/tls/S2nConnection.scala +++ b/io/native/src/main/scala/fs2/io/net/tls/S2nConnection.scala @@ -51,7 +51,11 @@ private[tls] trait S2nConnection[F[_]] { def shutdown: F[Unit] - def applicationProtocol: F[String] + /** Returns [[None]] if there is no protocol being negotiated. + * + * @see [[https://aws.github.io/s2n-tls/doxygen/s2n_8h.html#ae53faa26669e258afff875d45140f14e]] + */ + def applicationProtocol: F[Option[String]] def session: F[SSLSession] @@ -199,7 +203,7 @@ private[tls] object S2nConnection { .void def applicationProtocol = - F.delay(guard(s2n_get_application_protocol(conn))).map(fromCString(_)) + F.delay(Option(s2n_get_application_protocol(conn)).map(fromCString(_))) def session = F.delay { val len = guard(s2n_connection_get_session_length(conn)) 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..f715ca36f2 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 @@ -95,7 +95,13 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => def session: F[SSLSession] = connection.session - def applicationProtocol: F[String] = connection.applicationProtocol + def applicationProtocol: F[String] = connection.applicationProtocol.flatMap { + case Some(protocol) => F.pure(protocol) + case None => + F.raiseError(new NoSuchElementException("No application protocol was negotiated")) + } + + override def applicationProtocolOption: F[Option[String]] = connection.applicationProtocol def isOpen: F[Boolean] = socket.isOpen } diff --git a/io/shared/src/main/scala/fs2/io/net/tls/TLSSocket.scala b/io/shared/src/main/scala/fs2/io/net/tls/TLSSocket.scala index 22aaf888a8..18cef52160 100644 --- a/io/shared/src/main/scala/fs2/io/net/tls/TLSSocket.scala +++ b/io/shared/src/main/scala/fs2/io/net/tls/TLSSocket.scala @@ -38,9 +38,19 @@ sealed trait TLSSocket[F[_]] extends Socket[F] with TLSSocketPlatform[F] { def session: F[SSLSession] /** Provides access to the current application protocol that has been negotiated. + * + * Raises a [[NoSuchElementException]] if it has not yet been determined if application protocols might + * be used for this connection. See [[applicationProtocolOption]] for a safer option. + * + * @see https://discord.com/channels/632277896739946517/632310980449402880/1100649913223745597 */ def applicationProtocol: F[String] + /** Provides access to the current application protocol that has been negotiated. + * + * Returns [[None]] if it has not yet been determined if application protocols might be used for this connection. + */ + def applicationProtocolOption: F[Option[String]] } object TLSSocket extends TLSSocketCompanionPlatform {