homer.py 24.7 KB
Newer Older
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
1
2
#!/usr/bin/env python3

3
4
5
6
7
# Homer is a DoH (DNS-over-HTTPS) and DoT (DNS-over-TLS) client. Its
# main purpose is to test DoH and DoT resolvers. Reference site is
# <https://framagit.org/bortzmeyer/homer/> See author, documentation,
# etc, there, or in the README.md included with the distribution.

Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
8
9
10
11
12
13
# http://pycurl.io/docs/latest
import pycurl

# http://www.dnspython.org/
import dns.message

14
15
16
# https://github.com/drkjam/netaddr/
import netaddr

17
18
19
20
21
# Octobre 2019: the Python GnuTLS bindings don't work with Python 3. So we use OpenSSL.
# https://www.pyopenssl.org/
# https://pyopenssl.readthedocs.io/
import OpenSSL

Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
22
23
24
25
26
27
import io
import sys
import base64
import getopt
import urllib.parse
import time
28
29
import socket
import ctypes
30
import re
31
import os.path
32
33
import hashlib
import base64
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
34

35
# Values that can be changed from the command line
36
dot = False # DoH by default
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
37
38
verbose = False
insecure = False
39
post = False
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
40
head = False
41
42
dnssec = False
edns = True
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
43
rtype = 'AAAA'
44
vhostname = None
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
45
tests = 1 # Number of repeated tests
46
ifile = None # Input file
47
delay = None
48
49
50
forceIPv4 = False
forceIPv6 = False
connectTo = None
51
52
53
# Monitoring plugin only:
host = None
path = None
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
54

55
56
57
# Do not change these
re_host = re.compile(r'^([0-9a-z][0-9a-z-\.]*)|([0-9:]+)|([0-9\.])$')

58
59
60
61
62
63
64
# For the monitoring plugin
STATE_OK = 0
STATE_WARNING = 1
STATE_CRITICAL = 2
STATE_UNKNOWN = 3
STATE_DEPENDENT = 4

Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
65
66
67
def error(msg=None):
    if msg is None:
        msg = "Unknown error"
68
69
    if monitoring:
        print("%s: %s" % (url, msg))
Alexandre's avatar
Alexandre committed
70
        sys.exit(STATE_CRITICAL)
71
72
73
    else:
        print(msg,file=sys.stderr)
        sys.exit(1)
Alexandre's avatar
Alexandre committed
74

Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
75
76
77
def usage(msg=None):
    if msg:
        print(msg,file=sys.stderr)
78
79
    print("Usage: %s [--dot] url-or-servername domain-name [DNS type]" % sys.argv[0], file=sys.stderr)
    print("See the README.md for more details.", file=sys.stderr)
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
80

81
def is_valid_hostname(name):
82
    name = canonicalize(name)
83
    return re_host.search(name)
84

85
86
87
88
89
90
91
92
93
def canonicalize(hostname):
    result = hostname.lower()
    # TODO handle properly the case where it fails with UnicodeError
    # (two consecutive dots for instance) to get a custom exception
    result = result.encode('idna').decode()
    if result[len(result)-1] == '.':
        result = result[:-1]
    return result

94
95
96
97
def is_valid_ip_address(addr):
    try:
        baddr = netaddr.IPAddress(addr)
    except netaddr.core.AddrFormatError:
98
99
        return (False, None)
    return (True, baddr.version)
100

101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def is_valid_url(url):
  try:
    result = urllib.parse.urlparse(url) # A very poor validation, many
    # errors (for instance whitespaces, IPv6 address litterals without
    # brackets...) are ignored.
    return (result.scheme=="https" and result.netloc != "")
  except ValueError:
    return False

def get_certificate_san(x509cert):
    san = ""
    ext_count = x509cert.get_extension_count()
    for i in range(0, ext_count):
        ext = x509cert.get_extension(i)
        if "subjectAltName" in str(ext.get_short_name()):
            san = str(ext)
    return san

