Skip to content

Instantly share code, notes, and snippets.

@adiroiban
Forked from Lukasa/README.md
Created June 8, 2015 08:59
Show Gist options
  • Save adiroiban/a88e7127671ca24647c8 to your computer and use it in GitHub Desktop.
Save adiroiban/a88e7127671ca24647c8 to your computer and use it in GitHub Desktop.

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 OpenSSL import SSL
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']}
)
def info_callback(conn, where, ret):
# conn is a OpenSSL.SSL.Connection
# where is a set of flags telling where in the handshake we are
# http://www.openssl.org/docs/ssl/SSL_CTX_set_info_callback.html
if where & SSL.SSL_CB_HANDSHAKE_START:
conn.set_app_data({'handshake_done': False})
print('handshake start')
if where & SSL.SSL_CB_HANDSHAKE_DONE:
conn.set_app_data({'handshake_done': True})
print('handshake done')
options._ctx.set_info_callback(info_callback)
class BasicH2Request(protocol.Protocol):
def connectionMade(self):
print("connection made")
self.initial_read = False
self.complete = defer.Deferred()
# Write the HTTP/2 preamble.
self.transport.write(b'SOMETHING\r\n\r\n')
#f = SettingsFrame(0)
#f.settings[SettingsFrame.ENABLE_PUSH] = 0
#self.transport.write(f.serialize())
def dataReceived(self, data):
print('Next protocol is: %s' % (self.transport.getNextProtocol(),))
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):
print("connection lost %s" % (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