changing requests and responses to have two timestamps, one marking their initiation, and the other their complete

This commit is contained in:
Rouli 2013-01-17 17:32:56 +02:00
parent 440a9f6bda
commit 20fa6a3083
3 changed files with 75 additions and 19 deletions

40
libmproxy/flow.py Normal file → Executable file
View File

@ -256,17 +256,20 @@ class Request(HTTPMsg):
path: Path portion of the URL path: Path portion of the URL
timestamp: Seconds since the epoch timestamp_start: Seconds since the epoch signifying request transmission started
method: HTTP method method: HTTP method
timestamp_end: Seconds since the epoch signifying request transmission ended
""" """
def __init__(self, client_conn, httpversion, host, port, scheme, method, path, headers, content, timestamp=None): def __init__(self, client_conn, httpversion, host, port, scheme, method, path, headers, content, timestamp_start=None, timestamp_end=None):
assert isinstance(headers, ODictCaseless) assert isinstance(headers, ODictCaseless)
self.client_conn = client_conn self.client_conn = client_conn
self.httpversion = httpversion self.httpversion = httpversion
self.host, self.port, self.scheme = host, port, scheme self.host, self.port, self.scheme = host, port, scheme
self.method, self.path, self.headers, self.content = method, path, headers, content self.method, self.path, self.headers, self.content = method, path, headers, content
self.timestamp = timestamp or utils.timestamp() self.timestamp_start = timestamp_start or utils.timestamp()
self.timestamp_end = max(timestamp_end or utils.timestamp(), timestamp_start)
self.close = False self.close = False
controller.Msg.__init__(self) controller.Msg.__init__(self)
@ -330,7 +333,8 @@ class Request(HTTPMsg):
self.path = state["path"] self.path = state["path"]
self.headers = ODictCaseless._from_state(state["headers"]) self.headers = ODictCaseless._from_state(state["headers"])
self.content = state["content"] self.content = state["content"]
self.timestamp = state["timestamp"] self.timestamp_start = state["timestamp_start"]
self.timestamp_end = state["timestamp_end"]
def _get_state(self): def _get_state(self):
return dict( return dict(
@ -343,7 +347,8 @@ class Request(HTTPMsg):
path = self.path, path = self.path,
headers = self.headers._get_state(), headers = self.headers._get_state(),
content = self.content, content = self.content,
timestamp = self.timestamp, timestamp_start = self.timestamp_start,
timestamp_end = self.timestamp_end
) )
@classmethod @classmethod
@ -358,7 +363,8 @@ class Request(HTTPMsg):
str(state["path"]), str(state["path"]),
ODictCaseless._from_state(state["headers"]), ODictCaseless._from_state(state["headers"]),
state["content"], state["content"],
state["timestamp"] state["timestamp_start"],
state["timestamp_end"],
) )
def __hash__(self): def __hash__(self):
@ -545,15 +551,18 @@ class Response(HTTPMsg):
is content associated, but not present. CONTENT_MISSING evaluates is content associated, but not present. CONTENT_MISSING evaluates
to False to make checking for the presence of content natural. to False to make checking for the presence of content natural.
timestamp: Seconds since the epoch timestamp_start: Seconds since the epoch signifying response transmission started
timestamp_end: Seconds since the epoch signifying response transmission ended
""" """
def __init__(self, request, httpversion, code, msg, headers, content, cert, timestamp=None): def __init__(self, request, httpversion, code, msg, headers, content, cert, timestamp_start=None, timestamp_end=None):
assert isinstance(headers, ODictCaseless) assert isinstance(headers, ODictCaseless)
self.request = request self.request = request
self.httpversion, self.code, self.msg = httpversion, code, msg self.httpversion, self.code, self.msg = httpversion, code, msg
self.headers, self.content = headers, content self.headers, self.content = headers, content
self.cert = cert self.cert = cert
self.timestamp = timestamp or utils.timestamp() self.timestamp_start = timestamp_start or utils.timestamp()
self.timestamp_end = max(timestamp_end or utils.timestamp(), timestamp_start)
controller.Msg.__init__(self) controller.Msg.__init__(self)
self.replay = False self.replay = False
@ -589,7 +598,7 @@ class Response(HTTPMsg):
""" """
if not now: if not now:
now = time.time() now = time.time()
delta = now - self.timestamp delta = now - self.timestamp_start
refresh_headers = [ refresh_headers = [
"date", "date",
"expires", "expires",
@ -621,7 +630,8 @@ class Response(HTTPMsg):
self.msg = state["msg"] self.msg = state["msg"]
self.headers = ODictCaseless._from_state(state["headers"]) self.headers = ODictCaseless._from_state(state["headers"])
self.content = state["content"] self.content = state["content"]
self.timestamp = state["timestamp"] self.timestamp_start = state["timestamp_start"]
self.timestamp_end = state["timestamp_end"]
self.cert = certutils.SSLCert.from_pem(state["cert"]) if state["cert"] else None self.cert = certutils.SSLCert.from_pem(state["cert"]) if state["cert"] else None
def _get_state(self): def _get_state(self):
@ -630,9 +640,10 @@ class Response(HTTPMsg):
code = self.code, code = self.code,
msg = self.msg, msg = self.msg,
headers = self.headers._get_state(), headers = self.headers._get_state(),
timestamp = self.timestamp, timestamp_start = self.timestamp_start,
timestamp_end = self.timestamp_end,
cert = self.cert.to_pem() if self.cert else None, cert = self.cert.to_pem() if self.cert else None,
content = self.content content = self.content,
) )
@classmethod @classmethod
@ -645,7 +656,8 @@ class Response(HTTPMsg):
ODictCaseless._from_state(state["headers"]), ODictCaseless._from_state(state["headers"]),
state["content"], state["content"],
certutils.SSLCert.from_pem(state["cert"]) if state["cert"] else None, certutils.SSLCert.from_pem(state["cert"]) if state["cert"] else None,
state["timestamp"], state["timestamp_start"],
state["timestamp_end"],
) )
def __eq__(self, other): def __eq__(self, other):

