deno icon indicating copy to clipboard operation
deno copied to clipboard

Got UnexpectedEof when reading from a TlsConn

Open 1yefuwang1 opened this issue 1 year ago • 5 comments

Version: deno 1.43.6 (release, x86_64-unknown-linux-gnu) The following code throws UnexpectedEof after all html was printed to stdout.

import { writeAll } from "jsr:@std/io";

const conn = await Deno.connectTls({ hostname: "www.google.com", port: 443 });
await writeAll(conn, new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n"));
await conn.readable.pipeTo(Deno.stdout.writable);

The callstack is:

error: Uncaught (in promise) UnexpectedEof: unexpected end of file
    at Object.pull (ext:deno_web/06_streams.js:996:30)
    at Module.invokeCallbackFunction (ext:deno_webidl/00_webidl.js:946:16)
    at ReadableByteStreamController.pullAlgorithm (ext:deno_web/06_streams.js:3563:14)
    at readableByteStreamControllerCallPullIfNeeded (ext:deno_web/06_streams.js:1242:49)
    at ReadableByteStreamController.[[[PullSteps]]] (ext:deno_web/06_streams.js:5983:5)
    at readableStreamDefaultReaderRead (ext:deno_web/06_streams.js:2513:36)
    at ext:deno_web/06_streams.js:2742:9
    at new Promise (<anonymous>)
    at ext:deno_web/06_streams.js:2741:14
    at eventLoopTick (ext:core/01_core.js:168:7)

The problem doesn't exist when working with normal Tcp socket. The following code works fine.

import { writeAll } from "jsr:@std/io";

const conn = await Deno.connect({ hostname: "www.google.com", port: 80 });
await writeAll(conn, new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n"));
await conn.readable.pipeTo(Deno.stdout.writable);

1yefuwang1 avatar May 23 '24 08:05 1yefuwang1

Instead of using pipeTo API, I decide to go with reader API with the following code because it gives me a chance to handle EOF. But it seems EOF is not signaled by returning done=true.

import { writeAll } from "jsr:@std/io";

const conn = await Deno.connectTls({ hostname: "www.google.com", port: 443 });
await writeAll(conn, new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n"));
// await conn.readable.pipeTo(Deno.stdout.writable);

const reader = conn.readable.getReader(/*{ mode: "byob" }*/);

while (true) {
  const {done, value} = await reader.read(); // throws UnexpectedEof instead of returning done as true.
  if (done) {
    if (value) {
      await writeAll(Deno.stdout, value);
    }
    console.log(`done`);
    break;
  }
  await writeAll(Deno.stdout, value);
}

Callstack:

error: Uncaught (in promise) UnexpectedEof: unexpected end of file
  const {done, value} = await reader.read();
                                     ^
    at Object.pull (ext:deno_web/06_streams.js:996:30)
    at Module.invokeCallbackFunction (ext:deno_webidl/00_webidl.js:946:16)
    at ReadableByteStreamController.pullAlgorithm (ext:deno_web/06_streams.js:3563:14)
    at readableByteStreamControllerCallPullIfNeeded (ext:deno_web/06_streams.js:1242:49)
    at ReadableByteStreamController.[[[PullSteps]]] (ext:deno_web/06_streams.js:5983:5)
    at readableStreamDefaultReaderRead (ext:deno_web/06_streams.js:2513:36)
    at ReadableStreamDefaultReader.read (ext:deno_web/06_streams.js:5480:5)
    at file:///home/wyf/deno/main.ts:10:38
    at eventLoopTick (ext:core/01_core.js:168:7)

1yefuwang1 avatar May 23 '24 08:05 1yefuwang1

I tried to reproduce it, but I didn't get this error. Try it at version 1.44.0 it seems that it resolved. @1yefuwang1 @marvinhagemeister

yazan-abdalrahman avatar Jun 03 '24 08:06 yazan-abdalrahman

I can still reproduce it with 1.44.1. I'm using wsl btw.

1yefuwang1 avatar Jun 06 '24 03:06 1yefuwang1

I can reproduce - this indeed looks like a bug.

lucacasonato avatar Jun 18 '24 18:06 lucacasonato

Unfortunately, this issue still exists in deno 2

deno 2.0.0 (stable, release, x86_64-unknown-linux-gnu)
v8 12.9.202.13-rusty
typescript 5.6.2

with a slightly different error message:

error: Uncaught (in promise) UnexpectedEof: peer closed connection without sending TLS close_notify: https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#unexpected-eof
    at Object.pull (ext:deno_web/06_streams.js:988:30)
    at Module.invokeCallbackFunction (ext:deno_webidl/00_webidl.js:982:16)
    at ReadableByteStreamController.pullAlgorithm (ext:deno_web/06_streams.js:3559:14)
    at readableByteStreamControllerCallPullIfNeeded (ext:deno_web/06_streams.js:1234:49)
    at ReadableByteStreamController.[[[PullSteps]]] (ext:deno_web/06_streams.js:5987:5)
    at readableStreamDefaultReaderRead (ext:deno_web/06_streams.js:2509:36)
    at ext:deno_web/06_streams.js:2738:9
    at new Promise (<anonymous>)
    at ext:deno_web/06_streams.js:2737:14
    at eventLoopTick (ext:core/01_core.js:175:7)

Following the link in the error message is a detailed explanation of the error from rustls:

TLS has a close_notify mechanism to prevent truncation attacks[2](https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#fn2). According to the TLS RFCs, each party is required to send a close_notify message before closing the write side of the connection. However, some implementations don’t send it. So long as the application layer protocol (for instance HTTP/2) has message length framing and can reject truncated messages, this is not a security problem.

Rustls treats an EOF without close_notify as an error of type std::io::Error with ErrorKind::UnexpectedEof. In some situations it’s appropriate for the application to handle this error the same way it would handle a normal EOF (a read returning Ok(0)). In particular if UnexpectedEof occurs on an idle connection it is appropriate to treat it the same way as a clean shutdown. And if an application always uses messages with length framing (in other words, messages are never delimited by the close of the TCP connection), it can unconditionally ignore UnexpectedEof errors from rustls.

It seems deno doesn't handle ErrorKind::UnexpectedEof caused by EOF without close_notify properly.

1yefuwang1 avatar Oct 16 '24 06:10 1yefuwang1

I am also facing it. Any solution

sawrubgupta avatar Apr 24 '25 06:04 sawrubgupta