8000 extmod/modussl: Fix ussl read/recv/send/write errors when non-blocking. · micropython/micropython@2c1299b · GitHub
[go: up one dir, main page]

Skip to content

Commit 2c1299b

Browse files
tvedpgeorge
authored andcommitted
extmod/modussl: Fix ussl read/recv/send/write errors when non-blocking.
Also fix related problems with socket on esp32, improve docs for wrap_socket, and add more tests.
1 parent 2eed978 commit 2c1299b

File tree

10 files changed

+373
-20
lines changed

10 files changed

+373
-20
lines changed

docs/library/ussl.rst

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,23 @@ facilities for network sockets, both client-side and server-side.
1313
Functions
1414
---------
1515

16-
.. function:: ussl.wrap_socket(sock, server_side=False, keyfile=None, certfile=None, cert_reqs=CERT_NONE, ca_certs=None)
17-
16+
.. function:: ussl.wrap_socket(sock, server_side=False, keyfile=None, certfile=None, cert_reqs=CERT_NONE, ca_certs=None, do_handshake=True)
1817
Takes a `stream` *sock* (usually usocket.socket instance of ``SOCK_STREAM`` type),
1918
and returns an instance of ssl.SSLSocket, which wraps the underlying stream in
2019
an SSL context. Returned object has the usual `stream` interface methods like
21-
``read()``, ``write()``, etc. In MicroPython, the returned object does not expose
22-
socket interface and methods like ``recv()``, ``send()``. In particular, a
23-
server-side SSL socket should be created from a normal socket returned from
20+
``read()``, ``write()``, etc.
21+
A server-side SSL socket should be created from a normal socket returned from
2422
:meth:`~usocket.socket.accept()` on a non-SSL listening server socket.
2523

24+
- *do_handshake* determines whether the handshake is done as part of the ``wrap_socket``
25+
or whether it is deferred to be done as part of the initial reads or writes
26+
(there is no ``do_handshake`` method as in CPython).
27+
For blocking sockets doing the handshake immediately is standard. For non-blocking
28+
sockets (i.e. when the *sock* passed into ``wrap_socket`` is in non-blocking mode)
29+
the handshake should generally be deferred because otherwise ``wrap_socket`` blocks
30+
until it completes. Note that in AXTLS the handshake can be deferred until the first
31+
read or write but it then blocks until completion.
32+
2633
Depending on the underlying module implementation in a particular
2734
:term:`MicroPython port`, some or all keyword arguments above may be not supported.
2835

@@ -31,6 +38,11 @@ Functions
3138
Some implementations of ``ussl`` module do NOT validate server certificates,
3239
which makes an SSL connection established prone to man-in-the-middle attacks.
3340

41+
CPython's ``wrap_socket`` returns an ``SSLSocket`` object which has methods typical
42+
for sockets, such as ``send``, ``recv``, etc. MicroPython's ``wrap_socket``
43+
returns an object more similar to CPython's ``SSLObject`` which does not have
44+
these socket methods.
45+
3446
Exceptions
3547
----------
3648

