Commit ace9028f authored by Stéphane Bortzmeyer's avatar Stéphane Bortzmeyer
Browse files

Merge branch 'refactoring' into 'master'

Refactoring

See merge request bortzmeyer/homer!7
parents 779e2580 7296c3b5
......@@ -173,91 +173,71 @@ class CustomException(Exception):
pass
class RequestDoT:
class Request:
def __init__(self, qname, qtype=rtype, use_edns=edns, want_dnssec=dnssec):
self.message = dns.message.make_query(qname, dns.rdatatype.from_text(qtype), use_edns=use_edns, want_dnssec=want_dnssec)
self.message.flags |= dns.flags.AD # Ask for validation
def to_wire(self):
self.data = self.message.to_wire()
class RequestDoT(Request):
def check_response(self):
if self.response.id != self.message.id:
raise Exception("The ID in the answer does not match the one in the query")
class RequestDoH:
class RequestDoH(Request):
def __init__(self, qname, qtype=rtype, use_edns=edns, want_dnssec=dnssec):
self.message = dns.message.make_query(qname, dns.rdatatype.from_text(qtype), use_edns=use_edns, want_dnssec=want_dnssec)
Request.__init__(self, qname, qtype=rtype, use_edns=edns, want_dnssec=dnssec)
self.message.id = 0 # DoH requests that
self.message.flags |= dns.flags.AD # Ask for validation
self.data = self.message.to_wire()
def create_handle(self, curl_opt):
self.handle = pycurl.Curl()
for opt, value in curl_opt.items():
try:
self.handle.setopt(opt, value)
except AttributeError:
pass # Probably an old version of libcurl, without CONNECT_TO. It appeared with 7.49.0
def close_handle(self):
self.handle.close()
def prepare_query(self, curl_opt):
self.buffer = io.BytesIO()
curl_opt[pycurl.WRITEDATA] = self.buffer
self.create_handle(curl_opt)
def perform_query(self):
self.handle.perform()
def receive_query(self):
self.body = self.buffer.getvalue()
self.body_size = len(self.body)
self.buffer.close()
self.post = False
self.head = False
def check_response(self):
ok = True
self.rcode = self.handle.getinfo(pycurl.RESPONSE_CODE)
if self.rcode == 200:
ctype = self.handle.getinfo(pycurl.CONTENT_TYPE)
if ctype != "application/dns-message":
self.response = "Content type of the response (\"%s\") invalid" % ctype
if self.ctype != "application/dns-message":
self.response = "Content type of the response (\"%s\") invalid" % self.ctype
ok = False
else:
if not head:
if not self.head:
try:
self.response = dns.message.from_wire(self.body)
self.response = dns.message.from_wire(self.response)
except dns.message.TrailingJunk: # Not DNS. Should
# not happen for a content type
# application/dns-message but who knows?
self.response = "ERROR Not proper DNS data, trailing junk \"%s\"" % self.body
self.response = "ERROR Not proper DNS data, trailing junk \"%s\"" % self.response
ok = False
except dns.name.BadLabelType: # Not DNS.
self.response = "ERROR Not proper DNS data (wrong path in the URL?) \"%s\"" % self.body[:100]
self.response = "ERROR Not proper DNS data (wrong path in the URL?) \"%s\"" % self.response[:100]
ok = False
else:
if self.body_size == 0:
if self.response_size == 0:
self.response = "HEAD successful"
else:
self.response = "ERROR Body length is not null \"%s\"" % self.body[:100]
self.response = "ERROR Body length is not null \"%s\"" % self.response[:100]
ok = False
else:
ok = False
if self.body_size == 0:
if self.response_size == 0:
self.response = "[No details]"
else:
self.response = self.body
self.response = self.response
return ok
class Connection:
def __init__(self, server, servername=None, connect=None, forceIPv4=False, forceIPv6=False,
dot=False, verbose=verbose, insecure=insecure, post=post, head=head):
dot=dot, verbose=verbose, insecure=insecure):
if dot and not is_valid_hostname(server):
error("DoT requires a host name or IP address, not \"%s\"" % server)
if not dot and not is_valid_url(url):
error("DoH requires a valid HTTPS URL, not \"%s\"" % server)
if forceIPv4 and forceIPv6:
raise CustomException("Force IPv4 *or* IPv6 but not both")
self.dot = dot
self.server = server
self.servername = servername
if self.servername is not None:
......@@ -289,50 +269,16 @@ class Connection:
self.family = 0
def do_test(self, qname, qtype=rtype):
# Routine doing one actual test. Returns a tuple, first member is a
# result (boolean indicating success for DoT, HTTP status code for
# DoH), second member is a DNS message (or a string if there is an
# error), third member is the size of the DNS message (or None if no
# proper response).
# Routine doing one actual test. Returns a Request object
pass
def print_result(self, rcode, msg, size):
ok = True
if (self.dot and rcode) or (not self.dot and rcode == 200):
if not monitoring:
print(msg)
else:
if size is not None and size > 0:
print("%s OK - %s" % (self.server, "No error for %s/%s, %i bytes received" % (name, rtype, size)))
else:
print("%s OK - %s" % (self.server, "No error"))
sys.exit(STATE_OK)
else:
if not monitoring:
if dot:
print("Error: %s" % msg, file=sys.stderr)
else:
try:
msg = msg.decode()
except (UnicodeDecodeError, AttributeError):
pass # Sometimes, msg can be binary, or Latin-1
print("HTTP error %i: %s" % (rcode, msg), file=sys.stderr)
else:
if not dot:
print("%s HTTP error - %i: %s" % (self.server, rcode, msg))
else:
print("%s Error - %i: %s" % (self.server, rcode, msg))
sys.exit(STATE_CRITICAL)
ok = False
return ok
class ConnectionDoT(Connection):
def __init__(self, server, servername=None, connect=None, forceIPv4=False, forceIPv6=False,
dot=False, verbose=verbose, insecure=insecure, post=post, head=head):
verbose=verbose, insecure=insecure):
Connection.__init__(self, server, servername=servername, connect=connect,
forceIPv4=forceIPv4, forceIPv6=forceIPv6, dot=dot,
verbose=verbose, insecure=insecure, post=post, head=head)
forceIPv4=forceIPv4, forceIPv6=forceIPv6, dot=True,
verbose=verbose, insecure=insecure)
self.check_ip_address(self.server)
self.hasher = hashlib.sha256()
addrinfo = socket.getaddrinfo(server, 853, self.family)
......@@ -385,78 +331,174 @@ class ConnectionDoT(Connection):
length = len(data)
self.session.send(length.to_bytes(2, byteorder='big') + data)
def receive_data(self):
def receive_data(self, request):
buf = self.session.recv(2)
self.request.received = int.from_bytes(buf, byteorder='big')
buf = self.session.recv(self.request.received)
self.request.response = dns.message.from_wire(buf)
request.response_size = int.from_bytes(buf, byteorder='big')
buf = self.session.recv(request.response_size)
request.response = dns.message.from_wire(buf)
request.rcode = True
def send_and_receive(self, request):
request.to_wire()
self.send_data(request.data)
self.receive_data(request)
def do_test(self, qname, qtype=rtype):
self.request = RequestDoT(qname, qtype, want_dnssec=dnssec, use_edns=edns)
self.send_data(self.request.data)
self.receive_data()
self.request.check_response()
return (True, self.request.response, self.request.received)
request = RequestDoT(qname, qtype, want_dnssec=dnssec, use_edns=edns)
self.send_and_receive(request)
request.check_response()
return request
class ConnectionDoH(Connection):
def __init__(self, server, servername=None, connect=None, forceIPv4=False, forceIPv6=False,
dot=False, verbose=verbose, insecure=insecure, post=post, head=head):
verbose=verbose, insecure=insecure):
Connection.__init__(self, server, servername=servername, connect=connect,
forceIPv4=forceIPv4, forceIPv6=forceIPv6, dot=dot,
verbose=verbose, insecure=insecure, post=post, head=head)
self.post = post
self.head = head
forceIPv4=forceIPv4, forceIPv6=forceIPv6, dot=False,
verbose=verbose, insecure=insecure)
self.url = server
http_header = ["Content-type: application/dns-message"]
self.curl_opt = {}
self.curl_opt[pycurl.HTTP_VERSION] = pycurl.CURL_HTTP_VERSION_2 # Does not
# work if pycurl was not compiled with nghttp2 (recent Debian
# packages are OK) https://github.com/pycurl/pycurl/issues/477
self.curl_opt[pycurl.HTTPHEADER] = http_header
self.connect = connect
def create_handle(self):
self.curl = pycurl.Curl()
# Does not work if pycurl was not compiled with nghttp2 (recent Debian
# packages are OK) https://github.com/pycurl/pycurl/issues/477
self.curl.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_2)
if self.verbose:
self.curl_opt[pycurl.VERBOSE] = True
self.curl.setopt(pycurl.VERBOSE, True)
if self.insecure:
self.curl_opt[pycurl.SSL_VERIFYPEER] = False
self.curl_opt[pycurl.SSL_VERIFYHOST] = False
self.curl.setopt(pycurl.SSL_VERIFYPEER, False)
self.curl.setopt(pycurl.SSL_VERIFYHOST, False)
if forceIPv4:
self.curl_opt[pycurl.IPRESOLVE] = pycurl.IPRESOLVE_V4
self.curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4)
if forceIPv6:
self.curl_opt[pycurl.IPRESOLVE] = pycurl.IPRESOLVE_V6
if connect is not None:
self.check_ip_address(connect)
self.curl_opt[pycurl.CONNECT_TO] = [f'::{self.repraddress}:443',]
self.curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V6)
if self.connect is not None:
self.check_ip_address(self.connect)
self.curl.setopt(pycurl.CONNECT_TO, [f'::{self.repraddress}:443',])
self.curl.setopt(pycurl.HTTPHEADER, ["Content-type: application/dns-message"])
def end(self):
self.request.close_handle()
self.curl.close()
def set_opt(self, opt, value):
self.curl.setopt(opt, value)
def reset_opt_default(self):
opts = {
pycurl.NOBODY: False,
pycurl.POST: False,
pycurl.POSTFIELDS: '',
pycurl.URL: ''
}
for opt, value in opts.items():
self.set_opt(opt, value)
def prepare(self, request):
try:
self.reset_opt_default()
except AttributeError:
self.create_handle()
if request.post:
self.prepare_post(request)
elif request.head:
self.prepare_head(request)
else:
self.prepare_get(request)
def prepare_get(self, request):
self.set_opt(pycurl.HTTPGET, True)
dns_req = base64.urlsafe_b64encode(request.data).decode('UTF8').rstrip('=')
self.set_opt(pycurl.URL, self.server + ("?dns=%s" % dns_req))
def prepare_post(self, request):
request.post = True
self.set_opt(pycurl.POST, True)
self.set_opt(pycurl.POSTFIELDS, request.data)
self.set_opt(pycurl.URL, self.server)
def prepare_test_get(self, qname, qtype):
self.request = RequestDoH(qname, qtype, want_dnssec=dnssec, use_edns=edns)
dns_req = base64.urlsafe_b64encode(self.request.data).decode('UTF8').rstrip('=')
self.curl_opt[pycurl.URL] = self.server + ("?dns=%s" % dns_req)
def prepare_head(self, request):
request.head = True
self.prepare_get(request)
self.set_opt(pycurl.NOBODY, True)
def prepare_test_post(self, qname, qtype):
self.request = RequestDoH(qname, qtype, want_dnssec=dnssec, use_edns=edns)
self.curl_opt[pycurl.POST] = True
self.curl_opt[pycurl.POSTFIELDS] = self.request.data
self.curl_opt[pycurl.URL] = self.server
def perform(self):
self.buffer = io.BytesIO()
self.set_opt(pycurl.WRITEDATA, self.buffer)
self.curl.perform()
def receive(self, request):
body = self.buffer.getvalue()
body_size = len(body)
http_code = self.curl.getinfo(pycurl.RESPONSE_CODE)
content_type = self.curl.getinfo(pycurl.CONTENT_TYPE)
request.response = body
request.response_size = body_size
request.rcode = http_code
request.ctype = content_type
self.buffer.close()
def prepare_test_head(self, qname, qtype):
self.prepare_test_get(qname, qtype)
self.curl_opt[pycurl.NOBODY] = True
def send_and_receive(self, request):
request.to_wire()
self.prepare(request)
self.perform()
self.receive(request)
def do_test(self, qname, qtype=rtype):
if self.post:
self.prepare_test_post(qname, qtype)
elif self.head:
self.prepare_test_head(qname, qtype)
request = RequestDoH(qname, qtype, want_dnssec=dnssec, use_edns=edns)
request.head = head
request.post = post
self.send_and_receive(request)
request.check_response()
return request
def get_next_domain(input_file):
name, rtype = 'framagit.org', 'AAAA'
line = input_file.readline()
if line[:-1] == "":
error("Not enough data in %s for the %i tests" % (ifile, tests))
if line.find(' ') == -1:
name = line[:-1]
rtype = 'AAAA'
else:
(name, rtype) = line.split()
return name, rtype
def print_result(connection, request):
ok = True
dot = connection.dot
server = connection.server
rcode = request.rcode
msg = request.response
size = request.response_size
if (dot and rcode) or (not dot and rcode == 200):
if not monitoring:
print(msg)
else:
if size is not None and size > 0:
print("%s OK - %s" % (server, "No error for %s/%s, %i bytes received" % (name, rtype, size)))
else:
print("%s OK - %s" % (server, "No error"))
sys.exit(STATE_OK)
else:
if not monitoring:
if dot:
print("Error: %s" % msg, file=sys.stderr)
else:
try:
msg = msg.decode()
except (UnicodeDecodeError, AttributeError):
pass # Sometimes, msg can be binary, or Latin-1
print("HTTP error %i: %s" % (rcode, msg), file=sys.stderr)
else:
self.prepare_test_get(qname, qtype)
self.request.prepare_query(self.curl_opt)
self.request.perform_query()
self.request.receive_query()
self.request.check_response()
return (self.request.rcode, self.request.response, self.request.body_size)
if not dot:
print("%s HTTP error - %i: %s" % (server, rcode, msg))
else:
print("%s Error - %i: %s" % (server, rcode, msg))
sys.exit(STATE_CRITICAL)
ok = False
return ok
# Main program
me = os.path.basename(sys.argv[0])
......@@ -596,13 +638,13 @@ try:
else:
extracheck = None
if dot:
conn = ConnectionDoT(url, dot=dot, servername=extracheck, connect=connectTo, verbose=verbose,
conn = ConnectionDoT(url, servername=extracheck, connect=connectTo, verbose=verbose,
forceIPv4=forceIPv4, forceIPv6=forceIPv6,
insecure=insecure, post=post, head=head)
insecure=insecure)
else:
conn = ConnectionDoH(url, dot=dot, servername=extracheck, connect=connectTo, verbose=verbose,
conn = ConnectionDoH(url, servername=extracheck, connect=connectTo, verbose=verbose,
forceIPv4=forceIPv4, forceIPv6=forceIPv6,
insecure=insecure, post=post, head=head)
insecure=insecure)
except TimeoutError:
error("timeout")
except ConnectionRefusedError:
......@@ -615,16 +657,15 @@ for i in range (0, tests):
if tests > 1:
print("\nTest %i" % i)
if ifile is not None:
line = input.readline()
if line[:-1] == "":
error("Not enough data in %s for the %i tests" % (ifile, tests))
if line.find(' ') == -1:
name = line[:-1]
rtype = 'AAAA'
else:
(name, rtype) = line.split()
(rcode, msg, size) = conn.do_test(name, rtype)
ok = conn.print_result(rcode, msg, size)
name, rtype = get_next_domain(input)
try:
request = conn.do_test(name, rtype)
except (OpenSSL.SSL.Error, CustomException) as e:
ok = False
error(e)
break
if not print_result(conn, request):
ok = False
if tests > 1 and i == 0:
start2 = time.time()
if delay is not None:
......
Supports Markdown
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