Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jlaine committed Feb 5, 2019
0 parents commit a80c3bd
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitattributes
@@ -0,0 +1 @@
*.bin binary
8 changes: 8 additions & 0 deletions .gitignore
@@ -0,0 +1,8 @@
*.egg-info
*.pyc
.coverage
.eggs
.vscode
/build
/dist
/docs/_build
6 changes: 6 additions & 0 deletions .travis.yml
@@ -0,0 +1,6 @@
dist: xenial
install: pip3 install coverage flake8
language: python
python: "3.7"
script: .travis/script
sudo: true
15 changes: 15 additions & 0 deletions .travis/script
@@ -0,0 +1,15 @@
#!/bin/sh

set -e

if [ "$BUILD" = "sdist" ]; then
python3 setup.py sdist bdist_wheel
if [ -n "$TRAVIS_TAG" ]; then
pip3 install pyopenssl twine
python3 -m twine upload --skip-existing dist/*
fi
else
flake8 aioquic tests
coverage run setup.py test
curl -s https://codecov.io/bash | bash
fi
17 changes: 17 additions & 0 deletions README.rst
@@ -0,0 +1,17 @@
aioquic
=======

|travis| |codecov|

.. |travis| image:: https://img.shields.io/travis/com/aiortc/aioquic.svg
:target: https://travis-ci.com/aiortc/aioquic

.. |codecov| image:: https://img.shields.io/codecov/c/github/aiortc/aioquic.svg
:target: https://codecov.io/gh/aiortc/aioquic

What is ``aioquic``?
--------------------

``aioquic`` is a library for Quick UDP Internet Connections (QUIC) in Python.
It is built on top of ``asyncio``, Python's standard asynchronous I/O
framework.
Empty file added aioquic/__init__.py
Empty file.
79 changes: 79 additions & 0 deletions aioquic/packet.py
@@ -0,0 +1,79 @@
from dataclasses import dataclass
from struct import unpack_from

PACKET_LONG_HEADER = 0x80
PACKET_FIXED_BIT = 0x40

PACKET_TYPE_INITIAL = PACKET_LONG_HEADER | PACKET_FIXED_BIT | 0x00
PACKET_TYPE_0RTT = PACKET_LONG_HEADER | PACKET_FIXED_BIT | 0x10
PACKET_TYPE_HANDSHAKE = PACKET_LONG_HEADER | PACKET_FIXED_BIT | 0x20
PACKET_TYPE_RETRY = PACKET_LONG_HEADER | PACKET_FIXED_BIT | 0x30
PACKET_TYPE_MASK = 0xf0

PROTOCOL_VERSION = 0xFF000011 # draft 17

VARIABLE_LENGTH_FORMATS = [
(1, '!B', 0x3f),
(2, '!H', 0x3fff),
(4, '!L', 0x3fffffff),
(8, '!Q', 0x3fffffffffffffff),
]


def decode_cid_length(length):
return length + 3 if length else 0


def unpack_variable_length(data, pos=0):
kind = data[pos] // 64
length, fmt, mask = VARIABLE_LENGTH_FORMATS[kind]
return unpack_from(fmt, data, pos)[0] & mask, pos + length


@dataclass
class QuicHeader:
version: int
destination_cid: bytes
source_cid: bytes
token: bytes = b''

@classmethod
def parse(cls, data):
datagram_length = len(data)
if datagram_length < 2:
raise ValueError('Packet is too short (%d bytes)' % datagram_length)

first_byte = data[0]
if first_byte & PACKET_LONG_HEADER:
if datagram_length < 6:
raise ValueError('Long header is too short (%d bytes)' % datagram_length)

version, cid_lengths = unpack_from('!LB', data, 1)
pos = 6

destination_cid_length = decode_cid_length(cid_lengths // 16)
destination_cid = data[pos:pos + destination_cid_length]
pos += destination_cid_length

source_cid_length = decode_cid_length(cid_lengths % 16)
source_cid = data[pos:pos + source_cid_length]
pos += source_cid_length

packet_type = first_byte & PACKET_TYPE_MASK
if packet_type == PACKET_TYPE_INITIAL:
token_length, pos = unpack_variable_length(data, pos)
token = data[pos:pos + token_length]
pos += token_length

length, pos = unpack_variable_length(data, pos)
else:
raise ValueError('Long header packet type 0x%x is not supported' % packet_type)

return QuicHeader(
version=version,
destination_cid=destination_cid,
source_cid=source_cid,
token=token)
else:
# short header packet
raise ValueError('Short header is not supported yet')
5 changes: 5 additions & 0 deletions setup.cfg
@@ -0,0 +1,5 @@
[coverage:run]
source = aioquic

[flake8]
max-line-length=100
32 changes: 32 additions & 0 deletions setup.py
@@ -0,0 +1,32 @@
import os.path

import setuptools

root_dir = os.path.abspath(os.path.dirname(__file__))
readme_file = os.path.join(root_dir, 'README.rst')
with open(readme_file, encoding='utf-8') as f:
long_description = f.read()

setuptools.setup(
name='aioquic',
version='0.0.1',
description='An implementation of QUIC',
long_description=long_description,
url='https://github.com/aiortc/aioquic',
author='Jeremy Lainé',
author_email='jeremy.laine@m4x.org',
license='BSD',
classifiers=[
'Development Status :: 1 - Planning',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
],
packages=['aioquic'],
)
Empty file added tests/__init__.py
Empty file.
Binary file added tests/initial_client.bin
Binary file not shown.
Binary file added tests/initial_server.bin
Binary file not shown.
67 changes: 67 additions & 0 deletions tests/test_packet.py
@@ -0,0 +1,67 @@
import binascii
from unittest import TestCase

from aioquic.packet import QuicHeader, unpack_variable_length

from .utils import load


class UtilTest(TestCase):
def test_unpack_variable_length(self):
# 1 byte
self.assertEqual(unpack_variable_length(b'\x00'), (0, 1))
self.assertEqual(unpack_variable_length(b'\x01'), (1, 1))
self.assertEqual(unpack_variable_length(b'\x25'), (37, 1))
self.assertEqual(unpack_variable_length(b'\x3f'), (63, 1))

# 2 bytes
self.assertEqual(unpack_variable_length(b'\x7b\xbd'), (15293, 2))
self.assertEqual(unpack_variable_length(b'\x7f\xff'), (16383, 2))

# 4 bytes
self.assertEqual(unpack_variable_length(b'\x9d\x7f\x3e\x7d'), (494878333, 4))
self.assertEqual(unpack_variable_length(b'\xbf\xff\xff\xff'), (1073741823, 4))

# 8 bytes
self.assertEqual(unpack_variable_length(b'\xc2\x19\x7c\x5e\xff\x14\xe8\x8c'),
(151288809941952652, 8))
self.assertEqual(unpack_variable_length(b'\xff\xff\xff\xff\xff\xff\xff\xff'),
(4611686018427387903, 8))


class PacketTest(TestCase):
def test_parse_initial_client(self):
data = load('initial_client.bin')
header = QuicHeader.parse(data)
self.assertEqual(header.version, 0xff000011)
self.assertEqual(header.destination_cid, binascii.unhexlify('90ed1e1c7b04b5d3'))
self.assertEqual(header.source_cid, b'')
self.assertEqual(header.token, b'')

def test_parse_initial_server(self):
data = load('initial_server.bin')
header = QuicHeader.parse(data)
self.assertEqual(header.version, 0xff000011)
self.assertEqual(header.destination_cid, b'')
self.assertEqual(header.source_cid, binascii.unhexlify('0fcee9852fde8780'))
self.assertEqual(header.token, b'')

def test_parse_long_header_bad_packet_type(self):
with self.assertRaises(ValueError) as cm:
QuicHeader.parse(b'\x80\x00\x00\x00\x00\x00')
self.assertEqual(str(cm.exception), 'Long header packet type 0x80 is not supported')

def test_parse_long_header_too_short(self):
with self.assertRaises(ValueError) as cm:
QuicHeader.parse(b'\x80\x00')
self.assertEqual(str(cm.exception), 'Long header is too short (2 bytes)')

def test_parse_short_header(self):
with self.assertRaises(ValueError) as cm:
QuicHeader.parse(b'\x00\x00')
self.assertEqual(str(cm.exception), 'Short header is not supported yet')

def test_parse_too_short_header(self):
with self.assertRaises(ValueError) as cm:
QuicHeader.parse(b'\x00')
self.assertEqual(str(cm.exception), 'Packet is too short (1 bytes)')
7 changes: 7 additions & 0 deletions tests/utils.py
@@ -0,0 +1,7 @@
import os


def load(name):
path = os.path.join(os.path.dirname(__file__), name)
with open(path, 'rb') as fp:
return fp.read()

0 comments on commit a80c3bd

Please sign in to comment.