extmod/modussl_axtls.c

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,15 @@ STATIC mp_obj_ssl_socket_t *ussl_socket_new(mp_obj_t sock, struct ssl_args *args
167167
o->ssl_sock = ssl_client_new(o->ssl_ctx, (long)sock, NULL, 0, ext);
168168

169169
if (args->do_handshake.u_bool) {
170-
int res = ssl_handshake_status(o->ssl_sock);
171-
172-
if (res != SSL_OK) {
173-
ussl_raise_error(res);
170+
int r = ssl_handshake_status(o->ssl_sock);
171+
172+
if (r != SSL_OK) {
173+
if (r == SSL_CLOSE_NOTIFY) { // EOF
174+
r = MP_ENOTCONN;
175+
} else if (r == SSL_EAGAIN) {
176+
r = MP_EAGAIN;
177+
}
178+
ussl_raise_error(r);
174179
}
175180
}
176181

@@ -242,8 +247,24 @@ STATIC mp_uint_t ussl_socket_write(mp_obj_t o_in, const void *buf, mp_uint_t siz
242247
return MP_STREAM_ERROR;
243248
}
244249

245-
mp_int_t r = ssl_write(o->ssl_sock, buf, size);
250+
mp_int_t r;
251+
eagain:
252+
r = ssl_write(o->ssl_sock, buf, size);
253+
if (r == 0) {
254+
// see comment in ussl_socket_read above
255+
if (o->blocking) {
256+
goto eagain;
257+
} else {
258+
r = SSL_EAGAIN;
259+
}
260+
}
246261
if (r < 0) {
262+
if (r == SSL_CLOSE_NOTIFY || r == SSL_ERROR_CONN_LOST) {
263+
return 0; // EOF
264+
}
265+
if (r == SSL_EAGAIN) {
266+
r = MP_EAGAIN;
267+
}
247268
*errcode = r;
248269
return MP_STREAM_ERROR;
249270
}

extmod/modussl_mbedtls.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ STATIC int _mbedtls_ssl_send(void *ctx, const byte *buf, size_t len) {
133133
}
134134
}
135135

136+
// _mbedtls_ssl_recv is called by mbedtls to receive bytes from the underlying socket
136137
STATIC int _mbedtls_ssl_recv(void *ctx, byte *buf, size_t len) {
137138
mp_obj_t sock = *(mp_obj_t *)ctx;
138139

@@ -171,7 +172,7 @@ STATIC mp_obj_ssl_socket_t *socket_new(mp_obj_t sock, struct ssl_args *args) {
171172
mbedtls_pk_init(&o->pkey);
172173
mbedtls_ctr_drbg_init(&o->ctr_drbg);
173174
#ifdef MBEDTLS_DEBUG_C
174-
// Debug level (0-4)
175+
// Debug level (0-4) 1=warning, 2=info, 3=debug, 4=verbose
175176
mbedtls_debug_set_threshold(0);
176177
#endif
177178

ports/esp32/modsocket.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,8 @@ int _socket_send(socket_obj_t *sock, const char *data, size_t datalen) {
558558
MP_THREAD_GIL_EXIT();
559559
int r = lwip_write(sock->fd, data + sentlen, datalen - sentlen);
560560
MP_THREAD_GIL_ENTER();
561-
if (r < 0 && errno != EWOULDBLOCK) {
561+
// lwip returns EINPROGRESS when trying to send right after a non-blocking connect
562+
if (r < 0 && errno != EWOULDBLOCK && errno != EINPROGRESS) {
562563
mp_raise_OSError(errno);
563564
}
564565
if (r > 0) {
@@ -567,7 +568,7 @@ int _socket_send(socket_obj_t *sock, const char *data, size_t datalen) {
567568
check_for_exceptions();
568569
}
569570
if (sentlen == 0) {
570-
mp_raise_OSError(MP_ETIMEDOUT);
571+
mp_raise_OSError(sock->retries == 0 ? MP_EWOULDBLOCK : MP_ETIMEDOUT);
571572
}
572573
return sentlen;
573574
}
@@ -650,7 +651,8 @@ STATIC mp_uint_t socket_stream_write(mp_obj_t self_in, const void *buf, mp_uint_
650651
if (r > 0) {
651652
return r;
652653
}
653-
if (r < 0 && errno != EWOULDBLOCK) {
654+
// lwip returns MP_EINPROGRESS when trying to write right after a non-blocking connect
655+
if (r < 0 && errno != EWOULDBLOCK && errno != EINPROGRESS) {
654656
*errcode = errno;
655657
return MP_STREAM_ERROR;
656658
}

tests/net_hosted/accept_timeout.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# test that socket.accept() on a socket with timeout raises ETIMEDOUT
22

33
try:
4-
import usocket as socket
4+
import uerrno as errno, usocket as socket
55
except:
6-
import socket
6+
import errno, socket
77

88
try:
99
socket.socket.settimeout
@@ -18,5 +18,5 @@
1818
try:
1919
s.accept()
2020
except OSError as er:
21-
print(er.args[0] in (110, "timed out")) # 110 is ETIMEDOUT; CPython uses a string
21+
print(er.args[0] in (errno.ETIMEDOUT, "timed out")) # CPython uses a string instead of errno
2222
s.close()
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# test that socket.connect() on a non-blocking socket raises EINPROGRESS
2+
# and that an immediate write/send/read/recv does the right thing
3+
4+
try:
5+
import sys, time
6+
import uerrno as errno, usocket as socket, ussl as ssl
7+
except:
8+
import socket, errno, ssl
9+
isMP = sys.implementation.name == "micropython"
10+
11+
12+
def dp(e):
13+
# uncomment next line for development and testing, to print the actual exceptions
14+
# print(repr(e))
15+
pass
16+
17+
18+
# do_connect establishes the socket and wraps it if tls is True.
19+
# If handshake is true, the initial connect (and TLS handshake) is
20+
# allowed to be performed before returning.
21+
def do_connect(peer_addr, tls, handshake):
22+
s = socket.socket()
23+
s.setblocking(False)
24+
try:
25+
# print("Connecting to", peer_addr)
26+
s.connect(peer_addr)
27+
except OSError as er:
28+
print("connect:", er.args[0] == errno.EINPROGRESS)
29+
if er.args[0] != errno.EINPROGRESS:
30+
print(" got", er.args[0])
31+
# wrap with ssl/tls if desired
32+
if tls:
33+
try:
34+
if sys.implementation.name == "micropython":
35+
s = ssl.wrap_socket(s, do_handshake=handshake)
36+
else:
37+
s = ssl.wrap_socket(s, do_handshake_on_connect=handshake)
38+
print("wrap: True")
39+
except Exception as e:
40+
dp(e)
41+
print("wrap:", e)
42+
elif handshake:
43+
# just sleep a little bit, this allows any connect() errors to happen
44+
time.sleep(0.2)
45+
return s
46+
47+
48+
# test runs the test against a specific peer address.
49+
def test(peer_addr, tls=False, handshake=False):
50+
# MicroPython plain sockets have read/write, but CPython's don't
51+
# MicroPython TLS sockets and CPython's have read/write
52+
# hasRW captures this wonderful state of affairs
53+
hasRW = isMP or tls
54+
55+
# MicroPython plain sockets and CPython's have send/recv
56+
# MicroPython TLS sockets don't have send/recv, but CPython's do
57+
# hasSR captures this wonderful state of affairs
58+
hasSR = not (isMP and tls)
59+
60+
# connect + send
61+
if hasSR:
62+
s = do_connect(peer_addr, tls, handshake)
63+
# send -> 4 or EAGAIN
64+
try:
65+
ret = s.send(b"1234")
66+
print("send:", handshake and ret == 4)
67+
except OSError as er:
68+
#
69+
dp(er)
70+
print("send:", er.args[0] in (errno.EAGAIN, errno.EINPROGRESS))
71+
s.close()
72+
else: # fake it...
73+
print("connect:", True)
74+
if tls:
75+
print("wrap:", True)
76+
print("send:", True)
77+
78+
# connect + write
79+
if hasRW:
80+
s = do_connect(peer_addr, tls, handshake)
81+
# write -> None
82+
try:
83+
ret = s.write(b"1234")
84+
print("write:", ret in (4, None)) # SSL may accept 4 into buffer
85+
except OSError as er:
86+
dp(er)
87+
print("write:", False) # should not raise
88+
except ValueError as er: # CPython
89+
dp(er)
90+
print("write:", er.args[0] == "Write on closed or unwrapped SSL socket.")
91+
s.close()
92+
else: # fake it...
93+
print("connect:", True)
94+
if tls:
95+
print("wrap:", True)
96+
print("write:", True)
97+
98+
if hasSR:
99+
# connect + recv
100+
s = do_connect(peer_addr, tls, handshake)
101+
# recv -> EAGAIN
102+
try:
103+
print("recv:", s.recv(10))
104+
except OSError as er:
105+
dp(er)
106+
print("recv:", er.args[0] == errno.EAGAIN)
107+
s.close()
108+
else: # fake it...
109+
print("connect:", True)
110+
if tls:
111+
print("wrap:", True)
112+
print("recv:", True)
113+
114+
# connect + read
115+
if hasRW:
116+
s = do_connect(peer_addr, tls, handshake)
117+
# read -> None
118+
try:
119+
ret = s.read(10)
120+
print("read:", ret is None)
121+
except OSError as er:
122+
dp(er)
123+
print("read:", False) # should not raise
124+
except ValueError as er: # CPython
125+
dp(er)
126+
print("read:", er.args[0] == "Read on closed or unwrapped SSL socket.")
127+
s.close()
128+
else: # fake it...
129+
print("connect:", True)
130+
if tls:
131+
print("wrap:", True)
132+
print("read:", True)
133+
134+
135+
if __name__ == "__main__":
136+
# these tests use a non-existent test IP address, this way the connect takes forever and
137+
# we can see EAGAIN/None (https://tools.ietf.org/html/rfc5737)
138+
print("--- Plain sockets to nowhere ---")
139+
test(socket.getaddrinfo("192.0.2.1", 80)[0][-1], False, False)
140+
print("--- SSL sockets to nowhere ---")
141+
# this test fails with AXTLS because do_handshake=False blocks on first read/write and
142+
# there it times out until the connect is aborted
143+
test(socket.getaddrinfo("192.0.2.1", 443)[0][-1], True, False)
144+
print("--- Plain sockets ---")
145+
test(socket.getaddrinfo("micropython.org", 80)[0][-1], False, True)
146+
print("--- SSL sockets ---")
147+
test(socket.getaddrinfo("micropython.org", 443)[0][-1], True, True)

tests/net_inet/ssl_errors.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# test that socket.connect() on a non-blocking socket raises EINPROGRESS
2+
# and that an immediate write/send/read/recv does the right thing
3+
4+
import sys
5+
6+
try:
7+
import uerrno as errno, usocket as socket, ussl as ssl
8+
except:
9+
import errno, socket, ssl
10+
11+
12+
def test(addr, hostname, block=True):
13+
print("---", hostname or addr)
14+
s = socket.socket()
15+
s.setblocking(block)
16+
try:
17+
s.connect(addr)
18+
print("connected")
19+
except OSError as e:
20+
if e.args[0] != errno.EINPROGRESS:
21+
raise
22+
print("EINPROGRESS")
23+
24+
try:
25+
if sys.implementation.name == "micropython":
26+
s = ssl.wrap_socket(s, do_handshake=block)
27+
else:
28+
s = ssl.wrap_socket(s, do_handshake_on_connect=block)
29+
print("wrap: True")
30+
except OSError:
31+
print("wrap: error")
32+
33+
if not block:
34+
try:
35+
while s.write(b"0") is None:
36+
pass
37+
except (ValueError, OSError): # CPython raises ValueError, MicroPython raises OSError
38+
print("write: error")
39+
s.close()
40+
41+
42+
if __name__ == "__main__":
43+
# connect to plain HTTP port, oops!
44+
addr = socket.getaddrinfo("micropython.org", 80)[0][-1]
45+
test(addr, None)
46+
# connect to plain HTTP port, oops!
47+
addr = socket.getaddrinfo("micropython.org", 80)[0][-1]
48+
test(addr, None, False)
49+
# connect to server with self-signed cert, oops!
50+
addr = socket.getaddrinfo("test.mosquitto.org", 8883)[0][-1]
51+
test(addr, "test.mosquitto.org")

0 commit comments

Comments
 (0)
0