119
120
121
122
123
124
125
126
127
128
129
# Try one possible name. Names must be already canonicalized.
def match_hostname(hostname, possibleMatch):
    if possibleMatch.startswith("*."): # Wildcard
        base = possibleMatch[1:] # Skip the star
        # RFC 6125 says that we MAY accept left-most labels with
        # wildcards included (foo*bar). We don't do it here.
        try:
            (first, rest) = hostname.split(".", maxsplit=1)
        except ValueError: # One-label name
            rest = hostname
        if rest == base[1:]:
130
            return True
131
        if hostname == base[1:]:
132
            return True
133
134
135
136
137
138
139
140
        return False
    else:
        return hostname == possibleMatch

# Try all the names in the certificate
def validate_hostname(hostname, cert):
    # Complete specification is in RFC 6125. It is long and
    # complicated and I'm not sure we do it perfectly.
141
    (is_addr, family) = is_valid_ip_address(hostname)
142
    hostname = canonicalize(hostname)
143
    for alt_name in get_certificate_san(cert).split(", "):
144
        if alt_name.startswith("DNS:") and not is_addr:
145
            (start, base) = alt_name.split("DNS:")
146
            base = canonicalize(base)
147
148
            found = match_hostname(hostname, base)
            if found:
149
                return True
150
151
        elif alt_name.startswith("IP Address:") and is_addr:
            host_i = netaddr.IPAddress(hostname)
152
153
154
            (start, base) = alt_name.split("IP Address:")
            if base.endswith("\n"):
                base = base[:-1]
155
156
157
158
            try:
                base_i = netaddr.IPAddress(base)
            except netaddr.core.AddrFormatError:
                continue # Ignore broken IP addresses in certificates. Are we too liberal?
Alexandre's avatar
Alexandre committed
159
            if host_i == base_i:
160
161
                return True
        else:
162
163
164
            pass # Ignore unknown alternative name types. May be
                 # accept URI alternative names for DoH,
    # According to RFC 6125, we MUST NOT try the Common Name before the Subject Alternative Names.
165
    cn = canonicalize(cert.get_subject().commonName)
166
167
168
    found = match_hostname(hostname, cn)
    if found:
        return True
169
170
    return False

171

Alexandre's avatar
Alexandre committed
172
173
174
175
class CustomException(Exception):
    pass


Alexandre's avatar
Alexandre committed
176
class Request:
177
178
179
    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
Alexandre's avatar
Alexandre committed
180
181

    def to_wire(self):
182
183
        self.data = self.message.to_wire()

Alexandre's avatar
Alexandre committed
184
185

class RequestDoT(Request):
186
187
188
189
190
    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")


Alexandre's avatar
Alexandre committed
191
class RequestDoH(Request):
192
    def __init__(self, qname, qtype=rtype, use_edns=edns, want_dnssec=dnssec):
Alexandre's avatar
Alexandre committed
193
        Request.__init__(self, qname, qtype=rtype, use_edns=edns, want_dnssec=dnssec)
194
        self.message.id = 0 # DoH requests that
195
        self.post = False
196
        self.head = False
197
198
199
200

    def check_response(self):
        ok = True
        if self.rcode == 200:
201
202
            if self.ctype != "application/dns-message":
                self.response = "Content type of the response (\"%s\") invalid" % self.ctype
203
204
                ok = False
            else:
205
                if not self.head:
206
                    try:
207
                        self.response = dns.message.from_wire(self.response)
208
209
210
                    except dns.message.TrailingJunk: # Not DNS. Should
                        # not happen for a content type
                        # application/dns-message but who knows?
211
                        self.response = "ERROR Not proper DNS data, trailing junk \"%s\"" % self.response
212
213
                        ok = False
                    except dns.name.BadLabelType: # Not DNS.
214
                        self.response = "ERROR Not proper DNS data (wrong path in the URL?) \"%s\"" % self.response[:100]
215
216
                        ok = False
                else:
217
                    if self.response_size == 0:
