Skip to content

Commit c8521c0

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 902da05 commit c8521c0

File tree

10 files changed

+375
-21
lines changed

10 files changed

+375
-21
lines changed

docs/library/ussl.rst

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,36 @@ 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

29-
.. warning::
36+
.. warnings::
3037

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: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,16 @@ 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+
ssl_display_error(r);
174+
if (r == SSL_CLOSE_NOTIFY || r == SSL_ERROR_CONN_LOST) { // EOF
175+
r = MP_ENOTCONN;
176+
} else if (r == SSL_EAGAIN) {
177+
r = MP_EAGAIN;
178+
}
179+
ussl_raise_error(r);
174180
}
175181
}
176182

@@ -242,8 +248,24 @@ STATIC mp_uint_t ussl_socket_write(mp_obj_t o_in, const void *buf, mp_uint_t siz
242248
return MP_STREAM_ERROR;
243249
}
244250

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

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy