[feat] refactor events to use ZMQ
[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 import os
22
23 from zope.interface import implements
24
25 from OpenSSL.crypto import load_certificate
26 from OpenSSL.crypto import FILETYPE_PEM
27
28 from twisted.internet import reactor
29 from twisted.internet.ssl import ClientContextFactory
30 from twisted.internet.ssl import CertificateOptions
31 from twisted.internet.defer import succeed
32
33 from twisted.web.client import Agent
34 from twisted.web.client import HTTPConnectionPool
35 from twisted.web.client import readBody
36 from twisted.web.client import BrowserLikePolicyForHTTPS
37 from twisted.web.http_headers import Headers
38 from twisted.web.iweb import IBodyProducer
39
40
41 class HTTPClient(object):
42     """
43     HTTP client done the twisted way, with a main focus on pinning the SSL
44     certificate.
45     """
46
47     def __init__(self, cert_file=None):
48         """
49         Init the HTTP client
50
51         :param cert_file: The path to the certificate file, if None given the
52                           system's CAs will be used.
53         :type cert_file: str
54         """
55         self._pool = HTTPConnectionPool(reactor, persistent=True)
56         self._pool.maxPersistentPerHost = 10
57
58         if cert_file:
59             cert = self._load_cert(cert_file)
60             self._agent = Agent(
61                 reactor,
62                 HTTPClient.ClientContextFactory(cert),
63                 pool=self._pool)
64         else:
65             # trust the system's CAs
66             self._agent = Agent(
67                 reactor,
68                 BrowserLikePolicyForHTTPS(),
69                 pool=self._pool)
70
71     def _load_cert(self, cert_file):
72         """
73         Load a X509 certificate from a file.
74
75         :param cert_file: The path to the certificate file.
76         :type cert_file: str
77
78         :return: The X509 certificate.
79         :rtype: OpenSSL.crypto.X509
80         """
81         if os.path.exists(cert_file):
82             with open(cert_file) as f:
83                 data = f.read()
84                 return load_certificate(FILETYPE_PEM, data)
85
86     def request(self, url, method='GET', body=None, headers={}):
87         """
88         Perform an HTTP request.
89
90         :param url: The URL for the request.
91         :type url: str
92         :param method: The HTTP method of the request.
93         :type method: str
94         :param body: The body of the request, if any.
95         :type body: str
96         :param headers: The headers of the request.
97         :type headers: dict
98
99         :return: A deferred that fires with the body of the request.
100         :rtype: twisted.internet.defer.Deferred
101         """
102         if body:
103             body = HTTPClient.StringBodyProducer(body)
104         d = self._agent.request(
105             method, url, headers=Headers(headers), bodyProducer=body)
106         d.addCallback(readBody)
107         return d
108
109     class ClientContextFactory(ClientContextFactory):
110         """
111         A context factory that will verify the server's certificate against a
112         given CA certificate.
113         """
114
115         def __init__(self, cacert):
116             """
117             Initialize the context factory.
118
119             :param cacert: The CA certificate.
120             :type cacert: OpenSSL.crypto.X509
121             """
122             self._cacert = cacert
123
124         def getContext(self, hostname, port):
125             opts = CertificateOptions(verify=True, caCerts=[self._cacert])
126             return opts.getContext()
127
128     class StringBodyProducer(object):
129         """
130         A producer that writes the body of a request to a consumer.
131         """
132
133         implements(IBodyProducer)
134
135         def __init__(self, body):
136             """
137             Initialize the string produer.
138
139             :param body: The body of the request.
140             :type body: str
141             """
142             self.body = body
143             self.length = len(body)
144
145         def startProducing(self, consumer):
146             """
147             Write the body to the consumer.
148
149             :param consumer: Any IConsumer provider.
150             :type consumer: twisted.internet.interfaces.IConsumer
151
152             :return: A successful deferred.
153             :rtype: twisted.internet.defer.Deferred
154             """
155             consumer.write(self.body)
156             return succeed(None)
157
158         def pauseProducing(self):
159             pass
160
161         def stopProducing(self):
162             pass