218
219
                        self.response = "HEAD successful"
                    else:
220
                        self.response = "ERROR Body length is not null \"%s\"" % self.response[:100]
221
222
223
                        ok = False
        else:
            ok = False
224
            if self.response_size == 0:
225
226
                self.response = "[No details]"
            else:
227
                self.response = self.response
228
        return ok
229

230

231
class Connection:
232
    def __init__(self, server, servername=None, connect=None, forceIPv4=False, forceIPv6=False,
233
                 dot=False, verbose=verbose, insecure=insecure):
234
        if dot and not is_valid_hostname(server):
235
            error("DoT requires a host name or IP address, not \"%s\"" % server)
236
237
        if not dot and not is_valid_url(url):
            error("DoH requires a valid HTTPS URL, not \"%s\"" % server)
238
        if forceIPv4 and forceIPv6:
Alexandre's avatar
Alexandre committed
239
            raise CustomException("Force IPv4 *or* IPv6 but not both")
240
        self.server = server
241
242
        self.servername = servername
        if self.servername is not None:
Alexandre's avatar
Alexandre committed
243
            self.check = self.servername
244
        else:
Alexandre's avatar
Alexandre committed
245
            self.check = self.server
246
247
248
        self.dot = dot
        self.verbose = verbose
        self.insecure = insecure
249

250
251
    def __str__(self):
        return self.server
252

253
254
255
    def check_ip_address(self, addr):
        (is_addr, self.family) = is_valid_ip_address(addr)
        if not is_addr and not self.dot:
Alexandre's avatar
Alexandre committed
256
            raise CustomException("%s is not IPv4 and not IPv6" % addr)
257
        if forceIPv4 and self.family == 6:
Alexandre's avatar
Alexandre committed
258
            raise CustomException("You cannot force IPv4 with a litteral IPv6 address (%s)" % addr)
259
        elif forceIPv6 and self.family == 4:
Alexandre's avatar
Alexandre committed
260
            raise CustomException("You cannot force IPv6 with a litteral IPv4 address (%s)" % addr)
261
262
263
264
265
266
267
268
269
        if forceIPv4 or self.family == 4:
            self.family = socket.AF_INET
            self.repraddress = addr
        elif forceIPv6 or self.family == 6:
            self.family = socket.AF_INET6
            self.repraddress = f'[{addr}]'
        else:
            self.family = 0

Alexandre's avatar
Alexandre committed
270
271
272
273
274
275
276
277
    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).
        pass

278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
    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

308
309
310

class ConnectionDoT(Connection):
    def __init__(self, server, servername=None, connect=None, forceIPv4=False, forceIPv6=False,
311
                 dot=False, verbose=verbose, insecure=insecure):
312
313
        Connection.__init__(self, server, servername=servername, connect=connect,
                forceIPv4=forceIPv4, forceIPv6=forceIPv6, dot=dot,
314
                verbose=verbose, insecure=insecure)
315
        self.check_ip_address(self.server)
316
        self.hasher = hashlib.sha256()
