Fixed a couple of diagnostics checks
This commit is contained in:
parent
cbbedb5f1d
commit
e43587493e
|
@ -11,6 +11,7 @@ should be taken with a grain of salt and analyzed on their own merit.
|
|||
import argparse
|
||||
import contextlib
|
||||
import datetime
|
||||
import ipaddress
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
|
@ -122,7 +123,8 @@ class BaseCheck:
|
|||
proto = socket.IPPROTO_TCP
|
||||
addr_info = socket.getaddrinfo(host, port, family, type_, proto)
|
||||
if not addr_info:
|
||||
raise UserWarning("No address found for host")
|
||||
raise UserWarning(f"No {family} address found for host")
|
||||
# Gemini IPv6
|
||||
return addr_info[0][4]
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
@ -193,8 +195,10 @@ class IPv6Address(BaseCheck):
|
|||
def check(self) -> None:
|
||||
log(f"Looking up IPv6 address for {self.args.host!r}")
|
||||
addr = self.resolve_host(socket.AF_INET6)
|
||||
if addr[0].startswith("::ffff:"):
|
||||
raise UserWarning("Found an IPv4-mapped address, skipping check")
|
||||
if ipaddress.ip_address(addr[0]).ipv4_mapped:
|
||||
raise UserWarning(
|
||||
"Found IPv4-mapped address, is your network IPv6 enabled?"
|
||||
)
|
||||
log(f"{addr[0]!r}", style="success")
|
||||
log(f"Attempting to connect to [{addr[0]}]:{addr[1]}")
|
||||
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock:
|
||||
|
@ -225,12 +229,17 @@ class TLSClaims(BaseCheck):
|
|||
|
||||
def check(self) -> None:
|
||||
try:
|
||||
# $ pip install cryptography
|
||||
import cryptography
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.x509.oid import NameOID
|
||||
from cryptography.x509.oid import NameOID, ExtensionOID
|
||||
except ImportError:
|
||||
raise UserWarning("Could not import cryptography library, skipping")
|
||||
raise UserWarning("cryptography library not installed, skipping check")
|
||||
|
||||
with self.connection() as sock:
|
||||
# Python refuses to parse a certificate unless the issuer is validated.
|
||||
# Because many gemini servers use self-signed certs, we need to use
|
||||
# a third-party library to parse the certs from their binary form.
|
||||
der_x509 = sock.getpeercert(binary_form=True)
|
||||
cert = default_backend().load_der_x509_certificate(der_x509)
|
||||
now = datetime.datetime.utcnow()
|
||||
|
@ -243,11 +252,33 @@ class TLSClaims(BaseCheck):
|
|||
style = "success" if cert.not_valid_after >= now else "failure"
|
||||
log(f"{cert.not_valid_after} UTC", style)
|
||||
|
||||
log("Checking common name matches hostname")
|
||||
cn = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
|
||||
cert_dict = {"subject": ((("commonName", cn),),)}
|
||||
log("Checking subject claim matches server hostname")
|
||||
subject = []
|
||||
for cn in cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME):
|
||||
subject.append(("commonName", cn.value))
|
||||
|
||||
subject_alt_name = []
|
||||
try:
|
||||
ext = cert.extensions.get_extension_for_oid(
|
||||
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
|
||||
)
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
pass
|
||||
else:
|
||||
for dns in ext.value.get_values_for_type(cryptography.x509.DNSName):
|
||||
subject_alt_name.append(("DNS", dns))
|
||||
for ip_address in ext.value.get_values_for_type(
|
||||
cryptography.x509.IPAddress
|
||||
):
|
||||
subject_alt_name.append(("IP Address", ip_address))
|
||||
|
||||
cert_dict = {
|
||||
"subject": (tuple(subject),),
|
||||
"subjectAltName": tuple(subject_alt_name),
|
||||
}
|
||||
log(f"{cert_dict!r}", style="info")
|
||||
ssl.match_hostname(cert_dict, self.args.host)
|
||||
log(repr(cn), style="success")
|
||||
log(f"Hostname {self.args.host!r} matches claim", style="success")
|
||||
|
||||
|
||||
class TLSVerified(BaseCheck):
|
||||
|
@ -274,6 +305,7 @@ class TLSRequired(BaseCheck):
|
|||
|
||||
def check(self) -> None:
|
||||
log("Sending non-TLS request")
|
||||
try:
|
||||
with socket.create_connection((self.args.host, self.args.port)) as sock:
|
||||
sock.sendall(f"gemini://{self.netloc}\r\n".encode())
|
||||
fp = sock.makefile("rb")
|
||||
|
@ -282,6 +314,9 @@ class TLSRequired(BaseCheck):
|
|||
log(f"Received unexpected response {header!r}", style="failure")
|
||||
else:
|
||||
log(f"Connection closed by server", style="success")
|
||||
except Exception as e:
|
||||
# A connection error is a valid response
|
||||
log(f"{e}", style="success")
|
||||
|
||||
|
||||
class ConcurrentConnections(BaseCheck):
|
||||
|
@ -547,7 +582,7 @@ class URLSchemeMissing(BaseCheck):
|
|||
"""A URL without a scheme should be inferred as gemini"""
|
||||
|
||||
def check(self) -> None:
|
||||
url = f"://{self.netloc}/\r\n"
|
||||
url = f"//{self.netloc}/\r\n"
|
||||
response = self.make_request(url)
|
||||
|
||||
log("Status should be 20 (SUCCESS)")
|
||||
|
@ -591,6 +626,8 @@ class URLDotEscape(BaseCheck):
|
|||
log(f"{response.status!r}", style)
|
||||
|
||||
|
||||
# TODO: Test sending a transient client certificate
|
||||
# TODO: Test with client pinned to TLS v1.1
|
||||
CHECKS = [
|
||||
IPv4Address,
|
||||
IPv6Address,
|
||||
|
|
Loading…
Reference in New Issue