import http.client import socket import urllib.request, urllib.error, urllib.parse import ssl import hashlib from .utils import valid from . import settings import logging logger = logging.getLogger('link') class InvalidCertificateException(http.client.HTTPException, urllib.error.URLError): def __init__(self, fingerprint, cert, reason): http.client.HTTPException.__init__(self) self.fingerprint = fingerprint self.cert_fingerprint = hashlib.sha1(cert).hexdigest() self.reason = reason def __str__(self): return ('%s (local) != %s (remote) (%s)\n' % (self.fingerprint, self.cert_fingerprint, self.reason)) class CertValidatingHTTPSConnection(http.client.HTTPConnection): default_port = http.client.HTTPS_PORT def __init__(self, host, port=None, fingerprint=None, strict=None, **kwargs): http.client.HTTPConnection.__init__(self, host, port, strict, **kwargs) self.fingerprint = fingerprint if self.fingerprint: self.cert_reqs = ssl.CERT_REQUIRED else: self.cert_reqs = ssl.CERT_NONE self.cert_reqs = ssl.CERT_NONE def _ValidateCertificateFingerprint(self, cert): fingerprint = hashlib.sha1(cert).hexdigest() return fingerprint == self.fingerprint def connect(self): sock = socket.create_connection((self.host, self.port)) self.sock = ssl.wrap_socket(sock, cert_reqs=self.cert_reqs) #if self.cert_reqs & ssl.CERT_REQUIRED: if self.fingerprint: cert = self.sock.getpeercert(binary_form=True) if not self._ValidateCertificateFingerprint(cert): raise InvalidCertificateException(self.fingerprint, cert, 'fingerprint mismatch') #logger.debug('CIPHER %s VERSION %s', self.sock.cipher(), self.sock.ssl_version) class VerifiedHTTPSHandler(urllib.request.HTTPSHandler): def __init__(self, **kwargs): urllib.request.AbstractHTTPHandler.__init__(self) self._connection_args = kwargs def https_open(self, req): def http_class_wrapper(host, **kwargs): full_kwargs = dict(self._connection_args) full_kwargs.update(kwargs) if 'timeout' in full_kwargs: del full_kwargs['timeout'] return CertValidatingHTTPSConnection(host, **full_kwargs) try: return self.do_open(http_class_wrapper, req) except urllib.error.URLError as e: if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1: raise InvalidCertificateException(self.fingerprint, '', e.reason.args[1]) raise https_request = urllib.request.HTTPSHandler.do_request_ def get_opener(fingerprint): handler = VerifiedHTTPSHandler(fingerprint=fingerprint) opener = urllib.request.build_opener(handler) return opener class Response(object): headers = {} error = None body = None data = None user = None code = 200 def read(url, body=None, headers={}, fingerprint=None, timeout=None): if not timeout: timeout = settings.TIMEOUT if not body: body = None opener = get_opener(fingerprint) headers = dict(headers) request = urllib.request.Request(url, data=body, headers=headers) response = Response() logger.debug('open %s [%s]', url, fingerprint) logger.debug('headers: %s', headers) try: r = opener.open(request, timeout=timeout) except urllib.error.HTTPError as e: response.code = e.code if e.code >= 500: logger.debug('urllib.error.HTTPError %s %s', e, e.code) response.error = e else: response.headers = e.headers response.body = e.read() return response except urllib.error.URLError as e: logger.debug('urllib.error.URLError %s', e) response.error = e response.code = 500 return response except: logger.debug('unknown url error', exc_info=1) response.error = 'unkown url error' response.code = 500 return response response.headers = r.headers response.code = r.getcode() sig = r.headers.get('X-Ed25519-Signature') if sig: key = r.headers.get('X-Ed25519-Key') data = r.read() if valid(key, data, sig): response.data = data response.user = key else: response.error = 'Invalid Signature' response.code = 500 else: response.body = r logger.debug('response headers: %s', dict(r.headers)) return response def node_url(node): host = node['host'] port = node['port'] if ':' in host: url = 'https://[%s]:%s' % (host, port) else: url = 'https://%s:%s' % (host, port) return url def can_connect(data): r = read(node_url(data), fingerprint=data['cert'], timeout=1) if r.error: return False return True