summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuben Pollan <meskio@sindominio.net>2015-05-26 22:06:20 +0200
committerRuben Pollan <meskio@sindominio.net>2015-05-27 10:44:47 +0200
commit71c750ef9c3e53ef416d1de6e85458f16ca48d74 (patch)
tree746e71a258229df89acd49e0bc1cc1fbab853e04
parent00bfb22368c788bfc5de458e9159ee73cf3e66c7 (diff)
[refactor] move http twisted code from soledad
Implements an HTTP client the twisted way, with a focus on pinning the SSL certs. * Related: #6506
-rw-r--r--changes/feat-twisted_http_get1
-rw-r--r--pkg/requirements.pip2
-rw-r--r--src/leap/common/http.py162
3 files changed, 165 insertions, 0 deletions
diff --git a/changes/feat-twisted_http_get b/changes/feat-twisted_http_get
new file mode 100644
index 0000000..dbeb52f
--- /dev/null
+++ b/changes/feat-twisted_http_get
@@ -0,0 +1 @@
+- Add a HTTPClient the twisted way
diff --git a/pkg/requirements.pip b/pkg/requirements.pip
index c89fd19..7346fde 100644
--- a/pkg/requirements.pip
+++ b/pkg/requirements.pip
@@ -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
index 0000000..39f01ba
--- /dev/null
+++ b/src/leap/common/http.py
@@ -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