Skip to content

Commit

Permalink
tls/ssl: use systems CA certs by default and fix ssl_no_cert_checks
Browse files Browse the repository at this point in the history
Now uses system CA certs when available (Python >=2.7.9 and >=3.4).
Also fixes http.ssl_no_cert_checks option for these Python versions.

Fixes #262
  • Loading branch information
olt committed Jul 5, 2017
1 parent cb6ef0b commit 69c5890
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 120 deletions.
30 changes: 22 additions & 8 deletions doc/configuration.rst
Expand Up @@ -900,26 +900,40 @@ The following options define how tiles are created and stored. Most options can

HTTP related options.

Secure HTTPS Connections (HTTPS)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. note:: You need Python 2.6 or the `SSL module <http://pypi.python.org/pypi/ssl>`_ for this feature.
Secure HTTP Connections (HTTPS)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

MapProxy supports access to HTTPS servers. Just use ``https`` instead of ``http`` when
defining the URL of a source. MapProxy needs a file that contains the root and CA
certificates. If the server certificate is signed by a "standard" root certificate (i.e. your browser does not warn you), then you can use a cert file that is distributed with your system. On Debian based systems you can use ``/etc/ssl/certs/ca-certificates.crt``.
defining the URL of a source.

MapProxy verifies the SSL/TLS connections against your systems "certification authority" (CA) certificates. You can provide your own set of root certificates with the ``ssl_ca_certs`` option.
See the `Python SSL documentation <http://docs.python.org/dev/library/ssl.html#ssl-certificates>`_ for more information about the format.

::

http:
ssl_ca_certs: /etc/ssl/certs/ca-certificates.crt

If you want to use SSL but do not need certificate verification, then you can disable it with the ``ssl_no_cert_checks`` option. You can also disable this check on a source level, see :ref:`WMS source options <wms_source_ssl_no_cert_checks>`.

.. versionadded:: 1.11.0

MapProxy uses the systems CA files by default, if you use Python >=2.7.9 or >=3.4.


.. note::

You need to supply a CA file that includes the root certificates if you use older MapProxy or older Python versions. Otherwise MapProxy will fail to establish the connection. You can set the ``http.ssl_no_cert_checks`` options to ``true`` to disable this verification.


``ssl_no_cert_checks``

If you want to use SSL/TLS but do not need certificate verification, then you can disable it with the ``ssl_no_cert_checks`` option. You can also disable this check on a source level.

::

http:
ssl_no_cert_checks: True
ssl_no_cert_checks: true


``client_timeout``
^^^^^^^^^^^^^^^^^^
Expand Down
10 changes: 2 additions & 8 deletions doc/sources.rst
Expand Up @@ -178,16 +178,10 @@ You can configure the following HTTP related options for this source:
- ``headers``
- ``client_timeout``
- ``ssl_ca_certs``
- ``ssl_no_cert_checks`` (see below)
- ``ssl_no_cert_checks``

See :ref:`HTTP Options <http_ssl>` for detailed documentation.

.. _wms_source_ssl_no_cert_checks:

``ssl_no_cert_checks``

MapProxy checks the SSL server certificates for any ``req.url`` that use HTTPS. You need to supply a file (see) that includes that certificate, otherwise MapProxy will fail to establish the connection. You can set the ``http.ssl_no_cert_checks`` options to ``true`` to disable this verification.

.. _tagged_source_names:

Tagged source names
Expand Down Expand Up @@ -375,7 +369,7 @@ You can configure the following HTTP related options for this source:
- ``headers``
- ``client_timeout``
- ``ssl_ca_certs``
- ``ssl_no_cert_checks`` (:ref:`see above <wms_source_ssl_no_cert_checks>`)
- ``ssl_no_cert_checks``

See :ref:`HTTP Options <http_ssl>` for detailed documentation.

Expand Down
154 changes: 87 additions & 67 deletions mapproxy/client/http.py
@@ -1,5 +1,5 @@
# This file is part of the MapProxy project.
# Copyright (C) 2010 Omniscale <http://omniscale.de>
# Copyright (C) 2010-2017 Omniscale <http://omniscale.de>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -37,17 +37,87 @@
from http import client as httplib

import socket
import ssl

supports_ssl_default_context = False
if hasattr(ssl, 'create_default_context'):
# Python >=2.7.9 and >=3.4.0
supports_ssl_default_context = True

class HTTPClientError(Exception):
def __init__(self, arg, response_code=None):
Exception.__init__(self, arg)
self.response_code = response_code

try:
import ssl
ssl # prevent pyflakes warnings
except ImportError:
ssl = None

def build_https_handler(ssl_ca_certs, insecure):
if supports_ssl_default_context:
# python >=2.7.9 and >=3.4 supports ssl context in
# HTTPSHandler use this
if insecure:
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
ctx.verify_mode = ssl.CERT_NONE
elif ssl_ca_certs:
ctx = ssl.create_default_context(cafile=ssl_ca_certs)
else:
ctx = ssl.create_default_context()
return urllib2.HTTPSHandler(context=ctx)
else:
if insecure:
return None
else:
connection_class = verified_https_connection_with_ca_certs(
ssl_ca_certs)
return VerifiedHTTPSHandler(connection_class=connection_class)


class VerifiedHTTPSConnection(httplib.HTTPSConnection):
def __init__(self, *args, **kw):
self._ca_certs = kw.pop('ca_certs', None)
httplib.HTTPSConnection.__init__(self, *args, **kw)

def connect(self):
# overrides the version in httplib so that we do
# certificate verification

if hasattr(socket, 'create_connection') and hasattr(self, 'source_address'):
sock = socket.create_connection((self.host, self.port),
self.timeout, self.source_address)
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self.host, self.port))