317
        addrinfo = socket.getaddrinfo(server, 853, self.family)
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
        # May be loop over the results of getaddrinfo, to test all
        # the IP addresses? See #13.
        self.sock = socket.socket(addrinfo[0][0], socket.SOCK_STREAM)
        self.addr = addrinfo[0][4]
        if self.verbose:
            print("Connecting to %s ..." % str(self.addr))
        # With typical DoT servers, we *must* use TLS 1.2 (otherwise,
        # do_handshake fails with "OpenSSL.SSL.SysCallError: (-1, 'Unexpected
        # EOF')" Typical HTTP servers are more lax.
        self.context = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD)
        if self.insecure:
            self.context.set_verify(OpenSSL.SSL.VERIFY_NONE, lambda *x: True)
        else:
            self.context.set_default_verify_paths()
            self.context.set_verify_depth(4) # Seems ignored
            self.context.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT | \
                                    OpenSSL.SSL.VERIFY_CLIENT_ONCE,
                                    lambda conn, cert, errno, depth, preverify_ok: preverify_ok)
        self.session = OpenSSL.SSL.Connection(self.context, self.sock)
        self.session.set_tlsext_host_name(canonicalize(self.check).encode()) # Server Name Indication (SNI)
        self.session.connect((self.addr))
        # TODO We may here have exceptions such as OpenSSL.SSL.ZeroReturnError
        self.session.do_handshake()
        self.cert = self.session.get_peer_certificate()
        # RFC 7858, section 4.2 and appendix A
        self.publickey = self.cert.get_pubkey()
        if verbose:
            print("Certificate #%x for \"%s\", delivered by \"%s\"" % \
                  (self.cert.get_serial_number(),
                   self.cert.get_subject().commonName,
                   self.cert.get_issuer().commonName))
            self.hasher.update(OpenSSL.crypto.dump_publickey(OpenSSL.crypto.FILETYPE_ASN1,
                                                  self.publickey))
            self.digest = self.hasher.digest()
            print("Public key is pin-sha256=\"%s\"" % \
                  base64.standard_b64encode(self.digest).decode())
        if not insecure:
            valid = validate_hostname(self.check, self.cert)
            if not valid:
                error("Certificate error: \"%s\" is not in the certificate" % (self.check))

359
    def end(self):
360
361
362
        self.session.shutdown()
        self.session.close()

363
364
365
366
    def send_data(self, data):
        length = len(data)
        self.session.send(length.to_bytes(2, byteorder='big') + data)

367
    def receive_data(self, request):
Alexandre's avatar
Alexandre committed
368
        buf = self.session.recv(2)
369
370
371
372
373
374
        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):
Alexandre's avatar
Alexandre committed
375
        request.to_wire()
376
377
        self.send_data(request.data)
        self.receive_data(request)
378
379

    def do_test(self, qname, qtype=rtype):
380
381
382
383
        request = RequestDoT(qname, qtype, want_dnssec=dnssec, use_edns=edns)
        self.send_and_receive(request)
        request.check_response()
        return (request.rcode, request.response, request.response_size)
Alexandre's avatar
Alexandre committed
384

385
386
387

class ConnectionDoH(Connection):
    def __init__(self, server, servername=None, connect=None, forceIPv4=False, forceIPv6=False,
388
                 dot=False, verbose=verbose, insecure=insecure):
389
390
        Connection.__init__(self, server, servername=servername, connect=connect,
                forceIPv4=forceIPv4, forceIPv6=forceIPv6, dot=dot,
391
                verbose=verbose, insecure=insecure)
392
        self.url = server
393
394
395
396
397
398
399
        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)
400
        if self.verbose:
401
            self.curl.setopt(pycurl.VERBOSE, True)
402
        if self.insecure:
403
404
            self.curl.setopt(pycurl.SSL_VERIFYPEER, False)
            self.curl.setopt(pycurl.SSL_VERIFYHOST, False)
405
        if forceIPv4:
406
            self.curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4)
407
        if forceIPv6:
408
409
410
411
412
            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"])
413
414

    def end(self):
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
        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()
435
        if request.post:
436
            self.prepare_post(request)
437
        elif request.head:
438
439
440
441
442
443
444
445
446
447
            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):
448
        request.post = True
449
450
451
        self.set_opt(pycurl.POST, True)
        self.set_opt(pycurl.POSTFIELDS, request.data)
        self.set_opt(pycurl.URL, self.server)
452

453
    def prepare_head(self, request):
454
        request.head = True
455
456
        self.prepare_get(request)
        self.set_opt(pycurl.NOBODY, True)
457

458
459
460
461
462
463
464
465
466
467
    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)
468
469
        request.response = body
        request.response_size = body_size
470
471
472
        request.rcode = http_code
        request.ctype = content_type
        self.buffer.close()
473

474
    def send_and_receive(self, request):
Alexandre's avatar
Alexandre committed
475
        request.to_wire()
