Skip to content
This repository was archived by the owner on Nov 23, 2017. It is now read-only.

Make loop TCP APIs accept only TCP streaming sockets #453

Merged
merged 1 commit into from
Nov 9, 2016

Conversation

1st1
Copy link
Member

@1st1 1st1 commented Nov 6, 2016

Currently, loop.create_server, loop.create_connection, and loop.connect_accepted_socket accept any kind of socket, although they are documented (and used by asyncio users) as TCP API.

Allowing those methods to accept any kinds of sockets leads to hard to debug bugs. For instance, aiohttp gunicorn worker simply passes all sockets to create_server (including UNIX sockets). We even have a unittest in asyncio that does that: test_create_datagram_endpoint_sock passes a UDP socket to create_connection.

TCP, UDP and UNIX sockets are different from each other and have some subtle API differences. We shouldn't allow to blindly pass any of them to create_server and other TCP APIs.

if (sys.platform == 'win32' and
isinstance(self.loop, proactor_events.BaseProactorEventLoop)):
raise unittest.SkipTest(
'UCP not supported with proactor event loops')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo, UDP?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@@ -1394,7 +1399,7 @@ def test_create_datagram_endpoint_sock(self):
else:
assert False, 'Can not create socket.'

f = self.loop.create_connection(
f = self.loop.create_datagram_endpoint(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried here that this test doesn't test much...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right, it tests only that the method doesn't crash. Still it was testing a wrong method.

@1st1 1st1 force-pushed the tcp_strict branch 3 times, most recently from b04c696 to 2abb370 Compare November 7, 2016 19:14
Copy link
Member

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(OT: connect_accepted_socket seems undocumented.)

@@ -1027,6 +1031,9 @@ def create_server(self, protocol_factory, host=None, port=None,
else:
if sock is None:
raise ValueError('Neither host/port nor sock were specified')
if sock.family not in {socket.AF_INET, socket.AF_INET6}:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful, check line 972/976. AF_INET6 may not always exist in the socket module (apparently).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Will fix this.

@@ -1048,6 +1055,13 @@ def connect_accepted_socket(self, protocol_factory, sock, *, ssl=None):
This method is a coroutine. When completed, the coroutine
returns a (transport, protocol) pair.
"""
socket_families = {socket.AF_INET, socket.AF_INET6}
if hasattr(socket, 'AF_UNIX'):
socket_families.add(socket.AF_UNIX)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a UNIX socket is fine here, why not in create_connection()? They both end up calling _create_connection_transport().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in asyncio both create_connection and connect_accepted_socket use similar internal API. It's not the same in uvloop.

The problem is that we have create_connection and create_unix_connection, that are designed for TCP and UNIX sockets. uvloop implements this spec, using appropriate low-level libuv primitives for each case. It doesn't make a lot of sense to add "if" checks to accept Unix sockets in create_connection (as we already have dedicated APIs for that).

connect_accepted_socket on the other hand doesn't specify the kind of socket it accepts. And it doesn't make a lot of sense to add connect_accepted_unix_socket.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So maybe connect_accepted_socket() should only check whether it's a stream socket, not whether it's any particular address family?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That means that users will be able to pass AF_BLUETOOTH sockets or something. Those aren't supported in uvloop, and I'm not sure that even asyncio supports them properly.

In any case, I think you're right, let's only check for SOCK_STREAM here. I'll also add SOCK_STREAM checks in create_connection, create_server, as they simply won't work with anything else. AF_INET6 has to be properly handled too. Let me update the PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some additional worry now. You've mentioned twice (I think) "but uvloop doesn't support that". I don't think that's a valid argument. (Haven't we discussed this before?) I think it's fine if uvloop doesn't support e.g. bluetooth sockets but pure-Python asyncio does. That should be documented with uvloop; but I don't think "uvloop can't do this" is enough to conclude "hence asyncio shouldn't do this either". (Especially since I remember seeing a bug about code that would crash when passing a bluetooth address; IIRC it was a getaddrinfo() issue.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re uvloop - I agree. The updated PR only makes sure that:

  1. create_datagram_endpoint only accepts SOCK_DGRAM sockets (it doesn't make sense to accept anything else).
  2. create_connection and create_server only accept AF_INET, AF_INET6 and SOCK_STREAM sockets. We have separate asyncio API for AF_UNIX sockets.
  3. connect_accepted_socket only accepts SOCK_STREAM sockets (no restrictions on family, event AF_BLUETOOTH will be accepted just fine. Even though uvloop won't work with the latter).

All in all, this PR is not about uvloop, it only ensures that asyncio APIs are used correctly. It doesn't limit asyncio in any way.

@1st1
Copy link
Member Author

1st1 commented Nov 7, 2016

(OT: connect_accepted_socket seems undocumented.)

I've committed Jim's documentation patch: http://bugs.python.org/issue27392. I've also updated this PR to address your comments.

@gvanrossum
Copy link
Member

Thanks, LGTM now!

@1st1 1st1 force-pushed the tcp_strict branch 3 times, most recently from fb40156 to 2608734 Compare November 9, 2016 15:54
@1st1
Copy link
Member Author

1st1 commented Nov 9, 2016

I've updated the patch with more unittests and more robust checks of socket.type. Will merge it later.

_SOCKET_TYPE_MASK |= socket.SOCK_NONBLOCK
if hasattr(socket, 'SOCK_CLOEXEC'):
_SOCKET_TYPE_MASK |= socket.SOCK_CLOEXEC
def _is_stream_socket(sock_type):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rename this to _is_stream_socket_type(), and similar for the following functions (using _family as the suffix for _is_ip_socket) to emphasize which field of the socket is supposed to be passed into these functions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since _is_ip_socket no longer uses those methods, I refactored is_stream_socket to accept socket objects directly, which made the code simpler overall. See the update.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even better!

More specifically:

* loop.create_connection() and loop.create_server() can accept
  AF_INET or AF_INET6 SOCK_STREAM sockets;

* loop.create_datagram_endpoint() can accept only SOCK_DGRAM
  sockets;

* loop.connect_accepted_socket() can accept only SOCK_STREAM
  sockets;

* fixed a bug in create_unix_server() and create_unix_connection()
  to properly check for SOCK_STREAM sockets on Linux;

* fixed static DNS resolution to decline socket types that aren't
  strictly equal to SOCK_STREAM or SOCK_DGRAM.  On Linux socket
  type can be a bit mask, and we should let system getaddrinfo()
  to deal with it.
@1st1 1st1 merged commit eb4c3ff into python:master Nov 9, 2016
@1st1
Copy link
Member Author

1st1 commented Nov 9, 2016

Thanks for the review, Guido. I've implemented the same set of features in uvloop, and will watch closely if they cause any problems (I highly doubt that).

@gvanrossum
Copy link
Member

gvanrossum commented Nov 9, 2016 via email

@1st1
Copy link
Member Author

1st1 commented Nov 21, 2016

I rolled back the restriction of passing AF_UNIX to create_connection and create_server. Turns out too many people rely on that, so I guess we can just support that (it's easy to fix uvloop to support this as well).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants