c93e65b92f4de7bcaa7d0e02d1c45a56327de2f0
[leap_pycommon.git] / src / leap / common / http.py
1 # -*- coding: utf-8 -*-
2 # http.py
3 # Copyright (C) 2015 LEAP
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 """
18 Twisted HTTP/HTTPS client.
19 """
20
21 try:
22     import twisted
23 except ImportError:
24     print "*******"
25     print "Twisted is needed to use leap.common.http module"
26     print ""
27     print "Install the extra requirement of the package:"
28     print "$ pip install leap.common[Twisted]"
29     import sys
30     sys.exit(1)
31
32
33 from leap.common.certs import get_compatible_ssl_context_factory
34
35 from zope.interface import implements
36
37 from twisted.internet import reactor
38 from twisted.internet import defer
39 from twisted.python import failure
40
41 from twisted.web.client import Agent
42 from twisted.web.client import HTTPConnectionPool
43 from twisted.web.client import _HTTP11ClientFactory as HTTP11ClientFactory
44 from twisted.web.client import readBody
45 from twisted.web.http_headers import Headers
46 from twisted.web.iweb import IBodyProducer
47 from twisted.web._newclient import HTTP11ClientProtocol
48
49
50 __all__ = ["HTTPClient"]
51
52
53 # A default HTTP timeout is used for 2 distinct purposes:
54 #   1. as HTTP connection timeout, prior to connection estabilshment.
55 #   2. as data reception timeout, after the connection has been established.
56 DEFAULT_HTTP_TIMEOUT = 30  # seconds
57
58
59 class HTTPClient(object):
60     """
61     HTTP client done the twisted way, with a main focus on pinning the SSL
62     certificate.
63
64     By default, it uses a shared connection pool. If you want a dedicated
65     one, create and pass on __init__ pool parameter.
66     Please note that this client will limit the maximum amount of connections
67     by using a DeferredSemaphore.
68     This limit is equal to the maxPersistentPerHost used on pool and is needed
69     in order to avoid resource abuse on huge requests batches.
70     """
71
72     def __init__(self, cert_file=None, timeout=DEFAULT_HTTP_TIMEOUT):
73         """
74         Init the HTTP client
75
76         :param cert_file: The path to the certificate file, if None given the
77                           system's CAs will be used.
78         :type cert_file: str
79         :param timeout: The amount of time that this Agent will wait for the
80                         peer to accept a connection and for each request to be
81                         finished. If a pool is passed, then this argument is
82                         ignored.
83         :type timeout: float
84         """
85
86         self._timeout = timeout
87         self._pool = self._createPool()
88         self._agent = Agent(
89             reactor,
90             get_compatible_ssl_context_factory(cert_file),
91             pool=self._pool,
92             connectTimeout=self._timeout)
93         self._semaphore = defer.DeferredSemaphore(
94             self._pool.maxPersistentPerHost)
95
96     def _createPool(self, maxPersistentPerHost=10, persistent=True):
97         pool = _HTTPConnectionPool(reactor, persistent, self._timeout)
98         pool.maxPersistentPerHost = maxPersistentPerHost
99         return pool
100
101     def _request(self, url, method, body, headers):
102         """
103         Perform an HTTP request.
104
105         :param url: The URL for the request.
106         :type url: str
107         :param method: The HTTP method of the request.
108         :type method: str
109         :param body: The body of the request, if any.
110         :type body: str
111         :param headers: The headers of the request.
112         :type headers: dict
113
114         :return: A deferred that fires with the body of the request.
115         :rtype: twisted.internet.defer.Deferred
116         """
117         if body:
118             body = _StringBodyProducer(body)
119         d = self._agent.request(
120             method, url, headers=Headers(headers), bodyProducer=body)
121         d.addCallback(readBody)
122         return d
123
124     def request(self, url, method='GET', body=None, headers={}):
125         """
126         Perform an HTTP request, but limit the maximum amount of concurrent
127         connections.
128
129         :param url: The URL for the request.
130         :type url: str
131         :param method: The HTTP method of the request.
132         :type method: str
133         :param body: The body of the request, if any.
134         :type body: str
135         :param headers: The headers of the request.
136         :type headers: dict
137
138         :return: A deferred that fires with the body of the request.
139         :rtype: twisted.internet.defer.Deferred
140         """
141         return self._semaphore.run(self._request, url, method, body, headers)
142
143     def close(self):
144         """
145         Close any cached connections.
146         """
147         self._pool.closeCachedConnections()
148
149 #
150 # An IBodyProducer to write the body of an HTTP request as a string.
151 #
152
153 class _StringBodyProducer(object):
154     """
155     A producer that writes the body of a request to a consumer.
156     """
157
158     implements(IBodyProducer)
159
160     def __init__(self, body):
161         """
162         Initialize the string produer.
163
164         :param body: The body of the request.
165         :type body: str
166         """
167         self.body = body
168         self.length = len(body)
169
170     def startProducing(self, consumer):
171         """
172         Write the body to the consumer.
173
174         :param consumer: Any IConsumer provider.
175         :type consumer: twisted.internet.interfaces.IConsumer
176
177         :return: A successful deferred.
178         :rtype: twisted.internet.defer.Deferred
179         """
180         consumer.write(self.body)
181         return defer.succeed(None)
182
183     def pauseProducing(self):
184         pass
185
186     def stopProducing(self):
187         pass
188
189
190 #
191 # Patched twisted.web classes
192 #
193
194 class _HTTP11ClientProtocol(HTTP11ClientProtocol):
195     """
196     A timeout-able HTTP 1.1 client protocol, that is instantiated by the
197     _HTTP11ClientFactory below.
198     """
199
200     def __init__(self, quiescentCallback, timeout):
201         """
202         Initialize the protocol.
203
204         :param quiescentCallback:
205         :type quiescentCallback: callable
206         :param timeout: A timeout, in seconds, for requests made by this
207                         protocol.
208         :type timeout: float
209         """
210         HTTP11ClientProtocol.__init__(self, quiescentCallback)
211         self._timeout = timeout
212         self._timeoutCall = None
213
214     def request(self, request):
215         """
216         Issue request over self.transport and return a Deferred which
217         will fire with a Response instance or an error.
218
219         :param request: The object defining the parameters of the request to
220                         issue.
221         :type request: twisted.web._newclient.Request
222
223         :return: A deferred which fires after the request has finished.
224         :rtype: Deferred
225         """
226         d = HTTP11ClientProtocol.request(self, request)
227         if self._timeout:
228             self._last_buffer_len = 0
229             timeoutCall = reactor.callLater(
230                 self._timeout, self._doTimeout, request)
231             self._timeoutCall = timeoutCall
232         return d
233
234     def _doTimeout(self, request):
235         """
236         Give up the request because of a timeout.
237
238         :param request: The object defining the parameters of the request to
239                         issue.
240         :type request: twisted.web._newclient.Request
241         """
242         self._giveUp(
243             failure.Failure(
244                 defer.TimeoutError(
245                     "Getting %s took longer than %s seconds."
246                     % (request.absoluteURI, self._timeout))))
247
248     def _cancelTimeout(self):
249         """
250         Cancel the request timeout, when it's finished.
251         """
252         if self._timeoutCall.active():
253             self._timeoutCall.cancel()
254             self._timeoutCall = None
255
256     def _finishResponse_WAITING(self, rest):
257        """
258        Cancel the timeout when finished receiving the response.
259        """
260        self._cancelTimeout()
261        HTTP11ClientProtocol._finishResponse_WAITING(self, rest)
262
263     def _finishResponse_TRANSMITTING(self, rest):
264        """
265        Cancel the timeout when finished receiving the response.
266        """
267        self._cancelTimeout()
268        HTTP11ClientProtocol._finishResponse_TRANSMITTING(self, rest)
269
270     def dataReceived(self, bytes):
271         """
272         Receive some data and extend the timeout period of this request.
273
274         :param bytes: A string of indeterminate length.
275         :type bytes: str
276         """
277         HTTP11ClientProtocol.dataReceived(self, bytes)
278         if self._timeoutCall and self._timeoutCall.active():
279             self._timeoutCall.reset(self._timeout)
280
281
282 class _HTTP11ClientFactory(HTTP11ClientFactory):
283     """
284     A timeout-able HTTP 1.1 client protocol factory.
285     """
286
287     def __init__(self, quiescentCallback, timeout):
288         """
289         :param quiescentCallback: The quiescent callback to be passed to
290                                   protocol instances, used to return them to
291                                   the connection pool.
292         :type quiescentCallback: callable(Protocol)
293         :param timeout: The timeout, in seconds, for requests made by
294                         protocols created by this factory.
295         :type timeout: float
296         """
297         HTTP11ClientFactory.__init__(self, quiescentCallback)
298         self._timeout = timeout
299
300     def buildProtocol(self, _):
301         """
302         Build the HTTP 1.1 client protocol.
303         """
304         return _HTTP11ClientProtocol(self._quiescentCallback, self._timeout)
305
306
307 class _HTTPConnectionPool(HTTPConnectionPool):
308     """
309     A timeout-able HTTP connection pool.
310     """
311
312     _factory = _HTTP11ClientFactory
313
314     def __init__(self, reactor, persistent, timeout):
315         HTTPConnectionPool.__init__(self, reactor, persistent=persistent)
316         self._timeout = timeout
317
318     def _newConnection(self, key, endpoint):
319         def quiescentCallback(protocol):
320             self._putConnection(key, protocol)
321         factory = self._factory(quiescentCallback, timeout=self._timeout)
322         return endpoint.connect(factory)