├── quic_version_detector ├── __init__.py ├── cli.py ├── net.py ├── quic.py └── main.py ├── setup.cfg ├── .gitignore ├── usage.gif ├── requirements └── dev.txt ├── .travis.yml ├── README.rst ├── Makefile ├── setup.py ├── tests └── test_net_utils.py ├── CHANGELOG.rst └── LICENSE.txt /quic_version_detector/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | pyenv/ 3 | **/*.pyc 4 | .coverage 5 | -------------------------------------------------------------------------------- /usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/povilasb/quic-version-detector/HEAD/usage.gif -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | pytest==2.7.2 2 | pytest-describe==0.10.3 3 | pyhamcrest==1.8.5 4 | mock==1.3.0 5 | coverage==4.0.3 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.6" 5 | 6 | script: 7 | - pip install -r requirements/dev.txt 8 | - make test 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | About 3 | ===== 4 | 5 | This is a small tool that queries QUIC servers for their supported versions. 6 | 7 | It's implemented in python3. 8 | 9 | .. image:: usage.gif 10 | 11 | 12 | Installation 13 | ============ 14 | 15 | Global:: 16 | 17 | $ pip install quic-version-detector 18 | 19 | Or inside virtual environment:: 20 | 21 | $ virtualenv --python=python3 pyenv 22 | $ pyenv/bin/pip install quic-version-detector 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | python := python3 2 | src_dir := quic_version_detector 3 | 4 | virtualenv_dir := pyenv 5 | pip := $(virtualenv_dir)/bin/pip 6 | pytest := $(virtualenv_dir)/bin/py.test 7 | coverage := $(virtualenv_dir)/bin/coverage 8 | 9 | 10 | test: $(virtualenv_dir) 11 | PYTHONPATH=$(PYTHONPATH):. $(coverage) run \ 12 | --source $(src_dir) $(pytest) -s tests 13 | $(coverage) report -m 14 | .PHONY: test 15 | 16 | $(virtualenv_dir): requirements/dev.txt 17 | virtualenv $@ --python=$(python) 18 | $(pip) install -r $^ 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='quic-version-detector', 5 | version='0.2.0', 6 | description='Simple tool to query QUIC servers for their supported versions.', 7 | long_description=open('README.rst').read(), 8 | url='https://github.com/povilasb/quic-version-detector', 9 | author='Povilas Balciunas', 10 | author_email='balciunas90@gmail.com', 11 | license='MIT', 12 | packages=['quic_version_detector'], 13 | entry_points = { 14 | 'console_scripts': ['quicver = quic_version_detector.main:main'] 15 | }, 16 | zip_safe=False 17 | ) 18 | -------------------------------------------------------------------------------- /tests/test_net_utils.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, is_ 2 | from mock import ANY 3 | 4 | import quic_version_detector.net as net 5 | 6 | 7 | def describe_parse_hostname_ip(): 8 | def describe_when_address_info_is_an_empty_array(): 9 | def it_returns_none(): 10 | assert_that(net.parse_hostname_ip([]), is_(None)) 11 | 12 | def describe_when_address_info_is_an_array_which_has_address_information_tuple(): 13 | def it_returns_extracted_ip_address_from_that_tuple(): 14 | ip = net.parse_hostname_ip([(ANY, ANY, ANY, ANY, ('1.2.3.4', ANY))]) 15 | 16 | assert_that(ip, is_('1.2.3.4')) 17 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Change Log 3 | ========== 4 | 5 | All notable changes to this project will be documented in this file. 6 | This project adheres to `Semantic Versioning `_. 7 | 8 | [0.2.0] - 2016-07-08 9 | ==================== 10 | 11 | Added 12 | ----- 13 | 14 | * Request timeout. Version detector won't wait for response forever anymore. 15 | 16 | Changed 17 | ------- 18 | 19 | * Use asyncio to send UDP requests. 20 | * Send multiple QUIC queries - should improve the possibility to get a response. 21 | 22 | [0.1.1] - 2016-05-31 23 | ==================== 24 | 25 | Added 26 | ----- 27 | 28 | * Initial working version. 29 | -------------------------------------------------------------------------------- /quic_version_detector/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | def parse_args(args): 5 | """Parses CLI arguments. 6 | 7 | Args: 8 | args (list): usually this will be sys.argv. 9 | description (string): Text to display before the argument help. 10 | 11 | Returns: 12 | Namespace: with host and port fields. 13 | """ 14 | parser = argparse.ArgumentParser() 15 | parser.add_argument( 16 | metavar='HOSTNAME', 17 | dest='host', 18 | nargs='?', 19 | default='127.0.0.1', 20 | type=str, 21 | help='Server hostname or address.', 22 | ) 23 | parser.add_argument( 24 | metavar='PORT', 25 | dest='port', 26 | nargs='?', 27 | default=443, 28 | type=int, 29 | help='QUIC server port.', 30 | ) 31 | 32 | return parser.parse_args(args=args) 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Povilas Balciunas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /quic_version_detector/net.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from . import quic 4 | 5 | 6 | BIND_PORT = 5467 7 | RECV_PACKET_SIZE = 1400 8 | 9 | 10 | def send_recv_packet(addr: str, port: int, packet: quic.Packet) -> bytes: 11 | """Sends a UDP packet and waits for response. 12 | 13 | Returns: 14 | response from the server. 15 | """ 16 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 17 | sock.bind(('0.0.0.0', BIND_PORT)) 18 | sock.sendto(packet.to_buff(), (addr, port)) 19 | buff, _ = sock.recvfrom(RECV_PACKET_SIZE) 20 | return buff 21 | 22 | 23 | def parse_hostname_ip(addrinfo) -> str: 24 | """Gets IP address from sock.getaddrinfo result. 25 | 26 | Returns: 27 | IP address to which some hostname resolves. 28 | """ 29 | if len(addrinfo) == 0: 30 | return None 31 | 32 | _, _, _, _, socket_addr = addrinfo[0] 33 | ip_addr, _ = socket_addr 34 | 35 | return ip_addr 36 | 37 | 38 | def resolve_hostname(hostname: str, port: int=None) -> str: 39 | """DNS resolve hostname. 40 | 41 | Args: 42 | hostname: hostname to get IP address for. 43 | port: optional. Used to hint what DNS entry we're looking 44 | for. 45 | 46 | Returns: 47 | IP address used to connect to the specified hostname. 48 | """ 49 | try: 50 | return parse_hostname_ip( 51 | socket.getaddrinfo(hostname, port, socket.AF_INET, socket.SOCK_DGRAM) 52 | ) 53 | except socket.gaierror: 54 | return None 55 | -------------------------------------------------------------------------------- /quic_version_detector/quic.py: -------------------------------------------------------------------------------- 1 | """QUIC protocol related facilities. 2 | 3 | See "QUIC Wire Layout Specification": 4 | https://docs.google.com/document/d/1WJvyZflAO2pq77yOLbp9NsGjC1CHetAXV8I0fQe-B_U/edit#heading=h.qnqgv8t864a6 5 | """ 6 | 7 | import random 8 | 9 | 10 | class Packet: 11 | """QUIC packet class. 12 | 13 | Used to send queries to server. 14 | """ 15 | 16 | def __init__(self, public_flags: bytes, connection_id: bytes, 17 | version: bytes) -> None: 18 | self.public_flags = public_flags 19 | self.connection_id = connection_id 20 | self.version = version 21 | 22 | def to_buff(self) -> bytes: 23 | """ 24 | Returns: 25 | QUIC packet encoded as bytes. 26 | """ 27 | return self.public_flags + \ 28 | self.connection_id + self.version + bytes.fromhex('01') 29 | 30 | 31 | class VersionNegotationPacket: 32 | """Used to hold data for recieved version negotation packets.""" 33 | 34 | def __init__(self, public_flags, connection_id, supported_versions): 35 | self.public_flags = public_flags 36 | self.connection_id = connection_id 37 | self.supported_versions = supported_versions 38 | 39 | 40 | def parse_response(buff: bytes) -> VersionNegotationPacket: 41 | """Parses QUIC response. 42 | 43 | Args: 44 | buff: data received from the server - UDP packet. 45 | """ 46 | versions = buff[9:] 47 | supported_versions = [versions[i:i+4].decode('ascii') \ 48 | for i in range(0, len(versions), 4)] 49 | 50 | return VersionNegotationPacket( 51 | public_flags=int(buff[0]), 52 | connection_id=str(buff[1:9]), 53 | supported_versions=supported_versions, 54 | ) 55 | 56 | 57 | def dummy_version_packet() -> Packet: 58 | """Constructs a packet with a dummy version. 59 | 60 | Such packet makes the server return "Version Negotation Packet". 61 | 62 | Returns: 63 | quic.Packet 64 | """ 65 | connection_id = bytes([random.getrandbits(8) for _ in range(8)]) 66 | return Packet(public_flags=bytes.fromhex('0d'), 67 | connection_id=connection_id, 68 | version=bytes.fromhex('0a0a0a0a')) 69 | -------------------------------------------------------------------------------- /quic_version_detector/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import asyncio 3 | 4 | from quic_version_detector import quic, net, cli 5 | 6 | 7 | def print_results( 8 | host: str, port: int, 9 | version_negotiation_packet: quic.VersionNegotationPacket) -> None: 10 | """Prints retrieved results. 11 | 12 | Args: 13 | host: queried hostname. 14 | port: queried port. 15 | version_negotation_packet: received packet. 16 | """ 17 | print('"{}:{}" supported versions:'.format(host, port)) 18 | for version in version_negotiation_packet.supported_versions: 19 | print(' ', version) 20 | 21 | 22 | class UdpHandler: 23 | query_count = 10 24 | 25 | def __init__(self, target_hostname: str, target_port: int) -> None: 26 | self.target_hostname = target_hostname 27 | self.target_port = target_port 28 | 29 | def connection_made(self, transport) -> None: 30 | self.transport = transport 31 | 32 | for _ in range(self.query_count): 33 | self.transport.sendto(quic.dummy_version_packet().to_buff()) 34 | 35 | def datagram_received(self, data, addr) -> None: 36 | print_results( 37 | self.target_hostname, 38 | self.target_port, 39 | quic.parse_response(data), 40 | ) 41 | 42 | self.transport.close() 43 | 44 | def error_received(self, transport) -> None: 45 | print('Error received:', transport) 46 | 47 | def connection_lost(self, transport) -> None: 48 | loop = asyncio.get_event_loop() 49 | loop.stop() 50 | 51 | 52 | def stop_event_loop(event_loop, timeout: float) -> None: 53 | """Terminates event loop after the specified timeout.""" 54 | def timeout_handler(): 55 | event_loop.stop() 56 | 57 | print('Timeout\nExiting...') 58 | event_loop.call_later(timeout, timeout_handler) 59 | 60 | 61 | def main() -> None: 62 | """Main entry point.""" 63 | args = cli.parse_args(sys.argv[1:]) 64 | 65 | server_addr = net.resolve_hostname(args.host) 66 | 67 | event_loop = asyncio.get_event_loop() 68 | 69 | connect = event_loop.create_datagram_endpoint( 70 | lambda: UdpHandler(args.host, args.port), 71 | remote_addr=(server_addr, args.port) 72 | ) 73 | event_loop.run_until_complete(connect) 74 | 75 | stop_event_loop(event_loop, 10) 76 | event_loop.run_forever() 77 | 78 | 79 | if __name__ == '__main__': 80 | main() 81 | --------------------------------------------------------------------------------