13
libmproxy/proxy.py Normal file → Executable file
View File

@ -180,14 +180,16 @@ class ProxyHandler(tcp.BaseHandler):
scheme, host, port = request.scheme, request.host, request.port scheme, host, port = request.scheme, request.host, request.port
self.server_connect(scheme, host, port) self.server_connect(scheme, host, port)
self.server_conn.send(request) self.server_conn.send(request)
self.server_conn.rfile.reset_timestamps()
httpversion, code, msg, headers, content = http.read_response( httpversion, code, msg, headers, content = http.read_response(
self.server_conn.rfile, self.server_conn.rfile,
request.method, request.method,
self.config.body_size_limit self.config.body_size_limit
) )
response = flow.Response( response = flow.Response(
request, httpversion, code, msg, headers, content, self.server_conn.cert request, httpversion, code, msg, headers, content, self.server_conn.cert, self.server_conn.rfile.first_byte_timestamp, utils.timestamp()
) )
response = response._send(self.mqueue) response = response._send(self.mqueue)
if response is None: if response is None:
self.server_conn.terminate() self.server_conn.terminate()
@ -266,6 +268,7 @@ class ProxyHandler(tcp.BaseHandler):
self.sni = sn.decode("utf8").encode("idna") self.sni = sn.decode("utf8").encode("idna")
def read_request(self, client_conn): def read_request(self, client_conn):
self.rfile.reset_timestamps()
if self.config.transparent_proxy: if self.config.transparent_proxy:
host, port = self.config.transparent_proxy["resolver"].original_addr(self.connection) host, port = self.config.transparent_proxy["resolver"].original_addr(self.connection)
if not self.ssl_established and (port in self.config.transparent_proxy["sslports"]): if not self.ssl_established and (port in self.config.transparent_proxy["sslports"]):
@ -289,7 +292,7 @@ class ProxyHandler(tcp.BaseHandler):
content = http.read_http_body_request( content = http.read_http_body_request(
self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit
) )
return flow.Request(client_conn, httpversion, host, port, scheme, method, path, headers, content) return flow.Request(client_conn, httpversion, host, port, scheme, method, path, headers, content, self.rfile.first_byte_timestamp, utils.timestamp())
elif self.config.reverse_proxy: elif self.config.reverse_proxy:
line = self.get_line(self.rfile) line = self.get_line(self.rfile)
if line == "": if line == "":
@ -303,7 +306,7 @@ class ProxyHandler(tcp.BaseHandler):
content = http.read_http_body_request( content = http.read_http_body_request(
self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit
) )
return flow.Request(client_conn, httpversion, host, port, "http", method, path, headers, content) return flow.Request(client_conn, httpversion, host, port, "http", method, path, headers, content, self.rfile.first_byte_timestamp, utils.timestamp())
else: else:
line = self.get_line(self.rfile) line = self.get_line(self.rfile)
if line == "": if line == "":
@ -340,7 +343,7 @@ class ProxyHandler(tcp.BaseHandler):
content = http.read_http_body_request( content = http.read_http_body_request(
self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit
) )
return flow.Request(client_conn, httpversion, host, port, "https", method, path, headers, content) return flow.Request(client_conn, httpversion, host, port, "https", method, path, headers, content, self.rfile.first_byte_timestamp, utils.timestamp())
else: else:
r = http.parse_init_proxy(line) r = http.parse_init_proxy(line)
if not r: if not r:
@ -350,7 +353,7 @@ class ProxyHandler(tcp.BaseHandler):
content = http.read_http_body_request( content = http.read_http_body_request(
self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit
) )
return flow.Request(client_conn, httpversion, host, port, scheme, method, path, headers, content) return flow.Request(client_conn, httpversion, host, port, scheme, method, path, headers, content, self.rfile.first_byte_timestamp, utils.timestamp())
def read_headers(self, authenticate=False): def read_headers(self, authenticate=False):
headers = http.read_headers(self.rfile) headers = http.read_headers(self.rfile)

