Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ws_connect to ClientSession (cleaned up version of pull request #371 - figuring out the right way to do this) #374

Merged
merged 5 commits into from May 22, 2015
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
80 changes: 76 additions & 4 deletions aiohttp/client.py
@@ -1,15 +1,21 @@
"""HTTP Client for asyncio."""

import asyncio
import http.cookies
import urllib.parse
import warnings
import base64
import hashlib
import os
import sys
import traceback
import warnings
import http.cookies
import urllib.parse

import aiohttp
from .client_reqrep import ClientRequest, ClientResponse
from .errors import WSServerHandshakeError
from .multidict import MultiDictProxy, MultiDict, CIMultiDict
from .websocket import WS_KEY, WebSocketParser, WebSocketWriter
from .websocket_client import ClientWebSocketResponse
from . import hdrs


Expand All @@ -26,7 +32,8 @@ class ClientSession:

def __init__(self, *, connector=None, loop=None,
request_class=ClientRequest, response_class=ClientResponse,
cookies=None, headers=None, auth=None):
ws_response_class=ClientWebSocketResponse, cookies=None,
headers=None, auth=None):
if loop is None:
loop = asyncio.get_event_loop()
self._loop = loop
Expand Down Expand Up @@ -58,6 +65,7 @@ def __init__(self, *, connector=None, loop=None,

self._request_class = request_class
self._response_class = response_class
self._ws_response_class = ws_response_class

if PY_34:
def __del__(self):
Expand Down Expand Up @@ -167,6 +175,70 @@ def request(self, method, url, *,

return resp

@asyncio.coroutine
def ws_connect(self, url, *,
protocols=(),
timeout=10.0,
autoclose=True,
autoping=True):
"""Initiate websocket connection."""

sec_key = base64.b64encode(os.urandom(16))

headers = {
hdrs.UPGRADE: hdrs.WEBSOCKET,
hdrs.CONNECTION: hdrs.UPGRADE,
hdrs.SEC_WEBSOCKET_VERSION: '13',
hdrs.SEC_WEBSOCKET_KEY: sec_key.decode(),
}
if protocols:
headers[hdrs.SEC_WEBSOCKET_PROTOCOL] = ','.join(protocols)

# send request
resp = yield from self.request('get', url, headers=headers,
read_until_eof=False)

# check handshake
if resp.status != 101:
raise WSServerHandshakeError('Invalid response status')

if resp.headers.get(hdrs.UPGRADE, '').lower() != 'websocket':
raise WSServerHandshakeError('Invalid upgrade header')

if resp.headers.get(hdrs.CONNECTION, '').lower() != 'upgrade':
raise WSServerHandshakeError('Invalid connection header')

# key calculation
key = resp.headers.get(hdrs.SEC_WEBSOCKET_ACCEPT, '')
match = base64.b64encode(
hashlib.sha1(sec_key + WS_KEY).digest()).decode()
if key != match:
raise WSServerHandshakeError('Invalid challenge response')

# websocket protocol
protocol = None
if protocols and hdrs.SEC_WEBSOCKET_PROTOCOL in resp.headers:
resp_protocols = [
proto.strip() for proto in
resp.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(',')]

for proto in resp_protocols:
if proto in protocols:
protocol = proto
break

reader = resp.connection.reader.set_parser(WebSocketParser)
writer = WebSocketWriter(resp.connection.writer, use_mask=True)

return self._ws_response_class(reader,
writer,
protocol,
resp,
timeout,
autoclose,
autoping,
self._loop)

def _update_cookies(self, cookies):
"""Update shared cookies."""
if isinstance(cookies, dict):
Expand Down
97 changes: 31 additions & 66 deletions aiohttp/websocket_client.py
@@ -1,14 +1,10 @@
"""WebSocket client for asyncio."""

import asyncio
import base64
import hashlib
import os

from aiohttp import client, hdrs
from .errors import WSServerHandshakeError
from .websocket import WS_KEY, Message
from .websocket import WebSocketParser, WebSocketWriter, WebSocketError

import aiohttp
from .websocket import Message
from .websocket import WebSocketError
from .websocket import MSG_BINARY, MSG_TEXT, MSG_CLOSE, MSG_PING, MSG_PONG

__all__ = ('ws_connect', 'MsgType')
Expand All @@ -33,65 +29,34 @@ class MsgType(IntEnum):
closedMessage = Message(MsgType.closed, None, None)


@asyncio.coroutine
def ws_connect(url, protocols=(), timeout=10.0, connector=None,
response_class=None, autoclose=True, autoping=True, loop=None):
"""Initiate websocket connection."""
def ws_connect(url, *, protocols=(), timeout=10.0, connector=None,
Copy link
Member

Choose a reason for hiding this comment

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

Please add @asyncio.coroutine decorator.

ws_response_class=None, autoclose=True, autoping=True,
Copy link
Member

Choose a reason for hiding this comment

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

Don't hesitate to use ws_response_class=ClientWebSocketResponse.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm unsure about this. Would we still check that ws_response_class is not None? Otherwise, if the user were to call

ws_connect("ws...", response_class=None)

then _self.ws_response_class on the ClientSession would be None, and throw an error. Unless we change to ws_response_class=ClientWebSocketResponse, and I still check in the function body that ws_response_class is not None.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess that if they change the param we have to assume they know what they are doing anyway.

loop=None):

if loop is None:
loop = asyncio.get_event_loop()

sec_key = base64.b64encode(os.urandom(16))

headers = {
hdrs.UPGRADE: hdrs.WEBSOCKET,
hdrs.CONNECTION: hdrs.UPGRADE,
hdrs.SEC_WEBSOCKET_VERSION: '13',
hdrs.SEC_WEBSOCKET_KEY: sec_key.decode(),
}
if protocols:
headers[hdrs.SEC_WEBSOCKET_PROTOCOL] = ','.join(protocols)

# send request
resp = yield from client.request(
'get', url, headers=headers,
read_until_eof=False,
connector=connector, loop=loop)

# check handshake
if resp.status != 101:
raise WSServerHandshakeError('Invalid response status')

if resp.headers.get(hdrs.UPGRADE, '').lower() != 'websocket':
raise WSServerHandshakeError('Invalid upgrade header')

if resp.headers.get(hdrs.CONNECTION, '').lower() != 'upgrade':
raise WSServerHandshakeError('Invalid connection header')

# key calculation
key = resp.headers.get(hdrs.SEC_WEBSOCKET_ACCEPT, '')
match = base64.b64encode(hashlib.sha1(sec_key + WS_KEY).digest()).decode()
if key != match:
raise WSServerHandshakeError('Invalid challenge response')

# websocket protocol
protocol = None
if protocols and hdrs.SEC_WEBSOCKET_PROTOCOL in resp.headers:
resp_protocols = [proto.strip() for proto in
resp.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(',')]

for proto in resp_protocols:
if proto in protocols:
protocol = proto
break

reader = resp.connection.reader.set_parser(WebSocketParser)
writer = WebSocketWriter(resp.connection.writer, use_mask=True)

if response_class is None:
response_class = ClientWebSocketResponse

return response_class(
reader, writer, protocol, resp, timeout, autoclose, autoping, loop)
asyncio.get_event_loop()

if connector is None:
connector = aiohttp.TCPConnector(loop=loop, force_close=True)

kwargs = {}

if ws_response_class is not None:
kwargs['ws_response_class'] = ws_response_class

session = aiohttp.ClientSession(loop=loop, connector=connector, **kwargs)

try:
resp = yield from session.ws_connect(
url,
protocols=protocols,
timeout=timeout,
autoclose=autoclose,
autoping=autoping)
return resp

finally:
session.detach()


class ClientWebSocketResponse:
Expand Down
26 changes: 24 additions & 2 deletions docs/client_reference.rst
Expand Up @@ -30,8 +30,8 @@ Usage example::


.. class:: ClientSession(*, connector=None, loop=None, request_class=None,\
response_class=None, cookies=None, headers=None,\
auth=None)
response_class=None, ws_response_class=None,\
cookies=None, headers=None, auth=None)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I noticed here that in the docs it lists the default params: request_class=None, response_class=None (and now ws_response_class=None). However, in the code base these are set to the default classes. Is this an oversight? Or is it intended to be that way?

Copy link
Member

Choose a reason for hiding this comment

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

Documentation is wrong.
I've changed defaults but forgot to update doc.
Please fix to correct values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it. These params will not include the (optional) flag correct? Because some class must be passed as arg for each (although it is usually the default) . Like such:

:param request_class: Request class implementation. ClientRequest
by default.

:param response_class: Response class implementation.
ClientResponse by default.

:param ws_response_class: WebSocketResponse class implementation.
ClientWebSocketResponse by default.

Copy link
Member

Choose a reason for hiding this comment

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

Nice!


The class for creating client sessions and making requests.

Expand All @@ -51,6 +51,9 @@ Usage example::

:param response_class: Custom Response class implementation (optional)

:param ws_response_class: Custom WebSocketResponse class implementation
(optional)

:param dict cookies: Cookies to send with the request (optional)

:param dict headers: HTTP Headers to send with
Expand Down Expand Up @@ -223,6 +226,25 @@ Usage example::
:param data: Dictionary, bytes, or file-like object to
send in the body of the request (optional)


.. coroutinemethod:: ws_connect(url, *, protocols=(), timeout=10.0\
autoclose=True, autoping=True)

Create a websocket connection. Returns a :class:`ClientWebSocketResponse` object.

:param str url: Websocket server url

:param tuple protocols: Websocket protocols

:param float timeout: Timeout for websocket read. 10 seconds by default

:param bool autoclose: Automatically close websocket connection on close
message from server. If `autoclose` is False
them close procedure has to be handled manually

:param bool autoping: automatically send `pong` on `ping` message from server


Copy link
Member

Choose a reason for hiding this comment

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

..versionadded:: 0.16

.. method:: close()

Close underlying connector.
Expand Down
19 changes: 12 additions & 7 deletions docs/client_websockets.rst
Expand Up @@ -46,25 +46,30 @@ ClientWebSocketResponse
To connect to a websocket server you have to use the `aiohttp.ws_connect()` function,
do not create an instance of class :class:`ClientWebSocketResponse` manually.

.. py:function:: ws_connect(url, protocols=(), connector=None, response_class=None, autoclose=True, autoping=True, loop=None)
.. py:function:: ws_connect(url, *, protocols=(), timeout=10.0,\
connector=None, ws_response_class=None,\
Copy link
Member

Choose a reason for hiding this comment

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

ws_response_class=ClientWebSocketResponse

autoclose=True, autoping=True, loop=None)

This function creates a websocket connection, checks the response and
returns a :class:`ClientWebSocketResponse` object. In case of failure
it may raise a :exc:`~aiohttp.errors.WSServerHandshakeError` exception.

:param str url: websocket server url
:param str url: Websocket server url

:param tuple protocols: websocket protocols
:param tuple protocols: Websocket protocols

:param float timeout: Timeout for websocket read. 10 seconds by default

:param obj connector: object :class:`TCPConnector`

:param response_class: (optional) Custom Response class implementation.
:param ws_response_class: (optional) Custom WebSocketResponse class
implementation.

:param bool autoclose: automatically close websocket connection
on close message from server. if `autoclose` is
:param bool autoclose: Automatically close websocket connection
on close message from server. If `autoclose` is
False them close procedure has to be handled manually

:param bool autoping: automatically send `pong` on `ping` message from server
:param bool autoping: Automatically send `pong` on `ping` message from server

:param loop: :ref:`event loop<asyncio-event-loop>` used
for processing HTTP requests.
Expand Down