├── MANIFEST.in ├── .coveragerc ├── aioppspp ├── tests │ ├── __init__.py │ ├── test_datagrams.py │ ├── test_udp.py │ ├── test_ppspp.py │ ├── test_address.py │ ├── utils.py │ ├── test_messages.py │ ├── test_message_handshake.py │ ├── test_channel_ids.py │ ├── strategies.py │ ├── test_connection.py │ ├── test_protocol_options.py │ └── test_connector.py ├── __init__.py ├── constants.py ├── version.py ├── messages │ ├── types.py │ ├── __init__.py │ ├── handshake.py │ └── protocol_options.py ├── ppspp.py ├── datagrams.py ├── channel_ids.py ├── udp.py ├── connection.py └── connector.py ├── CHANGES.rst ├── docs ├── changes.rst ├── license.rst ├── modules │ ├── channel_ids.rst │ ├── connection.rst │ ├── connector.rst │ ├── udp.rst │ ├── ppspp.rst │ ├── datagrams.rst │ ├── index.rst │ └── messages.rst ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── README.rst ├── .gitignore ├── .travis.yml ├── setup.py ├── Makefile └── LICENSE /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude *.pyc 2 | graft aioppspp 3 | graft docs 4 | include CHANGES.rst 5 | include LICENSE 6 | include Makefile 7 | include README.rst 8 | prune docs/_build 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | include = 4 | aioppspp/*.py 5 | aioppspp/**/*.py 6 | omit = 7 | **/version.py 8 | 9 | [report] 10 | exclude_lines = 11 | @abc.abstractmethod 12 | @abc.abstractproperty 13 | NotImplementedError 14 | pragma: no cover 15 | __repr__ 16 | __str__ 17 | -------------------------------------------------------------------------------- /aioppspp/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | 0.1.0 (dev) 14 | =========== 15 | 16 | - TODO 17 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | ======= 14 | Changes 15 | ======= 16 | 17 | .. include:: ../CHANGES.rst 18 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | ======= 14 | License 15 | ======= 16 | 17 | .. literalinclude:: ../LICENSE 18 | -------------------------------------------------------------------------------- /docs/modules/channel_ids.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | Channel ID 14 | ========== 15 | 16 | .. automodule:: aioppspp.channel_ids 17 | -------------------------------------------------------------------------------- /docs/modules/connection.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | Connection 14 | ========== 15 | 16 | .. automodule:: aioppspp.connection 17 | -------------------------------------------------------------------------------- /aioppspp/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | # flake8: noqa 17 | 18 | from .version import __version__ 19 | -------------------------------------------------------------------------------- /docs/modules/connector.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | Base Connector and Protocol 14 | =========================== 15 | 16 | .. automodule:: aioppspp.connector 17 | -------------------------------------------------------------------------------- /docs/modules/udp.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | UDP Connector and Protocol 14 | ========================== 15 | 16 | .. automodule:: aioppspp.udp 17 | :members: 18 | -------------------------------------------------------------------------------- /docs/modules/ppspp.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | PPSPP application protocol 14 | ========================== 15 | 16 | .. automodule:: aioppspp.ppspp 17 | :members: 18 | -------------------------------------------------------------------------------- /docs/modules/datagrams.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | Datagrams 14 | ========= 15 | 16 | .. automodule:: aioppspp.datagrams 17 | :members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/modules/index.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | Modules 14 | ======= 15 | 16 | Contents: 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | 21 | channel_ids 22 | connection 23 | connector 24 | datagrams 25 | messages 26 | ppspp 27 | udp 28 | -------------------------------------------------------------------------------- /aioppspp/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | __all__ = ( 17 | 'BYTE', 18 | 'WORD', 19 | 'DWORD', 20 | 'QWORD', 21 | ) 22 | 23 | 24 | #: 25 | BYTE = 1 26 | #: 27 | WORD = 2 28 | #: 29 | DWORD = 4 30 | #: 31 | QWORD = 8 32 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | Welcome to aioppspp's documentation! 14 | ==================================== 15 | 16 | Contents: 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | 21 | modules/index 22 | changes 23 | license 24 | 25 | .. include:: ../README.rst 26 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | ======== 14 | aioppspp 15 | ======== 16 | 17 | :status: alpha 18 | 19 | **aioppspp** is an implementation of the IETF `PPSP`_ protocol, :rfc:`7574` in 20 | Python using `asyncio`_ framework, and released under the `Apache 2`_ license. 21 | 22 | .. _Apache 2: http://www.apache.org/licenses/LICENSE-2.0.html 23 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 24 | .. _PPSP: https://datatracker.ietf.org/doc/rfc7574/ 25 | -------------------------------------------------------------------------------- /aioppspp/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | __version_info__ = (0, 1, 0, 'dev', 0) 17 | __version__ = '%(version)s%(tag)s%(build)s' % { 18 | 'version': '.'.join(map(str, __version_info__[:3])), 19 | 'tag': '.' + __version_info__[3] if __version_info__[3] else '', 20 | 'build': '' + str(__version_info__[4]) if __version_info__[3] else '' 21 | } 22 | 23 | 24 | if __name__ == '__main__': 25 | import sys 26 | sys.stdout.write(__version__) 27 | sys.stdout.flush() 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | ### Python template 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | .hypothesis 42 | .tox/ 43 | htmlcov/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/modules/messages.rst: -------------------------------------------------------------------------------- 1 | .. Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | .. use this file except in compliance with the License. You may obtain a copy of 3 | .. the License at 4 | .. 5 | .. http://www.apache.org/licenses/LICENSE-2.0 6 | .. 7 | .. Unless required by applicable law or agreed to in writing, software 8 | .. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | .. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | .. License for the specific language governing permissions and limitations under 11 | .. the License. 12 | 13 | Messages 14 | ======== 15 | 16 | .. automodule:: aioppspp.messages 17 | :members: 18 | :show-inheritance: 19 | :undoc-members: 20 | 21 | Handshake 22 | --------- 23 | 24 | .. automodule:: aioppspp.messages.handshake 25 | :members: 26 | :show-inheritance: 27 | :undoc-members: 28 | 29 | Protocol Options 30 | ^^^^^^^^^^^^^^^^ 31 | .. automodule:: aioppspp.messages.protocol_options 32 | :members: 33 | :show-inheritance: 34 | :undoc-members: 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | language: python 17 | python: 18 | - 3.5 19 | 20 | env: 21 | matrix: 22 | - TARGET=lint 23 | - TARGET=check 24 | - TARGET=check-cov 25 | - TARGET=docs 26 | 27 | sudo: false 28 | 29 | before_script: 30 | - make dev 31 | 32 | script: 33 | - make ${TARGET} 34 | -------------------------------------------------------------------------------- /aioppspp/messages/types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import enum 17 | 18 | __all__ = ( 19 | 'MessageType', 20 | ) 21 | 22 | 23 | class MessageType(enum.IntEnum): 24 | """Enumeration of message types. 25 | 26 | .. seealso: 27 | 28 | - :rfc:`7574#section-8.2`. 29 | """ 30 | HANDSHAKE = 0 31 | DATA = 1 32 | ACK = 2 33 | HAVE = 3 34 | INTEGRITY = 4 35 | PEX_RESv4 = 5 36 | PEX_REQ = 6 37 | SIGNED_INTEGRITY = 7 38 | REQUEST = 8 39 | CANCEL = 9 40 | CHOKE = 10 41 | UNCHOKE = 11 42 | PEX_RESv6 = 12 43 | PEX_REScert = 13 44 | -------------------------------------------------------------------------------- /aioppspp/tests/test_datagrams.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import unittest 17 | 18 | import aioppspp.channel_ids 19 | import aioppspp.datagrams 20 | 21 | 22 | class DatagramsTestCase(unittest.TestCase): 23 | 24 | def test_decode_encode_empty(self): 25 | data = bytes(aioppspp.channel_ids.new()) 26 | 27 | datagram = aioppspp.datagrams.decode(memoryview(data)) 28 | self.assertIsInstance(datagram, aioppspp.datagrams.Datagram) 29 | self.assertIsInstance(datagram.channel_id, 30 | aioppspp.channel_ids.ChannelID) 31 | self.assertEqual(datagram.channel_id, data) 32 | self.assertEqual(datagram.messages, tuple()) 33 | 34 | encoded_datagram = aioppspp.datagrams.encode(datagram) 35 | self.assertIsInstance(encoded_datagram, bytes) 36 | self.assertEqual(encoded_datagram, data) 37 | -------------------------------------------------------------------------------- /aioppspp/tests/test_udp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import aioppspp.connection 17 | import aioppspp.tests.utils 18 | import aioppspp.udp 19 | 20 | 21 | class TestUDP(aioppspp.tests.utils.TestCase): 22 | 23 | def new_connector(self): 24 | return aioppspp.udp.Connector(loop=self.loop) 25 | 26 | async def test_ping_pong(self): 27 | connector = self.new_connector() 28 | server_address = aioppspp.connection.Address('127.0.0.1', 0) 29 | server = await connector.listen(server_address) 30 | client = await connector.connect(server.protocol.local_address) 31 | 32 | await client.send(b'ping', server.local_address) 33 | data, addr = await server.recv() 34 | self.assertEqual(data, b'ping') 35 | self.assertEqual(addr, client.local_address) 36 | 37 | await server.send(b'pong', addr) 38 | data, addr = await client.recv() 39 | self.assertEqual(data, b'pong') 40 | self.assertEqual(addr, server.local_address) 41 | 42 | server.close() 43 | client.close() 44 | -------------------------------------------------------------------------------- /aioppspp/ppspp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | from . import datagrams 17 | from . import udp 18 | 19 | __all__ = ( 20 | 'Connector', 21 | 'Protocol', 22 | ) 23 | 24 | 25 | class Protocol(udp.Protocol): 26 | """PPSPP application protocol implementation over UDP.""" 27 | 28 | async def recv(self): 29 | """Receives a datagram from remote peer. 30 | 31 | :rtype: :class:`aioppspp.datagrams.Datagram` 32 | 33 | This method is :term:`awaitable`. 34 | """ 35 | data, addr = await super().recv() 36 | return datagrams.decode(memoryview(data)), addr 37 | 38 | async def send(self, datagram, remote_address=None): 39 | """Sends a datagram to remote peer. 40 | 41 | :param aioppspp.datagrams.Datagram datagram: PPSPP datagram 42 | :param aioppspp.connection.Address remote_address: Remote peer address 43 | 44 | This method is :term:`awaitable`. 45 | """ 46 | return await super().send(datagrams.encode(datagram), remote_address) 47 | 48 | 49 | class Connector(udp.Connector): 50 | """PPSPP connector that implements application protocol.""" 51 | 52 | protocol_class = Protocol 53 | -------------------------------------------------------------------------------- /aioppspp/tests/test_ppspp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import aioppspp.channel_ids 17 | import aioppspp.connection 18 | import aioppspp.datagrams 19 | import aioppspp.ppspp 20 | import aioppspp.tests.utils 21 | 22 | 23 | class PPSPPTestCase(aioppspp.tests.utils.TestCase): 24 | 25 | def new_connector(self): 26 | return aioppspp.ppspp.Connector(loop=self.loop) 27 | 28 | async def test_keepalive_exchange(self): 29 | connector = self.new_connector() 30 | peer1_address = aioppspp.connection.Address('127.0.0.1', 0) 31 | peer1 = await connector.listen(peer1_address) 32 | peer2 = await connector.connect(peer1.local_address) 33 | 34 | channel_id1 = aioppspp.channel_ids.new() 35 | channel_id2 = aioppspp.channel_ids.new() 36 | 37 | datagram1 = aioppspp.datagrams.Datagram(channel_id1, []) 38 | datagram2 = aioppspp.datagrams.Datagram(channel_id2, []) 39 | 40 | await peer2.send(datagram2) 41 | 42 | datagram, address = await peer1.recv() 43 | self.assertEqual(datagram.channel_id, channel_id2) 44 | 45 | await peer1.send(datagram1, address) 46 | 47 | datagram, address = await peer2.recv() 48 | self.assertEqual(datagram.channel_id, channel_id1) 49 | 50 | peer1.close() 51 | peer2.close() 52 | connector.close() 53 | -------------------------------------------------------------------------------- /aioppspp/tests/test_address.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import ipaddress 17 | import unittest 18 | 19 | import hypothesis 20 | import hypothesis.strategies as st 21 | 22 | import aioppspp.connection 23 | 24 | 25 | def ipaddr(): 26 | return ipv4() | ipv6() 27 | 28 | 29 | def ipv4(): 30 | return st.builds( 31 | ipaddress.IPv4Address, 32 | st.integers(min_value=0, max_value=(2 ** ipaddress.IPV4LENGTH) - 1) 33 | ).map(str) 34 | 35 | 36 | def ipv6(): 37 | return st.builds( 38 | ipaddress.IPv6Address, 39 | st.integers(min_value=0, max_value=(2 ** ipaddress.IPV6LENGTH) - 1) 40 | ).map(str) 41 | 42 | 43 | def port(): 44 | return st.integers(0, 2 ** 16 - 1) 45 | 46 | 47 | class AddressTestCase(unittest.TestCase): 48 | @hypothesis.given(ipaddr(), port()) 49 | def test_address(self, ipaddr, port): 50 | addr = aioppspp.connection.Address(ipaddr, port) 51 | self.assertEqual(addr.ip, ipaddr) 52 | self.assertEqual(addr.port, port) 53 | 54 | def test_bad_ip(self): 55 | with self.assertRaises(ValueError): 56 | aioppspp.connection.Address('bad', 42) 57 | 58 | def test_bad_port_type(self): 59 | with self.assertRaises(TypeError): 60 | aioppspp.connection.Address('0.0.0.0', '42') 61 | 62 | def test_bad_port_value(self): 63 | with self.assertRaises(ValueError): 64 | aioppspp.connection.Address('0.0.0.0', -1) 65 | -------------------------------------------------------------------------------- /aioppspp/tests/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import asyncio 17 | import functools 18 | import inspect 19 | import unittest 20 | 21 | __all__ = ( 22 | 'MetaAioTestCase', 23 | 'TestCase', 24 | ) 25 | 26 | 27 | def run_in_loop(coro): 28 | @functools.wraps(coro) 29 | def wrapper(testcase, *args, **kwargs): 30 | future = asyncio.wait_for(coro(testcase, *args, **kwargs), 31 | timeout=getattr(testcase, 'timeout', 5)) 32 | return testcase.loop.run_until_complete(future) 33 | return wrapper 34 | 35 | 36 | class MetaAioTestCase(type): 37 | 38 | def __new__(cls, name, bases, attrs): 39 | for key, obj in attrs.items(): 40 | if key.startswith('test_'): 41 | if inspect.iscoroutinefunction(obj): 42 | attrs[key] = run_in_loop(obj) 43 | else: 44 | attrs[key] = obj 45 | return super().__new__(cls, name, bases, attrs) 46 | 47 | 48 | class TestCase(unittest.TestCase, metaclass=MetaAioTestCase): 49 | 50 | def setUp(self): 51 | self.loop = asyncio.new_event_loop() 52 | asyncio.set_event_loop(self.loop) 53 | 54 | def tearDown(self): 55 | self.loop.close() 56 | 57 | def future(self, result=..., exception=...): 58 | future = asyncio.Future(loop=self.loop) 59 | if result is not ...: 60 | future.set_result(result) # pragma: no cover 61 | if exception is not ...: 62 | future.set_exception(exception) 63 | return future 64 | -------------------------------------------------------------------------------- /aioppspp/tests/test_messages.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import unittest 17 | 18 | import aioppspp.messages 19 | 20 | 21 | class MessagesTestCase(unittest.TestCase): 22 | 23 | def test_decode_encode_empty(self): 24 | messages = aioppspp.messages.decode(memoryview(b'')) 25 | self.assertEqual(messages, tuple()) 26 | data = aioppspp.messages.encode(messages) 27 | self.assertEqual(data, b'') 28 | 29 | def test_unknown_message_type(self): 30 | class Message(aioppspp.messages.Message): 31 | @property 32 | def type(self): 33 | return aioppspp.messages.MessageType.ACK 34 | 35 | with self.assertRaises(ValueError): 36 | aioppspp.messages.decode(memoryview(b'\xcc')) 37 | 38 | with self.assertRaises(KeyError): 39 | aioppspp.messages.encode([Message()]) 40 | 41 | def test_dummy_message(self): 42 | class Message(aioppspp.messages.Message): 43 | @property 44 | def type(self): 45 | return aioppspp.messages.MessageType.ACK 46 | 47 | data = Message().type.value.to_bytes(1, 'big') 48 | messages = aioppspp.messages.decode( 49 | memoryview(data), 50 | handlers={Message().type: lambda d: (Message(), d)}) 51 | self.assertIsInstance(messages[0], Message) 52 | 53 | result = aioppspp.messages.encode( 54 | messages, 55 | handlers={Message().type: lambda m: b''}) 56 | self.assertIsInstance(result, bytearray) 57 | self.assertEqual(result, data) 58 | -------------------------------------------------------------------------------- /aioppspp/datagrams.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | from collections import ( 17 | namedtuple, 18 | ) 19 | 20 | from . import channel_ids 21 | from . import messages 22 | from .channel_ids import ( 23 | ChannelID, 24 | ) 25 | from .messages import ( 26 | Message, 27 | ) 28 | 29 | __all__ = ( 30 | 'Datagram', 31 | 'decode', 32 | 'encode', 33 | ) 34 | 35 | 36 | class Datagram(namedtuple('Datagram', ('channel_id', 'messages'))): 37 | """A sequence of messages that is offered as a unit to the 38 | underlying transport protocol (UDP, etc.) prefixed by channel ID. 39 | The datagram is PPSPP's Protocol Data Unit (PDU). 40 | 41 | .. seealso:: 42 | 43 | - :rfc:`7574#section-1.3` (:rfc:`page 6 <7574#page-6>`) 44 | """ 45 | __slots__ = () 46 | 47 | def __new__(cls, channel_id, messages): 48 | assert all(isinstance(message, Message) for message in messages) 49 | return super().__new__(cls, ChannelID(channel_id), tuple(messages)) 50 | 51 | 52 | def decode(data): 53 | """Decodes bytes into datagram instance. 54 | 55 | :param memoryview data: Binary data 56 | :rtype: :class:`Datagram` 57 | """ 58 | channel_id, rest = channel_ids.decode(data) 59 | return Datagram(channel_id, messages.decode(rest)) 60 | 61 | 62 | def encode(datagram): 63 | """Encodes datagram instance into bytes. 64 | 65 | :param Datagram datagram: Datagram instance 66 | :rtype: bytes 67 | """ 68 | data = bytearray() 69 | data.extend(channel_ids.encode(datagram.channel_id)) 70 | data.extend(messages.encode(datagram.messages)) 71 | return bytes(data) 72 | -------------------------------------------------------------------------------- /aioppspp/tests/test_message_handshake.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import unittest 17 | 18 | import hypothesis 19 | 20 | import aioppspp.channel_ids 21 | import aioppspp.messages 22 | import aioppspp.messages.handshake 23 | import aioppspp.messages.protocol_options as protocol_options 24 | from . import strategies as st 25 | 26 | 27 | class HandshakeTestCase(unittest.TestCase): 28 | 29 | def test_decode_empty(self): 30 | with self.assertRaises(ValueError): 31 | aioppspp.messages.handshake.decode(memoryview(b'')) 32 | 33 | @hypothesis.given(st.handshake()) 34 | def test_decode_encode(self, message): 35 | data = aioppspp.messages.handshake.encode(message) 36 | self.assertIsInstance(data, bytearray) 37 | result, _ = aioppspp.messages.handshake.decode(memoryview(data)) 38 | self.assertIsInstance(result, aioppspp.messages.Handshake) 39 | self.assertEqual(message, result) 40 | 41 | def test_init_with_bad_type(self): 42 | with self.assertRaises(ValueError): 43 | msgtype = 42 44 | aioppspp.messages.Handshake(msgtype, ..., ...) 45 | with self.assertRaises(ValueError): 46 | msgtype = aioppspp.messages.MessageType(2) 47 | aioppspp.messages.Handshake(msgtype, ..., ...) 48 | 49 | def test_init_cast_arguments(self): 50 | message = aioppspp.messages.Handshake(0, b'1234', {}) 51 | self.assertIsInstance(message.type, aioppspp.messages.MessageType) 52 | self.assertIsInstance(message.source_channel_id, 53 | aioppspp.channel_ids.ChannelID) 54 | self.assertIsInstance(message.protocol_options, 55 | protocol_options.ProtocolOptions) 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import importlib 17 | import os 18 | import sys 19 | from os.path import join 20 | from setuptools import setup, find_packages 21 | 22 | name = 'aioppspp' 23 | 24 | if sys.version_info < (3, 5, 0): 25 | raise RuntimeError('{} requires Python 3.5.0+'.format(name)) 26 | 27 | setup_dir = os.path.dirname(__file__) 28 | mod = importlib.import_module('{}.version'.format(name)) 29 | long_description = open(join(setup_dir, 'README.rst')).read().strip() 30 | 31 | setup( 32 | name=name, 33 | version=mod.__version__, 34 | license='Apache 2', 35 | url='https://github.com/aio-libs/aioppspp', 36 | 37 | description='Implementation of the RFC-7574 PPSPP in Python/asyncio', 38 | long_description=long_description, 39 | 40 | author='Alexander Shorin', 41 | author_email='kxepal@gmail.com', 42 | 43 | classifiers=[ 44 | 'Development Status :: 1 - Planning', 45 | 'Intended Audience :: Developers', 46 | 'License :: OSI Approved :: Apache Software License', 47 | 'Operating System :: OS Independent', 48 | 'Programming Language :: Python', 49 | 'Programming Language :: Python :: 3', 50 | 'Programming Language :: Python :: 3.5', 51 | 'Topic :: Software Development :: Libraries :: Python Modules' 52 | ], 53 | 54 | packages=find_packages(), 55 | zip_safe=False, 56 | test_suite='{}.tests'.format(name), 57 | 58 | install_requires=[ 59 | 60 | ], 61 | extras_require={ 62 | 'dev': [ 63 | 'coverage==4.0.3', 64 | 'flake8==2.5.1', 65 | ], 66 | 'docs': [ 67 | 'sphinx==1.3.1', 68 | ] 69 | }, 70 | tests_require=[ 71 | 'hypothesis==2.0.0', 72 | ], 73 | ) 74 | -------------------------------------------------------------------------------- /aioppspp/tests/test_channel_ids.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import unittest 17 | 18 | import hypothesis 19 | 20 | import aioppspp.channel_ids 21 | from . import strategies as st 22 | 23 | 24 | class ChannelIDTestCase(unittest.TestCase): 25 | 26 | def test_decode(self): 27 | data = memoryview(b'12345678') 28 | channel_id, rest = aioppspp.channel_ids.decode(data) 29 | self.assertIsInstance(channel_id, aioppspp.channel_ids.ChannelID) 30 | self.assertEqual(channel_id, b'1234') 31 | self.assertEqual(rest.tobytes(), b'5678') 32 | 33 | def test_encode(self): 34 | channel_id = aioppspp.channel_ids.new() 35 | data = aioppspp.channel_ids.encode(channel_id) 36 | self.assertEqual(data, bytes(channel_id)) 37 | 38 | def test_new(self): 39 | channel_id = aioppspp.channel_ids.new() 40 | self.assertIsInstance(channel_id, aioppspp.channel_ids.ChannelID) 41 | 42 | def test_bad_channel_id(self): 43 | with self.assertRaises(TypeError): 44 | aioppspp.channel_ids.ChannelID(42) 45 | with self.assertRaises(ValueError): 46 | aioppspp.channel_ids.ChannelID(b'') 47 | with self.assertRaises(ValueError): 48 | aioppspp.channel_ids.ChannelID(b'.....') 49 | with self.assertRaises(ValueError): 50 | aioppspp.channel_ids.ChannelID(b'...') 51 | 52 | def test_zero_channel_id(self): 53 | self.assertIsInstance(aioppspp.channel_ids.ZeroChannelID, 54 | aioppspp.channel_ids.ChannelID) 55 | self.assertEqual(aioppspp.channel_ids.ZeroChannelID, b'\x00' * 4) 56 | 57 | @hypothesis.given(st.channel_id()) 58 | def test_strategy(self, channel_id0): 59 | data = aioppspp.channel_ids.encode(channel_id0) 60 | channel_id1, _ = aioppspp.channel_ids.decode(memoryview(data)) 61 | self.assertEqual(channel_id0, channel_id1) 62 | -------------------------------------------------------------------------------- /aioppspp/channel_ids.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import os 17 | 18 | from .constants import ( 19 | DWORD, 20 | ) 21 | 22 | __all__ = ( 23 | 'ChannelID', 24 | 'ZeroChannelID', 25 | 'decode', 26 | 'encode', 27 | 'new', 28 | ) 29 | 30 | 31 | class ChannelID(bytes): 32 | """Unique, randomly chosen identifier for a channel, local to each peer. 33 | 34 | .. seealso:: 35 | 36 | - :rfc:`7574#section-1.3` (:rfc:`page 8 <7574#page-8>`) 37 | - :rfc:`7574#section-12.1` (:rfc:`page 69 <7574#page-69>`) 38 | """ 39 | __slots__ = () 40 | 41 | def __new__(cls, data): 42 | if not isinstance(data, (bytes, bytearray, memoryview)): 43 | raise TypeError('bytes expected, got {!r}'.format(data)) 44 | if len(data) != DWORD: 45 | raise ValueError('channel id must be 4 bytes sized') 46 | return super().__new__(cls, bytes(data)) 47 | 48 | 49 | #: All-zero channel ID is used for handshake procedure. If datagram is sent 50 | #: by the initiating peer, destination channel ID MUST be all-zero one. If 51 | #: peer want to explicitly close channel, it SHOULD send a handshake with 52 | #: all-zero source channel ID. 53 | #: 54 | #: .. seealso:: 55 | #: 56 | #: - :rfc:`7574#section-8.4` 57 | #: 58 | ZeroChannelID = ChannelID(b'\x00' * DWORD) 59 | 60 | 61 | def decode(data): 62 | """Decodes channel ID from PPSPP datagram. 63 | 64 | :param memoryview data: Binary data 65 | :returns: :class:`ChannelID` instance and the remaining data 66 | :rtype: tuple 67 | """ 68 | channel_id, rest = data[:DWORD], data[DWORD:] 69 | return ChannelID(channel_id), rest 70 | 71 | 72 | def encode(channel_id): 73 | """Encodes channel ID into bytes. 74 | 75 | :param ChannelID channel_id: Channel ID 76 | :rtype: bytes 77 | """ 78 | return bytes(ChannelID(channel_id)) 79 | 80 | 81 | def new(): 82 | """Returns new random channel ID. 83 | 84 | :rtype: :class:`ChannelID` 85 | """ 86 | return ChannelID(os.urandom(DWORD)) 87 | -------------------------------------------------------------------------------- /aioppspp/udp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import asyncio 17 | import functools 18 | import socket 19 | 20 | from .connection import ( 21 | Address, 22 | ) 23 | from .connector import ( 24 | BaseProtocol, 25 | BaseConnector, 26 | ) 27 | 28 | __all__ = ( 29 | 'Connector', 30 | 'Protocol', 31 | ) 32 | 33 | 34 | class Protocol(asyncio.DatagramProtocol, BaseProtocol): 35 | """UDP protocol implementation.""" 36 | 37 | def __init__(self, *, loop=None): 38 | super().__init__(loop=loop) 39 | self._buffer = asyncio.Queue() 40 | 41 | def datagram_received(self, data, addr): 42 | """Called when some datagram is received.""" 43 | self._buffer.put_nowait((data, Address(*addr))) 44 | 45 | async def recv(self): 46 | """Receives datagram from remote Peer. 47 | 48 | :returns: Pair of received data and remote peer address 49 | :rtype: tuple 50 | 51 | This method is :term:`awaitable`. 52 | """ 53 | return await self._buffer.get() 54 | 55 | async def send(self, data, remote_address=None): 56 | """Sends datagram to remote peer. 57 | 58 | :param bytes data: Data to send 59 | :param aioppspp.connection.Address remote_address: Recipient address 60 | 61 | This method is :term:`awaitable`. 62 | """ 63 | self._transport.sendto(data, remote_address) 64 | 65 | 66 | class Connector(BaseConnector): 67 | """UDP connector.""" 68 | 69 | #: UDP protocol implementation 70 | protocol_class = Protocol 71 | 72 | def protocol_factory(self) -> functools.partial: 73 | """Produces factory for protocol implementation.""" 74 | return functools.partial(self.protocol_class, loop=self._loop) 75 | 76 | async def create_endpoint(self, local_address=None, remote_address=None, *, 77 | family=socket.AF_INET): 78 | """Creates datagram endpoint. 79 | 80 | :param aioppspp.connection.Address local_address: Local peer address 81 | :param aioppspp.connection.Address remote_address: Remote peer address 82 | :param socket.AddressFamily family: Socket address family 83 | 84 | This method is :term:`awaitable`. 85 | """ 86 | _, protocol = await self._loop.create_datagram_endpoint( 87 | self.protocol_factory(), 88 | family=family, 89 | local_addr=local_address, 90 | remote_addr=remote_address) 91 | return protocol 92 | -------------------------------------------------------------------------------- /aioppspp/messages/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import abc 17 | from itertools import ( 18 | chain, 19 | ) 20 | 21 | from . import handshake 22 | from .handshake import ( 23 | Handshake, 24 | ) 25 | from .types import ( 26 | MessageType, 27 | ) 28 | from ..constants import ( 29 | BYTE, 30 | ) 31 | 32 | __all__ = ( 33 | 'Message', 34 | 'MessageType', 35 | 'decode', 36 | 'encode', 37 | ) 38 | 39 | 40 | class Message(tuple, metaclass=abc.ABCMeta): 41 | """The basic unit of PPSPP communication. A message will have 42 | different representations on the wire depending on the transport 43 | protocol used. Messages are typically multiplexed into a 44 | datagram for transmission. 45 | 46 | This is an abstract base class for all PPSPP messages. 47 | 48 | .. seealso:: 49 | 50 | - :rfc:`7574#section-1.3` (:rfc:`page 6 <7574#page-6>`) 51 | """ 52 | __slots__ = () 53 | 54 | @property 55 | @abc.abstractmethod 56 | def type(self): 57 | """Should return message type enumeration object.""" 58 | raise NotImplementedError 59 | 60 | 61 | Message.register(Handshake) 62 | 63 | 64 | def decode(data, *, handlers=None): 65 | """Decodes binary data into list of messages. 66 | 67 | :param memoryview data: Binary data 68 | :param dict handlers: Decode handlers mapping 69 | :returns: Tuple of :class:`Message` 70 | :rtype: tuple 71 | """ 72 | messages = [] 73 | while data: 74 | message, data = decode_message(data, handlers=handlers) 75 | messages.append(message) 76 | return tuple(messages) 77 | 78 | 79 | def decode_message(data, *, handlers=None): 80 | handlers = handlers or decode_message_handlers() 81 | return handlers[MessageType(data[0])](data[BYTE:]) 82 | 83 | 84 | def encode(messages, *, handlers=None): 85 | """Encodes list of messages into bytes. 86 | 87 | :param tuple messages: List of :class:`Message` 88 | :param dict handlers: Encode handlers mapping 89 | :rtype: bytearray 90 | """ 91 | data = bytearray() 92 | for message in messages: 93 | data.extend(encode_message(message, handlers=handlers)) 94 | return data 95 | 96 | 97 | def encode_message(message, *, handlers=None): 98 | handlers = handlers or encode_message_handlers() 99 | return bytearray(chain.from_iterable([ 100 | [message.type.value], 101 | handlers[message.type](message), 102 | ])) 103 | 104 | 105 | def decode_message_handlers(): 106 | return { 107 | MessageType.HANDSHAKE: handshake.decode 108 | } 109 | 110 | 111 | def encode_message_handlers(): 112 | return { 113 | MessageType.HANDSHAKE: handshake.encode 114 | } 115 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | # use this file except in compliance with the License. You may obtain a copy of 3 | # the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations under 11 | # the License. 12 | # 13 | 14 | PROJECT := aioppspp 15 | COVERAGE := coverage 16 | FLAKE8 := flake8 17 | GIT := git 18 | PIP := pip 19 | PYTHON := python3.5 20 | SPHINX := sphinx-build 21 | 22 | DISTCHECK_PATH := $(PROJECT)-`$(GIT) describe --always --tags` 23 | VENV_PATH := venv 24 | 25 | ENSURECMD=which $(1) > /dev/null 2>&1 || (echo "*** Make sure that $(1) is installed and on your path" && exit 1) 26 | 27 | 28 | .PHONY: all 29 | all: help 30 | 31 | 32 | .PHONY: check 33 | # target: check - Runs tests 34 | check: $(PYTHON) 35 | @$(PYTHON) setup.py test 36 | 37 | 38 | .PHONY: check-all 39 | # target: check-all - Runs lint checks, tests and generates coverage report 40 | check-all: lint check-cov 41 | 42 | 43 | .PHONY: check-cov 44 | # target: check-cov - Runs tests and generates coverage report 45 | check-cov: coverage-run coverage-report 46 | 47 | 48 | .PHONY: clean 49 | # target: clean - Removes intermediate and generated files 50 | clean: $(PYTHON) 51 | @find $(PROJECT) -type f -name '*.py[co]' -delete 52 | @find $(PROJECT) -type d -name '__pycache__' -delete 53 | @rm -f .coverage 54 | @rm -rf {build,cover,coverage} 55 | @rm -rf "$(PROJECT).egg-info" 56 | @make -C docs clean 57 | @$(PYTHON) setup.py clean 58 | 59 | 60 | coverage-run: $(COVERAGE) 61 | @$(COVERAGE) run setup.py test -q 62 | coverage-report: $(COVERAGE) 63 | @$(COVERAGE) report -m --fail-under=100 --show-missing 64 | 65 | 66 | .PHONY: dev 67 | # target: dev - Installs project for further developing 68 | dev: $(PYTHON) $(PIP) 69 | @$(PIP) install -e .[docs,dev] 70 | 71 | 72 | .PHONY: distcheck 73 | # target: distcheck - Checks if project is ready to ship 74 | distcheck: check-all distclean distcheck-35 75 | distcheck-35: $(PYTHON) $(GIT) 76 | @$(PYTHON) -m venv $(DISTCHECK_PATH)/venv-3.5 77 | @$(DISTCHECK_PATH)/venv-3.5/bin/python setup.py install 78 | @$(DISTCHECK_PATH)/venv-3.5/bin/python setup.py test 79 | distclean: 80 | @rm -rf $(DIST_PATH) 81 | 82 | 83 | .PHONY: docs 84 | # target: docs - Builds Sphinx html docs 85 | docs: $(SPHINX) 86 | @$(SPHINX) -b html -d docs/_build/doctrees docs/ docs/_build/html 87 | @$(SPHINX) -b doctest -d docs/_build/doctrees docs/ docs/_build/doctest 88 | 89 | 90 | flake: $(FLAKE8) 91 | @$(FLAKE8) --statistics \ 92 | --ignore=E501,F403 \ 93 | $(PROJECT) 94 | 95 | 96 | .PHONY: help 97 | # target: help - Prints this help 98 | help: 99 | @egrep "^# target: " Makefile \ 100 | | sed -e 's/^# target: //g' \ 101 | | sort -sh \ 102 | | awk '{printf(" %-10s", $$1); $$1=$$2=""; print "-" $$0}' 103 | 104 | 105 | .PHONY: install 106 | # target: install - Installs package 107 | install: $(PYTHON) 108 | @$(PYTHON) setup.py install 109 | 110 | 111 | .PHONY: lint 112 | # target: lint - Runs linter checks 113 | lint: flake 114 | 115 | 116 | .PHONY: purge 117 | # target: purge - Removes all unversioned files and resets repository 118 | purge: $(GIT) 119 | @$(GIT) reset --hard HEAD 120 | @$(GIT) clean -xdff 121 | 122 | 123 | .PHONY: pypi 124 | # target: pypi - Uploads package to PyPI 125 | pypi: $(PYTHON) 126 | @$(PYTHON) setup.py sdist register upload 127 | 128 | 129 | .PHONY: venv 130 | # target: venv - Creates virtual environment 131 | venv: $(PYTHON) 132 | @$(PYTHON) -m venv $(VENV_PATH) 133 | 134 | 135 | $(COVERAGE): 136 | @$(call ENSURECMD,$@) 137 | $(GIT): 138 | @$(call ENSURECMD,$@) 139 | $(FLAKE8): 140 | @$(call ENSURECMD,$@) 141 | $(PIP): 142 | @$(call ENSURECMD,$@) 143 | $(PYTHON): 144 | @$(call ENSURECMD,$@) 145 | $(SPHINX): 146 | @$(call ENSURECMD,$@) 147 | $(DIST_PATH): 148 | @mkdir -p $@ 149 | -------------------------------------------------------------------------------- /aioppspp/tests/strategies.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | from hypothesis.strategies import ( 17 | binary, 18 | booleans, 19 | builds, 20 | composite, 21 | integers, 22 | just, 23 | none, 24 | sampled_from, 25 | tuples, 26 | ) 27 | 28 | import aioppspp.channel_ids 29 | import aioppspp.messages 30 | import aioppspp.messages.protocol_options as protocol_options 31 | from aioppspp.constants import ( 32 | BYTE, 33 | WORD, 34 | DWORD, 35 | ) 36 | from aioppspp.messages import ( 37 | MessageType, 38 | ) 39 | 40 | 41 | def maybe(strategy): 42 | return strategy | none() 43 | 44 | 45 | def byte(): 46 | return binary(min_size=BYTE, max_size=BYTE) 47 | 48 | 49 | def word(): 50 | return binary(min_size=WORD, max_size=WORD) 51 | 52 | 53 | def dword(): 54 | return binary(min_size=DWORD, max_size=DWORD) 55 | 56 | 57 | def sampled_from_enum(enum): 58 | return sampled_from([item.value for item in enum]) 59 | 60 | 61 | def channel_id(): 62 | return builds(aioppspp.channel_ids.ChannelID, dword()) 63 | 64 | 65 | def handshake(): 66 | return builds(aioppspp.messages.handshake.new, 67 | channel_id(), 68 | generic_protocol_options()) 69 | 70 | 71 | @composite 72 | def generic_protocol_options(draw): 73 | cam = draw(maybe(option_cam())) 74 | args = map(draw, ( 75 | option_version(), 76 | option_minimum_version(), 77 | option_swarm_id(), 78 | option_cipm(), 79 | option_mhtf(), 80 | option_lsa(), 81 | just(cam), 82 | none() if cam is None else option_ldw(cam), 83 | option_supported_messages(), 84 | option_chunk_size(), 85 | )) 86 | return protocol_options.ProtocolOptions(*args) 87 | 88 | 89 | def option_version(): 90 | return sampled_from_enum(protocol_options.Version) 91 | 92 | 93 | def option_minimum_version(): 94 | return sampled_from_enum(protocol_options.Version) 95 | 96 | 97 | def option_swarm_id(): 98 | return word() 99 | 100 | 101 | @composite 102 | def raw_swarm_id(draw): 103 | length = draw(integers(min_value=1, max_value=2 ** 8 * WORD)) 104 | swarm_id = draw(binary(min_size=length, max_size=length)) 105 | return length.to_bytes(WORD, 'big') + swarm_id 106 | 107 | 108 | def option_cipm(): 109 | return sampled_from_enum(protocol_options.CIPM) 110 | 111 | 112 | def option_mhtf(): 113 | return sampled_from_enum(protocol_options.MHTF) 114 | 115 | 116 | def option_lsa(): 117 | return sampled_from_enum(protocol_options.LSA) 118 | 119 | 120 | def option_cam(): 121 | return sampled_from_enum(protocol_options.CAM) 122 | 123 | 124 | @composite 125 | def raw_option_ldw(draw): 126 | value = draw(sampled_from_enum(protocol_options.CAM)) 127 | method = protocol_options.CAM(value) 128 | window_size = protocol_options.LiveDiscardWindowSize[method.name].value 129 | ldw = draw(binary(min_size=window_size, max_size=window_size)) 130 | return method, ldw 131 | 132 | 133 | def option_ldw(cam): 134 | method = protocol_options.CAM(cam) 135 | window_size = protocol_options.LiveDiscardWindowSize[method.name].value 136 | return integers(min_value=window_size, max_value=window_size) 137 | 138 | 139 | def option_supported_messages(): 140 | return tuples(*[ 141 | tuples(just(msgtype), booleans()) for msgtype in MessageType 142 | ]).filter(lambda i: i[1]).map(lambda i: {v for v, _ in i}) 143 | 144 | 145 | def option_chunk_size(): 146 | return integers(min_value=1, max_value=2 ** 8 * DWORD) 147 | -------------------------------------------------------------------------------- /aioppspp/messages/handshake.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | from collections import ( 17 | namedtuple, 18 | ) 19 | from itertools import ( 20 | chain, 21 | ) 22 | 23 | from . import protocol_options 24 | from .. import channel_ids 25 | from .protocol_options import ( 26 | ProtocolOptions, 27 | ) 28 | from .types import ( 29 | MessageType, 30 | ) 31 | 32 | __all__ = ( 33 | 'Handshake', 34 | 'decode', 35 | 'encode', 36 | 'new', 37 | ) 38 | 39 | 40 | class Handshake(namedtuple('Handshake', ( 41 | 'type', 42 | 'source_channel_id', 43 | 'protocol_options' 44 | ))): 45 | """To establish communication, peers must first exchange HANDSHAKE messages 46 | by means of a handshake procedure. These messages contains all important 47 | metadata about the Swarm, transfer options and integrity checks. 48 | 49 | The payload of the HANDSHAKE message contains a sequence of protocol 50 | options. See :class:`aioppspp.messages.protocol_options.ProtocolOptions` 51 | for more info. 52 | 53 | .. seealso:: 54 | 55 | - :rfc:`7574#section-3.1` 56 | - :rfc:`7574#section-8.4` 57 | """ 58 | __slots__ = () 59 | 60 | def __new__(cls, type, source_channel_id, protocol_options): 61 | if not isinstance(type, MessageType): 62 | type = MessageType(type) 63 | if type is not MessageType.HANDSHAKE: 64 | raise ValueError('bad message type {}'.format(type)) 65 | if not isinstance(source_channel_id, channel_ids.ChannelID): 66 | source_channel_id = channel_ids.ChannelID(source_channel_id) 67 | if not isinstance(protocol_options, ProtocolOptions): 68 | protocol_options = ProtocolOptions(**protocol_options) 69 | return super().__new__(cls, type, source_channel_id, protocol_options) 70 | 71 | 72 | def decode(data): 73 | """Decodes HANDSHAKE message from bytes. 74 | 75 | :param memoryview data: Binary data 76 | :returns: Tuple of :class:`Handshake` message and the rest of the data 77 | :rtype: tuple 78 | """ 79 | # 8.4. HANDSHAKE 80 | # 81 | # 0 1 2 3 82 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 83 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 84 | # |0 0 0 0 0 0 0 0| Source Channel ID (32) | 85 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 86 | # | | ~ 87 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 88 | # | | 89 | # ~ Protocol Options ~ 90 | # | | 91 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 92 | # 93 | channel_id, rest = channel_ids.decode(data) 94 | options, rest = protocol_options.decode(rest) 95 | handshake = Handshake(MessageType.HANDSHAKE, channel_id, options) 96 | return handshake, rest 97 | 98 | 99 | def encode(message): 100 | """Encodes HANDSHAKE message to bytes. 101 | 102 | :param Handshake message: Handshake message instance 103 | :rtype: bytearray 104 | """ 105 | return bytearray(chain.from_iterable([ 106 | channel_ids.encode(message.source_channel_id), 107 | protocol_options.encode(message.protocol_options) 108 | ])) 109 | 110 | 111 | def new(source_channel_id, protocol_options): 112 | """Creates new Handshake message. 113 | 114 | :param aioppspp.channel_ids.ChannelID source_channel_id: Source channel ID 115 | :param aioppspp.messages.protocol_options.ProtocolOptions protocol_options: 116 | Protocol options 117 | :rtype: :class:`Handshake` 118 | """ 119 | return Handshake(MessageType.HANDSHAKE, 120 | source_channel_id, protocol_options) 121 | -------------------------------------------------------------------------------- /aioppspp/connection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import asyncio 17 | import ipaddress 18 | import sys 19 | import traceback 20 | import warnings 21 | from collections import namedtuple 22 | 23 | __all__ = ( 24 | 'Address', 25 | 'Connection', 26 | ) 27 | 28 | 29 | class Address(namedtuple('Address', ('ip', 'port'))): 30 | """Represents Peer address information as IP address (:class:`str`) 31 | and port (:class:`int`) as :class:`tuple`. 32 | """ 33 | __slots__ = () 34 | 35 | def __new__(cls, ip, port): 36 | ip = ipaddress.ip_address(ip) 37 | if not isinstance(port, int): 38 | raise TypeError('port must be an integer') 39 | if not (0 <= port <= 2 ** 16 - 1): 40 | raise ValueError('port must be in range [0, 65535]') 41 | return super().__new__(cls, str(ip), port) 42 | 43 | def __str__(self): 44 | return '{}:{}'.format(self.ip, self.port) 45 | 46 | 47 | class Connection(object): 48 | """Connection object is an interface to the underlying protocol 49 | implementation. 50 | 51 | You should use :class:`aioppspp.connector.BaseConnector` instance to create 52 | connections instead of doing that directly. 53 | """ 54 | 55 | _source_traceback = None 56 | _protocol = None 57 | 58 | def __init__(self, connector, key, protocol, *, loop=None): 59 | if loop is None: 60 | loop = asyncio.get_event_loop() 61 | if loop.get_debug(): # pragma: no cover 62 | self._source_traceback = traceback.extract_stack(sys._getframe(1)) 63 | 64 | self._connector = connector 65 | self._key = key 66 | self._loop = loop 67 | self._protocol = protocol 68 | 69 | def __del__(self): 70 | if self.closed: 71 | return 72 | 73 | if self._loop.is_closed(): 74 | return 75 | 76 | self._connector.close_connection(self) 77 | 78 | warnings.warn('Unclosed connection {!r}'.format(self), ResourceWarning) 79 | context = {'connection': self, 80 | 'message': 'Unclosed connection'} 81 | 82 | if self._source_traceback is not None: # pragma: no cover 83 | context['source_traceback'] = self._source_traceback 84 | 85 | self._loop.call_exception_handler(context) 86 | 87 | def __repr__(self): 88 | idx = id(self) 89 | closed_tag = ' [closed]' if self.closed else '' 90 | local_addr, remote_addr = self.local_address, self.remote_address 91 | return '<{}@{:x}: {} -> {}{}>'.format( 92 | self.__class__.__name__, idx, local_addr, remote_addr, closed_tag) 93 | 94 | @property 95 | def key(self): 96 | return self._key 97 | 98 | @property 99 | def closed(self): 100 | """Returns :const:`True` when connection is closed.""" 101 | return self._protocol is None or self._protocol.closed 102 | 103 | @property 104 | def local_address(self): 105 | """Returns local address information.""" 106 | return self._protocol.local_address 107 | 108 | @property 109 | def remote_address(self): 110 | """Returns remote peer address information. 111 | May return :const:`None` in case if connection is not bound with 112 | any remote peer (for instance, it in listening mode). 113 | """ 114 | return self._protocol.remote_address 115 | 116 | @property 117 | def protocol(self): 118 | """Returns the underlying protocol instance.""" 119 | return self._protocol 120 | 121 | @property 122 | def loop(self): 123 | return self._loop 124 | 125 | def close(self): 126 | """Closes connection.""" 127 | if self.closed: 128 | return 129 | self._connector.close_connection(self) 130 | self._protocol = None 131 | 132 | def release(self): 133 | """Releases connection and returns it back to the pool.""" 134 | if self.closed: 135 | return 136 | self._connector.release_connection(self) 137 | self._protocol = None 138 | 139 | async def recv(self): 140 | """Receives an incoming data from remote peer. 141 | Returned value is depended on the underlying protocol implementation. 142 | 143 | This method is :term:`awaitable`. 144 | """ 145 | if self.closed: 146 | raise ConnectionError('not connected') 147 | return await self._protocol.recv() 148 | 149 | async def send(self, data, remote_address=None): 150 | """Sends data to connected peer. 151 | 152 | Data type is depended on the underlying protocol implementation. 153 | If connection is not strictly bound with some specific remote peer, 154 | the remote address must be provided in order to know where to send 155 | the data. 156 | 157 | This method is :term:`awaitable`. 158 | """ 159 | if self.closed: 160 | raise ConnectionError('not connected') 161 | return await self._protocol.send(data, remote_address) 162 | -------------------------------------------------------------------------------- /aioppspp/tests/test_connection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import gc 17 | import unittest.mock 18 | 19 | import aioppspp.connection 20 | import aioppspp.tests.utils 21 | 22 | from .test_connector import Connector, Protocol 23 | 24 | 25 | class TestConnection(aioppspp.tests.utils.TestCase): 26 | def new_connector(self): 27 | return Connector(connection_class=aioppspp.connection.Connection, 28 | loop=self.loop) 29 | 30 | def new_connection(self): 31 | return aioppspp.connection.Connection( 32 | self.new_connector(), 33 | protocol=Protocol(loop=self.loop), 34 | key=aioppspp.connection.Address('0.0.0.0', 0), 35 | loop=self.loop) 36 | 37 | def test_loop(self): 38 | connection = self.new_connection() 39 | self.assertIs(connection.loop, self.loop) 40 | 41 | def test_default_loop(self): 42 | connection = aioppspp.connection.Connection( 43 | self.new_connector(), 44 | protocol=Protocol(loop=self.loop), 45 | key=aioppspp.connection.Address('0.0.0.0', 0)) 46 | self.assertIs(connection.loop, self.loop) 47 | 48 | def test_del(self): 49 | connection = self.new_connection() 50 | connection.protocol.connection_made(unittest.mock.Mock()) 51 | exc_handler = unittest.mock.Mock() 52 | self.loop.set_exception_handler(exc_handler) 53 | 54 | with self.assertWarns(ResourceWarning): 55 | del connection 56 | gc.collect() 57 | 58 | msg = {'connection': unittest.mock.ANY, 59 | 'message': 'Unclosed connection'} 60 | exc_handler.assert_called_with(self.loop, msg) 61 | 62 | def test_del_with_closed_loop(self): 63 | connection = self.new_connection() 64 | connection.protocol.connection_made(unittest.mock.Mock()) 65 | exc_handler = unittest.mock.Mock() 66 | self.loop.set_exception_handler(exc_handler) 67 | self.loop.close() 68 | 69 | with self.assertRaises(AssertionError): 70 | with self.assertWarns(ResourceWarning): 71 | del connection 72 | gc.collect() 73 | 74 | self.assertFalse(exc_handler.called) 75 | 76 | async def test_key(self): 77 | connector = self.new_connector() 78 | address = aioppspp.connection.Address('0.0.0.0', 0) 79 | connection = await connector.connect(address) 80 | connection.protocol.connection_made(unittest.mock.Mock()) 81 | self.assertEqual(connection.key, address) 82 | 83 | def test_not_connected(self): 84 | connection = self.new_connection() 85 | self.assertTrue(connection.closed) 86 | 87 | def test_connected(self): 88 | connection = self.new_connection() 89 | connection.protocol.connection_made(unittest.mock.Mock()) 90 | self.assertFalse(connection.closed) 91 | connection.close() 92 | 93 | def test_repr_closed(self): 94 | connection = self.new_connection() 95 | self.assertEqual( 96 | ' None [closed]>'.format(id(connection)), 97 | repr(connection)) 98 | 99 | def test_repr_connected(self): 100 | connection = self.new_connection() 101 | transport = unittest.mock.Mock() 102 | transport._sock.getsockname.return_value = ('127.0.0.1', 4242) 103 | transport._sock.getpeername.return_value = ('10.5.0.45', 4242) 104 | connection.protocol.connection_made(transport) 105 | self.assertEqual(' 10.5.0.45:4242>' 106 | ''.format(id(connection)), 107 | repr(connection)) 108 | connection.close() 109 | 110 | async def test_close(self): 111 | connector = self.new_connector() 112 | address = aioppspp.connection.Address('0.0.0.0', 0) 113 | connection = await connector.connect(address) 114 | connection.protocol.connection_made(unittest.mock.Mock()) 115 | connector.close_connection = unittest.mock.Mock( 116 | wraps=connector.close_connection) 117 | protocol_close = connection.protocol.close = unittest.mock.Mock( 118 | wraps=connection.protocol.close) 119 | connection.close() 120 | self.assertTrue(connector.close_connection.called) 121 | self.assertTrue(protocol_close.called) 122 | self.assertIsNone(connection.protocol) 123 | self.assertTrue(connection.closed) 124 | 125 | async def test_close_closed(self): 126 | connector = self.new_connector() 127 | address = aioppspp.connection.Address('0.0.0.0', 0) 128 | connection = await connector.connect(address) 129 | connection.protocol.connection_made(unittest.mock.Mock()) 130 | connection.close() 131 | connector.close_connection = unittest.mock.Mock() 132 | connection.close() 133 | self.assertFalse(connector.close_connection.called) 134 | 135 | async def test_release(self): 136 | connector = self.new_connector() 137 | address = aioppspp.connection.Address('0.0.0.0', 0) 138 | connection = await connector.connect(address) 139 | connection.protocol.connection_made(unittest.mock.Mock()) 140 | connector.release_connection = unittest.mock.Mock() 141 | connection.release() 142 | self.assertTrue(connector.release_connection.called) 143 | self.assertIsNone(connection.protocol) 144 | self.assertTrue(connection.closed) 145 | 146 | async def test_release_closed(self): 147 | connector = self.new_connector() 148 | address = aioppspp.connection.Address('0.0.0.0', 0) 149 | connection = await connector.connect(address) 150 | connection.protocol.connection_made(unittest.mock.Mock()) 151 | connection.release() 152 | connector.release_connection = unittest.mock.Mock() 153 | connection.release() 154 | self.assertFalse(connector.release_connection.called) 155 | connector.close() 156 | 157 | async def test_recv_not_connected(self): 158 | connector = self.new_connector() 159 | address = aioppspp.connection.Address('0.0.0.0', 0) 160 | connection = await connector.connect(address) 161 | with self.assertRaises(ConnectionError): 162 | await connection.recv() 163 | 164 | async def test_recv(self): 165 | connector = self.new_connector() 166 | address = aioppspp.connection.Address('0.0.0.0', 0) 167 | connection = await connector.connect(address) 168 | connection.protocol.connection_made(unittest.mock.Mock()) 169 | connection.protocol.recv = unittest.mock.Mock( 170 | wraps=connection.protocol.recv) 171 | await connection.recv() 172 | self.assertTrue(connection.protocol.recv.called) 173 | connection.close() 174 | 175 | async def test_send_not_connected(self): 176 | connector = self.new_connector() 177 | address = aioppspp.connection.Address('0.0.0.0', 0) 178 | connection = await connector.connect(address) 179 | with self.assertRaises(ConnectionError): 180 | await connection.send(b'...') 181 | 182 | async def test_send(self): 183 | connector = self.new_connector() 184 | address = aioppspp.connection.Address('0.0.0.0', 0) 185 | connection = await connector.connect(address) 186 | connection.protocol.connection_made(unittest.mock.Mock()) 187 | connection.protocol.send = unittest.mock.Mock( 188 | wraps=connection.protocol.send) 189 | await connection.send(b'...', None) 190 | self.assertTrue(connection.protocol.send.called) 191 | connection.close() 192 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | # use this file except in compliance with the License. You may obtain a copy of 3 | # the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations under 11 | # the License. 12 | # 13 | 14 | # Makefile for Sphinx documentation 15 | # 16 | 17 | # You can set these variables from the command line. 18 | SPHINXOPTS = 19 | SPHINXBUILD = sphinx-build 20 | PAPER = 21 | BUILDDIR = _build 22 | 23 | # User-friendly check for sphinx-build 24 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 25 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 26 | endif 27 | 28 | # Internal variables. 29 | PAPEROPT_a4 = -D latex_paper_size=a4 30 | PAPEROPT_letter = -D latex_paper_size=letter 31 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 32 | # the i18n builder cannot share the environment and doctrees with the others 33 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 34 | 35 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 36 | 37 | help: 38 | @echo "Please use \`make ' where is one of" 39 | @echo " html to make standalone HTML files" 40 | @echo " dirhtml to make HTML files named index.html in directories" 41 | @echo " singlehtml to make a single large HTML file" 42 | @echo " pickle to make pickle files" 43 | @echo " json to make JSON files" 44 | @echo " htmlhelp to make HTML files and a HTML help project" 45 | @echo " qthelp to make HTML files and a qthelp project" 46 | @echo " applehelp to make an Apple Help Book" 47 | @echo " devhelp to make HTML files and a Devhelp project" 48 | @echo " epub to make an epub" 49 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 50 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 51 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 52 | @echo " text to make text files" 53 | @echo " man to make manual pages" 54 | @echo " texinfo to make Texinfo files" 55 | @echo " info to make Texinfo files and run them through makeinfo" 56 | @echo " gettext to make PO message catalogs" 57 | @echo " changes to make an overview of all changed/added/deprecated items" 58 | @echo " xml to make Docutils-native XML files" 59 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 60 | @echo " linkcheck to check all external links for integrity" 61 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 62 | @echo " coverage to run coverage check of the documentation (if enabled)" 63 | 64 | clean: 65 | rm -rf $(BUILDDIR)/* 66 | 67 | html: 68 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 69 | @echo 70 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 71 | 72 | dirhtml: 73 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 74 | @echo 75 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 76 | 77 | singlehtml: 78 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 79 | @echo 80 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 81 | 82 | pickle: 83 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 84 | @echo 85 | @echo "Build finished; now you can process the pickle files." 86 | 87 | json: 88 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 89 | @echo 90 | @echo "Build finished; now you can process the JSON files." 91 | 92 | htmlhelp: 93 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 94 | @echo 95 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 96 | ".hhp project file in $(BUILDDIR)/htmlhelp." 97 | 98 | qthelp: 99 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 100 | @echo 101 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 102 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 103 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/aioppspp.qhcp" 104 | @echo "To view the help file:" 105 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/aioppspp.qhc" 106 | 107 | applehelp: 108 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 109 | @echo 110 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 111 | @echo "N.B. You won't be able to view it unless you put it in" \ 112 | "~/Library/Documentation/Help or install it in your application" \ 113 | "bundle." 114 | 115 | devhelp: 116 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 117 | @echo 118 | @echo "Build finished." 119 | @echo "To view the help file:" 120 | @echo "# mkdir -p $$HOME/.local/share/devhelp/aioppspp" 121 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/aioppspp" 122 | @echo "# devhelp" 123 | 124 | epub: 125 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 126 | @echo 127 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 128 | 129 | latex: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo 132 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 133 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 134 | "(use \`make latexpdf' here to do that automatically)." 135 | 136 | latexpdf: 137 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 138 | @echo "Running LaTeX files through pdflatex..." 139 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 140 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 141 | 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | text: 149 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 150 | @echo 151 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 152 | 153 | man: 154 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 155 | @echo 156 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 157 | 158 | texinfo: 159 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 160 | @echo 161 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 162 | @echo "Run \`make' in that directory to run these through makeinfo" \ 163 | "(use \`make info' here to do that automatically)." 164 | 165 | info: 166 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 167 | @echo "Running Texinfo files through makeinfo..." 168 | make -C $(BUILDDIR)/texinfo info 169 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 170 | 171 | gettext: 172 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 173 | @echo 174 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 175 | 176 | changes: 177 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 178 | @echo 179 | @echo "The overview file is in $(BUILDDIR)/changes." 180 | 181 | linkcheck: 182 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 183 | @echo 184 | @echo "Link check complete; look for any errors in the above output " \ 185 | "or in $(BUILDDIR)/linkcheck/output.txt." 186 | 187 | doctest: 188 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 189 | @echo "Testing of doctests in the sources finished, look at the " \ 190 | "results in $(BUILDDIR)/doctest/output.txt." 191 | 192 | coverage: 193 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 194 | @echo "Testing of coverage in the sources finished, look at the " \ 195 | "results in $(BUILDDIR)/coverage/python.txt." 196 | 197 | xml: 198 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 199 | @echo 200 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 201 | 202 | pseudoxml: 203 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 204 | @echo 205 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 206 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | REM Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | REM use this file except in compliance with the License. You may obtain a copy of 3 | REM the License at 4 | REM 5 | REM http://www.apache.org/licenses/LICENSE-2.0 6 | REM 7 | REM Unless required by applicable law or agreed to in writing, software 8 | REM distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | REM WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | REM License for the specific language governing permissions and limitations under 11 | REM the License. 12 | REM 13 | 14 | 15 | @ECHO OFF 16 | 17 | REM Command file for Sphinx documentation 18 | 19 | if "%SPHINXBUILD%" == "" ( 20 | set SPHINXBUILD=sphinx-build 21 | ) 22 | set BUILDDIR=_build 23 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 24 | set I18NSPHINXOPTS=%SPHINXOPTS% . 25 | if NOT "%PAPER%" == "" ( 26 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 27 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 28 | ) 29 | 30 | if "%1" == "" goto help 31 | 32 | if "%1" == "help" ( 33 | :help 34 | echo.Please use `make ^` where ^ is one of 35 | echo. html to make standalone HTML files 36 | echo. dirhtml to make HTML files named index.html in directories 37 | echo. singlehtml to make a single large HTML file 38 | echo. pickle to make pickle files 39 | echo. json to make JSON files 40 | echo. htmlhelp to make HTML files and a HTML help project 41 | echo. qthelp to make HTML files and a qthelp project 42 | echo. devhelp to make HTML files and a Devhelp project 43 | echo. epub to make an epub 44 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 45 | echo. text to make text files 46 | echo. man to make manual pages 47 | echo. texinfo to make Texinfo files 48 | echo. gettext to make PO message catalogs 49 | echo. changes to make an overview over all changed/added/deprecated items 50 | echo. xml to make Docutils-native XML files 51 | echo. pseudoxml to make pseudoxml-XML files for display purposes 52 | echo. linkcheck to check all external links for integrity 53 | echo. doctest to run all doctests embedded in the documentation if enabled 54 | echo. coverage to run coverage check of the documentation if enabled 55 | goto end 56 | ) 57 | 58 | if "%1" == "clean" ( 59 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 60 | del /q /s %BUILDDIR%\* 61 | goto end 62 | ) 63 | 64 | 65 | REM Check if sphinx-build is available and fallback to Python version if any 66 | %SPHINXBUILD% 2> nul 67 | if errorlevel 9009 goto sphinx_python 68 | goto sphinx_ok 69 | 70 | :sphinx_python 71 | 72 | set SPHINXBUILD=python -m sphinx.__init__ 73 | %SPHINXBUILD% 2> nul 74 | if errorlevel 9009 ( 75 | echo. 76 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 77 | echo.installed, then set the SPHINXBUILD environment variable to point 78 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 79 | echo.may add the Sphinx directory to PATH. 80 | echo. 81 | echo.If you don't have Sphinx installed, grab it from 82 | echo.http://sphinx-doc.org/ 83 | exit /b 1 84 | ) 85 | 86 | :sphinx_ok 87 | 88 | 89 | if "%1" == "html" ( 90 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 91 | if errorlevel 1 exit /b 1 92 | echo. 93 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 94 | goto end 95 | ) 96 | 97 | if "%1" == "dirhtml" ( 98 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 99 | if errorlevel 1 exit /b 1 100 | echo. 101 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 102 | goto end 103 | ) 104 | 105 | if "%1" == "singlehtml" ( 106 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 107 | if errorlevel 1 exit /b 1 108 | echo. 109 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 110 | goto end 111 | ) 112 | 113 | if "%1" == "pickle" ( 114 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 115 | if errorlevel 1 exit /b 1 116 | echo. 117 | echo.Build finished; now you can process the pickle files. 118 | goto end 119 | ) 120 | 121 | if "%1" == "json" ( 122 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 123 | if errorlevel 1 exit /b 1 124 | echo. 125 | echo.Build finished; now you can process the JSON files. 126 | goto end 127 | ) 128 | 129 | if "%1" == "htmlhelp" ( 130 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 131 | if errorlevel 1 exit /b 1 132 | echo. 133 | echo.Build finished; now you can run HTML Help Workshop with the ^ 134 | .hhp project file in %BUILDDIR%/htmlhelp. 135 | goto end 136 | ) 137 | 138 | if "%1" == "qthelp" ( 139 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 143 | .qhcp project file in %BUILDDIR%/qthelp, like this: 144 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\aioppspp.qhcp 145 | echo.To view the help file: 146 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\aioppspp.ghc 147 | goto end 148 | ) 149 | 150 | if "%1" == "devhelp" ( 151 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 152 | if errorlevel 1 exit /b 1 153 | echo. 154 | echo.Build finished. 155 | goto end 156 | ) 157 | 158 | if "%1" == "epub" ( 159 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 160 | if errorlevel 1 exit /b 1 161 | echo. 162 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 163 | goto end 164 | ) 165 | 166 | if "%1" == "latex" ( 167 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 168 | if errorlevel 1 exit /b 1 169 | echo. 170 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 171 | goto end 172 | ) 173 | 174 | if "%1" == "latexpdf" ( 175 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 176 | cd %BUILDDIR%/latex 177 | make all-pdf 178 | cd %~dp0 179 | echo. 180 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 181 | goto end 182 | ) 183 | 184 | if "%1" == "latexpdfja" ( 185 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 186 | cd %BUILDDIR%/latex 187 | make all-pdf-ja 188 | cd %~dp0 189 | echo. 190 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 191 | goto end 192 | ) 193 | 194 | if "%1" == "text" ( 195 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 196 | if errorlevel 1 exit /b 1 197 | echo. 198 | echo.Build finished. The text files are in %BUILDDIR%/text. 199 | goto end 200 | ) 201 | 202 | if "%1" == "man" ( 203 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 204 | if errorlevel 1 exit /b 1 205 | echo. 206 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 207 | goto end 208 | ) 209 | 210 | if "%1" == "texinfo" ( 211 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 212 | if errorlevel 1 exit /b 1 213 | echo. 214 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 215 | goto end 216 | ) 217 | 218 | if "%1" == "gettext" ( 219 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 220 | if errorlevel 1 exit /b 1 221 | echo. 222 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 223 | goto end 224 | ) 225 | 226 | if "%1" == "changes" ( 227 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.The overview file is in %BUILDDIR%/changes. 231 | goto end 232 | ) 233 | 234 | if "%1" == "linkcheck" ( 235 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Link check complete; look for any errors in the above output ^ 239 | or in %BUILDDIR%/linkcheck/output.txt. 240 | goto end 241 | ) 242 | 243 | if "%1" == "doctest" ( 244 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 245 | if errorlevel 1 exit /b 1 246 | echo. 247 | echo.Testing of doctests in the sources finished, look at the ^ 248 | results in %BUILDDIR%/doctest/output.txt. 249 | goto end 250 | ) 251 | 252 | if "%1" == "coverage" ( 253 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 254 | if errorlevel 1 exit /b 1 255 | echo. 256 | echo.Testing of coverage in the sources finished, look at the ^ 257 | results in %BUILDDIR%/coverage/python.txt. 258 | goto end 259 | ) 260 | 261 | if "%1" == "xml" ( 262 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 263 | if errorlevel 1 exit /b 1 264 | echo. 265 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 266 | goto end 267 | ) 268 | 269 | if "%1" == "pseudoxml" ( 270 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 271 | if errorlevel 1 exit /b 1 272 | echo. 273 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 274 | goto end 275 | ) 276 | 277 | :end 278 | -------------------------------------------------------------------------------- /aioppspp/tests/test_protocol_options.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import unittest 17 | 18 | import hypothesis 19 | 20 | import aioppspp.messages.protocol_options as protocol_options 21 | from aioppspp.messages import ( 22 | MessageType, 23 | ) 24 | from . import strategies as st 25 | 26 | 27 | class ProtocolOptionsTestCase(unittest.TestCase): 28 | 29 | @hypothesis.given(st.byte()) 30 | @hypothesis.example(b'\x01') 31 | def test_version(self, data): 32 | try: 33 | value, _ = protocol_options.decode_version( 34 | memoryview(data), 0, ...) 35 | except ValueError: 36 | with self.assertRaises(ValueError): 37 | protocol_options.Version(int.from_bytes(data, 'big')) 38 | else: 39 | self.assertIsInstance(value, protocol_options.Version) 40 | result = protocol_options.encode_version(value, ...) 41 | self.assertEqual(data, result) 42 | 43 | @hypothesis.given(st.byte()) 44 | @hypothesis.example(b'\x01') 45 | def test_minimum_version(self, data): 46 | try: 47 | value, _ = protocol_options.decode_minimum_version( 48 | memoryview(data), 0, ...) 49 | except ValueError: 50 | with self.assertRaises(ValueError): 51 | protocol_options.Version(int.from_bytes(data, 'big')) 52 | else: 53 | self.assertIsInstance(value, protocol_options.Version) 54 | result = protocol_options.encode_minimum_version(value, ...) 55 | self.assertEqual(data, result) 56 | 57 | @hypothesis.given(st.raw_swarm_id()) 58 | def test_swarm_identifier(self, data): 59 | value, _ = protocol_options.decode_swarm_id( 60 | memoryview(data), 0, ...) 61 | self.assertIsInstance(value, bytes) 62 | result = protocol_options.encode_swarm_id(value, ...) 63 | self.assertEqual(data, result) 64 | 65 | def test_malformed_swarm_identifier(self): 66 | with self.assertRaises(ValueError): 67 | protocol_options.decode_swarm_id( 68 | memoryview(b'\x00\x10\x00'), 0, ...) 69 | 70 | @hypothesis.given(st.byte()) 71 | def test_content_integrity_protection_method(self, data): 72 | decode = protocol_options.decode_content_integrity_protection_method 73 | encode = protocol_options.encode_content_integrity_protection_method 74 | try: 75 | value, _ = decode(memoryview(data), 0, ...) 76 | except ValueError: 77 | with self.assertRaises(ValueError): 78 | protocol_options.CIPM(int.from_bytes(data, 'big')) 79 | else: 80 | self.assertIsInstance(value, protocol_options.CIPM) 81 | result = encode(value, ...) 82 | self.assertEqual(data, result) 83 | 84 | @hypothesis.given(st.byte()) 85 | def test_merkle_hash_tree_function(self, data): 86 | try: 87 | value, _ = protocol_options.decode_merkle_hash_tree_function( 88 | memoryview(data), 0, ...) 89 | except ValueError: 90 | with self.assertRaises(ValueError): 91 | protocol_options.MHTF(int.from_bytes(data, 'big')) 92 | else: 93 | self.assertIsInstance(value, protocol_options.MHTF) 94 | result = protocol_options.encode_merkle_hash_tree_function( 95 | value, ...) 96 | self.assertEqual(data, result) 97 | 98 | @hypothesis.given(st.byte()) 99 | def test_live_signature_algorithm(self, data): 100 | try: 101 | value, _ = protocol_options.decode_live_signature_algorithm( 102 | memoryview(data), 0, ...) 103 | except ValueError: 104 | with self.assertRaises(ValueError): 105 | protocol_options.LSA(int.from_bytes(data, 'big')) 106 | else: 107 | self.assertIsInstance(value, protocol_options.LSA) 108 | result = protocol_options.encode_live_signature_algorithm( 109 | value, ...) 110 | self.assertEqual(data, result) 111 | 112 | @hypothesis.given(st.byte()) 113 | def test_chunk_addressing_method(self, data): 114 | try: 115 | value, _ = protocol_options.decode_chunk_addressing_method( 116 | memoryview(data), 0, ...) 117 | except ValueError: 118 | with self.assertRaises(ValueError): 119 | protocol_options.CAM(int.from_bytes(data, 'big')) 120 | else: 121 | self.assertIsInstance(value, protocol_options.CAM) 122 | result = protocol_options.encode_chunk_addressing_method( 123 | value, ...) 124 | self.assertEqual(data, result) 125 | 126 | @hypothesis.given(st.raw_option_ldw()) 127 | def test_live_discard_window(self, cam_ldw): 128 | cam, data = cam_ldw 129 | options = {'chunk_addressing_method': cam} 130 | value, _ = protocol_options.decode_live_discard_window( 131 | memoryview(data), 0, options) 132 | self.assertIsInstance(value, int) 133 | result = protocol_options.encode_live_discard_window( 134 | value, protocol_options.ProtocolOptions(**options)) 135 | self.assertEqual(data, result) 136 | 137 | def test_cannot_encode_ldw_without_cam(self): 138 | expected = b'\xff' 139 | options = protocol_options.ProtocolOptions(live_discard_window=42) 140 | result = protocol_options.encode(options) 141 | self.assertEqual(result, expected) 142 | 143 | @hypothesis.given(st.option_supported_messages()) 144 | def test_supported_messages(self, messages): 145 | data = protocol_options.encode_supported_messages(messages, ...) 146 | self.assertIsInstance(data, bytes) 147 | result, _ = protocol_options.decode_supported_messages( 148 | memoryview(data), 0, ...) 149 | self.assertIsInstance(result, set) 150 | self.assertEqual(messages, result) 151 | 152 | def test_supported_messages_spec(self): 153 | # 7.10. Supported Messages 154 | # An example of the first 16 bits of the compressed bitmap for a peer 155 | # supporting every message except ACKs and PEXs is 11011001 11110000. 156 | # 157 | data = b'\x02\xd9\xf0' 158 | offset = 0 159 | supported_messages, _ = protocol_options.decode_supported_messages( 160 | memoryview(data), offset, ...) 161 | self.assertIsInstance(supported_messages, set) 162 | not_supported = [MessageType.ACK, MessageType.PEX_REQ, 163 | MessageType.PEX_REScert, MessageType.PEX_RESv4, 164 | MessageType.PEX_RESv6] 165 | for msgtype in MessageType: 166 | if msgtype in not_supported: 167 | self.assertNotIn(msgtype, supported_messages) 168 | else: 169 | self.assertIn(msgtype, supported_messages) 170 | 171 | result = protocol_options.encode_supported_messages( 172 | supported_messages, ...) 173 | self.assertEqual(data, result) 174 | 175 | @hypothesis.given(st.dword()) 176 | def test_chunk_size(self, data): 177 | value, _ = protocol_options.decode_chunk_size( 178 | memoryview(data), 0, ...) 179 | self.assertIsInstance(value, int) 180 | result = protocol_options.encode_chunk_size(value, ...) 181 | self.assertEqual(data, result) 182 | 183 | @hypothesis.given(st.generic_protocol_options()) 184 | def test_encode_decode(self, options): 185 | data = protocol_options.encode(options) 186 | self.assertIsInstance(data, bytearray) 187 | result, _ = protocol_options.decode(memoryview(data)) 188 | from pprint import pformat 189 | self.assertEqual(result, options, pformat(list(zip(result, options)))) 190 | 191 | def test_encode_decode_empty(self): 192 | data = b'\xff' 193 | options, _ = protocol_options.decode(memoryview(data)) 194 | self.assertIsInstance(options, protocol_options.ProtocolOptions) 195 | self.assertTrue(all(option is None for option in options)) 196 | result = protocol_options.encode(options) 197 | self.assertEqual(result, data) 198 | 199 | def test_decode_duplicate_options(self): 200 | data = b'\x00\x01\x00\x01' 201 | with self.assertRaises(ValueError): 202 | protocol_options.decode(memoryview(data)) 203 | 204 | def test_init_bad_type(self): 205 | with self.assertRaises(TypeError): 206 | protocol_options.ProtocolOptions(version='42') 207 | -------------------------------------------------------------------------------- /aioppspp/connector.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import abc 17 | import asyncio 18 | import sys 19 | import traceback 20 | import warnings 21 | from collections import defaultdict 22 | from itertools import chain 23 | 24 | from .connection import ( 25 | Address, 26 | Connection, 27 | ) 28 | 29 | __all__ = ( 30 | 'BaseConnector', 31 | 'BaseProtocol', 32 | ) 33 | 34 | 35 | class BaseProtocol(asyncio.BaseProtocol, metaclass=abc.ABCMeta): 36 | """Base protocol interface. 37 | 38 | Should be used as mixin for regular asyncio Protocol classes. 39 | 40 | This is an :term:`abstract base class`. 41 | """ 42 | 43 | def __init__(self, *, loop=None): 44 | self._loop = loop 45 | self._transport = None 46 | 47 | @property 48 | def closed(self): 49 | """Returns :const:`True` if connection is not made or closed.""" 50 | return self._transport is None 51 | 52 | @property 53 | def local_address(self): 54 | """Returns local peer address or :const:`None` if not connected.""" 55 | if self._transport is None: 56 | return None 57 | return Address(*self._transport._sock.getsockname()[:2]) 58 | 59 | @property 60 | def remote_address(self): 61 | """Returns remote peer address or :const:`None` if not connected.""" 62 | if self._transport is None: 63 | return None 64 | try: 65 | return Address(*self._transport._sock.getpeername()[:2]) 66 | except OSError: 67 | # Transport is in "server" mode and not bounded with any specific 68 | # remote peer. No reason to crash then. 69 | return None 70 | 71 | @property 72 | def transport(self): 73 | """Returns the underlying transport instance.""" 74 | return self._transport 75 | 76 | def connection_made(self, transport): 77 | """Called when a connection is made.""" 78 | self._transport = transport 79 | 80 | def connection_lost(self, exc): 81 | """Called when the connection is lost or closed.""" 82 | self._transport = None 83 | super().connection_lost(exc) 84 | 85 | @abc.abstractmethod 86 | async def recv(self): 87 | """Receives data from remote peer. Must be implemented in subclass. 88 | 89 | This method is :term:`awaitable`. 90 | """ 91 | raise NotImplementedError 92 | 93 | @abc.abstractmethod 94 | async def send(self, data, remote_address): 95 | """Sends data to remote peer. Must be implemented in subclass. 96 | 97 | This method is :term:`awaitable`. 98 | """ 99 | raise NotImplementedError 100 | 101 | def close(self): 102 | """Closes protocol and the underlying transport.""" 103 | self._transport.close() 104 | self._transport = None 105 | 106 | 107 | class BaseConnector(object, metaclass=abc.ABCMeta): 108 | """Base connector. 109 | 110 | Connector is used as connections managers: it spawns them, controls them 111 | and kills them when time has come. 112 | 113 | This is an :term:`abstract base class`. 114 | """ 115 | _closed = True 116 | _source_traceback = None 117 | connection_class = Connection 118 | 119 | def __init__(self, *, connection_class=None, connection_timeout=None, 120 | loop=None): 121 | if loop is None: 122 | loop = asyncio.get_event_loop() 123 | if loop.get_debug(): # pragma: no cover 124 | self._source_traceback = traceback.extract_stack(sys._getframe(1)) 125 | 126 | if connection_class is not None: 127 | self.connection_class = connection_class 128 | 129 | self._acquired = defaultdict(set) 130 | self._closed = False 131 | self._loop = loop 132 | self._pool = {} 133 | self._connection_timeout = connection_timeout 134 | 135 | def __del__(self): 136 | if self.closed: 137 | return 138 | 139 | if not self._pool: 140 | return 141 | 142 | self.close() 143 | 144 | warnings.warn('Unclosed connector {!r}'.format(self), ResourceWarning) 145 | context = {'connector': self, 146 | 'message': 'Unclosed connector'} 147 | 148 | if self._source_traceback is not None: # pragma: no cover 149 | context['source_traceback'] = self._source_traceback 150 | 151 | self._loop.call_exception_handler(context) 152 | 153 | @property 154 | def closed(self): 155 | """Returns :const:`True` whenever connector is closed.""" 156 | return self._closed 157 | 158 | @property 159 | def loop(self): 160 | return self._loop 161 | 162 | @abc.abstractmethod 163 | async def create_endpoint(self, *args, **kwargs): 164 | """This method must be implemented in subclass in order to create 165 | an endpoint and return the BaseProtocol interface implementation. 166 | 167 | This method is :term:`awaitable`. 168 | """ 169 | raise NotImplementedError 170 | 171 | async def connect(self, remote_address, **kwargs): 172 | """Creates a new outgoing connection to the specified remote peer. 173 | 174 | :param aioppspp.connection.Address remote_address: Remote peer address 175 | :returns: :class:`aioppspp.connection.Connection` 176 | 177 | This method is :term:`awaitable`. 178 | """ 179 | return await self._connect(remote_address, 180 | remote_address=remote_address, **kwargs) 181 | 182 | async def listen(self, local_address, **kwargs): 183 | """Creates a new connection instance for incoming connections 184 | to the specified host and port pair. 185 | 186 | :param aioppspp.connection.Address local_address: Local peer address 187 | :returns: :class:`aioppspp.connection.Connection` 188 | 189 | This method is :term:`awaitable`. 190 | """ 191 | return await self._connect(local_address, 192 | local_address=local_address, **kwargs) 193 | 194 | def close(self): 195 | """Closes connector and all served connections.""" 196 | if self.closed: 197 | return 198 | 199 | try: 200 | if self._loop.is_closed(): 201 | return 202 | 203 | for key, protocols in self._pool.items(): 204 | for protocol in protocols: 205 | protocol.close() 206 | 207 | # Copy acquired values to prevent iterator change error 208 | connections = map(list, self._acquired.values()) 209 | for connection in chain.from_iterable(connections): 210 | connection.close() 211 | finally: 212 | self._pool.clear() 213 | self._acquired.clear() 214 | self._closed = True 215 | 216 | def close_connection(self, connection): 217 | """Closes the specified connection. 218 | 219 | :param aioppspp.connection.Connection connection: Connection to close 220 | """ 221 | if self.closed: 222 | return 223 | 224 | key = connection.key 225 | acquired = self._acquired[key] 226 | 227 | try: 228 | acquired.remove(connection) 229 | except KeyError: # pragma: no cover 230 | # this may be result of undetermined order of objects 231 | # finalization due garbage collection. 232 | pass 233 | finally: 234 | connection.protocol.close() 235 | 236 | def release_connection(self, connection): 237 | """Releases the connection and returns it back to the pool. 238 | 239 | :param aioppspp.connection.Connection connection: Connection to release 240 | """ 241 | if self.closed: 242 | return 243 | 244 | key = connection.key 245 | acquired = self._acquired[key] 246 | 247 | try: 248 | acquired.remove(connection) 249 | except KeyError: # pragma: no cover 250 | # this may be result of undetermined order of objects 251 | # finalization due garbage collection. 252 | pass 253 | finally: 254 | protocols = self._pool.get(key, None) 255 | if protocols is None: 256 | protocols = self._pool[key] = [] 257 | protocols.append(connection.protocol) 258 | 259 | async def _connect(self, key, **connection_kwargs): 260 | try: 261 | connection = self._get_connection(key) 262 | if connection is None: 263 | connect_future = self.create_endpoint(**connection_kwargs) 264 | if self._connection_timeout: 265 | connect_future = asyncio.wait_for( 266 | connect_future, 267 | self._connection_timeout, 268 | loop=self._loop) 269 | 270 | protocol = await connect_future 271 | connection = self._spawn_connection(key, protocol) 272 | 273 | except asyncio.TimeoutError as exc: 274 | raise TimeoutError( 275 | 'Connection timeout to host %s:%s' % key) from exc 276 | 277 | except OSError as exc: 278 | raise ConnectionError( 279 | 'Cannot connect to host %s:%s' % key) from exc 280 | 281 | else: 282 | self._acquired[key].add(connection) 283 | return connection 284 | 285 | def _get_connection(self, key): 286 | protocols = self._pool.get(key) 287 | while protocols: 288 | protocol = protocols.pop() 289 | if not protocols: 290 | # The very last connection was reclaimed: drop the key 291 | del self._pool[key] 292 | return self._spawn_connection(key, protocol) 293 | assert key not in self._pool # TODO: guard possible issue 294 | 295 | def _spawn_connection(self, key, protocol): 296 | return self.connection_class(self, key, protocol, loop=self._loop) 297 | -------------------------------------------------------------------------------- /aioppspp/tests/test_connector.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import gc 17 | import unittest.mock 18 | 19 | import aioppspp.connection 20 | import aioppspp.connector 21 | import aioppspp.tests.utils 22 | 23 | 24 | class Protocol(aioppspp.connector.BaseProtocol): 25 | 26 | async def recv(self): 27 | pass 28 | 29 | async def send(self, data, remote_address): 30 | pass 31 | 32 | 33 | class Connector(aioppspp.connector.BaseConnector): 34 | 35 | async def create_endpoint(self, *args, **kwargs): 36 | return Protocol(loop=self.loop) 37 | 38 | 39 | class TestProtocol(aioppspp.tests.utils.TestCase): 40 | 41 | def new_protocol(self): 42 | return Protocol(loop=self.loop) 43 | 44 | def test_not_connected_by_default(self): 45 | protocol = self.new_protocol() 46 | self.assertTrue(protocol.closed) 47 | 48 | def test_connected_by_default(self): 49 | protocol = self.new_protocol() 50 | protocol.connection_made(unittest.mock.Mock()) 51 | self.assertFalse(protocol.closed) 52 | 53 | def test_close(self): 54 | protocol = self.new_protocol() 55 | transport = unittest.mock.Mock() 56 | protocol.connection_made(transport) 57 | self.assertIs(transport, protocol.transport) 58 | protocol.close() 59 | self.assertTrue(protocol.closed) 60 | self.assertTrue(transport.close.called) 61 | 62 | def test_connection_lost(self): 63 | protocol = self.new_protocol() 64 | transport = unittest.mock.Mock() 65 | protocol.connection_made(transport) 66 | protocol.connection_lost(ConnectionError) 67 | self.assertTrue(protocol.closed) 68 | 69 | def test_local_address_not_connected(self): 70 | protocol = self.new_protocol() 71 | self.assertIsNone(protocol.local_address) 72 | 73 | def test_local_address(self): 74 | protocol = self.new_protocol() 75 | ipaddrport = ('127.0.0.1', 4242) 76 | transport = unittest.mock.Mock() 77 | transport._sock.getsockname.return_value = ipaddrport 78 | protocol.connection_made(transport) 79 | self.assertIsInstance(protocol.local_address, 80 | aioppspp.connection.Address) 81 | self.assertEqual(protocol.local_address, ipaddrport) 82 | 83 | def test_remote_address_not_connected(self): 84 | protocol = self.new_protocol() 85 | self.assertIsNone(protocol.remote_address) 86 | 87 | def test_remote_address(self): 88 | protocol = self.new_protocol() 89 | ipaddrport = ('127.0.0.1', 4242) 90 | transport = unittest.mock.Mock() 91 | transport._sock.getpeername.return_value = ipaddrport 92 | protocol.connection_made(transport) 93 | self.assertIsInstance(protocol.remote_address, 94 | aioppspp.connection.Address) 95 | self.assertEqual(protocol.remote_address, ipaddrport) 96 | 97 | def test_no_remote_address(self): 98 | protocol = self.new_protocol() 99 | transport = unittest.mock.Mock() 100 | transport._sock.getpeername.side_effect = OSError 101 | protocol.connection_made(transport) 102 | self.assertIsNone(protocol.remote_address) 103 | 104 | 105 | class TestConnector(aioppspp.tests.utils.TestCase): 106 | def new_connector(self): 107 | return Connector(connection_class=aioppspp.connector.Connection, 108 | connection_timeout=1, 109 | loop=self.loop) 110 | 111 | def test_default_init(self): 112 | Connector() 113 | 114 | def test_del(self): 115 | connector = self.new_connector() 116 | address = aioppspp.connection.Address('0.0.0.0', 0) 117 | connection = unittest.mock.Mock() 118 | connector._pool[address] = [connection] 119 | connections = connector._pool 120 | 121 | exc_handler = unittest.mock.Mock() 122 | self.loop.set_exception_handler(exc_handler) 123 | 124 | with self.assertWarns(ResourceWarning): 125 | del connector 126 | gc.collect() 127 | 128 | self.assertFalse(connections) 129 | connection.close.assert_called_with() 130 | msg = {'connector': unittest.mock.ANY, 131 | 'message': 'Unclosed connector'} 132 | if self.loop.get_debug(): # pragma: no cover 133 | msg['source_traceback'] = unittest.mock.ANY 134 | exc_handler.assert_called_with(self.loop, msg) 135 | 136 | def test_del_with_closed_loop(self): 137 | connector = self.new_connector() 138 | address = aioppspp.connection.Address('0.0.0.0', 0) 139 | connection = unittest.mock.Mock() 140 | connector._pool[address] = [connection] 141 | connections = connector._pool 142 | 143 | exc_handler = unittest.mock.Mock() 144 | self.loop.set_exception_handler(exc_handler) 145 | self.loop.close() 146 | 147 | with self.assertWarns(ResourceWarning): 148 | del connector 149 | gc.collect() 150 | 151 | self.assertFalse(connections) 152 | self.assertFalse(connection.close.called) 153 | self.assertTrue(exc_handler.called) 154 | 155 | def test_del_empty_connector(self): 156 | connector = self.new_connector() 157 | 158 | exc_handler = unittest.mock.Mock() 159 | self.loop.set_exception_handler(exc_handler) 160 | 161 | del connector 162 | 163 | self.assertFalse(exc_handler.called) 164 | 165 | async def test_create_connection(self): 166 | connector = self.new_connector() 167 | address = aioppspp.connection.Address('0.0.0.0', 0) 168 | connection = await connector.connect(address) 169 | self.assertIsInstance(connection, connector.connection_class) 170 | 171 | def test_loop(self): 172 | connector = self.new_connector() 173 | self.assertIs(connector.loop, self.loop) 174 | 175 | def test_default_loop(self): 176 | connector = Connector() 177 | self.assertIs(connector.loop, self.loop) 178 | 179 | async def test_connect(self): 180 | connector = self.new_connector() 181 | address = aioppspp.connection.Address('0.0.0.0', 0) 182 | connector.create_endpoint = unittest.mock.Mock( 183 | wraps=connector.create_endpoint) 184 | await connector.connect(address) 185 | connector.create_endpoint.assert_called_with(remote_address=address) 186 | 187 | async def test_listen(self): 188 | connector = self.new_connector() 189 | address = aioppspp.connection.Address('0.0.0.0', 0) 190 | connector.create_endpoint = unittest.mock.Mock( 191 | wraps=connector.create_endpoint) 192 | await connector.listen(address) 193 | connector.create_endpoint.assert_called_with(local_address=address) 194 | 195 | def test_not_closed_by_default(self): 196 | connector = self.new_connector() 197 | self.assertFalse(connector.closed) 198 | 199 | def test_close(self): 200 | connector = self.new_connector() 201 | connector.close() 202 | self.assertTrue(connector.closed) 203 | 204 | def test_close_closed_connector(self): 205 | connector = self.new_connector() 206 | connector.close() 207 | connector.close() 208 | 209 | async def test_close_closes_active_connections(self): 210 | connector = self.new_connector() 211 | address = aioppspp.connection.Address('0.0.0.0', 0) 212 | connections = [] 213 | for _ in range(3): 214 | connection = await connector.connect(address) 215 | connection.protocol.connection_made(unittest.mock.Mock()) 216 | connections.append(connection) 217 | connector.close() 218 | for connection in connections: 219 | self.assertTrue(connection.closed) 220 | 221 | async def test_close_connection_when_closed(self): 222 | connector = self.new_connector() 223 | address = aioppspp.connection.Address('0.0.0.0', 0) 224 | connection = await connector.connect(address) 225 | connector.close() 226 | connector.close_connection(connection) 227 | 228 | async def test_release_reuse_connections(self): 229 | connector = self.new_connector() 230 | address = aioppspp.connection.Address('0.0.0.0', 0) 231 | connection0 = await connector.connect(address) 232 | connection0.protocol.connection_made(unittest.mock.Mock()) 233 | connection0.release() 234 | connection1 = await connector.connect(address) 235 | self.assertIsNot(connection0, connection1) 236 | connection1.protocol.connection_made(unittest.mock.Mock()) 237 | connection1.close() 238 | 239 | async def test_release(self): 240 | connector = self.new_connector() 241 | address = aioppspp.connection.Address('0.0.0.0', 0) 242 | connection0 = await connector.connect(address) 243 | connection1 = await connector.connect(address) 244 | connection0.protocol.connection_made(unittest.mock.Mock()) 245 | connection1.protocol.connection_made(unittest.mock.Mock()) 246 | connection0.release() 247 | connection1.release() 248 | await connector.connect(address) 249 | await connector.connect(address) 250 | connector.close() 251 | 252 | def test_release_connection_while_closed(self): 253 | connector = self.new_connector() 254 | connector.close() 255 | connector.release_connection(...) 256 | 257 | async def test_connect_timeout(self): 258 | connector = self.new_connector() 259 | address = aioppspp.connection.Address('0.0.0.0', 0) 260 | connector.create_endpoint = unittest.mock.Mock( 261 | return_value=self.future()) 262 | with self.assertRaises(TimeoutError): 263 | await connector.connect(address) 264 | 265 | async def test_connection_error(self): 266 | connector = self.new_connector() 267 | address = aioppspp.connection.Address('0.0.0.0', 0) 268 | connector.create_endpoint = unittest.mock.Mock( 269 | return_value=self.future(exception=OSError('...'))) 270 | with self.assertRaises(ConnectionError): 271 | await connector.connect(address) 272 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2015-2038 Alexander Shorin 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under 14 | # the License. 15 | # 16 | 17 | 18 | import sys 19 | import os 20 | import shlex 21 | 22 | import importlib 23 | 24 | # If extensions (or modules to document with autodoc) are in another directory, 25 | # add these directories to sys.path here. If the directory is relative to the 26 | # documentation root, use os.path.abspath to make it absolute, like shown here. 27 | sys.path.insert(0, os.path.abspath('..')) 28 | 29 | aioppspp = importlib.import_module('aioppspp') 30 | 31 | # -- General configuration ------------------------------------------------ 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | #needs_sphinx = '1.0' 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | 'sphinx.ext.autodoc', 41 | 'sphinx.ext.doctest', 42 | 'sphinx.ext.intersphinx', 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # The suffix(es) of source filenames. 49 | # You can specify multiple suffix as a list of string: 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = '.rst' 52 | 53 | # The encoding of source files. 54 | #source_encoding = 'utf-8-sig' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # General information about the project. 60 | project = 'aioppspp' 61 | copyright = '2015-2016, Alexander Shorin' 62 | author = 'Alexander Shorin' 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | # 68 | # The short X.Y version. 69 | version = aioppspp.__version__.split('-')[0] 70 | # The full version, including alpha/beta/rc tags. 71 | release = aioppspp.__version__ 72 | 73 | # The language for content autogenerated by Sphinx. Refer to documentation 74 | # for a list of supported languages. 75 | # 76 | # This is also used if you do content translation via gettext catalogs. 77 | # Usually you set "language" from the command line for these cases. 78 | language = None 79 | 80 | # There are two options for replacing |today|: either, you set today to some 81 | # non-false value, then it is used: 82 | #today = '' 83 | # Else, today_fmt is used as the format for a strftime call. 84 | #today_fmt = '%B %d, %Y' 85 | 86 | # List of patterns, relative to source directory, that match files and 87 | # directories to ignore when looking for source files. 88 | exclude_patterns = ['_build'] 89 | 90 | # The reST default role (used for this markup: `text`) to use for all 91 | # documents. 92 | #default_role = None 93 | 94 | # If true, '()' will be appended to :func: etc. cross-reference text. 95 | #add_function_parentheses = True 96 | 97 | # If true, the current module name will be prepended to all description 98 | # unit titles (such as .. function::). 99 | #add_module_names = True 100 | 101 | # If true, sectionauthor and moduleauthor directives will be shown in the 102 | # output. They are ignored by default. 103 | #show_authors = False 104 | 105 | # The name of the Pygments (syntax highlighting) style to use. 106 | pygments_style = 'sphinx' 107 | 108 | # A list of ignored prefixes for module index sorting. 109 | #modindex_common_prefix = [] 110 | 111 | # If true, keep warnings as "system message" paragraphs in the built documents. 112 | #keep_warnings = False 113 | 114 | # If true, `todo` and `todoList` produce output, else they produce nothing. 115 | todo_include_todos = False 116 | 117 | 118 | # -- Options for HTML output ---------------------------------------------- 119 | 120 | # The theme to use for HTML and HTML Help pages. See the documentation for 121 | # a list of builtin themes. 122 | html_theme = 'haiku' 123 | 124 | # Theme options are theme-specific and customize the look and feel of a theme 125 | # further. For a list of options available for each theme, see the 126 | # documentation. 127 | #html_theme_options = {} 128 | 129 | # Add any paths that contain custom themes here, relative to this directory. 130 | #html_theme_path = [] 131 | 132 | # The name for this set of Sphinx documents. If None, it defaults to 133 | # " v documentation". 134 | #html_title = None 135 | 136 | # A shorter title for the navigation bar. Default is the same as html_title. 137 | #html_short_title = None 138 | 139 | # The name of an image file (relative to this directory) to place at the top 140 | # of the sidebar. 141 | #html_logo = None 142 | 143 | # The name of an image file (within the static path) to use as favicon of the 144 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 145 | # pixels large. 146 | #html_favicon = None 147 | 148 | # Add any paths that contain custom static files (such as style sheets) here, 149 | # relative to this directory. They are copied after the builtin static files, 150 | # so a file named "default.css" will overwrite the builtin "default.css". 151 | html_static_path = ['_static'] 152 | 153 | # Add any extra paths that contain custom files (such as robots.txt or 154 | # .htaccess) here, relative to this directory. These files are copied 155 | # directly to the root of the documentation. 156 | #html_extra_path = [] 157 | 158 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 159 | # using the given strftime format. 160 | #html_last_updated_fmt = '%b %d, %Y' 161 | 162 | # If true, SmartyPants will be used to convert quotes and dashes to 163 | # typographically correct entities. 164 | #html_use_smartypants = True 165 | 166 | # Custom sidebar templates, maps document names to template names. 167 | #html_sidebars = {} 168 | 169 | # Additional templates that should be rendered to pages, maps page names to 170 | # template names. 171 | #html_additional_pages = {} 172 | 173 | # If false, no module index is generated. 174 | #html_domain_indices = True 175 | 176 | # If false, no index is generated. 177 | #html_use_index = True 178 | 179 | # If true, the index is split into individual pages for each letter. 180 | #html_split_index = False 181 | 182 | # If true, links to the reST sources are added to the pages. 183 | #html_show_sourcelink = True 184 | 185 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 186 | #html_show_sphinx = True 187 | 188 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 189 | #html_show_copyright = True 190 | 191 | # If true, an OpenSearch description file will be output, and all pages will 192 | # contain a tag referring to it. The value of this option must be the 193 | # base URL from which the finished HTML is served. 194 | #html_use_opensearch = '' 195 | 196 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 197 | #html_file_suffix = None 198 | 199 | # Language to be used for generating the HTML full-text search index. 200 | # Sphinx supports the following languages: 201 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 202 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 203 | #html_search_language = 'en' 204 | 205 | # A dictionary with options for the search language support, empty by default. 206 | # Now only 'ja' uses this config value 207 | #html_search_options = {'type': 'default'} 208 | 209 | # The name of a javascript file (relative to the configuration directory) that 210 | # implements a search results scorer. If empty, the default will be used. 211 | #html_search_scorer = 'scorer.js' 212 | 213 | # Output file base name for HTML help builder. 214 | htmlhelp_basename = 'aioppsppdoc' 215 | 216 | # -- Options for LaTeX output --------------------------------------------- 217 | 218 | latex_elements = { 219 | # The paper size ('letterpaper' or 'a4paper'). 220 | #'papersize': 'letterpaper', 221 | 222 | # The font size ('10pt', '11pt' or '12pt'). 223 | #'pointsize': '10pt', 224 | 225 | # Additional stuff for the LaTeX preamble. 226 | #'preamble': '', 227 | 228 | # Latex figure (float) alignment 229 | #'figure_align': 'htbp', 230 | } 231 | 232 | # Grouping the document tree into LaTeX files. List of tuples 233 | # (source start file, target name, title, 234 | # author, documentclass [howto, manual, or own class]). 235 | latex_documents = [ 236 | (master_doc, 'aioppspp.tex', 'aioppspp Documentation', 237 | 'Alexander Shorin', 'manual'), 238 | ] 239 | 240 | # The name of an image file (relative to this directory) to place at the top of 241 | # the title page. 242 | #latex_logo = None 243 | 244 | # For "manual" documents, if this is true, then toplevel headings are parts, 245 | # not chapters. 246 | #latex_use_parts = False 247 | 248 | # If true, show page references after internal links. 249 | #latex_show_pagerefs = False 250 | 251 | # If true, show URL addresses after external links. 252 | #latex_show_urls = False 253 | 254 | # Documents to append as an appendix to all manuals. 255 | #latex_appendices = [] 256 | 257 | # If false, no module index is generated. 258 | #latex_domain_indices = True 259 | 260 | 261 | # -- Options for manual page output --------------------------------------- 262 | 263 | # One entry per manual page. List of tuples 264 | # (source start file, name, description, authors, manual section). 265 | man_pages = [ 266 | (master_doc, 'aioppspp', 'aioppspp Documentation', 267 | [author], 1) 268 | ] 269 | 270 | # If true, show URL addresses after external links. 271 | #man_show_urls = False 272 | 273 | 274 | # -- Options for Texinfo output ------------------------------------------- 275 | 276 | # Grouping the document tree into Texinfo files. List of tuples 277 | # (source start file, target name, title, author, 278 | # dir menu entry, description, category) 279 | texinfo_documents = [ 280 | (master_doc, 'aioppspp', 'aioppspp Documentation', 281 | author, 'aioppspp', 'One line description of project.', 282 | 'Miscellaneous'), 283 | ] 284 | 285 | # Documents to append as an appendix to all manuals. 286 | #texinfo_appendices = [] 287 | 288 | # If false, no module index is generated. 289 | #texinfo_domain_indices = True 290 | 291 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 292 | #texinfo_show_urls = 'footnote' 293 | 294 | # If true, do not generate a @detailmenu in the "Top" node's menu. 295 | #texinfo_no_detailmenu = False 296 | 297 | # Example configuration for intersphinx: refer to the Python standard library. 298 | intersphinx_mapping = { 299 | 'http://docs.python.org/3': None, 300 | } 301 | 302 | # Both the class’ and the __init__ method’s docstring are concatenated 303 | # and inserted. 304 | autoclass_content = 'both' 305 | 306 | # List of autodoc directive flags that should be automatically applied to all 307 | # autodoc directives. 308 | autodoc_default_flags = ['members', 'show-inheritance'] 309 | -------------------------------------------------------------------------------- /aioppspp/messages/protocol_options.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | # 15 | 16 | import enum 17 | from collections import ( 18 | namedtuple, 19 | ) 20 | 21 | from .types import ( 22 | MessageType, 23 | ) 24 | from ..constants import ( 25 | BYTE, 26 | WORD, 27 | DWORD, 28 | QWORD, 29 | ) 30 | 31 | __all__ = ( 32 | 'ProtocolOptions', 33 | 'Version', 34 | 'ContentIntegrityProtectionMethod', 35 | 'CIPM', 36 | 'MerkleHashTreeFunction', 37 | 'MHTF', 38 | 'LiveSignatureAlgorithm', 39 | 'LSA', 40 | 'ChunkAddressingMethod', 41 | 'CAM', 42 | ) 43 | 44 | 45 | class ProtocolOptionsId(enum.IntEnum): 46 | """Protocol options identifiers enumeration. 47 | 48 | This enumeration is used to map protocol options codes with the record 49 | field names. 50 | 51 | .. seealso:: 52 | 53 | - :rfc:`7574#section-7` 54 | """ 55 | version = 0 56 | minimum_version = 1 57 | swarm_identifier = 2 58 | content_integrity_protection_method = 3 59 | merkle_hash_tree_function = 4 60 | live_signature_algorithm = 5 61 | chunk_addressing_method = 6 62 | live_discard_window = 7 63 | supported_messages = 8 64 | chunk_size = 9 65 | end_option = 255 66 | 67 | 68 | class Version(enum.IntEnum): 69 | """Supported PPSPP version. 70 | 71 | +---------+----------------------------------------+ 72 | | Version | Description | 73 | +=========+========================================+ 74 | | 0 | Reserved | 75 | +---------+----------------------------------------+ 76 | | 1 | Protocol as described in this document | 77 | +---------+----------------------------------------+ 78 | | 2-255 | Unassigned | 79 | +---------+----------------------------------------+ 80 | 81 | .. seealso:: 82 | 83 | - :rfc:`7574#section-7.2` 84 | """ 85 | rfc7574 = 1 86 | 87 | 88 | class ContentIntegrityProtectionMethod(enum.IntEnum): 89 | """Content Integrity Protection Method enumeration. 90 | 91 | The "Merkle Hash Tree" method is the default for static content, see 92 | :rfc:`Section 5.1 <7574#section-5.1>. "Sign All", and "Unified Merkle Tree" 93 | are for live content, see :rfc:`Section 6.1 <7574#section-6.1>`, with 94 | "Unified Merkle Tree" being the default. 95 | 96 | +--------+-------------------------+ 97 | | Method | Description | 98 | +========+=========================+ 99 | | 0 | No integrity protection | 100 | +--------+-------------------------+ 101 | | 1 | Merkle Hash Tree | 102 | +--------+-------------------------+ 103 | | 2 | Sign All | 104 | +--------+-------------------------+ 105 | | 3 | Unified Merkle Tree | 106 | +--------+-------------------------+ 107 | | 4-255 | Unassigned | 108 | +--------+-------------------------+ 109 | 110 | .. seealso:: 111 | 112 | - :rfc:`7574#section-7.5` 113 | """ 114 | no_protection = 0 115 | merkle_hash_tree = 1 116 | sign_all = 2 117 | unified_merkle_tree = 3 118 | 119 | 120 | class MerkleHashTreeFunction(enum.IntEnum): 121 | """Merkle Hash Tree function enumeration. 122 | 123 | When the content integrity protection method is "Merkle Hash Tree", 124 | this option defining which hash function is used for the tree MUST be 125 | included. 126 | 127 | +----------+-------------+ 128 | | Function | Description | 129 | +==========+=============+ 130 | | 0 | SHA-1 | 131 | +----------+-------------+ 132 | | 1 | SHA-224 | 133 | +----------+-------------+ 134 | | 2 | SHA-256 | 135 | +----------+-------------+ 136 | | 3 | SHA-384 | 137 | +----------+-------------+ 138 | | 4 | SHA-512 | 139 | +----------+-------------+ 140 | | 5-255 | Unassigned | 141 | +----------+-------------+ 142 | 143 | .. seealso:: 144 | 145 | - :rfc:`7574#section-7.6` 146 | """ 147 | sha1 = 0 148 | sha224 = 1 149 | sha256 = 2 150 | sha384 = 3 151 | sha512 = 4 152 | 153 | 154 | class LiveSignatureAlgorithm(enum.IntEnum): 155 | """Live Signature Algorithm enumeration. 156 | 157 | When the content integrity protection method is "Sign All" or 158 | "Unified Merkle Tree", this option MUST be defined. The 8-bit value of 159 | this option is one of the values listed in the "Domain Name System Security 160 | (DNSSEC) Algorithm Numbers" registry. The :rfc:`RSASHA1 <4034>`, 161 | :rfc:`RSASHA256 <5702>`, :rfc:`ECDSAP256SHA256 <6605>` and 162 | :rfc:`ECDSAP384SHA384 <6605>` algorithms are mandatory to implement. 163 | Default is ECDSAP256SHA256. 164 | 165 | .. seealso:: 166 | 167 | - :rfc:`7574#section-7.6` 168 | - `Domain Name System Security (DNSSEC) Algorithm Numbers `_ 169 | """ 170 | #: RSA/MD5 (deprecated, see 5) (:rfc:`3110`, :rfc:`4034`) 171 | rsamd5 = 1 172 | #: Diffie-Hellman (:rfc:`2539`) 173 | dh = 2 174 | #: DSA/SHA1 (:rfc:`3755`, :rfc:`2536`) 175 | dsa = 3 176 | #: RSA/SHA-1 (:rfc:`3110`, :rfc:`4034`) 177 | rsasha1 = 5 178 | #: DSA-NSEC3-SHA1 (:rfc:`5155`) 179 | dsa_nsec3_sha1 = 6 180 | #: RSASHA1-NSEC3-SHA1 (:rfc:`5155`) 181 | rsasha1_nsec3_sha1 = 7 182 | #: RSA/SHA-256 (:rfc:`5702`) 183 | rsasha256 = 8 184 | #: RSA/SHA-512 (:rfc:`5702`) 185 | rsasha512 = 10 186 | #: GOST R 34.10-2001 (:rfc:`5933`) 187 | ecc_gost = 12 188 | #: ECDSA Curve P-256 with SHA-256 (:rfc:`6605`) 189 | ecdsap256sha256 = 13 190 | #: ECDSA Curve P-384 with SHA-384 (:rfc:`6605`) 191 | ecdsap384sha384 = 13 192 | #: Private algorithm (:rfc:`4034`) 193 | privatedns = 253 194 | #: Private algorithm OID (:rfc:`4034`) 195 | privateoid = 254 196 | 197 | 198 | class ChunkAddressingMethod(enum.IntEnum): 199 | """Chunk Addressing Method enumeration. 200 | 201 | Implementations MUST support "32-bit chunk ranges" and "64-bit chunk 202 | ranges". Default is "32-bit chunk ranges". 203 | 204 | +--------+---------------------+ 205 | | Method | Description | 206 | +========+=====================+ 207 | | 0 | 32-bit bins | 208 | +--------+---------------------+ 209 | | 1 | 64-bit byte ranges | 210 | +--------+---------------------+ 211 | | 2 | 32-bit chunk ranges | 212 | +--------+---------------------+ 213 | | 3 | 64-bit bins | 214 | +--------+---------------------+ 215 | | 4 | 64-bit chunk ranges | 216 | +--------+---------------------+ 217 | | 5-255 | Unassigned | 218 | +--------+---------------------+ 219 | 220 | .. seealso:: 221 | 222 | - :rfc:`7574#section-7.8` 223 | """ 224 | bins32 = 0 225 | bytes64 = 1 226 | chunks32 = 2 227 | bins64 = 3 228 | chunks64 = 4 229 | 230 | 231 | class LiveDiscardWindowSize(enum.IntEnum): 232 | bins32 = DWORD 233 | bytes64 = QWORD 234 | chunks32 = DWORD 235 | bins64 = QWORD 236 | chunks64 = QWORD 237 | 238 | 239 | # Short aliases because ETOOLONGNAMES 240 | CAM = ChunkAddressingMethod 241 | CIPM = ContentIntegrityProtectionMethod 242 | LDWS = LiveDiscardWindowSize 243 | LSA = LiveSignatureAlgorithm 244 | MHTF = MerkleHashTreeFunction 245 | 246 | 247 | class ProtocolOptions(namedtuple('ProtocolOptions', ( 248 | 'version', 249 | 'minimum_version', 250 | 'swarm_identifier', 251 | 'content_integrity_protection_method', 252 | 'merkle_hash_tree_function', 253 | 'live_signature_algorithm', 254 | 'chunk_addressing_method', 255 | 'live_discard_window', 256 | 'supported_messages', 257 | 'chunk_size' 258 | ))): 259 | """Protocol options record. 260 | 261 | The payload of the HANDSHAKE message contains a sequence of protocol 262 | options. The protocol options encode the swarm metadata just 263 | described to enable an end-to-end check to see whether the peers are 264 | in the right swarm. Additionally, the options encode a number of 265 | per-peer configuration parameters. 266 | 267 | The list of protocol options MUST be sorted on code value (ascending) in 268 | a HANDSHAKE message: 269 | 270 | +--------+-------------------------------------+ 271 | | Code | Description | 272 | +========+=====================================+ 273 | | 0 | Version | 274 | +--------+-------------------------------------+ 275 | | 1 | Minimum Version | 276 | +--------+-------------------------------------+ 277 | | 2 | Swarm Identifier | 278 | +--------+-------------------------------------+ 279 | | 3 | Content Integrity Protection Method | 280 | +--------+-------------------------------------+ 281 | | 4 | Merkle Hash Tree Function | 282 | +--------+-------------------------------------+ 283 | | 5 | Live Signature Algorithm | 284 | +--------+-------------------------------------+ 285 | | 6 | Chunk Addressing Method | 286 | +--------+-------------------------------------+ 287 | | 7 | Live Discard Window | 288 | +--------+-------------------------------------+ 289 | | 8 | Supported Messages | 290 | +--------+-------------------------------------+ 291 | | 9 | Chunk Size | 292 | +--------+-------------------------------------+ 293 | | 10-254 | Unassigned | 294 | +--------+-------------------------------------+ 295 | | 255 | End Option | 296 | +--------+-------------------------------------+ 297 | 298 | 299 | .. seealso:: 300 | 301 | - :rfc:`7574#section-7` 302 | """ 303 | __slots__ = () 304 | 305 | def __new__(cls, 306 | version: Version=None, 307 | minimum_version: Version=None, 308 | swarm_identifier: bytes=None, 309 | content_integrity_protection_method: CIPM=None, 310 | merkle_hash_tree_function: MHTF=None, 311 | live_signature_algorithm: LSA=None, 312 | chunk_addressing_method: CAM=None, 313 | live_discard_window: int=None, 314 | supported_messages: set=None, 315 | chunk_size: int=None): 316 | 317 | params = {key: value for key, value in locals().items() 318 | if key not in {'cls', '__class__'}} 319 | 320 | for param, value in params.items(): 321 | if value is None: 322 | continue 323 | 324 | typespec = cls.__new__.__annotations__[param] 325 | if isinstance(value, typespec): 326 | continue 327 | 328 | try: 329 | params[param] = typespec(value) 330 | except Exception as exc: 331 | raise TypeError('{} expected to be {}, got {}'.format( 332 | param, typespec, type(value))) from exc 333 | 334 | return super().__new__(cls, **params) 335 | 336 | 337 | def decode(data): 338 | handlers = decode_handlers() 339 | offset = 0 340 | options = {key.name: None for key in ProtocolOptionsId 341 | if key is not ProtocolOptionsId.end_option} 342 | while True: 343 | option_id, offset = decode_value(data, offset, BYTE) 344 | option = ProtocolOptionsId(option_id[0]) 345 | if option is ProtocolOptionsId.end_option: 346 | break 347 | if options[option.name] is not None: 348 | raise ValueError('duplicate option {!r} provided'.format(option)) 349 | value, offset = handlers[option](data, offset, options) 350 | options[option.name] = value 351 | return ProtocolOptions(**options), data[offset:] 352 | 353 | 354 | def encode(options): 355 | data = bytearray() 356 | handlers = encode_handlers() 357 | for option_id, value in zip(ProtocolOptionsId, options): 358 | if value is None: 359 | continue 360 | value = handlers[option_id](value, options) 361 | if value is not None: 362 | data.append(option_id.value) 363 | data.extend(value) 364 | data.append(ProtocolOptionsId.end_option.value) 365 | return data 366 | 367 | 368 | def decode_handlers(): 369 | return { 370 | ProtocolOptionsId.chunk_addressing_method: 371 | decode_chunk_addressing_method, 372 | ProtocolOptionsId.chunk_size: 373 | decode_chunk_size, 374 | ProtocolOptionsId.content_integrity_protection_method: 375 | decode_content_integrity_protection_method, 376 | ProtocolOptionsId.live_discard_window: 377 | decode_live_discard_window, 378 | ProtocolOptionsId.live_signature_algorithm: 379 | decode_live_signature_algorithm, 380 | ProtocolOptionsId.merkle_hash_tree_function: 381 | decode_merkle_hash_tree_function, 382 | ProtocolOptionsId.minimum_version: 383 | decode_minimum_version, 384 | ProtocolOptionsId.supported_messages: 385 | decode_supported_messages, 386 | ProtocolOptionsId.swarm_identifier: 387 | decode_swarm_id, 388 | ProtocolOptionsId.version: 389 | decode_version, 390 | } 391 | 392 | 393 | def encode_handlers(): 394 | return { 395 | ProtocolOptionsId.chunk_addressing_method: 396 | encode_chunk_addressing_method, 397 | ProtocolOptionsId.chunk_size: 398 | encode_chunk_size, 399 | ProtocolOptionsId.content_integrity_protection_method: 400 | encode_content_integrity_protection_method, 401 | ProtocolOptionsId.live_discard_window: 402 | encode_live_discard_window, 403 | ProtocolOptionsId.live_signature_algorithm: 404 | encode_live_signature_algorithm, 405 | ProtocolOptionsId.merkle_hash_tree_function: 406 | encode_merkle_hash_tree_function, 407 | ProtocolOptionsId.minimum_version: 408 | encode_minimum_version, 409 | ProtocolOptionsId.supported_messages: 410 | encode_supported_messages, 411 | ProtocolOptionsId.swarm_identifier: 412 | encode_swarm_id, 413 | ProtocolOptionsId.version: 414 | encode_version, 415 | } 416 | 417 | 418 | def decode_version(data, offset, _): 419 | # 7.2. Version 420 | # 421 | # 0 1 422 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 423 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 424 | # |0 0 0 0 0 0 0 0| Version (8) | 425 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 426 | # 427 | value, offset = decode_value(data, offset, BYTE) 428 | return Version(value[0]), offset 429 | 430 | 431 | def encode_version(version, _): 432 | return version.value.to_bytes(BYTE, 'big') 433 | 434 | 435 | def decode_minimum_version(data, offset, _): 436 | # 7.3. Minimum Version 437 | # 438 | # 0 1 439 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 440 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 441 | # |0 0 0 0 0 0 0 1| Min. Ver. (8) | 442 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 443 | # 444 | value, offset = decode_value(data, offset, BYTE) 445 | return Version(value[0]), offset 446 | 447 | 448 | def encode_minimum_version(version, _): 449 | return version.to_bytes(BYTE, 'big') 450 | 451 | 452 | def decode_swarm_id(data, offset, _): 453 | # 7.4. Swarm Identifier 454 | # This option has the following structure: 455 | # 456 | # 0 1 2 3 457 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 458 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 459 | # |0 0 0 0 0 0 1 0| Swarm ID Length (16) | ~ 460 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 461 | # ~ Swarm Identifier (variable) ~ 462 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 463 | # 464 | # The Swarm ID Length field contains the length of the single Swarm 465 | # Identifier that follows in bytes. The Length field is 16 bits wide 466 | # to allow for large public keys as identifiers in live streaming. 467 | # 468 | swarm_id_length, offset = decode_value(data, offset, WORD) 469 | swarm_id_length = int.from_bytes(swarm_id_length, 'big') 470 | swarm_id, offset = decode_value(data, offset, swarm_id_length) 471 | return bytes(swarm_id), offset 472 | 473 | 474 | def encode_swarm_id(swarm_id: bytes, _): 475 | swarm_id_length = len(swarm_id).to_bytes(WORD, 'big') 476 | return swarm_id_length + swarm_id 477 | 478 | 479 | def decode_content_integrity_protection_method(data, offset, _): 480 | # 7.5. Content Integrity Protection Method 481 | # 482 | # 0 1 483 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 484 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 485 | # |0 0 0 0 0 0 1 1| CIPM (8) | 486 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 487 | # 488 | value, offset = decode_value(data, offset, BYTE) 489 | return ContentIntegrityProtectionMethod(value[0]), offset 490 | 491 | 492 | def encode_content_integrity_protection_method(cipm, _): 493 | return cipm.to_bytes(BYTE, 'big') 494 | 495 | 496 | def decode_merkle_hash_tree_function(data, offset, _): 497 | # 7.6. Merkle Tree Hash Function 498 | # 499 | # 0 1 500 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 501 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 502 | # |0 0 0 0 0 1 0 0| MHF (8) | 503 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 504 | # 505 | value, offset = decode_value(data, offset, BYTE) 506 | return MerkleHashTreeFunction(value[0]), offset 507 | 508 | 509 | def encode_merkle_hash_tree_function(mhtf, _): 510 | return mhtf.to_bytes(BYTE, 'big') 511 | 512 | 513 | def decode_live_signature_algorithm(data, offset, _): 514 | # 7.7. Live Signature Algorithm 515 | # 516 | # 0 1 517 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 518 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 519 | # |0 0 0 0 0 1 0 1| LSA (8) | 520 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 521 | # 522 | value, offset = decode_value(data, offset, BYTE) 523 | return LiveSignatureAlgorithm(value[0]), offset 524 | 525 | 526 | def encode_live_signature_algorithm(lsa: LSA, _): 527 | return lsa.to_bytes(BYTE, 'big') 528 | 529 | 530 | def decode_chunk_addressing_method(data, offset, _): 531 | # 7.8. Chunk Addressing Method 532 | # 533 | # 0 1 534 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 535 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 536 | # |0 0 0 0 0 1 1 0| CAM (8) | 537 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 538 | # 539 | value, offset = decode_value(data, offset, BYTE) 540 | return ChunkAddressingMethod(value[0]), offset 541 | 542 | 543 | def encode_chunk_addressing_method(cam, _): 544 | return cam.to_bytes(BYTE, 'big') 545 | 546 | 547 | def decode_live_discard_window(data, offset, options): 548 | # 7.9. Live Discard Window 549 | # The unit of the discard window depends on the chunk addressing method 550 | # used, see Table 6. For bins and chunk ranges, it is a number of chunks; 551 | # for byte ranges, it is a number of bytes. Its data type is the same as 552 | # for a bin, or one value in a range specification. In other words, its 553 | # value is a 32-bit or 64-bit integer in big-endian format. 554 | # If this option is used, the Chunk Addressing Method MUST appear before 555 | # it in the list. 556 | # 557 | # 0 1 2 3 558 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 559 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 560 | # |0 0 0 0 0 1 1 1| Live Discard Window (32 or 64) ~ 561 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 562 | # ~ ~ 563 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 564 | # 565 | method = options[ProtocolOptionsId.chunk_addressing_method.name] 566 | window_size = LiveDiscardWindowSize[method.name].value 567 | value, offset = decode_value(data, offset, window_size) 568 | return int.from_bytes(value, 'big'), offset 569 | 570 | 571 | def encode_live_discard_window(live_discard_window, options): 572 | method = options.chunk_addressing_method 573 | if method is None: 574 | return None 575 | window_size = LiveDiscardWindowSize[method.name].value 576 | return live_discard_window.to_bytes(window_size, 'big') 577 | 578 | 579 | def decode_supported_messages(data, offset, _): 580 | # 7.10. Supported Messages 581 | # The set of messages supported can be derived from the compressed 582 | # bitmap by padding it with bytes of value 0 until it is 256 bits in 583 | # length. Then, a 1 bit in the resulting bitmap at position X 584 | # (numbering left to right) corresponds to support for message type X, 585 | # see Table 7. In other words, to construct the compressed bitmap, 586 | # create a bitmap with a 1 for each message type supported and a 0 for 587 | # a message type that is not, store it as an array of bytes, and 588 | # truncate it to the last non-zero byte. An example of the first 16 589 | # bits of the compressed bitmap for a peer supporting every message 590 | # except ACKs and PEXs is 11011001 11110000. 591 | # 592 | # 0 1 2 3 593 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 594 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 595 | # |0 0 0 0 1 0 0 0| SupMsgLen (8) | ~ 596 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 597 | # ~ Supported Messages Bitmap (variable, max 256) ~ 598 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 599 | # 600 | message_types_length, offset = decode_value(data, offset, BYTE) 601 | message_types_length = int.from_bytes(message_types_length, 'big') 602 | all_message_types = list(MessageType) 603 | supported_messages = set() 604 | for n in range(message_types_length): 605 | value, offset = decode_value(data, offset, BYTE) 606 | value = int.from_bytes(value, 'big') 607 | message_types = all_message_types[n * 8: n * 8 + 8] 608 | mapping = list(zip(map(int, bin(value)[2:]), message_types)) 609 | supported_messages |= {msgtype for bit, msgtype in mapping if bit} 610 | return supported_messages, offset 611 | 612 | 613 | def encode_supported_messages(messages, _): 614 | mask = [1 if msgtype in messages else 0 for msgtype in MessageType] 615 | fits = len(mask) % 8 == 0 616 | mask += [0] * (0 if fits else 8 - len(mask) % 8) 617 | supported_messages = int(''.join(map(str, mask)), 2) 618 | supported_messages = supported_messages.to_bytes(len(mask) // 8, 'big') 619 | supported_messages_length = len(supported_messages).to_bytes(BYTE, 'big') 620 | return supported_messages_length + supported_messages 621 | 622 | 623 | def decode_chunk_size(data, offset, _): 624 | # 7.11. Chunk Size 625 | # Its value is a 32-bit integer denoting the size of the chunks in bytes 626 | # in big-endian format. When variable chunk sizes are used, this option 627 | # MUST be set to the special value 0xFFFFFFFF. Section 8.1 explains how 628 | # content publishers can determine a value for this option. 629 | # 630 | # 0 1 2 3 631 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 632 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 633 | # |0 0 0 0 1 0 0 1| Chunk Size (32) ~ 634 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 635 | # ~ | 636 | # +-+-+-+-+-+-+-+-+ 637 | # 638 | value, offset = decode_value(data, offset, DWORD) 639 | return int.from_bytes(value, 'big'), offset 640 | 641 | 642 | def encode_chunk_size(chunk_size, _): 643 | return chunk_size.to_bytes(DWORD, 'big') 644 | 645 | 646 | def decode_value(data, offset, length): 647 | value = data[offset:offset + length] 648 | if len(value) != length: 649 | raise ValueError('Expected read {} bytes, got only {}' 650 | ''.format(length, len(value))) 651 | return value, offset + length 652 | --------------------------------------------------------------------------------