pallets / werkzeug

The comprehensive WSGI web application library.
https://werkzeug.palletsprojects.com
BSD 3-Clause "New" or "Revised" License
6.63k stars 1.73k forks source link

SSLEOFError when serving static MP4 video files greater than a certain size in Webkit browsers using Python >= 3.10.0 with SSL enabled #2926

Closed peterhorsley closed 3 weeks ago

peterhorsley commented 1 month ago

When serving static MP4 video files greater than a certain size in Webkit browsers using Python >= 3.10.0 with SSL enabled, an SSLEOFError occurs:

ERROR:werkzeug:Error on request: Traceback (most recent call last): File "C:\Users\phorsley.conda\envs\ssl-eof-3.10.0\lib\site-packages\werkzeug\serving.py", line 365, in run_wsgi execute(self.server.app) File "C:\Users\phorsley.conda\envs\ssl-eof-3.10.0\lib\site-packages\werkzeug\serving.py", line 329, in execute write(data) File "C:\Users\phorsley.conda\envs\ssl-eof-3.10.0\lib\site-packages\werkzeug\serving.py", line 304, in write self.wfile.write(data) File "C:\Users\phorsley.conda\envs\ssl-eof-3.10.0\lib\socketserver.py", line 826, in write self._sock.sendall(b) File "C:\Users\phorsley.conda\envs\ssl-eof-3.10.0\lib\ssl.py", line 1236, in sendall v = self.send(byte_view[count:]) File "C:\Users\phorsley.conda\envs\ssl-eof-3.10.0\lib\ssl.py", line 1205, in send return self._sslobj.write(data) ssl.SSLEOFError: EOF occurred in violation of protocol (_ssl.c:2384)

This has previously been reported via issue #2852, but I'm providing a tiny cut-down example app (including MP4 files) that reproduces the problem, along with additional debug logs, to help evaluate if a code change may be needed in werkzeug's handling of 206 PARTIAL RESPONSEs.

Example app: ssl-eof.zip

This is the example app code for reference.

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return  "<ul>" + \
            "<li><a href=\"static/1mb.mp4\">1mb mp4 file (works)</a></li>" + \
            "<li><a href=\"static/8mb.mp4\">8mb mp4 file (ssl error)</a></li>" + \
            "</ul>"

if __name__ == "__main__":
    app.run(ssl_context='adhoc')

Here's a screen capture of the example app demonstrating the problem:

https://github.com/user-attachments/assets/a744a094-a334-41d3-9461-e91b80c71db1

A 1Mb MP4 file does not trigger the error, but an 8Mb MP4 file does.

It reproduces using Chrome, Brave and Edge, but not Firefox.

Using Flask 3.0.3, Werkzeug 3.0.3 and pyOpenSSL 24.2.1 on Windows 10, I tested the example app with following versions of python and discovered the problem started with Python 3.10.0:

Using Python 3.8.10 (uses OpenSSL 1.1.1w): no error Using Python 3.9.19 (uses OpenSSL 3.0.14): no error Using Python 3.10.0 (uses OpenSSL 1.1.1w): error Using Python 3.10.14 (uses OpenSSL 3.0.14): error Using Python 3.12.4 (uses OpenSSL 3.0.14): error

I added this log line before the call to self.wfile.write(data) on line 304 of serving.py:

logging.warning(f'writing {len(data)} bytes with status {status_sent} {headers_sent}')

Attached are the console outputs for both Python 3.9.19 and 3.10.0. The only difference is the presence of the call stack (twice) in 3.10.0.

python-3.10.0-logs.txt

python-3.9.19-logs.txt

From these logs it does appear the problem is related to 206 partial response / range request handling in werkzeug, however I am not sure where to start to identify root cause as I am not an expert on these types of HTTP responses.

I did check the python 3.10.0 release notes and it does appear that something related to ssl was changed, as they mention 'PEP 644 -- Require OpenSSL 1.1.1 or newer'.

Tip - I used miniconda and the following commands to test different versions of python:

conda create --name ssl-eof-3.x.x python=3.x.x
conda activate ssl-eof-3.x.x
pip install flask pyopenssl
python hello.py

Then load https://127.0.0.1:5000/ in a webkit browser (ignore security warning).

Happy to do any further testing / debugging to help narrow this down!

Environment: Windows 10

davidism commented 1 month ago

Thank you for providing a minimal example. However, I can't reproduce the issue with the information provided. The file was successfully served in Safari and Vivaldi, two WebKit based browsers.

This would be something to report to Python, the development server is a basic wrapper around http.server and ssl, we're not doing anything that would affect how SSL works.

From these logs it does appear the problem is related to 206 partial response / range request handling in werkzeug, however I am not sure where to start to identify root cause as I am not an expert on these types of HTTP responses.

That's not clear from those logs, which are hundreds of lines of dense output. I'm happy to fix an issue if we're handling 206 wrong, but you'll need to investigate and report that issue instead.

peterhorsley commented 1 month ago

Thanks for testing and responding quickly. I have reported it against python here: https://github.com/python/cpython/issues/122254

peterhorsley commented 1 month ago

A python dev responded and confirmed it's due to a regression introduced in python 3.10, and the fix will be available in python 3.13 once that is released later this year. Unfortunately the fix will not be backported due to various reasons, so that leaves 3.10/3.11/3.12 as python versions that cannot reliably be used to serve large files over ssl with werkzeug. However, as the python dev suggests it's ok to ignore this exception and treat it like the connection has been dropped, there is a small code change that can fix this in werkzeug, which I have tested - to add ssl.SSLEOFError as an additional exception caught to handle client-side connection drops on line 365 of src/werkzeug/serving.py:

try:
    execute(self.server.app)
except (ConnectionError, socket.timeout, ssl.SSLEOFError) as e:
    self.connection_dropped(e, environ)

If this looks acceptable, I can raise a PR for this change.

davidism commented 1 month ago

Sure, that makes sense.