8000 unix/modsocket: Accept a host+port array for socket.connect. · micropython/micropython@354d67a · GitHub
[go: up one dir, main page]

Skip to content

Commit 354d67a

Browse files
committed
unix/modsocket: Accept a host+port array for socket.connect.
This commit lets socket.connect accept an array containing a hostname string and a port number as the address to connect to, if the socket family is either AF_INET or AF_INET6. This brings the behaviour of socket.connect in line with the other embedded ports' versions that use LWIP, and also with CPython - even though CPython can only 10000 accept a tuple as its host+port argument. The existing behaviour of accepting a serialised sockaddr structure as the connection address is still present, maintaining compatibility with existing code. Signed-off-by: Alessandro Gatti <a.gatti@frob.it>
1 parent a05766f commit 354d67a

File tree

2 files changed

+163
-1
lines changed

2 files changed

+163
-1
lines changed

ports/unix/modsocket.c

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include <sys/stat.h>
3838
#include <sys/types.h>
3939
#include <sys/socket.h>
40+
#include <sys/un.h>
4041
#include <netinet/in.h>
4142
#include <arpa/inet.h>
4243
#include <netdb.h>
@@ -82,6 +83,25 @@ static inline mp_obj_t mp_obj_from_sockaddr(const struct sockaddr *addr, socklen
8283
return mp_obj_new_bytes((const byte *)addr, len);
8384
}
8485

86+
static int mp_socket_family_from_fd(mp_obj_t socket_in) {
87+
MP_STATIC_ASSERT(sizeof(struct sockaddr_un) > sizeof(struct sockaddr_in6));
88+
mp_obj_socket_t *socket = MP_OBJ_TO_PTR(socket_in);
89+
// A sockaddr_un struct is big enough to store either a sockaddr_in6 or a
90+
// sockaddr_in.
91+
struct sockaddr_un address;
92+
socklen_t address_len = sizeof(struct sockaddr_un);
93+
MP_THREAD_GIL_EXIT();
94+
int r = getsockname(socket->fd, (struct sockaddr *)&address, &address_len);
95+
MP_THREAD_GIL_ENTER();
96+
// sockaddr_un, sockaddr_in6, and sockaddr_in share the same field
97+
// structure for the first two fields, and the family identifier happens
98+
// to be the first one.
99+
return r == -1 ? -1 : address.sun_family;
100+
}
101+
102+
// Forward definitions
103+
static mp_obj_t mod_socket_getaddrinfo(size_t n_args, const mp_obj_t *args);
104+
85105
static mp_obj_socket_t *socket_new(int fd) {
86106
mp_obj_socket_t *o = mp_obj_malloc(mp_obj_socket_t, &mp_type_socket);
87107
o->fd = fd;
@@ -194,8 +214,49 @@ static MP_DEFINE_CONST_FUN_OBJ_1(socket_fileno_obj, socket_fileno);
194214

195215
static mp_obj_t socket_connect(mp_obj_t self_in, mp_obj_t addr_in) {
196216
mp_obj_socket_t *self = MP_OBJ_TO_PTR(self_in);
217+
int family = mp_socket_family_from_fd(self_in);
197218
mp_buffer_info_t bufinfo;
198-
mp_get_buffer_raise(addr_in, &bufinfo, MP_BUFFER_READ);
219+
mp_obj_t addr_src;
220+
221+
if ((mp_obj_is_type(addr_in, &mp_type_tuple) || mp_obj_is_type(addr_in, &mp_type_list)) && (family == AF_INET || family == AF_INET6)) {
222+
// Check if the address is in the form <"host", port> for a socket
223+
// that is either of type AF_INET or AF_INET6, and if so perform
224+
// name resolution via getaddrinfo. This deviates slightly from
225+
// CPython in two ways:
226+
//
227+
// * Numeric host addresses are not supported, whilst CPython also
228+
// supports numeric addresses (and probably much more).
229+
// * socket.connect argument can be either a tuple or a list,
230+
// whilst CPython only accepts tuples for AF_INET or AF_INET6
231+
// sockets.
232+
//
233+
// Another limitation that is shared with CPython is that if a name
234+
// resolves to multiple addresses for the given family, the first
235+
// one is always the one the socket will attempt to connect to.
236+
//
237+
// For more complex requirements, then the usual method of calling
238+
// socket.getaddrinfo yourself and pass the raw buffer data should
239+
// allow handling of pretty much all possible conditions.
240+
241+
mp_obj_t *addr_args;
242+
size_t addr_len;
243+
mp_obj_get_array(addr_in, &addr_len, &addr_args);
244+
if (addr_len != 2) {
245+
mp_raise_ValueError(MP_ERROR_TEXT("address must contain two elements"));
246+
}
247+
mp_obj_t info_args[3] = { addr_args[0], addr_args[1], MP_OBJ_NEW_SMALL_INT(family) };
248+
mp_obj_t info = mod_socket_getaddrinfo(MP_ARRAY_SIZE(info_args), info_args);
249+
mp_obj_list_t *list = MP_OBJ_TO_PTR(info);
250+
if (list->len == 0) {
251+
mp_raise_OSError(MP_ENOENT);
252+
}
253+
mp_obj_tuple_t *addr_tuple = MP_OBJ_TO_PTR(list->items[0]);
254+
addr_src = addr_tuple->items[4];
255+
} else {
256+
// Default to the usual pre-resolved sockaddr representation.
257+
addr_src = addr_in;
258+
}
259+
mp_get_buffer_raise(addr_src, &bufinfo, MP_BUFFER_READ);
199260

200261
// special case of PEP 475 to retry only if blocking so we can't use
201262
// MP_HAL_RETRY_SYSCALL() here

tests/ports/unix/modsocket_connect.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Test socket.socket.connect() for both IPv4 and IPv6.
2+
3+
try:
4+
import socket
5+
except ImportError:
6+
print("SKIP")
7+
raise SystemExit
8+
import os
9+
import unittest
10+
11+
12+
TEST_HOSTNAME = "example.com"
13+
TEST_PORT = 80
14+
15+
16+
class Test(unittest.TestCase):
17+
@classmethod
18+
def setUpClass(cls):
19+
# If the IPv6 address for example.com fails then skip AF_INET6 tests.
20+
try:
21+
entries = socket.getaddrinfo(TEST_HOSTNAME, TEST_PORT, socket.AF_INET6)
22+
cls._has_ipv6 = len(entries) > 0
23+
except:
24+
cls._has_ipv6 = False
25+
26+
# The GitHub CI runner just doesn't work when it comes to IPv6.
27+
if os.getenv("GITHUB_ACTIONS") == "true":
28+
cls._has_ipv6 = False
29+
30+
# If the IPv4 address for example.com fails then skip AF_INET tests.
31+
try:
32+
entries = socket.getaddrinfo(TEST_HOSTNAME, TEST_PORT, socket.AF_INET)
33+
cls._has_ipv4 = len(entries) > 0
34+
except:
35+
cls._has_ipv4 = False
36+
37+
def test_hostname_connect_ipv4(self):
38+
if not self._has_ipv4:
39+
self.skipTest(
40+
"IPv4 connectivity may not be available or temporary name resolution failure detected"
41+
)
42+
43+
s = socket.socket(socket.AF_INET)
44+
try:
45+
s.connect((TEST_HOSTNAME, TEST_PORT))
46+
s.send(b"GET /\n\n")
47+
data = s.recv(8)
48+
self.assertTrue(len(data) > 0)
49+
finally:
50+
s.close()
51+
52+
def test_hostname_connect_ipv6(self):
53+
if not self._has_ipv6:
54+
self.skipTest(
55+
"IPv6 connectivity may not be available or temporary name resolution failure detected"
56+
)
57+
58+
s = socket.socket(socket.AF_INET6)
59+
try:
60+
s.connect((TEST_HOSTNAME, TEST_PORT))
61+
s.send(b"GET /\n\n")
62+
data = s.recv(8)
63+
self.assertTrue(len(data) > 0)
64+
finally:
65+
s.close()
66+
67+
def test_sockaddr_connect_ipv4(self):
68+
if not self._has_ipv4:
69+
self.skipTest(
70+
"IPv4 connectivity may not be available or temporary name resolution failure detected"
71+
)
72+
73+
sockaddr = socket.getaddrinfo(TEST_HOSTNAME, TEST_PORT, socket.AF_INET)[0][-1]
74+
s = socket.socket(socket.AF_INET)
75+
try:
76+
s.connect(sockaddr)
77+
s.send(b"GET /\n\n")
78+
data = s.recv(8)
79+
self.assertTrue(len(data) > 0)
80+
finally:
81+
s.close()
82+
83+
def test_sockaddr_connect_ipv6(self):
84+
if not self._has_ipv6:
85+
self.skipTest(
86+
"IPv6 connectivity may not be available or temporary name resolution failure detected"
87+
)
88+
89+
sockaddr = socket.getaddrinfo(TEST_HOSTNAME, TEST_PORT, socket.AF_INET6)[0][-1]
90+
s = socket.socket(socket.AF_INET6)
91+
try:
92+
s.connect(sockaddr)
93+
s.send(b"GET /\n\n")
94+
data = s.recv(8)
95+
self.assertTrue(len(data) > 0)
96+
finally:
97+
s.close()
98+
99+
100+
if __name__ == "__main__":
101+
unittest.main()

0 commit comments

< 3053 div class="h4 pr-2">Comments
 (0)
0