Skip to content

Instantly share code, notes, and snippets.

@Lukasa
Created June 1, 2015 13:42
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Lukasa/57bd9a7332991821329d to your computer and use it in GitHub Desktop.
Save Lukasa/57bd9a7332991821329d to your computer and use it in GitHub Desktop.
Basic Twisted HTTP/2 client.

Simple Twisted H2 Client

This gist contains a really stupid implementation of a Twisted HTTP/2 client. It opens a connection, uses ALPN/NPN to negotiate HTTP/2, and then sends a single HTTP/2 GET request to the /ip endpoint.

To execute this:

  1. Create a clean virtual environment.
  2. Install the requirements: pip install -r requirements.txt.
  3. Install the development version of Twisted that contains the nextProtocols code.
  4. Execute the code using python h2test.py.

If everything has gone correctly, you should see output that looks like this:

Received settings frame
Received headers: [(u':status', u'200'), (u'server', u'h2o/1.1.1'), (u'date', u'Mon, 01 Jun 2015 13:38:28 GMT'), (u'content-type', u'application/json'), (u'access-control-allow-origin', u'*'), (u'access-control-allow-credentials', u'true'), (u'x-clacks-overhead', u'GNU Terry Pratchett')]
Received data: {
  "origin": "85.255.232.146"
}

import hpack.hpack
from hyperframe.frame import (
SettingsFrame, HeadersFrame, Frame, DataFrame, GoAwayFrame
)
from twisted.internet import ssl, protocol, defer, endpoints, reactor, task
def decodeFrame(frame_data):
f, length = Frame.parse_frame_header(frame_data[:9])
f.parse_body(memoryview(frame_data[9:9 + length]))
return f, length+9
def getFrames(data):
while data:
f, consumed = decodeFrame(data)
data = data[consumed:]
yield f
def main(reactor):
e = hpack.hpack.Encoder()
d = hpack.hpack.Decoder()
options = ssl.optionsForClientTLS(
hostname=u'http2bin.org',
extraCertificateOptions={'nextProtocols': [b'h2', b'http/1.1']}
)
class BasicH2Request(protocol.Protocol):
def connectionMade(self):
self.initial_read = False
self.complete = defer.Deferred()
# Write the HTTP/2 preamble.
self.transport.write(b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n')
f = SettingsFrame(0)
f.settings[SettingsFrame.ENABLE_PUSH] = 0
self.transport.write(f.serialize())
def dataReceived(self, data):
assert self.transport.getNextProtocol() == 'h2'
for f in getFrames(data):
# If this is a settings frame with no ACK, send back an ACK.
if isinstance(f, SettingsFrame):
if 'ACK' not in f.flags:
print "Received settings frame"
f = SettingsFrame(0)
f.flags.add('ACK')
self.transport.write(f.serialize())
# Send the request.
self.sendRequest()
elif isinstance(f, HeadersFrame):
headers = d.decode(f.data)
print "Received headers: %s" % headers
elif isinstance(f, DataFrame):
print "Received data: %s" % f.data
elif isinstance(f, GoAwayFrame):
print "Go away: error code %s, data %s" % (
f.error_code, f.additional_data
)
if 'END_STREAM' in f.flags:
self.transport.loseConnection()
self.complete.callback(None)
self.complete = None
break
def connectionLost(self, reason):
if self.complete is not None:
self.complete.callback(None)
def sendRequest(self):
headers = [
(':method', 'GET'),
(':authority', 'http2bin.org'),
(':path', '/ip'),
(':scheme', 'https'),
('user-agent', 'Twisted/15.2.0')
]
f = HeadersFrame(1)
f.data = e.encode(headers)
f.flags = set(['END_HEADERS', 'END_STREAM'])
self.transport.write(f.serialize())
return endpoints.connectProtocol(
endpoints.SSL4ClientEndpoint(reactor, 'http2bin.org', 443, options),
BasicH2Request()
).addCallback(lambda protocol: protocol.complete)
task.react(main)
cffi==1.1.0
characteristic==14.3.0
cryptography==0.9
enum34==1.0.4
hpack==1.0.1
hyperframe==1.0.0
idna==2.0
ipaddress==1.0.7
pyasn1==0.1.7
pyasn1-modules==0.0.5
pycparser==2.13
pyOpenSSL==0.15.1
service-identity==14.0.0
six==1.9.0
wheel==0.24.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment