diff options
author | Ruben Pollan <meskio@sindominio.net> | 2015-05-26 22:06:20 +0200 |
---|---|---|
committer | Ruben Pollan <meskio@sindominio.net> | 2015-05-27 10:44:47 +0200 |
commit | 71c750ef9c3e53ef416d1de6e85458f16ca48d74 (patch) | |
tree | 746e71a258229df89acd49e0bc1cc1fbab853e04 | |
parent | 00bfb22368c788bfc5de458e9159ee73cf3e66c7 (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_get | 1 | ||||
-rw-r--r-- | pkg/requirements.pip | 2 | ||||
-rw-r--r-- | src/leap/common/http.py | 162 |
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 |