Merge pull request #25 from Kriechi/pathoc-http2

[WIP] pathoc: HTTP/2
This commit is contained in:
Aldo Cortesi 2015-06-08 23:06:09 +12:00
commit 05efcf0a78
5 changed files with 226 additions and 34 deletions

View File

@ -65,6 +65,17 @@ def args_pathoc(argv, stdout=sys.stdout, stderr=sys.stderr):
"-t", dest="timeout", type=int, default=None, "-t", dest="timeout", type=int, default=None,
help="Connection timeout" help="Connection timeout"
) )
parser.add_argument(
"--http2", dest="use_http2", action="store_true", default=False,
help='Perform all requests over a single HTTP/2 connection.'
)
parser.add_argument(
"--http2-skip-connection-preface",
dest="http2_skip_connection_preface",
action="store_true",
default=False,
help='Skips the HTTP/2 connection preface before sending requests.')
parser.add_argument( parser.add_argument(
'host', type=str, 'host', type=str,
metavar = "host[:port]", metavar = "host[:port]",
@ -77,6 +88,7 @@ def args_pathoc(argv, stdout=sys.stdout, stderr=sys.stderr):
specifcations specifcations
""" """
) )
group = parser.add_argument_group( group = parser.add_argument_group(
'SSL', 'SSL',
) )
@ -189,7 +201,7 @@ def args_pathoc(argv, stdout=sys.stdout, stderr=sys.stderr):
data = open(r).read() data = open(r).read()
r = data r = data
try: try:
reqs.append(language.parse_pathoc(r)) reqs.append(language.parse_pathoc(r, args.use_http2))
except language.ParseException as v: except language.ParseException as v:
print >> stderr, "Error parsing request spec: %s" % v.msg print >> stderr, "Error parsing request spec: %s" % v.msg
print >> stderr, v.marked() print >> stderr, v.marked()

View File

@ -3,7 +3,7 @@ import time
import pyparsing as pp import pyparsing as pp
from . import http, websockets, writer, exceptions from . import http, http2, websockets, writer, exceptions
from exceptions import * from exceptions import *
from base import Settings from base import Settings
@ -39,20 +39,24 @@ def parse_pathod(s):
return itertools.chain(*[expand(i) for i in reqs]) return itertools.chain(*[expand(i) for i in reqs])
def parse_pathoc(s): def parse_pathoc(s, use_http2=False):
try: try:
s = s.decode("ascii") s = s.decode("ascii")
except UnicodeError: except UnicodeError:
raise exceptions.ParseException("Spec must be valid ASCII.", 0, 0) raise exceptions.ParseException("Spec must be valid ASCII.", 0, 0)
try: try:
reqs = pp.OneOrMore( if use_http2:
pp.Or( expressions = [
[ # http2.Frame.expr(),
http2.Request.expr(),
]
else:
expressions = [
websockets.WebsocketClientFrame.expr(), websockets.WebsocketClientFrame.expr(),
http.Request.expr(), http.Request.expr(),
] ]
)
).parseString(s, parseAll=True) reqs = pp.OneOrMore(pp.Or(expressions)).parseString(s, parseAll=True)
except pp.ParseException as v: except pp.ParseException as v:
raise exceptions.ParseException(v.msg, v.line, v.col) raise exceptions.ParseException(v.msg, v.line, v.col)
return itertools.chain(*[expand(i) for i in reqs]) return itertools.chain(*[expand(i) for i in reqs])

View File

@ -15,13 +15,15 @@ class Settings:
staticdir = None, staticdir = None,
unconstrained_file_access = False, unconstrained_file_access = False,
request_host = None, request_host = None,
websocket_key = None websocket_key = None,
protocol = None,
): ):
self.is_client = is_client
self.staticdir = staticdir self.staticdir = staticdir
self.unconstrained_file_access = unconstrained_file_access self.unconstrained_file_access = unconstrained_file_access
self.request_host = request_host self.request_host = request_host
self.websocket_key = websocket_key self.websocket_key = websocket_key # TODO: refactor this into the protocol
self.is_client = is_client self.protocol = protocol
Sep = pp.Optional(pp.Literal(":")).suppress() Sep = pp.Optional(pp.Literal(":")).suppress()

119
libpathod/language/http2.py Normal file
View File

