From ed6a8d5a73ba7e3e817552e1908a31772e9dc2e9 Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 30 Jan 2026 20:06:02 +0000 Subject: [PATCH] optimize CL/TE check; replace the heavyhanded connection:close added in b4fddbc3d with a comparison of content-length to num bytes consumed this approach also covers incorrectly configured servers where the reverseproxy was not detected also adds explicit TE/CL handling, even though most (all?) reverseproxies already prevent such issues also adds explicit sanchk of up2k chunk-receiver, in case any bugs are ever added there --- copyparty/httpcli.py | 29 ++++++++++++++++++++++++++--- copyparty/httpconn.py | 15 +++++++++++++++ copyparty/util.py | 2 ++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index f0ffb1a1..7ca6ceee 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -374,6 +374,7 @@ class HttpCli(object): return False + self.sr.nb = 0 self.conn.hsrv.nreq += 1 self.ua = self.headers.get("user-agent", "") @@ -383,6 +384,19 @@ class HttpCli(object): self.keepalive = "close" not in zs and ( self.http_ver != "HTTP/1.0" or zs == "keep-alive" ) + + if ( + "transfer-encoding" in self.headers + and self.headers["transfer-encoding"].lower() != "identity" + ): + self.sr.te = 1 + if "content-length" in self.headers: + # rfc9112:6.2: ignore CL if TE + self.keepalive = False + self.headers.pop("content-length") + t = "suspicious request (has both TE and CL); ignoring CL and disabling keepalive" + self.log(t, 3) + self.host = self.headers.get("host") or "" if not self.host: if self.s.family == socket.AF_UNIX: @@ -396,7 +410,6 @@ class HttpCli(object): if n: zso = self.headers.get(self.args.xff_hdr) if zso: - self.keepalive = False if n > 0: n -= 1 @@ -881,7 +894,11 @@ class HttpCli(object): self.terse_reply(b"", 500) return False - post = self.mode in ("POST", "PUT") or "content-length" in self.headers + post = ( + self.mode in ("POST", "PUT") + or "content-length" in self.headers + or self.sr.te + ) if pex.code >= (300 if post else 400): self.keepalive = False @@ -3200,7 +3217,7 @@ class HttpCli(object): t = "your chunk got corrupted somehow (received {} bytes); expected vs received hash:\n{}\n{}" raise Pebkac(400, t.format(post_sz, chash, sha_b64)) - remains -= chunksize + remains -= post_sz if len(cstart) > 1 and path != os.devnull: t = " & ".join(unicode(x) for x in cstart[1:]) @@ -3278,6 +3295,12 @@ class HttpCli(object): spd = self._spd(postsize) self.log("%70s thank %r" % (spd, cinf)) + + if remains: + t = "incorrect content-length from client" + self.log("%s; header=%d, remains=%d" % (t, postsize, remains), 3) + raise Pebkac(400, t) + self.reply(b"thank") return True diff --git a/copyparty/httpconn.py b/copyparty/httpconn.py index e36e5122..b7522374 100644 --- a/copyparty/httpconn.py +++ b/copyparty/httpconn.py @@ -222,6 +222,21 @@ class HttpConn(object): if not self.cli.run(): return + if self.sr.te == 1: + self.log("closing socket (leftover TE)", "90") + return + + if ( + "content-length" in self.cli.headers + and int(self.cli.headers["content-length"]) != self.sr.nb + ): + self.log("closing socket (CL mismatch)", "90") + return + + # note: proxies reject PUT sans Content-Length; illegal for HTTP/1.1 + + self.sr.nb = self.sr.te = 0 + if self.u2idx: self.hsrv.put_u2idx(str(self.addr), self.u2idx) self.u2idx = None diff --git a/copyparty/util.py b/copyparty/util.py index 52a16519..a2e0e784 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -971,6 +971,7 @@ class _Unrecv(object): self.log = log self.buf: bytes = b"" self.nb = 0 + self.te = 0 def recv(self, nbytes: int, spins: int = 1) -> bytes: if self.buf: @@ -3008,6 +3009,7 @@ def read_socket_chunked( if chunklen == 0: x = sr.recv_ex(2, False) if x == b"\r\n": + sr.te = 2 return t = "protocol error after final chunk: want b'\\r\\n', got {!r}"