if hasattr(self, '_tunnel_host') and self._tunnel_host:
# for Python >= 2.6 with proxy support
self.sock = sock
self._tunnel()

# wrap the socket using verification with the root
# certs in self.ca_certs_path
self.sock = ssl.wrap_socket(sock,
self.key_file,
self.cert_file,
cert_reqs=ssl.CERT_REQUIRED,
ca_certs=self._ca_certs)


def verified_https_connection_with_ca_certs(ca_certs):
"""
Creates VerifiedHTTPSConnection classes with given ca_certs file.
"""
def wrapper(*args, **kw):
kw['ca_certs'] = ca_certs
return VerifiedHTTPSConnection(*args, **kw)
return wrapper


class VerifiedHTTPSHandler(urllib2.HTTPSHandler):
def __init__(self, connection_class=VerifiedHTTPSConnection):
self.specialized_conn_class = connection_class
urllib2.HTTPSHandler.__init__(self)

def https_open(self, req):
return self.do_open(self.specialized_conn_class, req)


class _URLOpenerCache(object):
Expand All @@ -60,12 +130,12 @@ class _URLOpenerCache(object):
def __init__(self):
self._opener = {}

def __call__(self, ssl_ca_certs, url, username, password):
if ssl_ca_certs not in self._opener:
def __call__(self, ssl_ca_certs, url, username, password, insecure=False):
cache_key = (ssl_ca_certs, insecure)
if cache_key not in self._opener:
handlers = []
if ssl_ca_certs:
connection_class = verified_https_connection_with_ca_certs(ssl_ca_certs)
https_handler = VerifiedHTTPSHandler(connection_class=connection_class)
https_handler = build_https_handler(ssl_ca_certs, insecure)
if https_handler:
handlers.append(https_handler)
passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
authhandler = urllib2.HTTPBasicAuthHandler(passman)
Expand All @@ -74,11 +144,12 @@ def __call__(self, ssl_ca_certs, url, username, password):
handlers.append(authhandler)

opener = urllib2.build_opener(*handlers)

opener.addheaders = [('User-agent', 'MapProxy-%s' % (version,))]

self._opener[ssl_ca_certs] = (opener, passman)
self._opener[cache_key] = (opener, passman)
else:
opener, passman = self._opener[ssl_ca_certs]
opener, passman = self._opener[cache_key]

if url is not None and username is not None and password is not None:
passman.add_password(None, url, username, password)
Expand All @@ -94,16 +165,11 @@ def __init__(self, url=None, username=None, password=None, insecure=False,
if url and url.startswith('https'):
if insecure:
ssl_ca_certs = None
else:
if ssl is None:
raise ImportError('No ssl module found. SSL certificate '
'verification requires Python 2.6 or ssl module. Upgrade '
'or disable verification with http.ssl_no_cert_checks option.')
if ssl_ca_certs is None:
elif ssl_ca_certs is None and not supports_ssl_default_context:
raise HTTPClientError('No ca_certs file set (http.ssl_ca_certs). '
'Set file or disable verification with http.ssl_no_cert_checks option.')

self.opener = create_url_opener(ssl_ca_certs, url, username, password)
self.opener = create_url_opener(ssl_ca_certs, url, username, password, insecure=insecure)
self.header_list = headers.items() if headers else []

def open(self, url, data=None):
Expand All @@ -127,7 +193,7 @@ def open(self, url, data=None):
reraise_exception(HTTPClientError('HTTP Error "%s": %d'
% (url, e.code), response_code=code), sys.exc_info())
except URLError as e:
if ssl and isinstance(e.reason, ssl.SSLError):
if isinstance(e.reason, ssl.SSLError):
e = HTTPClientError('Could not verify connection to URL "%s": %s'
% (url, e.reason.args[1]))
reraise_exception(e, sys.exc_info())
Expand Down Expand Up @@ -202,49 +268,3 @@ def retrieve_image(url, client=None):
raise HTTPClientError('response is not an image: (%s)' % (resp.read()))
return ImageSource(resp)


class VerifiedHTTPSConnection(httplib.HTTPSConnection):
def __init__(self, *args, **kw):
self._ca_certs = kw.pop('ca_certs', None)
httplib.HTTPSConnection.__init__(self, *args, **kw)

def connect(self):
# overrides the version in httplib so that we do
# certificate verification

if hasattr(socket, 'create_connection') and hasattr(self, 'source_address'):
sock = socket.create_connection((self.host, self.port),
self.timeout, self.source_address)
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self.host, self.port))

if hasattr(self, '_tunnel_host') and self._tunnel_host:
# for Python >= 2.6 with proxy support
self.sock = sock
self._tunnel()

# wrap the socket using verification with the root
# certs in self.ca_certs_path
self.sock = ssl.wrap_socket(sock,
self.key_file,
self.cert_file,
cert_reqs=ssl.CERT_REQUIRED,
ca_certs=self._ca_certs)

def verified_https_connection_with_ca_certs(ca_certs):
"""
Creates VerifiedHTTPSConnection classes with given ca_certs file.
"""
def wrapper(*args, **kw):
kw['ca_certs'] = ca_certs
return VerifiedHTTPSConnection(*args, **kw)
return wrapper

class VerifiedHTTPSHandler(urllib2.HTTPSHandler):
def __init__(self, connection_class=VerifiedHTTPSConnection):
self.specialized_conn_class = connection_class
urllib2.HTTPSHandler.__init__(self)

def https_open(self, req):
return self.do_open(self.specialized_conn_class, req)

0 comments on commit 69c5890

Please sign in to comment.