mirror of
https://github.com/Hopiu/linkchecker.git
synced 2026-03-16 22:10:26 +00:00
179 lines
6.9 KiB
Python
179 lines
6.9 KiB
Python
# -*- coding: iso-8859-1 -*-
|
|
# Copyright (C) 2000-2014 Bastian Kleineidam
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along
|
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
"""
|
|
Handle https links.
|
|
"""
|
|
import time
|
|
from . import httpurl
|
|
from .const import WARN_HTTPS_CERTIFICATE
|
|
from .. import log, LOG_CHECK, strformat
|
|
|
|
|
|
class HttpsUrl (httpurl.HttpUrl):
|
|
"""
|
|
Url link with https scheme.
|
|
"""
|
|
|
|
def local_check (self):
|
|
"""
|
|
Check connection if SSL is supported, else ignore.
|
|
"""
|
|
if httpurl.supportHttps:
|
|
super(HttpsUrl, self).local_check()
|
|
else:
|
|
self.add_info(_("%s URL ignored.") % self.scheme.capitalize())
|
|
|
|
def get_http_object (self, scheme, host, port):
|
|
"""Open a HTTP connection and check the SSL certificate."""
|
|
super(HttpsUrl, self).get_http_object(scheme, host, port)
|
|
self.check_ssl_certificate(self.url_connection.sock, host)
|
|
|
|
def check_ssl_certificate(self, ssl_sock, host):
|
|
"""Run all SSL certificate checks that have not yet been done.
|
|
OpenSSL already checked the SSL notBefore and notAfter dates.
|
|
"""
|
|
if not hasattr(ssl_sock, "getpeercert"):
|
|
# the URL was a HTTPS -> HTTP redirect
|
|
return
|
|
cert = ssl_sock.getpeercert()
|
|
log.debug(LOG_CHECK, "Got SSL certificate %s", cert)
|
|
if not cert:
|
|
return
|
|
if 'subject' in cert:
|
|
self.check_ssl_hostname(ssl_sock, cert, host)
|
|
else:
|
|
msg = _('certificate did not include "subject" information')
|
|
self.add_ssl_warning(ssl_sock, msg)
|
|
if 'notAfter' in cert:
|
|
self.check_ssl_valid_date(ssl_sock, cert)
|
|
else:
|
|
msg = _('certificate did not include "notAfter" information')
|
|
self.add_ssl_warning(ssl_sock, msg)
|
|
|
|
def check_ssl_hostname(self, ssl_sock, cert, host):
|
|
"""Check the hostname against the certificate according to
|
|
RFC2818.
|
|
"""
|
|
try:
|
|
match_hostname(cert, host)
|
|
except CertificateError as msg:
|
|
self.add_ssl_warning(ssl_sock, msg)
|
|
|
|
def check_ssl_valid_date(self, ssl_sock, cert):
|
|
"""Check if the certificate is still valid, or if configured check
|
|
if it's at least a number of days valid.
|
|
"""
|
|
import ssl
|
|
checkDaysValid = self.aggregate.config["warnsslcertdaysvalid"]
|
|
try:
|
|
notAfter = ssl.cert_time_to_seconds(cert['notAfter'])
|
|
except ValueError as msg:
|
|
msg = _('invalid certficate "notAfter" value %r') % cert['notAfter']
|
|
self.add_ssl_warning(ssl_sock, msg)
|
|
return
|
|
curTime = time.time()
|
|
# Calculate seconds until certifcate expires. Can be negative if
|
|
# the certificate is already expired.
|
|
secondsValid = notAfter - curTime
|
|
if secondsValid < 0:
|
|
msg = _('certficate is expired on %s') % cert['notAfter']
|
|
self.add_ssl_warning(ssl_sock, msg)
|
|
elif checkDaysValid > 0 and \
|
|
secondsValid < (checkDaysValid * strformat.SECONDS_PER_DAY):
|
|
strSecondsValid = strformat.strduration_long(secondsValid)
|
|
msg = _('certificate is only %s valid') % strSecondsValid
|
|
self.add_ssl_warning(ssl_sock, msg)
|
|
|
|
def add_ssl_warning(self, ssl_sock, msg):
|
|
"""Add a warning message about an SSL certificate error."""
|
|
cipher_name, ssl_protocol, secret_bits = ssl_sock.cipher()
|
|
err = _(u"SSL warning: %(msg)s. Cipher %(cipher)s, %(protocol)s.")
|
|
attrs = dict(msg=msg, cipher=cipher_name, protocol=ssl_protocol)
|
|
self.add_warning(err % attrs, tag=WARN_HTTPS_CERTIFICATE)
|
|
|
|
|
|
# Copied from ssl.py in Python 3:
|
|
# Wrapper module for _ssl, providing some additional facilities
|
|
# implemented in Python. Written by Bill Janssen.
|
|
import re
|
|
|
|
class CertificateError(ValueError):
|
|
"""Raised on certificate errors."""
|
|
pass
|
|
|
|
|
|
def _dnsname_to_pat(dn, max_wildcards=1):
|
|
"""Convert a DNS certificate name to a hostname matcher."""
|
|
pats = []
|
|
for frag in dn.split(r'.'):
|
|
if frag.count('*') > max_wildcards:
|
|
# Issue #17980: avoid denials of service by refusing more
|
|
# than one wildcard per fragment. A survery of established
|
|
# policy among SSL implementations showed it to be a
|
|
# reasonable choice.
|
|
raise CertificateError(
|
|
"too many wildcards in certificate DNS name: " + repr(dn))
|
|
if frag == '*':
|
|
# When '*' is a fragment by itself, it matches a non-empty dotless
|
|
# fragment.
|
|
pats.append('[^.]+')
|
|
else:
|
|
# Otherwise, '*' matches any dotless fragment.
|
|
frag = re.escape(frag)
|
|
pats.append(frag.replace(r'\*', '[^.]*'))
|
|
return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
|
|
|
|
|
|
def match_hostname(cert, hostname):
|
|
"""Verify that *cert* (in decoded format as returned by
|
|
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
|
|
are mostly followed, but IP addresses are not accepted for *hostname*.
|
|
|
|
CertificateError is raised on failure. On success, the function
|
|
returns nothing.
|
|
"""
|
|
if not cert:
|
|
raise ValueError("empty or no certificate")
|
|
dnsnames = []
|
|
san = cert.get('subjectAltName', ())
|
|
for key, value in san:
|
|
if key == 'DNS':
|
|
if _dnsname_to_pat(value).match(hostname):
|
|
return
|
|
dnsnames.append(value)
|
|
if not dnsnames:
|
|
# The subject is only checked when there is no dNSName entry
|
|
# in subjectAltName
|
|
for sub in cert.get('subject', ()):
|
|
for key, value in sub:
|
|
# XXX according to RFC 2818, the most specific Common Name
|
|
# must be used.
|
|
if key == 'commonName':
|
|
if _dnsname_to_pat(value).match(hostname):
|
|
return
|
|
dnsnames.append(value)
|
|
if len(dnsnames) > 1:
|
|
raise CertificateError("hostname %r "
|
|
"doesn't match either of %s"
|
|
% (hostname, ', '.join(map(repr, dnsnames))))
|
|
elif len(dnsnames) == 1:
|
|
raise CertificateError("hostname %r "
|
|
"doesn't match %r"
|
|
% (hostname, dnsnames[0]))
|
|
else:
|
|
raise CertificateError("no appropriate commonName or "
|
|
"subjectAltName fields were found")
|