Merge lp:~mandel/ubuntu-sso-client/fix-alecus-mess into lp:ubuntu-sso-client

Proposed by Manuel de la Peña
Status: Merged
Approved by: Manuel de la Peña
Approved revision: 934
Merged at revision: 928
Proposed branch: lp:~mandel/ubuntu-sso-client/fix-alecus-mess
Merge into: lp:ubuntu-sso-client
Diff against target: 333 lines (+87/-129)
4 files modified
ubuntu_sso/utils/webclient/__init__.py (+3/-1)
ubuntu_sso/utils/webclient/common.py (+10/-4)
ubuntu_sso/utils/webclient/qtnetwork.py (+65/-116)
ubuntu_sso/utils/webclient/tests/test_webclient.py (+9/-8)
To merge this branch: bzr merge lp:~mandel/ubuntu-sso-client/fix-alecus-mess
Reviewer Review Type Date Requested Status
Roberto Alsina (community) Approve
Alejandro J. Cura (community) Approve
Review via email: mp+98308@code.launchpad.net

Commit message

- Changed the way in which we deal with proxies to work around bugs found in the QNetworkAccessManager (LP: #957313).

Description of the change

- Changed the way in which we deal with proxies to work around bugs found in the QNetworkAccessManager (LP: #957313).

To post a comment you must log in.
Revision history for this message
Alejandro J. Cura (alecu) wrote :

Ran tests, extensively tested IRL, and code looks as good as if I had written half of it.
Very well done!

review: Approve
Revision history for this message
Roberto Alsina (ralsina) wrote :

+1 code review, trust alecu's IRL

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ubuntu_sso/utils/webclient/__init__.py'
2--- ubuntu_sso/utils/webclient/__init__.py 2012-03-19 15:08:56 +0000
3+++ ubuntu_sso/utils/webclient/__init__.py 2012-03-19 23:59:18 +0000
4@@ -34,10 +34,12 @@
5
6 try:
7 from PyQt4.QtCore import QCoreApplication
8+ from PyQt4.QtGui import QApplication
9
10 # we could be running a process with or without ui, and those are diff
11 # apps.
12- result = QCoreApplication.instance() is not None
13+ result = (QCoreApplication.instance() is not None
14+ or QApplication.instance() is not None)
15 except ImportError:
16 pass
17
18
19=== modified file 'ubuntu_sso/utils/webclient/common.py'
20--- ubuntu_sso/utils/webclient/common.py 2012-03-19 15:08:56 +0000
21+++ ubuntu_sso/utils/webclient/common.py 2012-03-19 23:59:18 +0000
22@@ -178,15 +178,21 @@
23 from ubuntu_sso.keyring import Keyring
24 keyring = Keyring()
25 try:
26- creds = yield keyring.get_credentials(domain)
27+ creds = yield keyring.get_credentials(str(domain))
28 logger.debug('Got credentials from keyring.')
29 except Exception, e:
30 logger.error('Error when retrieving the creds.')
31 raise WebClientError('Error when retrieving the creds.', e)
32 if creds is not None:
33- self.proxy_username = creds['username']
34- self.proxy_password = creds['password']
35- defer.returnValue(True)
36+ # if we are loading the same creds it means that we got the wrong
37+ # ones
38+ if (self.proxy_username == creds['username'] and
39+ self.proxy_password == creds['password']):
40+ defer.returnValue(False)
41+ else:
42+ self.proxy_username = creds['username']
43+ self.proxy_password = creds['password']
44+ defer.returnValue(True)
45 logger.debug('Proxy creds not in keyring.')
46 defer.returnValue(False)
47
48
49=== modified file 'ubuntu_sso/utils/webclient/qtnetwork.py'
50--- ubuntu_sso/utils/webclient/qtnetwork.py 2012-03-19 16:43:22 +0000
51+++ ubuntu_sso/utils/webclient/qtnetwork.py 2012-03-19 23:59:18 +0000
52@@ -48,107 +48,75 @@
53 logger = setup_logging("ubuntu_sso.utils.webclient.qtnetwork")
54
55
56-class WebClientProxyFactory(QNetworkProxyFactory):
57- """Proxy factory used by the web client.
58-
59- The proxy factory ensures that the correct proxy is used according to
60- the request performed by the web client. For example, if the system has
61- different proxies set for the http and the https the proxy factory will
62- return the correct proxy according to the scheme of the request.
63- """
64-
65- def __init__(self):
66- """Create a new instance."""
67- super(WebClientProxyFactory, self).__init__()
68- self._proxies = dict(default=QNetworkProxy(QNetworkProxy.DefaultProxy))
69- self._setup_proxies()
70-
71- def _get_proxy_for_settings(self, settings):
72- """Return a proxy that uses the given settings."""
73- if "username" in settings and "password" in settings:
74- logger.debug('Setting auth %s:%s proxy',
75- settings.get("host", ""), settings.get("port", 0))
76- proxy = QNetworkProxy(QNetworkProxy.HttpProxy,
77- hostName=settings.get("host", ""),
78- port=settings.get("port", 0),
79- user=settings.get("username", ""),
80- password=settings.get("password", ""))
81- else:
82- logger.debug('Using nonauth proxy %s:%s', settings.get("host", ""),
83- settings.get("port", 0))
84- proxy = QNetworkProxy(QNetworkProxy.HttpProxy,
85- hostName=settings.get("host", ""),
86- port=settings.get("port", 0))
87- return proxy
88-
89- def _setup_proxies(self):
90- """Set the different proxies used in the system."""
91- proxy_settings = gsettings.get_proxy_settings()
92- for scheme, settings in proxy_settings.iteritems():
93- self._proxies[scheme] = self._get_proxy_for_settings(settings)
94-
95- # ignore the complain since we have to use the Qt naming convention
96- # pylint: disable=C0103
97- def queryProxy(self, query):
98- """Return the proxy servers to be used in order of preference."""
99- if 'force' in self._proxies:
100- return [self._proxies['force']]
101- if self._proxies:
102- scheme = str(query.protocolTag())
103- if scheme in self._proxies:
104- return [self._proxies[scheme]]
105- # this function most return always at least one item, since we do not
106- # have a proxy set for the query let's return the system default proxy
107- return [self._proxies['default']]
108- # pylint: enable=C0103
109-
110- def update_proxy(self, scheme, settings):
111- """Allows to update the proxy to be used for a scheme."""
112- self._proxies[scheme] = self._get_proxy_for_settings(settings)
113-
114- def force_use_proxy(self, settings):
115- """Force to use the proxies in the given settings."""
116- self._proxies['force'] = self._get_proxy_for_settings(settings)
117-
118- def find_proxy_schemes(self, proxy):
119- """Return the scheme in wich the proxy is used."""
120- schemes = []
121- for scheme, scheme_proxy in self._proxies.iteritems():
122- if scheme_proxy == proxy:
123- schemes.append(scheme)
124- return schemes
125-
126- def find_proxy_for_scheme(self, scheme):
127- """Return the proxy used for the scheme or default otherwise."""
128- if scheme in self._proxies:
129- return self._proxies[scheme]
130- return self._proxies['default']
131+def build_proxy(settings_groups):
132+ """Create a QNetworkProxy from these settings."""
133+ proxy_groups = [
134+ ("socks", QNetworkProxy.Socks5Proxy),
135+ ("https", QNetworkProxy.HttpProxy),
136+ ("http", QNetworkProxy.HttpProxy),
137+ ]
138+ for group, proxy_type in proxy_groups:
139+ if group not in settings_groups:
140+ continue
141+ settings = settings_groups[group]
142+ if "host" in settings and "port" in settings:
143+ return QNetworkProxy(proxy_type,
144+ hostName=settings.get("host", ""),
145+ port=settings.get("port", 0),
146+ user=settings.get("username", ""),
147+ password=settings.get("password", ""))
148+ logger.error("No proxy correctly configured.")
149+ return QNetworkProxy(QNetworkProxy.DefaultProxy)
150
151
152 class WebClient(BaseWebClient):
153 """A webclient with a qtnetwork backend."""
154
155+ proxy_instance = None
156+
157 def __init__(self, *args, **kwargs):
158 """Initialize this instance."""
159 super(WebClient, self).__init__(*args, **kwargs)
160 self.nam = QNetworkAccessManager(QCoreApplication.instance())
161 self.nam.finished.connect(self._handle_finished)
162 self.nam.authenticationRequired.connect(self._handle_authentication)
163+ self.nam.proxyAuthenticationRequired.connect(self.handle_proxy_auth)
164 # Disabled until we make this a per-instance option
165 #self.nam.sslErrors.connect(self._handle_ssl_errors)
166 self.replies = {}
167- self.proxy_factory = None
168+ self.proxy_retry = False
169 self.setup_proxy()
170
171 def setup_proxy(self):
172 """Setup the proxy settings if needed."""
173 # QtNetwork knows how to use the system settings on both Win and Mac
174 if sys.platform.startswith("linux"):
175- self.proxy_factory = WebClientProxyFactory()
176- self.nam.setProxyFactory(self.proxy_factory)
177+ settings = gsettings.get_proxy_settings()
178+ enabled = len(settings) > 0
179+ if enabled and WebClient.proxy_instance is None:
180+ proxy = build_proxy(settings)
181+ QNetworkProxy.setApplicationProxy(proxy)
182+ WebClient.proxy_instance = proxy
183+ elif enabled and WebClient.proxy_instance:
184+ logger.info("Proxy already in use.")
185+ else:
186+ logger.info("Proxy is disabled.")
187 else:
188 QNetworkProxyFactory.setUseSystemConfiguration(True)
189
190+ def handle_proxy_auth(self, proxy, authenticator):
191+ """Proxy authentication is required."""
192+ logger.info("auth_required %r, %r", self.proxy_username,
193+ proxy.hostName())
194+ if (self.proxy_username is not None and
195+ self.proxy_username != str(authenticator.user())):
196+ authenticator.setUser(self.proxy_username)
197+ WebClient.proxy_instance.setUser(self.proxy_username)
198+ if (self.proxy_password is not None and
199+ self.proxy_password != str(authenticator.password())):
200+ authenticator.setPassword(self.proxy_password)
201+ WebClient.proxy_instance.setPassword(self.proxy_password)
202+
203 def _perform_request(self, request, method, post_buffer):
204 """Return a deferred that will be fired with a Response object."""
205 d = defer.Deferred()
206@@ -158,11 +126,6 @@
207 reply = self.nam.head(request)
208 else:
209 reply = self.nam.sendCustomRequest(request, method, post_buffer)
210- # deal with auth errors in case we get
211- # ProxyAuthenticationRequiredError
212- scheme = str(request.url().scheme())
213- d.addErrback(self._handle_proxy_authentication, scheme, method, reply,
214- post_buffer)
215 self.replies[reply] = d
216 return d
217
218@@ -189,7 +152,20 @@
219
220 post_buffer = QBuffer()
221 post_buffer.setData(post_content)
222- result = yield self._perform_request(request, method, post_buffer)
223+ try:
224+ result = yield self._perform_request(request, method, post_buffer)
225+ except ProxyUnauthorizedError, e:
226+ app_proxy = QNetworkProxy.applicationProxy()
227+ proxy_host = app_proxy.hostName() if app_proxy else "proxy server"
228+ got_creds = yield self.request_proxy_auth_credentials(
229+ proxy_host, self.proxy_retry)
230+ if got_creds:
231+ self.proxy_retry = True
232+ result = yield self.request(iri, method, extra_headers,
233+ oauth_credentials, post_content)
234+ else:
235+ excp = WebClientError('Proxy creds needed.', e)
236+ defer.returnValue(excp)
237 defer.returnValue(result)
238
239 def _handle_authentication(self, reply, authenticator):
240@@ -199,36 +175,6 @@
241 if authenticator.password() != self.password:
242 authenticator.setPassword(self.password)
243
244- @defer.inlineCallbacks
245- def _handle_proxy_authentication(self, failure, scheme, method, reply,
246- post_buffer=None, retry=False):
247- """The reply needs authentication."""
248- failure.trap(ProxyUnauthorizedError)
249- current_proxy = self.proxy_factory.find_proxy_for_scheme(scheme)
250- host = unicode(current_proxy.hostName()).encode('utf8')
251- port = current_proxy.port()
252- settings = dict(host=host, port=port)
253- # we have to auth the request and retry it
254- if not retry and self.proxy_username and self.proxy_password:
255- settings.update({'username': self.proxy_username,
256- 'password': self.proxy_password})
257- if retry or self.proxy_username is None or self.proxy_password is None:
258- got_creds = yield self.request_proxy_auth_credentials(host, retry)
259- if got_creds:
260- logger.debug('Got proxy credentials from user.')
261- settings.update({'username': self.proxy_username,
262- 'password': self.proxy_password})
263- if (settings and 'username' in settings
264- and 'password' in settings):
265- request = reply.request()
266- scheme = str(request.url().scheme())
267- self.proxy_factory.update_proxy(scheme, settings)
268- # let's retry using the request
269- result = yield self._perform_request(request, method, post_buffer)
270- defer.returnValue(result)
271- else:
272- defer.returnValue(ProxyUnauthorizedError())
273-
274 def _handle_finished(self, reply):
275 """The reply has finished processing."""
276 assert reply in self.replies
277@@ -282,9 +228,12 @@
278 if trust_cert:
279 reply.ignoreSslErrors()
280
281- def force_use_proxy(self, settings):
282+ def force_use_proxy(self, https_settings):
283 """Setup this webclient to use the given proxy settings."""
284- self.proxy_factory.force_use_proxy(settings)
285+ settings = {"https": https_settings}
286+ proxy = build_proxy(settings)
287+ QNetworkProxy.setApplicationProxy(proxy)
288+ WebClient.proxy_instance = proxy
289
290 def shutdown(self):
291 """Shut down all pending requests (if possible)."""
292
293=== modified file 'ubuntu_sso/utils/webclient/tests/test_webclient.py'
294--- ubuntu_sso/utils/webclient/tests/test_webclient.py 2012-03-19 16:34:27 +0000
295+++ ubuntu_sso/utils/webclient/tests/test_webclient.py 2012-03-19 23:59:18 +0000
296@@ -452,6 +452,8 @@
297 class BasicProxyTestCase(SquidTestCase):
298 """Test that the proxy works at all."""
299
300+ timeout = 3
301+
302 @defer.inlineCallbacks
303 def setUp(self):
304 yield super(BasicProxyTestCase, self).setUp()
305@@ -595,14 +597,6 @@
306 test_auth_proxy_is_requested_creds_bad_details_everywhere.skip = reason
307 test_auth_proxy_is_requested_user_cancels.skip = reason
308
309- if WEBCLIENT_MODULE_NAME.endswith(".qtnetwork"):
310- reason = 'QNetworkAccessManager is buggy.'
311- test_auth_proxy_is_used_creds_requested.skip = reason
312- test_auth_proxy_is_requested_creds_bad_details.skip = reason
313- test_auth_proxy_is_requested_creds_bad_details_user.skip = reason
314- test_auth_proxy_is_requested_creds_bad_details_everywhere.skip = reason
315- test_auth_proxy_is_requested_user_cancels.skip = reason
316-
317
318 class HeaderDictTestCase(TestCase):
319 """Tests for the case insensitive header dictionary."""
320@@ -1003,6 +997,13 @@
321 test_https_request.skip = reason
322 test_http_request.skip = reason
323
324+ if WEBCLIENT_MODULE_NAME.endswith(".qtnetwork"):
325+ reason = ('Updating proxy settings is not well support due to bug'
326+ ' QTBUG-14850: https://bugreports.qt-project.org/'
327+ 'browse/QTBUG-14850')
328+ test_https_request.skip = reason
329+ test_http_request.skip = reason
330+
331
332 class SSLTestCase(BaseSSLTestCase):
333 """Test error handling when dealing with ssl."""

Subscribers

People subscribed via source and target branches