Skip to content

Commit a80c3bd

Browse files
committed
Initial commit
0 parents  commit a80c3bd

14 files changed

+237
-0
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.bin binary

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
*.egg-info
2+
*.pyc
3+
.coverage
4+
.eggs
5+
.vscode
6+
/build
7+
/dist
8+
/docs/_build

.travis.yml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
dist: xenial
2+
install: pip3 install coverage flake8
3+
language: python
4+
python: "3.7"
5+
script: .travis/script
6+
sudo: true

.travis/script

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/sh
2+
3+
set -e
4+
5+
if [ "$BUILD" = "sdist" ]; then
6+
python3 setup.py sdist bdist_wheel
7+
if [ -n "$TRAVIS_TAG" ]; then
8+
pip3 install pyopenssl twine
9+
python3 -m twine upload --skip-existing dist/*
10+
fi
11+
else
12+
flake8 aioquic tests
13+
coverage run setup.py test
14+
curl -s https://codecov.io/bash | bash
15+
fi

README.rst

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
aioquic
2+
=======
3+
4+
|travis| |codecov|
5+
6+
.. |travis| image:: https://img.shields.io/travis/com/aiortc/aioquic.svg
7+
:target: https://travis-ci.com/aiortc/aioquic
8+
9+
.. |codecov| image:: https://img.shields.io/codecov/c/github/aiortc/aioquic.svg
10+
:target: https://codecov.io/gh/aiortc/aioquic
11+
12+
What is ``aioquic``?
13+
--------------------
14+
15+
``aioquic`` is a library for Quick UDP Internet Connections (QUIC) in Python.
16+
It is built on top of ``asyncio``, Python's standard asynchronous I/O
17+
framework.

aioquic/__init__.py

Whitespace-only changes.

aioquic/packet.py

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from dataclasses import dataclass
2+
from struct import unpack_from
3+
4+
PACKET_LONG_HEADER = 0x80
5+
PACKET_FIXED_BIT = 0x40
6+
7+
PACKET_TYPE_INITIAL = PACKET_LONG_HEADER | PACKET_FIXED_BIT | 0x00
8+
PACKET_TYPE_0RTT = PACKET_LONG_HEADER | PACKET_FIXED_BIT | 0x10
9+
PACKET_TYPE_HANDSHAKE = PACKET_LONG_HEADER | PACKET_FIXED_BIT | 0x20
10+
PACKET_TYPE_RETRY = PACKET_LONG_HEADER | PACKET_FIXED_BIT | 0x30
11+
PACKET_TYPE_MASK = 0xf0
12+
13+
PROTOCOL_VERSION = 0xFF000011 # draft 17
14+
15+
VARIABLE_LENGTH_FORMATS = [
16+
(1, '!B', 0x3f),
17+
(2, '!H', 0x3fff),
18+
(4, '!L', 0x3fffffff),
19+
(8, '!Q', 0x3fffffffffffffff),
20+
]
21+
22+
23+
def decode_cid_length(length):
24+
return length + 3 if length else 0
25+
26+
27+
def unpack_variable_length(data, pos=0):
28+
kind = data[pos] // 64
29+
length, fmt, mask = VARIABLE_LENGTH_FORMATS[kind]
30+
return unpack_from(fmt, data, pos)[0] & mask, pos + length
31+
32+
33+
@dataclass
34+
class QuicHeader:
35+
version: int
36+
destination_cid: bytes
37+
source_cid: bytes
38+
token: bytes = b''
39+
40+
@classmethod
41+
def parse(cls, data):
42+
datagram_length = len(data)
43+
if datagram_length < 2:
44+
raise ValueError('Packet is too short (%d bytes)' % datagram_length)
45+
46+
first_byte = data[0]
47+
if first_byte & PACKET_LONG_HEADER:
48+
if datagram_length < 6:
49+
raise ValueError('Long header is too short (%d bytes)' % datagram_length)
50+
51+
version, cid_lengths = unpack_from('!LB', data, 1)
52+
pos = 6
53+
54+
destination_cid_length = decode_cid_length(cid_lengths // 16)
55+
destination_cid = data[pos:pos + destination_cid_length]
56+
pos += destination_cid_length
57+
58+
source_cid_length = decode_cid_length(cid_lengths % 16)
59+
source_cid = data[pos:pos + source_cid_length]
60+
pos += source_cid_length
61+
62+
packet_type = first_byte & PACKET_TYPE_MASK
63+
if packet_type == PACKET_TYPE_INITIAL:
64+
token_length, pos = unpack_variable_length(data, pos)
65+
token = data[pos:pos + token_length]
66+
pos += token_length
67+
68+
length, pos = unpack_variable_length(data, pos)
69+
else:
70+
raise ValueError('Long header packet type 0x%x is not supported' % packet_type)
71+
72+
return QuicHeader(
73+
version=version,
74+
destination_cid=destination_cid,
75+
source_cid=source_cid,
76+
token=token)
77+
else:
78+
# short header packet
79+
raise ValueError('Short header is not supported yet')

setup.cfg

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[coverage:run]
2+
source = aioquic
3+
4+
[flake8]
5+
max-line-length=100

setup.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import os.path
2+
3+
import setuptools
4+
5+
root_dir = os.path.abspath(os.path.dirname(__file__))
6+
readme_file = os.path.join(root_dir, 'README.rst')
7+
with open(readme_file, encoding='utf-8') as f:
8+
long_description = f.read()
9+
10+
setuptools.setup(
11+
name='aioquic',
12+
version='0.0.1',
13+
description='An implementation of QUIC',
14+
long_description=long_description,
15+
url='https://github.com/aiortc/aioquic',
16+
author='Jeremy Lainé',
17+
author_email='[email protected]',
18+
license='BSD',
19+
classifiers=[
20+
'Development Status :: 1 - Planning',
21+
'Environment :: Web Environment',
22+
'Intended Audience :: Developers',
23+
'License :: OSI Approved :: BSD License',
24+
'Operating System :: OS Independent',
25+
'Programming Language :: Python',
26+
'Programming Language :: Python :: 3',
27+
'Programming Language :: Python :: 3.5',
28+
'Programming Language :: Python :: 3.6',
29+
'Programming Language :: Python :: 3.7',
30+
],
31+
packages=['aioquic'],
32+
)

tests/__init__.py

Whitespace-only changes.

tests/initial_client.bin

1.25 KB
Binary file not shown.

tests/initial_server.bin

1.23 KB
Binary file not shown.

tests/test_packet.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import binascii
2+
from unittest import TestCase
3+
4+
from aioquic.packet import QuicHeader, unpack_variable_length
5+
6+
from .utils import load
7+
8+
9+
class UtilTest(TestCase):
10+
def test_unpack_variable_length(self):
11+
# 1 byte
12+
self.assertEqual(unpack_variable_length(b'\x00'), (0, 1))
13+
self.assertEqual(unpack_variable_length(b'\x01'), (1, 1))
14+
self.assertEqual(unpack_variable_length(b'\x25'), (37, 1))
15+
self.assertEqual(unpack_variable_length(b'\x3f'), (63, 1))
16+
17+
# 2 bytes
18+
self.assertEqual(unpack_variable_length(b'\x7b\xbd'), (15293, 2))
19+
self.assertEqual(unpack_variable_length(b'\x7f\xff'), (16383, 2))
20+
21+
# 4 bytes
22+
self.assertEqual(unpack_variable_length(b'\x9d\x7f\x3e\x7d'), (494878333, 4))
23+
self.assertEqual(unpack_variable_length(b'\xbf\xff\xff\xff'), (1073741823, 4))
24+
25+
# 8 bytes
26+
self.assertEqual(unpack_variable_length(b'\xc2\x19\x7c\x5e\xff\x14\xe8\x8c'),
27+
(151288809941952652, 8))
28+
self.assertEqual(unpack_variable_length(b'\xff\xff\xff\xff\xff\xff\xff\xff'),
29+
(4611686018427387903, 8))
30+
31+
32+
class PacketTest(TestCase):
33+
def test_parse_initial_client(self):
34+
data = load('initial_client.bin')
35+
header = QuicHeader.parse(data)
36+
self.assertEqual(header.version, 0xff000011)
37+
self.assertEqual(header.destination_cid, binascii.unhexlify('90ed1e1c7b04b5d3'))
38+
self.assertEqual(header.source_cid, b'')
39+
self.assertEqual(header.token, b'')
40+
41+
def test_parse_initial_server(self):
42+
data = load('initial_server.bin')
43+
header = QuicHeader.parse(data)
44+
self.assertEqual(header.version, 0xff000011)
45+
self.assertEqual(header.destination_cid, b'')
46+
self.assertEqual(header.source_cid, binascii.unhexlify('0fcee9852fde8780'))
47+
self.assertEqual(header.token, b'')
48+
49+
def test_parse_long_header_bad_packet_type(self):
50+
with self.assertRaises(ValueError) as cm:
51+
QuicHeader.parse(b'\x80\x00\x00\x00\x00\x00')
52+
self.assertEqual(str(cm.exception), 'Long header packet type 0x80 is not supported')
53+
54+
def test_parse_long_header_too_short(self):
55+
with self.assertRaises(ValueError) as cm:
56+
QuicHeader.parse(b'\x80\x00')
57+
self.assertEqual(str(cm.exception), 'Long header is too short (2 bytes)')
58+
59+
def test_parse_short_header(self):
60+
with self.assertRaises(ValueError) as cm:
61+
QuicHeader.parse(b'\x00\x00')
62+
self.assertEqual(str(cm.exception), 'Short header is not supported yet')
63+
64+
def test_parse_too_short_header(self):
65+
with self.assertRaises(ValueError) as cm:
66+
QuicHeader.parse(b'\x00')
67+
self.assertEqual(str(cm.exception), 'Packet is too short (1 bytes)')

tests/utils.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import os
2+
3+
4+
def load(name):
5+
path = os.path.join(os.path.dirname(__file__), name)
6+
with open(path, 'rb') as fp:
7+
return fp.read()

0 commit comments

Comments
 (0)