homer.py 24.4 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
    def do_test(self, qname, qtype=rtype):
271
        # Routine doing one actual test. Returns a Request object
Alexandre's avatar
Alexandre committed
272
273
        pass

274
    def print_result(self, request):
275
        ok = True
276
277
278
        rcode = request.rcode
        msg = request.response
        size = request.response_size
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
        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

307
308
309

class ConnectionDoT(Connection):
    def __init__(self, server, servername=None, connect=None, forceIPv4=False, forceIPv6=False,
310
                 dot=False, verbose=verbose, insecure=insecure):
311
312
        Connection.__init__(self, server, servername=servername, connect=connect,
                forceIPv4=forceIPv4, forceIPv6=forceIPv6, dot=dot,
313
                verbose=verbose, insecure=insecure)
314
        self.check_ip_address(self.server)
315
        self.hasher = hashlib.sha256()
316
        addrinfo = socket.getaddrinfo(server, 853, self.family)
317
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
        # 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))

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

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

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

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

384
385
386

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

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

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

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

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

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


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
499

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