@ -0,0 +1,119 @@
import os
import netlib.http2
import pyparsing as pp
from . import base, generators, actions, message
"""
Normal HTTP requests:
<method>:<path>:<header>:<body>
e.g.:
GET:/
GET:/:foo=bar
POST:/:foo=bar:'content body payload'
Individual HTTP/2 frames:
h2f:<payload_length>:<type>:<flags>:<stream_id>:<payload>
e.g.:
h2f:0:PING
h2f:42:HEADERS:END_HEADERS:0x1234567:foo=bar,host=example.com
h2f:42:DATA:END_STREAM,PADDED:0x1234567:'content body payload'
"""
class Method(base.OptionsOrValue):
options = [
"GET",
"HEAD",
"POST",
"PUT",
"DELETE",
]
class Path(base.Value):
pass
class Header(base.KeyValue):
preamble = "h"
class Body(base.Value):
preamble = "b"
class Times(base.Integer):
preamble = "x"
class Request(message.Message):
comps = (
Header,
Body,
Times,
)
@property
def method(self):
return self.tok(Method)
@property
def path(self):
return self.tok(Path)
@property
def headers(self):
return self.toks(Header)
@property
def body(self):
return self.tok(Body)
@property
def times(self):
return self.tok(Times)
@property
def actions(self):
return []
@classmethod
def expr(klass):
parts = [i.expr() for i in klass.comps]
atom = pp.MatchFirst(parts)
resp = pp.And(
[
Method.expr(),
base.Sep,
Path.expr(),
base.Sep,
pp.ZeroOrMore(base.Sep + atom)
]
)
resp = resp.setParseAction(klass)
return resp
def resolve(self, settings, msg=None):
tokens = self.tokens[:]
return self.__class__(
[i.resolve(settings, self) for i in tokens]
)
def values(self, settings):
return settings.protocol.create_request(
self.method.value.get_generator(settings),
self.path,
self.headers,
self.body)
def spec(self):
return ":".join([i.spec() for i in self.tokens])
# class H2F(base.CaselessLiteral):
# TOK = "h2f"
#
#
# class WebsocketFrame(message.Message):
# pass

View File

