homer.py 24.3 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=dot, 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.dot = dot
241
        self.server = server
242
243
        self.servername = servername
        if self.servername is not None:
Alexandre's avatar
Alexandre committed
244
            self.check = self.servername
245
        else:
Alexandre's avatar
Alexandre committed
246
            self.check = self.server
247
248
249
        self.dot = dot
        self.verbose = verbose
        self.insecure = insecure
250

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

254
255
256
    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
257
            raise CustomException("%s is not IPv4 and not IPv6" % addr)
258
        if forceIPv4 and self.family == 6:
Alexandre's avatar
Alexandre committed
259
            raise CustomException("You cannot force IPv4 with a litteral IPv6 address (%s)" % addr)
260
        elif forceIPv6 and self.family == 4:
Alexandre's avatar
Alexandre committed
261
            raise CustomException("You cannot force IPv6 with a litteral IPv4 address (%s)" % addr)
262
263
264
265
266
267
268
269
270
        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
271
    def do_test(self, qname, qtype=rtype):
272
        # Routine doing one actual test. Returns a Request object
Alexandre's avatar
Alexandre committed
273
274
        pass

275
276
277

class ConnectionDoT(Connection):
    def __init__(self, server, servername=None, connect=None, forceIPv4=False, forceIPv6=False,
278
                 verbose=verbose, insecure=insecure):
279
        Connection.__init__(self, server, servername=servername, connect=connect,
280
                forceIPv4=forceIPv4, forceIPv6=forceIPv6, dot=True,
281
                verbose=verbose, insecure=insecure)
282
        self.check_ip_address(self.server)
283
        self.hasher = hashlib.sha256()
284
        addrinfo = socket.getaddrinfo(server, 853, self.family)
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
        # 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))

326
    def end(self):
327
328
329
        self.session.shutdown()
        self.session.close()

330
331
332
333
    def send_data(self, data):
        length = len(data)
        self.session.send(length.to_bytes(2, byteorder='big') + data)

334
    def receive_data(self, request):
Alexandre's avatar
Alexandre committed
335
        buf = self.session.recv(2)
336
337
338
339
340
341
        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
342
        request.to_wire()
343
344
        self.send_data(request.data)
        self.receive_data(request)
345
346

    def do_test(self, qname, qtype=rtype):
347
348
349
        request = RequestDoT(qname, qtype, want_dnssec=dnssec, use_edns=edns)
        self.send_and_receive(request)
        request.check_response()
350
        return request
Alexandre's avatar
Alexandre committed
351

352
353
354

class ConnectionDoH(Connection):
    def __init__(self, server, servername=None, connect=None, forceIPv4=False, forceIPv6=False,
355
                 verbose=verbose, insecure=insecure):
356
        Connection.__init__(self, server, servername=servername, connect=connect,
357
                forceIPv4=forceIPv4, forceIPv6=forceIPv6, dot=False,
358
                verbose=verbose, insecure=insecure)
359
        self.url = server
360
361
362
363
364
365
366
        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)
367
        if self.verbose:
368
            self.curl.setopt(pycurl.VERBOSE, True)
369
        if self.insecure:
370
371
            self.curl.setopt(pycurl.SSL_VERIFYPEER, False)
            self.curl.setopt(pycurl.SSL_VERIFYHOST, False)
372
        if forceIPv4:
373
            self.curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4)
374
        if forceIPv6:
375
376
377
378
379
            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"])
380
381

    def end(self):
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
        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()
402
        if request.post:
403
            self.prepare_post(request)
404
        elif request.head:
405
406
407
408
409
410
411
412
413
414
            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):
415
        request.post = True
416
417
418
        self.set_opt(pycurl.POST, True)
        self.set_opt(pycurl.POSTFIELDS, request.data)
        self.set_opt(pycurl.URL, self.server)
419

420
    def prepare_head(self, request):
421
        request.head = True
422
423
        self.prepare_get(request)
        self.set_opt(pycurl.NOBODY, True)
424

425
426
427
428
429
430
431
432
433
434
    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)
435
436
        request.response = body
        request.response_size = body_size
437
438
439
        request.rcode = http_code
        request.ctype = content_type
        self.buffer.close()
440

441
    def send_and_receive(self, request):
Alexandre's avatar
Alexandre committed
442
        request.to_wire()
443
444
445
        self.prepare(request)
        self.perform()
        self.receive(request)
446

Alexandre's avatar
Alexandre committed
447
    def do_test(self, qname, qtype=rtype):
448
        request = RequestDoH(qname, qtype, want_dnssec=dnssec, use_edns=edns)
449
450
        request.head = head
        request.post = post
451
452
        self.send_and_receive(request)
        request.check_response()
453
        return request
454
455
456
457
458
459
460
461
462
463
464
465
466


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
467

468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
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:
            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

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