[refactor] move http twisted code from soledad
authorRuben Pollan <meskio@sindominio.net>
Tue, 26 May 2015 20:06:20 +0000 (22:06 +0200)
committerRuben Pollan <meskio@sindominio.net>
Wed, 27 May 2015 08:44:47 +0000 (10:44 +0200)
Implements an HTTP client the twisted way, with a focus on pinning the
SSL certs.

* Related: #6506

changes/feat-twisted_http_get [new file with mode: 0644]
pkg/requirements.pip
src/leap/common/http.py [new file with mode: 0644]

diff --git a/changes/feat-twisted_http_get b/changes/feat-twisted_http_get
new file mode 100644 (file)
index 0000000..dbeb52f
--- /dev/null
@@ -0,0 +1 @@
+- Add a HTTPClient the twisted way
index c89fd19..7346fde 100644 (file)
@@ -4,5 +4,7 @@ protobuf>=2.4.1
 protobuf.socketrpc
 pyopenssl
 python-dateutil
+Twisted>=12.1
+zope.interface
 
 #autopep8 -- ???
diff --git a/src/leap/common/http.py b/src/leap/common/http.py
new file mode 100644 (file)
index 0000000..39f01ba
--- /dev/null
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+# http.py
+# Copyright (C) 2015 LEAP
+#
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+"""
+Twisted HTTP/HTTPS client.
+"""
+
+import os
+
+from zope.interface import implements
+
+from OpenSSL.crypto import load_certificate
+from OpenSSL.crypto import FILETYPE_PEM
+
+from twisted.internet import reactor
+from twisted.internet.ssl import ClientContextFactory
+from twisted.internet.ssl import CertificateOptions
+from twisted.internet.defer import succeed
+
+from twisted.web.client import Agent
+from twisted.web.client import HTTPConnectionPool
+from twisted.web.client import readBody
+from twisted.web.client import BrowserLikePolicyForHTTPS
+from twisted.web.http_headers import Headers
+from twisted.web.iweb import IBodyProducer
+
+
+class HTTPClient(object):
+    """
+    HTTP client done the twisted way, with a main focus on pinning the SSL
+    certificate.
+    """
+
+    def __init__(self, cert_file=None):
+        """
+        Init the HTTP client
+
+        :param cert_file: The path to the certificate file, if None given the
+                          system's CAs will be used.
+        :type cert_file: str
+        """
+        self._pool = HTTPConnectionPool(reactor, persistent=True)
+        self._pool.maxPersistentPerHost = 10
+
+        if cert_file:
+            cert = self._load_cert(cert_file)
+            self._agent = Agent(
+                reactor,
+                HTTPClient.ClientContextFactory(cert),
+                pool=self._pool)
+        else:
+            # trust the system's CAs
+            self._agent = Agent(
+                reactor,
+                BrowserLikePolicyForHTTPS(),
+                pool=self._pool)
+
+    def _load_cert(self, cert_file):
+        """
+        Load a X509 certificate from a file.
+
+        :param cert_file: The path to the certificate file.
+        :type cert_file: str
+
+        :return: The X509 certificate.
+        :rtype: OpenSSL.crypto.X509
+        """
+        if os.path.exists(cert_file):
+            with open(cert_file) as f:
+                data = f.read()
+                return load_certificate(FILETYPE_PEM, data)
+
+    def request(self, url, method='GET', body=None, headers={}):
+        """
+        Perform an HTTP request.
+
+        :param url: The URL for the request.
+        :type url: str
+        :param method: The HTTP method of the request.
+        :type method: str
+        :param body: The body of the request, if any.
+        :type body: str
+        :param headers: The headers of the request.
+        :type headers: dict
+
+        :return: A deferred that fires with the body of the request.
+        :rtype: twisted.internet.defer.Deferred
+        """
+        if body:
+            body = HTTPClient.StringBodyProducer(body)
+        d = self._agent.request(
+            method, url, headers=Headers(headers), bodyProducer=body)
+        d.addCallback(readBody)
+        return d
+
+    class ClientContextFactory(ClientContextFactory):
+        """
+        A context factory that will verify the server's certificate against a
+        given CA certificate.
+        """
+
+        def __init__(self, cacert):
+            """
+            Initialize the context factory.
+
+            :param cacert: The CA certificate.
+            :type cacert: OpenSSL.crypto.X509
+            """
+            self._cacert = cacert
+
+        def getContext(self, hostname, port):
+            opts = CertificateOptions(verify=True, caCerts=[self._cacert])
+            return opts.getContext()
+
+    class StringBodyProducer(object):
+        """
+        A producer that writes the body of a request to a consumer.
+        """
+
+        implements(IBodyProducer)
+
+        def __init__(self, body):
+            """
+            Initialize the string produer.
+
+            :param body: The body of the request.
+            :type body: str
+            """
+            self.body = body
+            self.length = len(body)
+
+        def startProducing(self, consumer):
+            """
+            Write the body to the consumer.
+
+            :param consumer: Any IConsumer provider.
+            :type consumer: twisted.internet.interfaces.IConsumer
+
+            :return: A successful deferred.
+            :rtype: twisted.internet.defer.Deferred
+            """
+            consumer.write(self.body)
+            return succeed(None)
+
+        def pauseProducing(self):
+            pass
+
+        def stopProducing(self):
+            pass