Commit 3a37a553 authored by Alexandre's avatar Alexandre

Merge branch 'dot-catch-closed-connection' into 'master'

[DoT] Catch closed connection

See merge request bortzmeyer/homer!30
parents aabc9550 4510d871
......@@ -499,8 +499,10 @@ def run_default(name, connection, opts):
connection.init_multi()
for i in range (0, opts.tests):
if opts.tests > 1 and (opts.verbose or opts.display_results):
print("\nTest %i" % i)
if not opts.pipelining and not opts.multistreams:
if opts.tests > 1 and (opts.verbose or opts.display_results):
print("\nTest %i" % i)
if opts.ifile is not None:
name, opts.rtype = get_next_domain(input)
......@@ -519,64 +521,81 @@ def run_default(name, connection, opts):
request.head = opts.head
request.post = opts.post
if not opts.pipelining:
if opts.pipelining: # We do pipelining (DoT)
connection.pipelining_add_request(request)
elif opts.multistreams: # We do multistreams (DoH)
connection.multistreams_add_request(request)
else:
try:
connection.do_test(request, synchronous = not opts.multistreams)
except (OpenSSL.SSL.Error, homer.DOHException) as e:
connection.do_test(request) # perform the query
except (OpenSSL.SSL.Error, homer.ConnectionDOTException, homer.DOHException) as e:
ok = False
error(e)
break
if not opts.multistreams:
ok = request.success
print_result(connection, request)
ok = request.success
print_result(connection, request)
if opts.tests > 1 and i == 0:
start2 = time.time()
if opts.delay is not None:
time.sleep(opts.delay)
else: # We do pipelining
connection.pipelining_add_request(request)
if opts.multistreams:
connection.perform_multi(opts.show_time, display_results=opts.display_results)
if opts.dot and opts.pipelining:
print()
elif opts.pipelining:
done = 0
current = connection.pipelining_init_pending(opts.max_in_flight)
while done < opts.tests:
if time.time() > start + homer.MAX_DURATION: # if we send thousands of requests
# MAX_DURATION will be reached
# need to increase MAX_DURATION based
# on the number of queries
# or to define a relation such as
# f(tests) = MAX_DURATION
print("Elapsed time too long, %i requests never got a reply" % (opts.tests-done))
ok = False
break
id = connection.read_result(connection, connection.pending, display_results=opts.display_results)
if id is None: # Probably a timeout
time.sleep(homer.SLEEP_TIMEOUT)
continue
done += 1
over, rank, request = connection.pending[id]
if not over:
error("Internal error, request %i should be over" % id)
if current < len(connection.all_requests):
connection.pipelining_fill_pending(current)
current += 1
try:
current = connection.pipelining_init_pending(opts.max_in_flight)
except homer.ConnectionDOTException as e:
ok = False
error("%s, %i/%i requests never got a reply" % (e, opts.tests - connection.nbr_finished_queries, opts.tests))
else:
while done < opts.tests:
if time.time() > start + homer.MAX_DURATION: # if we send thousands of requests
# MAX_DURATION will be reached
# need to increase MAX_DURATION based
# on the number of queries
# or to define a relation such as
# f(tests) = MAX_DURATION
error("Elapsed time too long, %i/%i requests never got a reply" % (opts.tests-done, opts.tests))
ok = False
break
id = connection.read_result(connection, connection.pending, display_results=opts.display_results)
if id is None: # Probably a timeout
time.sleep(homer.SLEEP_TIMEOUT)
continue
done += 1
over, rank, request = connection.pending[id]
if not over:
error("Internal error, request %i should be over" % id)
if current < len(connection.all_requests):
try:
connection.pipelining_fill_pending(current)
except homer.ConnectionDOTException as e:
ok = False
error("%s, %i/%i requests never got a reply" % (e, opts.tests - connection.nbr_finished_queries, opts.tests))
break
current += 1
stop = time.time()
if opts.tests > 1 and not opts.pipelining and not opts.multistreams:
extra = ", %.2f ms/request if we ignore the first one" % ((stop-start2)*1000/(opts.tests-1))
n_queries = connection.nbr_finished_queries
if n_queries > 1 and not opts.pipelining and not opts.multistreams:
extra = ", %.2f ms/request if we ignore the first one" % ((stop-start2)*1000/(n_queries-1))
else:
extra = ""
if not opts.check or opts.verbose:
time_tot = stop - start
time_per_request = time_tot / opts.tests * 1000
print("\nTotal elapsed time: %.2f seconds (%.2f ms/request%s)" % (time_tot, time_per_request, extra))
if n_queries > 1:
time_per_request = " (%.2f ms/request%s)" % (time_tot / n_queries * 1000, extra)
else:
time_per_request = ""
print("\nTotal elapsed time: %.2f seconds%s" % (time_tot, time_per_request))
if opts.multistreams and opts.verbose:
for rcode, n in conn.finished['http'].items():
print("HTTP %d : %d %.2f%%" % (rcode, n, n / opts.tests * 100))
print("HTTP %d : %d %.2f%%" % (rcode, n, n / n_queries * 100))
return ok
# Main program
......@@ -683,7 +702,8 @@ for ip in ip_set:
if opts.ifile is not None:
input.close()
conn.end()
if conn.state == 'CONN_OK':
conn.end()
if ok:
if opts.check or opts.pipelining:
......
......@@ -52,6 +52,8 @@ class Connection:
self.forceIPv4 = forceIPv4
self.forceIPv6 = forceIPv6
self.connect_to = connect_to
self.state = 'CONN_OK'
self.nbr_finished_queries = 0
def __str__(self):
return self.server
......@@ -174,15 +176,20 @@ class ConnectionDOT(Connection):
self.session.connect((addr))
self.session.do_handshake()
except homer.exceptions.TimeoutConnectionError:
self.state = 'CONN_TIMEOUT'
raise homer.ConnectionDOTException("Timeout")
except OSError:
self.state = 'CONN_FAILED'
raise homer.ConnectionDOTException("Cannot connect")
except OpenSSL.SSL.SysCallError as e:
self.state = e.args[1]
raise homer.ConnectionDOTException("OpenSSL error: %s" % e.args[1])
except OpenSSL.SSL.ZeroReturnError:
# see #18
self.state = 'CONN_CLOSED'
raise homer.ConnectionDOTException("Error: The SSL connection has been closed (try with --nosni to avoid sending SNI ?)")
except OpenSSL.SSL.Error as e:
self.state = 'CONN_ERROR'
raise homer.ConnectionDOTException("OpenSSL error: %s" % ', '.join(err[0][2] for err in e.args))
# RFC 7858, section 4.2 and appendix A
......@@ -218,18 +225,30 @@ class ConnectionDOT(Connection):
def end(self):
self.session.shutdown()
self.session.close()
self.state = 'CLOSED'
def send_data(self, data, dump=False):
if dump:
homer.dump_data(data, 'data sent')
length = len(data)
self.session.send(length.to_bytes(2, byteorder='big') + data)
try:
self.session.send(length.to_bytes(2, byteorder='big') + data)
except OpenSSL.SSL.SysCallError as e:
self.state = e.args[1]
raise homer.ConnectionDOTException('OpenSSL error : %s' % self.state)
except OpenSSL.SSL.ZeroReturnError:
self.state = 'CONN_CLOSED'
raise homer.ConnectionDOTException('The SSL connection has been closed')
def receive_data(self, dump=False):
try:
buf = self.session.recv(2)
self.nbr_finished_queries += 1
except OpenSSL.SSL.WantReadError:
return (False, None, None)
except OpenSSL.SSL.ZeroReturnError:
self.state = 'CONN_CLOSED'
raise homer.ConnectionDOTException('The SSL connection has been closed')
size = int.from_bytes(buf, byteorder='big')
data = self.session.recv(size)
if dump:
......@@ -241,13 +260,12 @@ class ConnectionDOT(Connection):
rcode, data, size = self.receive_data(dump=dump)
request.store_response(rcode, data, size)
# this function might need to be move outside
def do_test(self, request, synchronous=True):
# this function might need to be moved outside
def do_test(self, request):
self.send_data(request.data)
if synchronous:
rcode, data, size = self.receive_data()
request.store_response(rcode, data, size)
request.check_response(self.debug)
rcode, data, size = self.receive_data()
request.store_response(rcode, data, size)
request.check_response(self.debug)
# should the pipelining methods be part of ConnectionDOT ?
def pipelining_add_request(self, request):
......@@ -259,7 +277,7 @@ class ConnectionDOT(Connection):
id = request.message.id
# TODO check there is no duplicate in IDs
self.pending[id] = (False, index, request)
self.do_test(request, synchronous = False)
self.send_data(request.data)
def pipelining_init_pending(self, max_in_flight):
for i in range(0, max_in_flight):
......@@ -384,7 +402,7 @@ class ConnectionDOH(Connection):
print("Establishing multistreams connection...")
request = homer.RequestDOH('.', qtype='NS')
request.to_wire()
self.do_test(request, synchronous=False)
self.multistreams_add_request(request)
self.perform_multi(silent=True, display_results=False, show_time=False)
self.all_handles = []
self.finished = { 'http': {} }
......@@ -395,6 +413,7 @@ class ConnectionDOH(Connection):
else:
self.remove_handles()
self.multi.close()
self.state = 'CLOSED'
def remove_handles(self):
n, handle_success, handle_fail = self.multi.info_read()
......@@ -429,9 +448,11 @@ class ConnectionDOH(Connection):
try:
handle.perform()
except pycurl.error as e:
self.state = e.args[1]
raise homer.DOHException(e.args[1])
def receive(self, handle):
self.nbr_finished_queries += 1
request = handle.request
body = handle.buffer.getvalue()
body_size = len(body)
......@@ -479,15 +500,14 @@ class ConnectionDOH(Connection):
print(f'{handle.time * 1000:8.3f} ms', end=' ')
print(f'{(handle.time - handle.pretime) * 1000:8.3f} ms')
def do_test(self, request, synchronous=True):
if synchronous:
handle = self.curl_handle
else:
handle = create_handle(self)
self.all_handles.append(handle)
def multistreams_add_request(self, request):
handle = create_handle(self)
self.all_handles.append(handle)
handle.prepare(handle, self, request)
if synchronous:
self.send_and_receive(handle)
request.check_response(self.debug)
else:
self.multi.add_handle(handle)
self.multi.add_handle(handle)
def do_test(self, request):
handle = self.curl_handle
handle.prepare(handle, self, request)
self.send_and_receive(handle)
request.check_response(self.debug)
......@@ -507,6 +507,20 @@ tests:
retcode: 0
partstdout: "Test 4\n"
- name: '[dot] Connection closed by remote after to many repeats'
exe: './homer.py'
markers:
- 'dot'
timeout: 10
args:
- '--dot'
- '--repeat'
- '500'
- 'dot.bortzmeyer.fr'
- 'framasoft.org'
retcode: 1
partstderr: "The SSL connection has been closed"
- name: '[dot] Use IPv4 address (raise certificate error)'
exe: './homer.py'
markers:
......@@ -703,6 +717,20 @@ tests:
retcode: 1
partstderr: 'certificate verify failed'
# Invalid behaviour - errors should be caught
###############################################################################
- name: '[doh] Invalide certificate'
exe: './homer.py'
markers:
- 'doh'
args:
- 'https://dot.bortzmeyer.fr'
- 'chatons.org'
retcode: 1
partstderr: 'does not match target host name'
partstdout: 'Total elapsed time'
# Use --check option
###############################################################################
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment