-
Notifications
You must be signed in to change notification settings - Fork 419
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WANT_WRITE_ERROR occurs for sendall #176
Comments
Can you explain why you think this needs to be applied to all methods, not just |
exarkun: sorry--I did not see this for a long time. sendall needs the fix, as it's impossible for the user to retry the call as described above. However, if you chose not to handle these errors, the userspace code CAN handle them. |
pyOpenSSL is meant as a fairly low-level interface. It supports a variety of use-cases - for example, non-blocking TLS. That use-case would be impeded by automatic retries in |
Understood. Thanks for your time. We've implemented a workaround in sendall for our wrapper, so we're all set with this issue. |
Great. Thanks for the follow-up. |
Actually, oops. Leaving this open because I still think doing retry in |
Yes totally. Happy for PRs. :) |
Looks like the patch with retries needs to be applied around these lines: https://github.com/pyca/pyopenssl/blob/124a013/src/OpenSSL/SSL.py#L1670-L1673 |
Per https://www.openssl.org/docs/man1.1.1/man3/SSL_write_ex.html, |
Hm... This https://github.com/pyca/pyopenssl/blob/124a013/src/OpenSSL/SSL.py#L1623-L1625 suggests that |
So looking at the source, it seems like I think that the patch would roughly be wrapping Line 1673 in 124a013
def sendall(self, buf, flags=0):
"""
Send "all" data on the connection. This calls send() repeatedly until
all data is sent. If an error occurs, it's impossible to tell how much
data has been sent.
:param buf: The string, buffer or memoryview to send
:param flags: (optional) Included for compatibility with the socket
API, the value is ignored
:return: The number of bytes written
"""
buf = _text_to_bytes_and_warn("buf", buf)
with _from_buffer(buf) as data:
left_to_send = len(buf)
total_sent = 0
while left_to_send:
# SSL_write's num arg is an int,
# so we cannot send more than 2**31-1 bytes at once.
result = _lib.SSL_write(
self._ssl, data + total_sent, min(left_to_send, 2147483647)
)
- self._raise_ssl_error(self._ssl, result)
+ try:
+ self._raise_ssl_error(self._ssl, result)
+ except (WantReadError, WantWriteError):
+ continue
total_sent += result
left_to_send -= result
return total_sent |
This change introduces retries in `OpenSSL.SSL.Connection.sendall()` when `WANT_WRITE_ERROR` or `WANT_READ_ERROR` happen. It relies on `SSL_MODE_ENABLE_PARTIAL_WRITE` being set on the context, that changes the mode of `SSL_write()` to return errors only if zero bytes has been sent making it safe to retry in these cases. Ideally, the calling code is supposed to `poll()`/`select()` the socket to know when it's okay to attempt the next retry (hence it is readable or writable) but it's not available in the `sendall()` method and just retrying the operation is good enough. Fixes pyca#176 Refs: * http://openssl.6102.n7.nabble.com/SSL-MODE-ACCEPT-MOVING-WRITE-BUFFER-td6421.html * https://stackoverflow.com/a/28992313/595220 * https://www.openssl.org/docs/manmaster/man3/SSL_write.html * https://stackoverflow.com/a/20817394/595220
This change introduces retries in `OpenSSL.SSL.Connection.sendall()` when `WANT_WRITE_ERROR` or `WANT_READ_ERROR` happen. It relies on `SSL_MODE_ENABLE_PARTIAL_WRITE` being set on the context, that changes the mode of `SSL_write()` to return errors only if zero bytes has been sent making it safe to retry in these cases. Ideally, the calling code is supposed to `poll()`/`select()` the socket to know when it's okay to attempt the next retry (hence it is readable or writable) but it's not available in the `sendall()` method and just retrying the operation is good enough. Fixes pyca#176 Refs: * http://openssl.6102.n7.nabble.com/SSL-MODE-ACCEPT-MOVING-WRITE-BUFFER-td6421.html * https://stackoverflow.com/a/28992313/595220 * https://www.openssl.org/docs/manmaster/man3/SSL_write.html * https://stackoverflow.com/a/20817394/595220
Here's the PR: #954 |
@exarkun FTR CPython implements retries in their And their |
CPython's approach may not be the best approach. Some decisions and workarounds pre-date OpenSSL 0.9.8. |
I'm not too involved in pyOpenSSL development anymore (and haven't been for years). But I think it would be great if this PR were augmented with automated tests demonstrating the desired behavior has been achieved. |
This change introduces retries in `OpenSSL.SSL.Connection.sendall()` when `WANT_WRITE_ERROR` or `WANT_READ_ERROR` happen. It relies on `SSL_MODE_ENABLE_PARTIAL_WRITE` being set on the context, that changes the mode of `SSL_write()` to return errors only if zero bytes has been sent making it safe to retry in these cases. Ideally, the calling code is supposed to `poll()`/`select()` the socket to know when it's okay to attempt the next retry (hence it is readable or writable) but it's not available in the `sendall()` method and just retrying the operation is good enough. Fixes pyca#176 Refs: * http://openssl.6102.n7.nabble.com/SSL-MODE-ACCEPT-MOVING-WRITE-BUFFER-td6421.html * https://stackoverflow.com/a/28992313/595220 * https://www.openssl.org/docs/manmaster/man3/SSL_write.html * https://stackoverflow.com/a/20817394/595220
The OpenSSL documentation says that in the event of a WANT_WRITE_ERROR or WANT_READ_ERROR, the same OpenSSL method call is to be repeated, otherwise you will get a bad write retry error.
See here:
https://www.openssl.org/docs/ssl/SSL_write.html
http://stackoverflow.com/questions/2997218/why-am-i-getting-error1409f07fssl-routinesssl3-write-pending-bad-write-retr
For pyOpenSSL.sendAll() this is problematic. Because python's sendAll() call does not return the number of bytes already sent if an error is thrown, it's impossible to continue with an identical SSL.Write() call. The state of the last SSL.Write() call is only known inside the pyOpenSSL.sendAll() function, and since this function has thrown an exception, this state is lost.
To illustrate the problem, see the following source for SSL.sendall() in pyOpenSSL:
Specifically, inside the while loop, there are repeated calls to _lib.SSL_write(). _lib is the openSSL library, so the correct contract is that if SSL_write throws a WantWriteError or WantReadError, it should be handled by sleeping for a short duration and re-issuing the same call. However, pyOpenSSL does not do this, and the caller cannot either.
As a workaround, I implemented a wrapper around pyOpenSSL, and implemented my own sendAll() to handle any WANT_WRITE_ERROR or WANT_READ_ERRORS in a loop, re-issuing the same send() call that the error threw (after a small sleep).
Finally, keep in mind that any write or read calls that are made to pyOpenSSL can fail with either a WANT_WRITE_ERROR or a WANT_READ_ERROR, because these calls could trigger an SSL handshake. If network buffers are full or nearly full when the handshake triggers, these errors can be thrown. For this reason, both error conditions must be handled by all network calls, either in pyOpenSSL or by developer using pyOpenSSL.
Recommended fix:
pyOpenSSL should write exception handlers to catch WANT_WRITE_ERROR and WANT_READ_ERRORS inside all functions to catch those exceptions, sleep for a small period of time, and re-issue the same SSL_write() or SSL_read() call. The specific implementation will need to be considerate of blocking vs non-blocking mode, as well as timeouts, and is likely not trivial.
Thank you for all the work you do on pyOpenSSL. It really is a fantastic library.
The text was updated successfully, but these errors were encountered: