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

Skip to content

Commit 32e83f1

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 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 32e83f1

File tree

2 files changed

+172
-1
lines changed

2 files changed

+172
-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 8000 ->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: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 unittest
9+
10+
11+
TEST_HOSTNAME = "example.com"
12+
TEST_PORT = 80
13+
14+
15+
class Test(unittest.TestCase):
16+
@classmethod
17+
def setUpClass(cls):
18+
# If the IPv6 address for example.com fails then skip AF_INET6 tests.
19+
try:
20+
entries = socket.getaddrinfo(TEST_HOSTNAME, TEST_PORT, socket.AF_INET6)
21+
cls._has_ipv6 = len(entries) > 0
22+
except:
23+
cls._has_ipv6 = False
24+
25+
# If the IPv4 address for example.com fails then skip AF_INET tests.
26+
try:
27+
entries = socket.getaddrinfo(TEST_HOSTNAME, TEST_PORT, socket.AF_INET)
28+
cls._has_ipv4 = len(entries) > 0
29+
except:
30+
cls._has_ipv4 = False
31+
32+
def test_hostname_connect_ipv4(self):
33+
if not self._has_ipv4:
34+
self.skipTest(
35+
"IPv4 connectivity may not be available or temporary name resolution failure detected"
36+
)
37+
38+
s = socket.socket(socket.AF_INET)
39+
try:
40+
s.connect((TEST_HOSTNAME, TEST_PORT))
41+
s.send(b"GET /\n\n")
42+
data = s.recv(8)
43+
F438 self.assertTrue(len(data) > 0)
44+
finally:
45+
s.close()
46+
47+
def test_hostname_connect_ipv6(self):
48+
if not self._has_ipv6:
49+
self.skipTest(
50+
"IPv6 connectivity may not be available or temporary name resolution failure detected"
51+
)
52+
53+
s = socket.socket(socket.AF_INET6)
54+
try:
55+
s.connect((TEST_HOSTNAME, TEST_PORT))
56+
s.send(b"GET /\n\n")
57+
data = s.recv(8)
58+
self.assertTrue(len(data) > 0)
59+
except OSError as e:
60+
if e.errno == 101:
61+
self.skipTest(
62+
"this test may fail when executed from the CI runner (IPv6 connectivity not available)"
63+
)
64+
else:
65+
raise
66+
finally:
67+
s.close()
68+
69+
def test_sockaddr_connect_ipv4(self):
70+
if not self._has_ipv4:
71+
self.skipTest(
72+
"IPv4 connectivity may not be available or temporary name resolution failure detected"
73+
)
74+
75+
sockaddr = socket.getaddrinfo(TEST_HOSTNAME, TEST_PORT, socket.AF_INET)[0][-1]
76+
s = socket.socket(socket.AF_INET)
77+
try:
78+
s.connect(sockaddr)
79+
s.send(b"GET /\n\n")
80+
data = s.recv(8)
81+
self.assertTrue(len(data) > 0)
82+
finally:
83+
s.close()
84+
85+
def test_sockaddr_connect_ipv6(self):
86+
if not self._has_ipv6:
87+
self.skipTest(
88+
"IPv6 connectivity may not be available or temporary name resolution failure detected"
89+
)
90+
91+
sockaddr = socket.getaddrinfo(TEST_HOSTNAME, TEST_PORT, socket.AF_INET6)[0][-1]
92+
s = socket.socket(socket.AF_INET6)
93+
try:
94+
s.connect(sockaddr)
95+
s.send(b"GET /\n\n")
96+
data = s.recv(8)
97+
self.assertTrue(len(data) > 0)
98+
except OSError as e:
99+
if e.errno == 101:
100+
self.skipTest(
101+
"this test may fail when executed from the CI runner (IPv6 connectivity not available)"
102+
)
103+
else:
104+
raise
105+
finally:
106+
s.close()
107+
108+
109+
if __name__ == "__main__":
110+
unittest.main()

0 commit comments

Comments
 (0)
0