@ -11,23 +11,32 @@ import threading
import OpenSSL.crypto import OpenSSL.crypto
from netlib import tcp, http, certutils, websockets from netlib import tcp, http, http2, certutils, websockets
import language.http import language.http
import language.websockets import language.websockets
from . import utils, log from . import utils, log
import logging
logging.getLogger("hpack").setLevel(logging.WARNING)
class PathocError(Exception): class PathocError(Exception):
pass pass
class SSLInfo: class SSLInfo:
def __init__(self, certchain, cipher): def __init__(self, certchain, cipher, alp):
self.certchain, self.cipher = certchain, cipher self.certchain, self.cipher, self.alp = certchain, cipher, alp
def __str__(self): def __str__(self):
if self.alp:
alp = self.alp
else:
alp = '<no protocol negotiated>'
parts = [ parts = [
"Application Layer Protocol: %s" % alp,
"Cipher: %s, %s bit, %s" % self.cipher, "Cipher: %s, %s bit, %s" % self.cipher,
"SSL certificate chain:" "SSL certificate chain:"
] ]
@ -150,6 +159,10 @@ class Pathoc(tcp.TCPClient):
clientcert=None, clientcert=None,
ciphers=None, ciphers=None,
# HTTP/2
use_http2=False,
http2_skip_connection_preface=False,
# Websockets # Websockets
ws_read_limit = None, ws_read_limit = None,
@ -177,18 +190,16 @@ class Pathoc(tcp.TCPClient):
ignorecodes: Sequence of return codes to ignore ignorecodes: Sequence of return codes to ignore
""" """
tcp.TCPClient.__init__(self, address) tcp.TCPClient.__init__(self, address)
self.settings = language.Settings(
staticdir = os.getcwd(),
unconstrained_file_access = True,
request_host = self.address.host,
is_client = True
)
self.ssl, self.sni = ssl, sni self.ssl, self.sni = ssl, sni
self.clientcert = clientcert self.clientcert = clientcert
self.sslversion = utils.SSLVERSIONS[sslversion] self.sslversion = utils.SSLVERSIONS[sslversion]
self.ciphers = ciphers self.ciphers = ciphers
self.sslinfo = None self.sslinfo = None
self.use_http2 = use_http2
self.http2_skip_connection_preface = http2_skip_connection_preface
self.ws_read_limit = ws_read_limit self.ws_read_limit = ws_read_limit
self.timeout = timeout self.timeout = timeout
@ -204,6 +215,20 @@ class Pathoc(tcp.TCPClient):
self.ws_framereader = None self.ws_framereader = None
if self.use_http2:
self.protocol = http2.HTTP2Protocol(self)
else:
# TODO: create HTTP or Websockets protocol
self.protocol = None
self.settings = language.Settings(
is_client = True,
staticdir = os.getcwd(),
unconstrained_file_access = True,
request_host = self.address.host,
protocol = self.protocol,
)
def log(self): def log(self):
return log.Log( return log.Log(
self.fp, self.fp,
@ -233,26 +258,44 @@ class Pathoc(tcp.TCPClient):
connect_to: A (host, port) tuple, which will be connected to with connect_to: A (host, port) tuple, which will be connected to with
an HTTP CONNECT request. an HTTP CONNECT request.
""" """
if self.use_http2 and not self.ssl:
raise ValueError("HTTP2 without SSL is not supported.")
tcp.TCPClient.connect(self) tcp.TCPClient.connect(self)
if connect_to: if connect_to:
self.http_connect(connect_to) self.http_connect(connect_to)
self.sslinfo = None self.sslinfo = None
if self.ssl: if self.ssl:
try: try:
alpn_protos = [b'http1.1'] # TODO: move to a new HTTP1 protocol
if self.use_http2:
alpn_protos.append(http2.HTTP2Protocol.ALPN_PROTO_H2)
self.convert_to_ssl( self.convert_to_ssl(
sni=self.sni, sni=self.sni,
cert=self.clientcert, cert=self.clientcert,
method=self.sslversion, method=self.sslversion,
cipher_list = self.ciphers cipher_list=self.ciphers,
alpn_protos=alpn_protos
) )
except tcp.NetLibError as v: except tcp.NetLibError as v:
raise PathocError(str(v)) raise PathocError(str(v))
self.sslinfo = SSLInfo( self.sslinfo = SSLInfo(
self.connection.get_peer_cert_chain(), self.connection.get_peer_cert_chain(),
self.get_current_cipher() self.get_current_cipher(),
self.get_alpn_proto_negotiated()
) )
if showssl: if showssl:
print >> fp, str(self.sslinfo) print >> fp, str(self.sslinfo)
if self.use_http2:
self.protocol.check_alpn()
if not self.http2_skip_connection_preface:
self.protocol.perform_connection_preface()
if self.timeout: if self.timeout:
self.settimeout(self.timeout) self.settimeout(self.timeout)
@ -337,6 +380,11 @@ class Pathoc(tcp.TCPClient):
try: try:
req = language.serve(r, self.wfile, self.settings) req = language.serve(r, self.wfile, self.settings)
self.wfile.flush() self.wfile.flush()
if self.use_http2:
status_code, headers, body = self.protocol.read_response()
resp = Response("HTTP/2", status_code, "", headers, body, self.sslinfo)
else:
resp = list( resp = list(
http.read_response( http.read_response(
self.rfile, self.rfile,
@ -374,7 +422,8 @@ class Pathoc(tcp.TCPClient):
May raise http.HTTPError, tcp.NetLibError May raise http.HTTPError, tcp.NetLibError
""" """
if isinstance(r, basestring): if isinstance(r, basestring):
r = language.parse_pathoc(r).next() r = language.parse_pathoc(r, self.use_http2).next()
if isinstance(r, language.http.Request): if isinstance(r, language.http.Request):
if r.ws: if r.ws:
return self.websocket_start(r) return self.websocket_start(r)
@ -382,6 +431,10 @@ class Pathoc(tcp.TCPClient):
return self.http(r) return self.http(r)
elif isinstance(r, language.websockets.WebsocketFrame): elif isinstance(r, language.websockets.WebsocketFrame):
self.websocket_send_frame(r) self.websocket_send_frame(r)
elif isinstance(r, language.http2.Request):
return self.http(r)
# elif isinstance(r, language.http2.Frame):
# TODO: do something
def main(args): # pragma: nocover def main(args): # pragma: nocover
@ -407,6 +460,8 @@ def main(args): # pragma: nocover
sslversion = args.sslversion, sslversion = args.sslversion,
clientcert = args.clientcert, clientcert = args.clientcert,
ciphers = args.ciphers, ciphers = args.ciphers,
use_http2 = args.use_http2,
http2_skip_connection_preface = args.http2_skip_connection_preface,
showreq = args.showreq, showreq = args.showreq,
showresp = args.showresp, showresp = args.showresp,
explain = args.explain, explain = args.explain,