View File

@ -74,3 +74,44 @@ class TestProxy(tutils.HTTPProxTest):
assert l.request.client_conn.address assert l.request.client_conn.address
assert "host" in l.request.headers assert "host" in l.request.headers
assert l.response.code == 304 assert l.response.code == 304
def test_response_timestamps(self):
# test that we notice at least 2 sec delay between timestamps
# in response object
f = self.pathod("304:b@1k:p50,2")
assert f.status_code == 304
response = self.master.state.view[0].response
assert 2 <= response.timestamp_end - response.timestamp_start <= 2.2
def test_request_timestamps(self):
# test that we notice at least 2 sec delay between timestamps
# in request object
connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connection.connect(("127.0.0.1", self.proxy.port))
# call pathod server, wait a second to complete the request
connection.send("GET http://localhost:%d/p/304:b@1k HTTP/1.1\r\n"%self.server.port)
sleep(2.1)
connection.send("\r\n");
connection.recv(50000)
connection.close()
request, response = self.master.state.view[0].request, self.master.state.view[0].response
assert response.code == 304 # sanity test for our low level request
assert 2 <= request.timestamp_end - request.timestamp_start <= 2.2
def test_request_timestamps_not_affected_by_client_time(self):
# test that don't include user wait time in request's timestamps
f = self.pathod("304:b@10k")
assert f.status_code == 304
sleep(1)
f = self.pathod("304:b@10k")
assert f.status_code == 304
request = self.master.state.view[0].request
assert request.timestamp_end - request.timestamp_start <= 0.1
request = self.master.state.view[1].request
assert request.timestamp_end - request.timestamp_start <= 0.1