476
477
478
        self.prepare(request)
        self.perform()
        self.receive(request)
479

Alexandre's avatar
Alexandre committed
480
    def do_test(self, qname, qtype=rtype):
481
        request = RequestDoH(qname, qtype, want_dnssec=dnssec, use_edns=edns)
482
483
        request.head = head
        request.post = post
484
485
        self.send_and_receive(request)
        request.check_response()
486
        return (request.rcode, request.response, request.response_size)
487
488
489
490
491
492
493
494
495
496
497
498
499


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
Alexandre's avatar
Alexandre committed
500

501
# Main program
502
503
504
505
506
507
me = os.path.basename(sys.argv[0])
monitoring = (me == "check_doh" or me == "check_dot")
if not monitoring:
    name = None
    message = None
    try:
508
        optlist, args = getopt.getopt (sys.argv[1:], "hvPkeV:r:f:d:t46",
509
510
511
                                       ["help", "verbose", "dot", "head",
                                        "insecure", "POST", "vhost=",
                                        "dnssec", "noedns","repeat=", "file=", "delay=", "v4only", "v6only"])
512
513
514
515
        for option, value in optlist:
            if option == "--help" or option == "-h":
                usage()
                sys.exit(0)
516
517
            elif option == "--dot" or option == "-t":
                dot = True
518
519
            elif option == "--verbose" or option == "-v":
                verbose = True
520
            elif option == "--HEAD" or option == "--head" or option == "-e":
521
                head = True
522
            elif option == "--POST" or option == "--post" or option == "-P":
523
                post = True
524
525
            elif option == "--vhost" or option == "-V":
                vhostname = value
526
527
            elif option == "--insecure" or option == "-k":
                insecure = True
528
529
530
531
            elif option == "--dnssec":
                dnssec = True
            elif option == "--noedns":
                edns = False
532
533
534
535
536
537
538
539
540
541
            elif option == "--repeat" or option == "-r":
                tests = int(value)
                if tests <= 1:
                    error("--repeat needs a value > 1")
            elif option == "--delay" or option == "-d":
                delay = float(value)
                if delay <= 0:
                    error("--delay needs a value > 0")
            elif option == "--file" or option == "-f":
                ifile = value
542
543
544
545
            elif option == "-4" or option == "v4only":
                forceIPv4 = True
            elif option == "-6" or option == "v6only":
                forceIPv6 = True
546
547
548
549
550
551
552
553
554
555
556
557
            else:
                error("Unknown option %s" % option)
    except getopt.error as reason:
        usage(reason)
        sys.exit(1)
    if tests <= 1 and delay is not None:
        error("--delay makes no sense if there is no repetition")
    if post and head:
        usage("POST or HEAD but not both")
        sys.exit(1)
    if dot and (post or head):
        usage("POST or HEAD makes non sense for DoT")
Alexandre's avatar
Alexandre committed
558
        sys.exit(1)
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
    if ifile is None and (len(args) != 2 and len(args) != 3):
        usage("Wrong number of arguments")
        sys.exit(1)
    if ifile is not None and len(args) != 1:
        usage("Wrong number of arguments (if --file is used, do not indicate the domain name)")
        sys.exit(1)
    url = args[0]
    if ifile is None:
        name = args[1]
        if len(args) == 3:
            rtype = args[2]
else: # Monitoring plugin
    dot = (me == "check_dot")
    name = None
    try:
574
        optlist, args = getopt.getopt (sys.argv[1:], "H:n:p:V:t:Pih46")
575
576
577
578
        for option, value in optlist:
            if option == "-H":
                host = value
            elif option == "-V":
Alexandre's avatar
Alexandre committed
579
                vhostname = value
580
581
582
583
584
585
586
587
588
589
            elif option == "-n":
                name = value
            elif option == "-t":
                rtype = value
            elif option == "-p":
                path = value
            elif option == "-P":
                post = True
            elif option == "-h":
                head = True
