From 4cc55fabc76aba06b221071d31ee45195a7ef1de Mon Sep 17 00:00:00 2001 From: Justin Reardon Date: Wed, 14 Oct 2026 21:53:55 -0400 Subject: [PATCH] Fix SN TLSSocket throwing when no application protocol Changes the behavior of `applicationProtocol` on SN to return the empty string when no protocol is negotiated, instead of throwing an exception. `s2n_get_application_protocol` intentionally returns `NULL` to indicate no protocol was negotiated (see: https://aws.github.io/s2n-tls/doxygen/s2n_8h.html#ae53faa26669e258afff875d45140f14e). But FS2 wrapped the call in `guard` which throws on a null pointer. Fixes #3778 --- .../scala/fs2/io/net/tls/S2nConnection.scala | 5 +- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 73 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) 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 3a26097f7d..d5dea005fe 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 @@ -199,7 +199,10 @@ private[tls] object S2nConnection { .void def applicationProtocol = - F.delay(guard(s2n_get_application_protocol(conn))).map(fromCString(_)) + F.delay(s2n_get_application_protocol(conn)).map { + case null => "" + case ptr => fromCString(ptr) + } def session = F.delay { val len = guard(s2n_connection_get_session_length(conn)) 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 07883b8404..b58de41ae7 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 @@ -354,4 +354,77 @@ class TLSSocketSuite extends TLSSuite { checkStarvation.both(checkBlinding).void } } + + test("present application protocol") { + val setup = for { + tlsContext <- testTlsContext + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + client = Network[IO] + .connect(serverSocket.address) + .flatMap( + tlsContext + .clientBuilder(_) + .withParameters( + TLSParameters(serverName = Some("Unknown"), protocolPreferences = Some(List("h2"))) + ) + .build + ) + } yield serverSocket.accept.flatMap(s => + Stream.resource( + tlsContext + .serverBuilder(s) + .withParameters(TLSParameters(protocolPreferences = Some(List("h2")))) + .build + ) + ) -> client + + Stream + .resource(setup) + .flatMap { case (server, clientSocket) => + val echoServer = server.map { socket => + socket.reads.chunks.foreach(socket.write(_)) + }.parJoinUnbounded + + val client = Stream.resource(clientSocket).evalMap { clientSocket => + clientSocket.applicationProtocol + } + + client.concurrently(echoServer) + } + .compile + .lastOrError + .map(assertEquals(_, "h2")) + } + + test("missing application protocol") { + val setup = for { + tlsContext <- testTlsContext + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + client = Network[IO] + .connect(serverSocket.address) + .flatMap( + tlsContext + .clientBuilder(_) + .withParameters(TLSParameters(serverName = Some("Unknown"))) + .build + ) + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + + Stream + .resource(setup) + .flatMap { case (server, clientSocket) => + val echoServer = server.map { socket => + socket.reads.chunks.foreach(socket.write(_)) + }.parJoinUnbounded + + val client = Stream.resource(clientSocket).evalMap { clientSocket => + clientSocket.applicationProtocol + } + + client.concurrently(echoServer) + } + .compile + .lastOrError + .map(assertEquals(_, "")) + } }