[tests] implemented http feature according to test
[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 _HTTP11ClientFactory(HTTP11ClientFactory):
60     """
61     A timeout-able HTTP 1.1 client protocol factory.
62     """
63
64     def __init__(self, quiescentCallback, timeout):
65         """
66         :param quiescentCallback: The quiescent callback to be passed to
67                                   protocol instances, used to return them to
68                                   the connection pool.
69         :type quiescentCallback: callable(Protocol)
70         :param timeout: The timeout, in seconds, for requests made by
71                         protocols created by this factory.
72         :type timeout: float
73         """
74         HTTP11ClientFactory.__init__(self, quiescentCallback)
75         self._timeout = timeout
76
77     def buildProtocol(self, _):
78         """
79         Build the HTTP 1.1 client protocol.
80         """
81         return _HTTP11ClientProtocol(self._quiescentCallback, self._timeout)
82
83
84 class _HTTPConnectionPool(HTTPConnectionPool):
85     """
86     A timeout-able HTTP connection pool.
87     """
88
89     _factory = _HTTP11ClientFactory
90
91     def __init__(self, reactor, persistent, timeout, maxPersistentPerHost=10):
92         HTTPConnectionPool.__init__(self, reactor, persistent=persistent)
93         self.maxPersistentPerHost = maxPersistentPerHost
94         self._timeout = timeout
95
96     def _newConnection(self, key, endpoint):
97         def quiescentCallback(protocol):
98             self._putConnection(key, protocol)
99         factory = self._factory(quiescentCallback, timeout=self._timeout)
100         return endpoint.connect(factory)
101
102
103 class HTTPClient(object):
104     """
105     HTTP client done the twisted way, with a main focus on pinning the SSL
106     certificate.
107
108     By default, it uses a shared connection pool. If you want a dedicated
109     one, create and pass on __init__ pool parameter.
110     Please note that this client will limit the maximum amount of connections
111     by using a DeferredSemaphore.
112     This limit is equal to the maxPersistentPerHost used on pool and is needed
113     in order to avoid resource abuse on huge requests batches.
114     """
115
116     _pool = _HTTPConnectionPool(
117         reactor,
118         persistent=True,
119         timeout = DEFAULT_HTTP_TIMEOUT,
120         maxPersistentPerHost=10
121     )
122
123     def __init__(self, cert_file=None, timeout=DEFAULT_HTTP_TIMEOUT, pool=None):
124         """
125         Init the HTTP client
126
127         :param cert_file: The path to the certificate file, if None given the
128                           system's CAs will be used.
129         :type cert_file: str
130         :param timeout: The amount of time that this Agent will wait for the
131                         peer to accept a connection and for each request to be
132                         finished. If a pool is passed, then this argument is
133                         ignored.
134         :type timeout: float
135         """
136
137         self._timeout = timeout
138         self._pool = pool if pool is not None else self._pool
139         self._agent = Agent(
140             reactor,
141             get_compatible_ssl_context_factory(cert_file),
142             pool=self._pool,
143             connectTimeout=self._timeout)
144         self._semaphore = defer.DeferredSemaphore(
145             self._pool.maxPersistentPerHost)
146
147     def _createPool(self, maxPersistentPerHost=10, persistent=True):
148         pool = _HTTPConnectionPool(reactor, persistent, self._timeout)
149         pool.maxPersistentPerHost = maxPersistentPerHost
150         return pool
151
152     def _request(self, url, method, body, headers):
153         """
154         Perform an HTTP request.
155
156         :param url: The URL for the request.
157         :type url: str
158         :param method: The HTTP method of the request.
159         :type method: str
160         :param body: The body of the request, if any.
161         :type body: str
162         :param headers: The headers of the request.
163         :type headers: dict
164
165         :return: A deferred that fires with the body of the request.
166         :rtype: twisted.internet.defer.Deferred
167         """
168         if body:
169             body = _StringBodyProducer(body)
170         d = self._agent.request(
171             method, url, headers=Headers(headers), bodyProducer=body)
172         d.addCallback(readBody)
173         return d
174
175     def request(self, url, method='GET', body=None, headers={}):
176         """
177         Perform an HTTP request, but limit the maximum amount of concurrent
178         connections.
179
180         :param url: The URL for the request.
181         :type url: str
182         :param method: The HTTP method of the request.
183         :type method: str
184         :param body: The body of the request, if any.
185         :type body: str
186         :param headers: The headers of the request.
187         :type headers: dict
188
189         :return: A deferred that fires with the body of the request.
190         :rtype: twisted.internet.defer.Deferred
191         """
192         return self._semaphore.run(self._request, url, method, body, headers)
193
194     def close(self):
195         """
196         Close any cached connections.
197         """
198         self._pool.closeCachedConnections()
199
200 #
201 # An IBodyProducer to write the body of an HTTP request as a string.
202 #
203
204
205 class _StringBodyProducer(object):
206     """
207     A producer that writes the body of a request to a consumer.
208     """
209
210     implements(IBodyProducer)
211
212     def __init__(self, body):
213         """
214         Initialize the string produer.
215
216         :param body: The body of the request.
217         :type body: str
218         """
219         self.body = body
220         self.length = len(body)
221
222     def startProducing(self, consumer):
223         """
224         Write the body to the consumer.
225
226         :param consumer: Any IConsumer provider.
227         :type consumer: twisted.internet.interfaces.IConsumer
228
229         :return: A successful deferred.
230         :rtype: twisted.internet.defer.Deferred
231         """
232         consumer.write(self.body)
233         return defer.succeed(None)
234
235     def pauseProducing(self):
236         pass
237
238     def stopProducing(self):
239         pass
240
241
242 #
243 # Patched twisted.web classes
244 #
245
246 class _HTTP11ClientProtocol(HTTP11ClientProtocol):
247     """
248     A timeout-able HTTP 1.1 client protocol, that is instantiated by the
249     _HTTP11ClientFactory below.
250     """
251
252     def __init__(self, quiescentCallback, timeout):
253         """
254         Initialize the protocol.
255
256         :param quiescentCallback:
257         :type quiescentCallback: callable
258         :param timeout: A timeout, in seconds, for requests made by this
259                         protocol.
260         :type timeout: float
261         """
262         HTTP11ClientProtocol.__init__(self, quiescentCallback)
263         self._timeout = timeout
264         self._timeoutCall = None
265
266     def request(self, request):
267         """
268         Issue request over self.transport and return a Deferred which
269         will fire with a Response instance or an error.
270
271         :param request: The object defining the parameters of the request to
272                         issue.
273         :type request: twisted.web._newclient.Request
274
275         :return: A deferred which fires after the request has finished.
276         :rtype: Deferred
277         """
278         d = HTTP11ClientProtocol.request(self, request)
279         if self._timeout:
280             self._last_buffer_len = 0
281             timeoutCall = reactor.callLater(
282                 self._timeout, self._doTimeout, request)
283             self._timeoutCall = timeoutCall
284         return d
285
286     def _doTimeout(self, request):
287         """
288         Give up the request because of a timeout.
289
290         :param request: The object defining the parameters of the request to
291                         issue.
292         :type request: twisted.web._newclient.Request
293         """
294         self._giveUp(
295             failure.Failure(
296                 defer.TimeoutError(
297                     "Getting %s took longer than %s seconds."
298                     % (request.absoluteURI, self._timeout))))
299
300     def _cancelTimeout(self):
301         """
302         Cancel the request timeout, when it's finished.
303         """
304         if self._timeoutCall.active():
305             self._timeoutCall.cancel()
306             self._timeoutCall = None
307
308     def _finishResponse_WAITING(self, rest):
309         """
310         Cancel the timeout when finished receiving the response.
311         """
312         self._cancelTimeout()
313         HTTP11ClientProtocol._finishResponse_WAITING(self, rest)
314
315     def _finishResponse_TRANSMITTING(self, rest):
316         """
317         Cancel the timeout when finished receiving the response.
318         """
319         self._cancelTimeout()
320         HTTP11ClientProtocol._finishResponse_TRANSMITTING(self, rest)
321
322     def dataReceived(self, bytes):
323         """
324         Receive some data and extend the timeout period of this request.
325
326         :param bytes: A string of indeterminate length.
327         :type bytes: str
328         """
329         HTTP11ClientProtocol.dataReceived(self, bytes)
330         if self._timeoutCall and self._timeoutCall.active():
331             self._timeoutCall.reset(self._timeout)