590
591
            elif option == "-i":
                insecure = True
592
593
594
595
            elif option == "-4":
                forceIPv4 = True
            elif option == "-6":
                forceIPv6 = True
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
            else:
                # Should never occur, it is trapped by getopt
                print("Unknown option %s" % option)
                sys.exit(STATE_UNKNOWN)
    except getopt.error as reason:
        print("Option parsing problem %s" % reason)
        sys.exit(STATE_UNKNOWN)
    if len(args) > 0:
        print("Too many arguments (\"%s\")" % args)
        sys.exit(STATE_UNKNOWN)
    if host is None or name is None:
        print("Host (-H) and name to lookup (-n) are necessary")
        sys.exit(STATE_UNKNOWN)
    if post and head:
        print("POST or HEAD but not both")
        sys.exit(STATE_UNKNOWN)
612
613
614
615
616
617
    if dot and (post or head):
        print("POST or HEAD makes no sense for DoT")
        sys.exit(STATE_UNKNOWN)
    if dot and path:
        print("URL path makes no sense for DoT")
        sys.exit(STATE_UNKNOWN)
618
619
620
    if dot:
        url = host
    else:
621
622
        if vhostname is None or vhostname == host:
            connectTo = None
623
            url = "https://%s/" % host
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
624
        else:
625
            connectTo = host
Alexandre's avatar
Alexandre committed
626
            url = "https://%s/" % vhostname
627
628
629
630
        if path is not None:
            if path.startswith("/"):
                path = path[1:]
            url += path
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
631
ok = True
Alexandre's avatar
Alexandre committed
632
start = time.time()
633
try:
634
    if dot and vhostname is not None:
635
636
637
        extracheck = vhostname
    else:
        extracheck = None
638
639
640
    if dot:
        conn = ConnectionDoT(url, dot=dot, servername=extracheck, connect=connectTo, verbose=verbose,
                          forceIPv4=forceIPv4, forceIPv6=forceIPv6,
641
                          insecure=insecure)
642
643
644
    else:
        conn = ConnectionDoH(url, dot=dot, servername=extracheck, connect=connectTo, verbose=verbose,
                          forceIPv4=forceIPv4, forceIPv6=forceIPv6,
645
                          insecure=insecure)
646
647
except TimeoutError:
    error("timeout")
648
649
except ConnectionRefusedError:
    error("Connection to server refused")
Alexandre's avatar
Alexandre committed
650
651
except CustomException as e:
    error(e)
652
653
if ifile is not None:
    input = open(ifile)
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
654
for i in range (0, tests):
655
656
657
    if tests > 1:
        print("\nTest %i" % i)
    if ifile is not None:
658
659
660
661
662
663
664
665
666
        name, rtype = get_next_domain(input)
    try:
        (rcode, msg, size) = conn.do_test(name, rtype)
    except (OpenSSL.SSL.Error, CustomException) as e:
        ok = False
        error(e)
        break
    if not conn.print_result(rcode, msg, size):
        ok = False
667
668
    if tests > 1 and i == 0:
        start2 = time.time()
669
670
    if delay is not None:
        time.sleep(delay)
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
671
stop = time.time()
672
673
674
675
if tests > 1:
    extra = ", %.2f ms/request if we ignore the first one" % ((stop-start2)*1000/(tests-1))
else:
    extra = ""
676
677
if not monitoring:
    print("\nTotal elapsed time: %.2f seconds (%.2f ms/request %s)" % (stop-start, (stop-start)*1000/tests, extra))
678
679
if ifile is not None:
    input.close()
680
conn.end()
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
681
if ok:
682
683
684
685
    if not monitoring:
        sys.exit(0)
    else:
        sys.exit(STATE_OK)
Stephane Bortzmeyer's avatar
Stephane Bortzmeyer committed
686
else:
687
688
689
690
    if not monitoring:
        sys.exit(1)
    else:
        sys.exit(STATE_CRITICAL)
Alexandre's avatar
Alexandre committed
691