diff options
-rw-r--r-- | client/src/leap/soledad/client/http_client.py | 194 | ||||
-rw-r--r-- | client/src/leap/soledad/client/http_target.py | 38 |
2 files changed, 31 insertions, 201 deletions
diff --git a/client/src/leap/soledad/client/http_client.py b/client/src/leap/soledad/client/http_client.py deleted file mode 100644 index b08d199e..00000000 --- a/client/src/leap/soledad/client/http_client.py +++ /dev/null @@ -1,194 +0,0 @@ -# -*- coding: utf-8 -*- -# http_client.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.http_headers import Headers -from twisted.web.error import Error -from twisted.web.iweb import IBodyProducer - - -from leap.soledad.common.errors import InvalidAuthTokenError - - -# -# Setup a pool of connections -# - -_pool = HTTPConnectionPool(reactor, persistent=True) -_pool.maxPersistentPerHost = 10 -_agent = None - -# if we ever want to trust the system's CAs, we should use an agent like this: -# from twisted.web.client import BrowserLikePolicyForHTTPS -# _agent = Agent(reactor, BrowserLikePolicyForHTTPS(), pool=_pool) - - -# -# SSL/TLS certificate configuration -# - -def configure_certificate(cert_file): - """ - Configure an agent that verifies server certificates against a CA cert - file. - - :param cert_file: The path to the certificate file. - :type cert_file: str - """ - global _agent - cert = _load_cert(cert_file) - _agent = Agent( - reactor, - SoledadClientContextFactory(cert), - pool=_pool) - - -def _load_cert(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) - - -class SoledadClientContextFactory(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() - - -# -# HTTP request facilities -# - -def _unauth_to_invalid_token_error(failure): - """ - An errback to translate unauthorized errors to our own invalid token - class. - - :param failure: The original failure. - :type failure: twisted.python.failure.Failure - - :return: Either the original failure or an invalid auth token error. - :rtype: twisted.python.failure.Failure - """ - failure.trap(Error) - if failure.getErrorMessage() == "401 Unauthorized": - raise InvalidAuthTokenError - return failure - - -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 - - -def httpRequest(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 = StringBodyProducer(body) - d = _agent.request( - method, url, headers=Headers(headers), bodyProducer=body) - d.addCallbacks(readBody, _unauth_to_invalid_token_error) - return d diff --git a/client/src/leap/soledad/client/http_target.py b/client/src/leap/soledad/client/http_target.py index dc6c0e0a..5eef2df3 100644 --- a/client/src/leap/soledad/client/http_target.py +++ b/client/src/leap/soledad/client/http_target.py @@ -31,12 +31,16 @@ from functools import partial from twisted.internet import defer from twisted.internet import reactor +from twisted.web.error import Error from u1db import errors from u1db import SyncTarget from u1db.remote import utils +from leap.common.http import HTTPClient + from leap.soledad.common.document import SoledadDocument +from leap.soledad.common.errors import InvalidAuthTokenError from leap.soledad.client.crypto import is_symmetrically_encrypted from leap.soledad.client.crypto import encrypt_doc @@ -45,8 +49,6 @@ from leap.soledad.client.events import SOLEDAD_SYNC_SEND_STATUS from leap.soledad.client.events import SOLEDAD_SYNC_RECEIVE_STATUS from leap.soledad.client.events import signal from leap.soledad.client.encdecpool import SyncDecrypterPool -from leap.soledad.client.http_client import httpRequest -from leap.soledad.client.http_client import configure_certificate logger = logging.getLogger(__name__) @@ -107,7 +109,7 @@ class SoledadHTTPSyncTarget(SyncTarget): # asynchronous encryption/decryption attributes self._decryption_callback = None self._sync_decr_pool = None - configure_certificate(cert_file) + self._http = HTTPClient(cert_file) def set_creds(self, creds): """ @@ -148,7 +150,7 @@ class SoledadHTTPSyncTarget(SyncTarget): source_replica_last_known_transaction_id) :rtype: twisted.internet.defer.Deferred """ - raw = yield httpRequest(self._url, headers=self._auth_header) + raw = yield self._http_request(self._url, headers=self._auth_header) res = json.loads(raw) defer.returnValue([ res['target_replica_uid'], @@ -193,7 +195,7 @@ class SoledadHTTPSyncTarget(SyncTarget): }) headers = self._auth_header.copy() headers.update({'content-type': ['application/json']}) - return httpRequest( + return self._http_request( self._url, method='PUT', headers=headers, @@ -330,7 +332,7 @@ class SoledadHTTPSyncTarget(SyncTarget): doc_idx=doc_idx) entries.append('\r\n]') data = ''.join(entries) - result = yield httpRequest( + result = yield self._http_request( self._url, method='POST', headers=headers, @@ -481,7 +483,7 @@ class SoledadHTTPSyncTarget(SyncTarget): ',', entries, received=received) entries.append('\r\n]') # send headers - return httpRequest( + return self._http_request( self._url, method='POST', headers=headers, @@ -596,3 +598,25 @@ class SoledadHTTPSyncTarget(SyncTarget): self._sync_db, insert_doc_cb=self._insert_doc_cb, source_replica_uid=self.source_replica_uid) + + def _http_request(self, url, method='GET', body=None, headers={}): + d = self._http.request(url, method, body, headers) + d.addErrback(_unauth_to_invalid_token_error) + return d + + +def _unauth_to_invalid_token_error(failure): + """ + An errback to translate unauthorized errors to our own invalid token + class. + + :param failure: The original failure. + :type failure: twisted.python.failure.Failure + + :return: Either the original failure or an invalid auth token error. + :rtype: twisted.python.failure.Failure + """ + failure.trap(Error) + if failure.getErrorMessage() == "401 Unauthorized": + raise InvalidAuthTokenError + return failure |