Merge lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update into lp:ubuntu-sso-client/stable-3-0
- stable-3-0-update
- Merge into stable-3-0
Status: | Merged | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Approved by: | Natalia Bidart | ||||||||||||||||
Approved revision: | 817 | ||||||||||||||||
Merged at revision: | 816 | ||||||||||||||||
Proposed branch: | lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update | ||||||||||||||||
Merge into: | lp:ubuntu-sso-client/stable-3-0 | ||||||||||||||||
Diff against target: |
1552 lines (+944/-228) 23 files modified
bin/ubuntu-sso-login (+14/-59) bin/windows-ubuntu-sso-login (+0/-86) run-tests.bat (+1/-1) ubuntu_sso/gtk/gui.py (+1/-1) ubuntu_sso/gtk/tests/test_gui.py (+1/-1) ubuntu_sso/main/__init__.py (+8/-8) ubuntu_sso/main/linux.py (+48/-7) ubuntu_sso/main/tests/test_linux.py (+4/-1) ubuntu_sso/main/tests/test_windows.py (+78/-51) ubuntu_sso/main/windows.py (+46/-3) ubuntu_sso/qt/controllers.py (+7/-3) ubuntu_sso/qt/tests/test_controllers.py (+18/-1) ubuntu_sso/utils/__init__.py (+1/-1) ubuntu_sso/utils/tests/test_tcpactivation.py (+17/-2) ubuntu_sso/utils/tests/test_txsecrets.py (+1/-1) ubuntu_sso/utils/ui.py (+2/-2) ubuntu_sso/utils/webclient/__init__.py (+48/-0) ubuntu_sso/utils/webclient/common.py (+81/-0) ubuntu_sso/utils/webclient/libsoup.py (+87/-0) ubuntu_sso/utils/webclient/qtnetwork.py (+101/-0) ubuntu_sso/utils/webclient/tests/__init__.py (+17/-0) ubuntu_sso/utils/webclient/tests/test_webclient.py (+303/-0) ubuntu_sso/utils/webclient/txweb.py (+60/-0) |
||||||||||||||||
To merge this branch: | bzr merge lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update | ||||||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Roberto Alsina (community) | Approve | ||
Review via email: mp+86283@code.launchpad.net |
Commit message
[ Alejandro J. Cura <email address hidden> ]
- An async webclient with qtnetwork and libsoup backends, to be used for
proxy support.
- Use the dedicated time url (LP: #891644).
[ Diego Sarmentero <email address hidden> ]
- Fixed pep8 issue.
- Fix double back navigation in SSO reset code page (LP: #862403).
[ Manuel de la Pena <email address hidden> ]
- Fixed the tests by ensuring that the server and the client are correctly
closed.
- Changed the import from ubuntuone-dev-tools so that we do not use the
deprecated API.
[ Natalia B. Bidart <email address hidden> ]
- Do not hardcode the app_name when showing the TC_NOT_ACCEPTED message
(LP: #904917).
- Pass module for test to u1trial properly.
- Have a single executable to start the service (LP: #890416).
- Lint fixes (LP: #890349).
Description of the change
All green in ubuntu, and windows (tested windows 7).
IRL tested on Ubuntu.
Preview Diff
1 | === modified file 'bin/ubuntu-sso-login' |
2 | --- bin/ubuntu-sso-login 2011-09-27 14:06:12 +0000 |
3 | +++ bin/ubuntu-sso-login 2011-12-19 19:46:24 +0000 |
4 | @@ -1,9 +1,5 @@ |
5 | -#!/usr/bin/python |
6 | - |
7 | -# ubuntu-sso-login - Client side log-in utility for Ubuntu One |
8 | -# |
9 | -# Author: Rodney Dawes <rodney.dawes@canonical.com> |
10 | -# Author: Natalia B. Bidart <natalia.bidart@canonical.com> |
11 | +#!/usr/bin/env python |
12 | +# -*- coding: utf-8 -*- |
13 | # |
14 | # Copyright 2009-2010 Canonical Ltd. |
15 | # |
16 | @@ -19,7 +15,7 @@ |
17 | # You should have received a copy of the GNU General Public License along |
18 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | |
20 | -"""Run the dbus service for UserManagement and ApplicationCredentials.""" |
21 | +"""Start the sso service.""" |
22 | |
23 | # Invalid name "ubuntu-sso-login", pylint: disable=C0103 |
24 | |
25 | @@ -41,60 +37,19 @@ |
26 | # val = globals()[globalname] |
27 | # KeyError: 'ROUND_CEiLiNG' |
28 | |
29 | -import signal |
30 | import sys |
31 | |
32 | -import dbus.mainloop.glib |
33 | -import dbus.service |
34 | -import gtk |
35 | - |
36 | -from dbus.mainloop.glib import DBusGMainLoop |
37 | - |
38 | -from ubuntu_sso import (DBUS_BUS_NAME, DBUS_ACCOUNT_PATH, DBUS_CRED_PATH, |
39 | - DBUS_CREDENTIALS_PATH) |
40 | -from ubuntu_sso.main import SSOLogin, CredentialsManagement |
41 | - |
42 | -from ubuntu_sso.logger import setup_logging |
43 | - |
44 | -# Invalid name "ubuntu-sso-login" |
45 | -# pylint: disable=C0103 |
46 | - |
47 | - |
48 | -logger = setup_logging("ubuntu-sso-login") |
49 | -dbus.mainloop.glib.threads_init() |
50 | -gtk.gdk.threads_init() |
51 | -DBusGMainLoop(set_as_default=True) |
52 | - |
53 | - |
54 | -def sighup_handler(*a, **kw): |
55 | - """Stop the service.""" |
56 | - # This handler may be called in any thread, so is not thread safe. |
57 | - # See the link below for info: |
58 | - # www.listware.net/201004/gtk-devel-list/115067-unix-signals-in-glib.html |
59 | - # |
60 | - # gtk.main_quit and the logger methods are safe to be called from any |
61 | - # thread. Just don't call other random stuff here. |
62 | - logger.info("Stoping Ubuntu SSO login manager since SIGHUP was received.") |
63 | - gtk.main_quit() |
64 | +if sys.platform == 'win32': |
65 | + from PyQt4 import QtGui |
66 | + # need to create the QApplication before installing the reactor |
67 | + QtGui.QApplication(sys.argv) |
68 | + |
69 | + # pylint: disable=F0401 |
70 | + import qt4reactor |
71 | + qt4reactor.install() |
72 | + |
73 | +from ubuntu_sso.main import main |
74 | |
75 | |
76 | if __name__ == "__main__": |
77 | - # Register DBus service for making sure we run only one instance |
78 | - bus = dbus.SessionBus() |
79 | - name = bus.request_name(DBUS_BUS_NAME, |
80 | - dbus.bus.NAME_FLAG_DO_NOT_QUEUE) |
81 | - if name == dbus.bus.REQUEST_NAME_REPLY_EXISTS: |
82 | - logger.error("Ubuntu SSO login manager already running, quitting.") |
83 | - sys.exit(0) |
84 | - |
85 | - logger.debug("Hooking up SIGHUP with handler %r.", sighup_handler) |
86 | - signal.signal(signal.SIGHUP, sighup_handler) |
87 | - |
88 | - logger.info("Starting Ubuntu SSO login manager for bus %r.", DBUS_BUS_NAME) |
89 | - bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus()) |
90 | - SSOLogin(bus_name, object_path=DBUS_ACCOUNT_PATH) |
91 | - CredentialsManagement(timeout_func=gtk.timeout_add, |
92 | - shutdown_func=gtk.main_quit, |
93 | - bus_name=bus_name, object_path=DBUS_CREDENTIALS_PATH) |
94 | - |
95 | - gtk.main() |
96 | + main() |
97 | |
98 | === removed file 'bin/windows-ubuntu-sso-login' |
99 | --- bin/windows-ubuntu-sso-login 2011-11-10 20:14:29 +0000 |
100 | +++ bin/windows-ubuntu-sso-login 1970-01-01 00:00:00 +0000 |
101 | @@ -1,86 +0,0 @@ |
102 | -#!/usr/bin/env python |
103 | -# -*- coding: utf-8 -*- |
104 | -# Authors: Manuel de la Pena <manuel@canonical.com> |
105 | -# Alejandro J. Cura <alecu@canonical.com> |
106 | -# |
107 | -# Copyright 2011 Canonical Ltd. |
108 | -# |
109 | -# This program is free software: you can redistribute it and/or modify it |
110 | -# under the terms of the GNU General Public License version 3, as published |
111 | -# by the Free Software Foundation. |
112 | -# |
113 | -# This program is distributed in the hope that it will be useful, but |
114 | -# WITHOUT ANY WARRANTY; without even the implied warranties of |
115 | -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
116 | -# PURPOSE. See the GNU General Public License for more details. |
117 | -# |
118 | -# You should have received a copy of the GNU General Public License along |
119 | -# with this program. If not, see <http://www.gnu.org/licenses/>. |
120 | -"""Start the sso service on a windows machine.""" |
121 | - |
122 | -# disable the name warning and complains about twisted |
123 | -# pylint: disable=C0103, E1101, F0401 |
124 | -import sys |
125 | - |
126 | -from PyQt4 import QtGui |
127 | -# need to create the QApplication before installing the reactor |
128 | -app = QtGui.QApplication(sys.argv) |
129 | -import qt4reactor |
130 | -qt4reactor.install() |
131 | - |
132 | -from twisted.internet import reactor, defer |
133 | -from twisted.spread.pb import PBServerFactory |
134 | -from twisted.internet.task import LoopingCall |
135 | -from twisted.python import log |
136 | - |
137 | -from ubuntu_sso.logger import setup_logging |
138 | -from ubuntu_sso.main.windows import ( |
139 | - CredentialsManagement, |
140 | - LOCALHOST, |
141 | - SSOLogin, |
142 | - UbuntuSSORoot, |
143 | - get_activation_config, |
144 | -) |
145 | -from ubuntu_sso.utils import tcpactivation |
146 | - |
147 | - |
148 | -logger = setup_logging("windows-ubuntu-sso-login") |
149 | - |
150 | - |
151 | -def add_timeout(interval, callback, *args, **kwargs): |
152 | - """Add a timeout callback as a task.""" |
153 | - time_out_task = LoopingCall(callback, *args, **kwargs) |
154 | - time_out_task.start(interval/1000, now=False) |
155 | - |
156 | - |
157 | -@defer.inlineCallbacks |
158 | -def main(): |
159 | - """Initialize and start this process.""" |
160 | - ai = tcpactivation.ActivationInstance(get_activation_config()) |
161 | - port = yield ai.get_port() |
162 | - |
163 | - login = SSOLogin('ignored') |
164 | - creds_management = CredentialsManagement(add_timeout, reactor.stop) |
165 | - root = UbuntuSSORoot(sso_login=login, cred_manager=creds_management) |
166 | - |
167 | - reactor.listenTCP(port, PBServerFactory(root), interface=LOCALHOST) |
168 | - |
169 | - |
170 | -def handle_already_started(failure): |
171 | - """Handle the already started error by shutting down this process.""" |
172 | - failure.trap(tcpactivation.AlreadyStartedError) |
173 | - print "Ubuntu SSO login manager already running." |
174 | - reactor.stop() |
175 | - |
176 | - |
177 | -def utter_failure(failure): |
178 | - """Handle an utter failure by logging it and quiting.""" |
179 | - log.err(failure) |
180 | - reactor.stop() |
181 | - |
182 | - |
183 | -if __name__ == '__main__': |
184 | - d = main() |
185 | - d.addErrback(handle_already_started) |
186 | - d.addErrback(utter_failure) |
187 | - reactor.run() |
188 | |
189 | === modified file 'run-tests.bat' |
190 | --- run-tests.bat 2011-11-10 19:45:36 +0000 |
191 | +++ run-tests.bat 2011-12-19 19:46:24 +0000 |
192 | @@ -55,7 +55,7 @@ |
193 | "%PYTHONEXEPATH%\python.exe" setup.py build |
194 | ECHO Running tests |
195 | :: execute the tests with a number of ignored linux only modules |
196 | -"%PYTHONEXEPATH%\python.exe" "%PYTHONEXEPATH%\Scripts\u1trial" -c ubuntu_sso -i "test_gui.py, test_linux.py, test_txsecrets.py" --reactor=qt4 --gui %* |
197 | +"%PYTHONEXEPATH%\python.exe" "%PYTHONEXEPATH%\Scripts\u1trial" -c -i "test_gui.py, test_linux.py, test_txsecrets.py" --reactor=qt4 --gui %* ubuntu_sso |
198 | :: Clean the build from the setupt.py |
199 | ECHO Cleaning the generated code before running the style checks... |
200 | "%PYTHONEXEPATH%\python.exe" setup.py clean |
201 | |
202 | === modified file 'ubuntu_sso/gtk/gui.py' |
203 | --- ubuntu_sso/gtk/gui.py 2011-11-14 12:02:39 +0000 |
204 | +++ ubuntu_sso/gtk/gui.py 2011-12-19 19:46:24 +0000 |
205 | @@ -743,7 +743,7 @@ |
206 | # check T&C |
207 | if not self.yes_to_tc_checkbutton.get_active(): |
208 | self._set_warning_message(self.tc_warning_label, |
209 | - TC_NOT_ACCEPTED) |
210 | + TC_NOT_ACCEPTED % {'app_name': self.app_name}) |
211 | error = True |
212 | |
213 | captcha_solution = self.captcha_solution_entry.get_text() |
214 | |
215 | === modified file 'ubuntu_sso/gtk/tests/test_gui.py' |
216 | --- ubuntu_sso/gtk/tests/test_gui.py 2011-11-11 17:12:19 +0000 |
217 | +++ ubuntu_sso/gtk/tests/test_gui.py 2011-12-19 19:46:24 +0000 |
218 | @@ -1420,7 +1420,7 @@ |
219 | self.ui.join_ok_button.clicked() |
220 | |
221 | self.assert_correct_label_warning(self.ui.tc_warning_label, |
222 | - gui.TC_NOT_ACCEPTED) |
223 | + gui.TC_NOT_ACCEPTED % {'app_name': APP_NAME}) |
224 | self.assertNotIn('register_user', self.ui.backend._called) |
225 | |
226 | def test_warning_is_shown_if_not_captcha_solution(self): |
227 | |
228 | === modified file 'ubuntu_sso/main/__init__.py' |
229 | --- ubuntu_sso/main/__init__.py 2011-09-27 14:06:12 +0000 |
230 | +++ ubuntu_sso/main/__init__.py 2011-12-19 19:46:24 +0000 |
231 | @@ -338,16 +338,16 @@ |
232 | |
233 | if sys.platform == 'win32': |
234 | from ubuntu_sso.main import windows |
235 | - SSOLogin = windows.SSOLogin |
236 | - CredentialsManagement = windows.CredentialsManagement |
237 | + source = windows |
238 | TIMEOUT_INTERVAL = 10000000000 # forever |
239 | - thread_execute = windows.blocking |
240 | - get_sso_login_backend = windows.get_sso_login_backend |
241 | else: |
242 | from ubuntu_sso.main import linux |
243 | - SSOLogin = linux.SSOLogin |
244 | - CredentialsManagement = linux.CredentialsManagement |
245 | - thread_execute = linux.blocking |
246 | - get_sso_login_backend = linux.get_sso_login_backend |
247 | + source = linux |
248 | + |
249 | +CredentialsManagement = source.CredentialsManagement |
250 | +get_sso_login_backend = source.get_sso_login_backend |
251 | +main = source.main |
252 | +SSOLogin = source.SSOLogin |
253 | +thread_execute = source.blocking |
254 | |
255 | # pylint: enable=C0103 |
256 | |
257 | === modified file 'ubuntu_sso/main/linux.py' |
258 | --- ubuntu_sso/main/linux.py 2011-09-29 20:49:45 +0000 |
259 | +++ ubuntu_sso/main/linux.py 2011-12-19 19:46:24 +0000 |
260 | @@ -1,11 +1,6 @@ |
261 | # -*- coding: utf-8 -*- |
262 | # |
263 | -# ubuntu_sso.main - main login handling interface |
264 | -# |
265 | -# Author: Natalia Bidart <natalia.bidart@canonical.com> |
266 | -# Author: Alejandro J. Cura <alecu@canonical.com> |
267 | -# |
268 | -# Copyright 2009 Canonical Ltd. |
269 | +# Copyright 2009-2011 Canonical Ltd. |
270 | # |
271 | # This program is free software: you can redistribute it and/or modify it |
272 | # under the terms of the GNU General Public License version 3, as published |
273 | @@ -28,12 +23,19 @@ |
274 | """ |
275 | |
276 | import threading |
277 | +import signal |
278 | +import sys |
279 | |
280 | +import dbus.mainloop.glib |
281 | import dbus.service |
282 | +import gtk |
283 | + |
284 | |
285 | from ubuntu_sso import ( |
286 | DBUS_ACCOUNT_PATH, |
287 | + DBUS_BUS_NAME, |
288 | DBUS_CREDENTIALS_IFACE, |
289 | + DBUS_CREDENTIALS_PATH, |
290 | DBUS_IFACE_USER_NAME, |
291 | NO_OP, |
292 | ) |
293 | @@ -50,7 +52,7 @@ |
294 | # pylint: disable=C0103 |
295 | |
296 | |
297 | -logger = setup_logging("ubuntu_sso.main") |
298 | +logger = setup_logging("ubuntu_sso.main.linux") |
299 | |
300 | |
301 | def blocking(f, app_name, result_cb, error_cb): |
302 | @@ -410,3 +412,42 @@ |
303 | def get_sso_login_backend(): |
304 | """Get the backend for the Login service.""" |
305 | raise NotImplementedError() |
306 | + |
307 | + |
308 | +def sighup_handler(*a, **kw): |
309 | + """Stop the service.""" |
310 | + # This handler may be called in any thread, so is not thread safe. |
311 | + # See the link below for info: |
312 | + # www.listware.net/201004/gtk-devel-list/115067-unix-signals-in-glib.html |
313 | + # |
314 | + # gtk.main_quit and the logger methods are safe to be called from any |
315 | + # thread. Just don't call other random stuff here. |
316 | + logger.info("Stoping Ubuntu SSO login manager since SIGHUP was received.") |
317 | + gtk.main_quit() |
318 | + |
319 | + |
320 | +def main(): |
321 | + """Run the backend service.""" |
322 | + dbus.mainloop.glib.threads_init() |
323 | + gtk.gdk.threads_init() |
324 | + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) |
325 | + |
326 | + bus = dbus.SessionBus() |
327 | + # Register DBus service for making sure we run only one instance |
328 | + name = bus.request_name(DBUS_BUS_NAME, |
329 | + dbus.bus.NAME_FLAG_DO_NOT_QUEUE) |
330 | + if name == dbus.bus.REQUEST_NAME_REPLY_EXISTS: |
331 | + logger.error("Ubuntu SSO login manager already running, quitting.") |
332 | + sys.exit(0) |
333 | + |
334 | + logger.debug("Hooking up SIGHUP with handler %r.", sighup_handler) |
335 | + signal.signal(signal.SIGHUP, sighup_handler) |
336 | + |
337 | + logger.info("Starting Ubuntu SSO login manager for bus %r.", DBUS_BUS_NAME) |
338 | + bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus()) |
339 | + SSOLogin(bus_name, object_path=DBUS_ACCOUNT_PATH) |
340 | + CredentialsManagement(timeout_func=gtk.timeout_add, |
341 | + shutdown_func=gtk.main_quit, |
342 | + bus_name=bus_name, object_path=DBUS_CREDENTIALS_PATH) |
343 | + |
344 | + gtk.main() |
345 | |
346 | === modified file 'ubuntu_sso/main/tests/test_linux.py' |
347 | --- ubuntu_sso/main/tests/test_linux.py 2011-11-14 18:31:46 +0000 |
348 | +++ ubuntu_sso/main/tests/test_linux.py 2011-12-19 19:46:24 +0000 |
349 | @@ -585,6 +585,7 @@ |
350 | self.memento = MementoHandler() |
351 | self.memento.setLevel(logging.DEBUG) |
352 | ubuntu_sso.main.logger.addHandler(self.memento) |
353 | + self.addCleanup(ubuntu_sso.main.logger.removeHandler, self.memento) |
354 | |
355 | @defer.inlineCallbacks |
356 | def tearDown(self): |
357 | @@ -1204,7 +1205,9 @@ |
358 | |
359 | self.memento = MementoHandler() |
360 | self.memento.setLevel(logging.DEBUG) |
361 | - ubuntu_sso.main.logger.addHandler(self.memento) |
362 | + ubuntu_sso.main.linux.logger.addHandler(self.memento) |
363 | + self.addCleanup(ubuntu_sso.main.linux.logger.removeHandler, |
364 | + self.memento) |
365 | |
366 | def assert_dbus_signal_correct(self, signal, signature): |
367 | """Check that 'signal' is a dbus signal with proper 'signature'.""" |
368 | |
369 | === modified file 'ubuntu_sso/main/tests/test_windows.py' (properties changed: -x to +x) |
370 | --- ubuntu_sso/main/tests/test_windows.py 2011-11-02 16:56:12 +0000 |
371 | +++ ubuntu_sso/main/tests/test_windows.py 2011-12-19 19:46:24 +0000 |
372 | @@ -28,6 +28,7 @@ |
373 | DeadReferenceError, |
374 | PBClientFactory, |
375 | PBServerFactory, |
376 | + Broker, |
377 | ) |
378 | from ubuntu_sso import main |
379 | from ubuntu_sso.main import windows |
380 | @@ -46,7 +47,7 @@ |
381 | |
382 | # because we are using twisted we have java like names C0103 and |
383 | # the issues that mocker brings with it like W0104 |
384 | -# pylint: disable=C0103,W0104 |
385 | +# pylint: disable=C0103,W0104,E1101,W0201 |
386 | |
387 | |
388 | class SaveProtocolServerFactory(PBServerFactory): |
389 | @@ -59,6 +60,77 @@ |
390 | self.protocolInstance = protocol |
391 | |
392 | |
393 | +class SaveClientFactory(PBClientFactory): |
394 | + """Client Factory that knows when we disconnected.""" |
395 | + |
396 | + def __init__(self, connected_d, disconnected_d): |
397 | + """Create a new instance.""" |
398 | + PBClientFactory.__init__(self) |
399 | + self.connected_d = connected_d |
400 | + self.disconnected_d = disconnected_d |
401 | + |
402 | + def clientConnectionMade(self, broker): |
403 | + """Connection made.""" |
404 | + PBClientFactory.clientConnectionMade(self, broker) |
405 | + self.connected_d.callback(True) |
406 | + |
407 | + def clientConnectionLost(self, connector, reason, reconnecting=0): |
408 | + """Connection lost.""" |
409 | + self.disconnected_d.callback(True) |
410 | + |
411 | + |
412 | +class ServerProtocol(Broker): |
413 | + """Server protocol that allows us to clean the tests.""" |
414 | + |
415 | + def connectionLost(self, *a): |
416 | + self.factory.onConnectionLost.callback(self) |
417 | + |
418 | + |
419 | +class ConnectedTestCase(TestCase): |
420 | + """Base test case with a client and a server.""" |
421 | + |
422 | + @defer.inlineCallbacks |
423 | + def setUp(self): |
424 | + """Set up for the tests.""" |
425 | + yield super(ConnectedTestCase, self).setUp() |
426 | + self.server_disconnected = defer.Deferred() |
427 | + self.client_disconnected = defer.Deferred() |
428 | + self.listener = None |
429 | + self.connector = None |
430 | + self.server_factory = None |
431 | + self.client_factory = None |
432 | + |
433 | + def setup_client_server(self, sso_root): |
434 | + """Set tests.""" |
435 | + port = get_sso_pb_port() |
436 | + self.listener = self._listen_server(sso_root, self.server_disconnected, |
437 | + port) |
438 | + connected = defer.Deferred() |
439 | + self.connector = self._connect_client(connected, |
440 | + self.client_disconnected, port) |
441 | + self.addCleanup(self.teardown_client_server) |
442 | + return connected |
443 | + |
444 | + def _listen_server(self, sso_root, d, port): |
445 | + """Start listenting.""" |
446 | + self.server_factory = SaveProtocolServerFactory(sso_root) |
447 | + self.server_factory.onConnectionLost = d |
448 | + self.server_factory.protocol = ServerProtocol |
449 | + return reactor.listenTCP(port, self.server_factory) |
450 | + |
451 | + def _connect_client(self, d1, d2, port): |
452 | + """Connect client.""" |
453 | + self.client_factory = SaveClientFactory(d1, d2) |
454 | + return reactor.connectTCP(LOCALHOST, port, self.client_factory) |
455 | + |
456 | + def teardown_client_server(self): |
457 | + """Clean resources.""" |
458 | + self.connector.disconnect() |
459 | + d = defer.maybeDeferred(self.listener.stopListening) |
460 | + return defer.gatherResults([d, self.client_disconnected, |
461 | + self.server_disconnected]) |
462 | + |
463 | + |
464 | class FakeDecoratedObject(object): |
465 | """An object that has decorators.""" |
466 | |
467 | @@ -240,7 +312,7 @@ |
468 | self.assertEqual(name, self.signal_names[signal_index]) |
469 | |
470 | |
471 | -class SSOLoginTestCase(SignalHandlingTestCase): |
472 | +class SSOLoginTestCase(ConnectedTestCase, SignalHandlingTestCase): |
473 | """Test the login class.""" |
474 | |
475 | signal_names = [ |
476 | @@ -268,35 +340,12 @@ |
477 | self.login = SSOLogin(None) |
478 | # start pb |
479 | self.sso_root = UbuntuSSORoot(sso_login=self.login) |
480 | - self.server_factory = SaveProtocolServerFactory(self.sso_root) |
481 | # pylint: disable=E1101 |
482 | - port = get_sso_pb_port() |
483 | - self.listener = reactor.listenTCP(port, self.server_factory) |
484 | - self.client_factory = PBClientFactory() |
485 | - self.connector = reactor.connectTCP(LOCALHOST, port, |
486 | - self.client_factory) |
487 | + yield self.setup_client_server(self.sso_root) |
488 | self.client = yield self._get_client() |
489 | # pylint: enable=E1101 |
490 | |
491 | @defer.inlineCallbacks |
492 | - def tearDown(self): |
493 | - """Clean reactor.""" |
494 | - yield super(SSOLoginTestCase, self).tearDown() |
495 | - if self.server_factory.protocolInstance is not None: |
496 | - self.server_factory.protocolInstance.transport.loseConnection() |
497 | - yield defer.gatherResults([self._tearDownServer(), |
498 | - self._tearDownClient()]) |
499 | - |
500 | - def _tearDownServer(self): |
501 | - """Teardown the server.""" |
502 | - return defer.maybeDeferred(self.listener.stopListening) |
503 | - |
504 | - def _tearDownClient(self): |
505 | - """Tear down the client.""" |
506 | - self.connector.disconnect() |
507 | - return defer.succeed(None) |
508 | - |
509 | - @defer.inlineCallbacks |
510 | def _get_client(self): |
511 | """Get the client.""" |
512 | # request the remote object and create a client |
513 | @@ -541,7 +590,7 @@ |
514 | self.mocker.verify() |
515 | |
516 | |
517 | -class CredentialsManagementTestCase(TestCase): |
518 | +class CredentialsManagementTestCase(ConnectedTestCase, TestCase): |
519 | """Test the management class.""" |
520 | |
521 | @defer.inlineCallbacks |
522 | @@ -556,35 +605,12 @@ |
523 | self.creds.root = self.root |
524 | # start pb |
525 | self.sso_root = UbuntuSSORoot(cred_manager=self.creds) |
526 | - self.server_factory = SaveProtocolServerFactory(self.sso_root) |
527 | # pylint: disable=E1101 |
528 | - port = get_sso_pb_port() |
529 | - self.listener = reactor.listenTCP(port, self.server_factory) |
530 | - self.client_factory = PBClientFactory() |
531 | - self.connector = reactor.connectTCP(LOCALHOST, port, |
532 | - self.client_factory) |
533 | + yield self.setup_client_server(self.sso_root) |
534 | self.client = yield self._get_client() |
535 | # pylint: enable=E1101 |
536 | |
537 | @defer.inlineCallbacks |
538 | - def tearDown(self): |
539 | - """Clean reactor.""" |
540 | - yield super(CredentialsManagementTestCase, self).tearDown() |
541 | - if self.server_factory.protocolInstance is not None: |
542 | - self.server_factory.protocolInstance.transport.loseConnection() |
543 | - yield defer.gatherResults([self._tearDownServer(), |
544 | - self._tearDownClient()]) |
545 | - |
546 | - def _tearDownServer(self): |
547 | - """Teardown the server.""" |
548 | - return defer.maybeDeferred(self.listener.stopListening) |
549 | - |
550 | - def _tearDownClient(self): |
551 | - """Tear down the client.""" |
552 | - self.connector.disconnect() |
553 | - return defer.succeed(None) |
554 | - |
555 | - @defer.inlineCallbacks |
556 | def _get_client(self): |
557 | """Get the client.""" |
558 | # request the remote object and create a client |
559 | @@ -592,6 +618,7 @@ |
560 | remote = yield root.callRemote('get_cred_manager') |
561 | client = CredentialsManagementClient(remote) |
562 | yield client.register_to_signals() |
563 | + self.addCleanup(client.unregister_to_signals) |
564 | # set the cb |
565 | for signal_name in ['on_authorization_denied_cb', |
566 | 'on_credentials_found_cb', |
567 | |
568 | === modified file 'ubuntu_sso/main/windows.py' |
569 | --- ubuntu_sso/main/windows.py 2011-09-27 14:06:12 +0000 |
570 | +++ ubuntu_sso/main/windows.py 2011-12-19 19:46:24 +0000 |
571 | @@ -1,6 +1,4 @@ |
572 | # -*- coding: utf-8 -*- |
573 | -# Authors: Manuel de la Pena <manuel@canonical.com> |
574 | -# Alejandro J. Cura <alecu@canonical.com> |
575 | # |
576 | # Copyright 2011 Canonical Ltd. |
577 | # |
578 | @@ -26,10 +24,12 @@ |
579 | # pylint: enable=F0401 |
580 | |
581 | from twisted.internet import defer, reactor |
582 | +from twisted.internet.task import LoopingCall |
583 | from twisted.internet.threads import deferToThread |
584 | from twisted.spread.pb import ( |
585 | DeadReferenceError, |
586 | PBClientFactory, |
587 | + PBServerFactory, |
588 | Referenceable, |
589 | Root, |
590 | ) |
591 | @@ -41,7 +41,12 @@ |
592 | SSOLoginRoot, |
593 | except_to_errdict, |
594 | ) |
595 | -from ubuntu_sso.utils.tcpactivation import ActivationConfig, ActivationClient |
596 | +from ubuntu_sso.utils.tcpactivation import ( |
597 | + ActivationClient, |
598 | + ActivationConfig, |
599 | + ActivationInstance, |
600 | + AlreadyStartedError, |
601 | +) |
602 | |
603 | |
604 | logger = setup_logging("ubuntu_sso.main.windows") |
605 | @@ -857,3 +862,41 @@ |
606 | root = UbuntuSSOClient() |
607 | yield root.connect() |
608 | defer.returnValue(root.sso_login) |
609 | + |
610 | + |
611 | +def add_timeout(interval, callback, *args, **kwargs): |
612 | + """Add a timeout callback as a task.""" |
613 | + time_out_task = LoopingCall(callback, *args, **kwargs) |
614 | + time_out_task.start(interval / 1000, now=False) |
615 | + |
616 | + |
617 | +# the reactor does have run and stop methods |
618 | +# pylint: disable=E1101 |
619 | + |
620 | +@defer.inlineCallbacks |
621 | +def start_service(): |
622 | + """Initialize and start this process.""" |
623 | + try: |
624 | + ai = ActivationInstance(get_activation_config()) |
625 | + port = yield ai.get_port() |
626 | + logger.info("Starting Ubuntu SSO login manager for port %r.", port) |
627 | + |
628 | + login = SSOLogin('ignored') |
629 | + creds_management = CredentialsManagement(add_timeout, reactor.stop) |
630 | + root = UbuntuSSORoot(sso_login=login, cred_manager=creds_management) |
631 | + |
632 | + reactor.listenTCP(port, PBServerFactory(root), interface=LOCALHOST) |
633 | + except AlreadyStartedError: |
634 | + logger.error("Ubuntu SSO login manager already running, quitting.") |
635 | + reactor.stop() |
636 | + except: |
637 | + logger.exception('Can not start Ubuntu SSO login manager:') |
638 | + reactor.stop() |
639 | + |
640 | + |
641 | +def main(): |
642 | + """Run the backend service.""" |
643 | + start_service() |
644 | + reactor.run() |
645 | + |
646 | +# pylint: enable=E1101 |
647 | |
648 | === modified file 'ubuntu_sso/qt/controllers.py' |
649 | --- ubuntu_sso/qt/controllers.py 2011-11-11 20:34:57 +0000 |
650 | +++ ubuntu_sso/qt/controllers.py 2011-12-19 19:46:24 +0000 |
651 | @@ -828,8 +828,12 @@ |
652 | email = unicode(self.view.wizard().forgotten.ui.email_line_edit.text()) |
653 | self.view.wizard().current_user.ui.email_edit.setText(email) |
654 | self.view.wizard().overlay.hide() |
655 | - self.view.wizard().back() |
656 | - self.view.wizard().back() |
657 | + current_user_id = self.view.wizard().current_user_page_id |
658 | + visited_pages = self.view.wizard().visitedPages() |
659 | + for index in reversed(visited_pages): |
660 | + if index == current_user_id: |
661 | + break |
662 | + self.view.wizard().back() |
663 | |
664 | def on_password_change_error(self, app_name, error): |
665 | """Let the user know that there was an error.""" |
666 | @@ -844,7 +848,7 @@ |
667 | email = unicode(self.view.wizard().forgotten.ui.email_line_edit.text()) |
668 | code = unicode(self.view.ui.reset_code_line_edit.text()) |
669 | password = unicode(self.view.ui.password_line_edit.text()) |
670 | - logger.info('Settig new password for %s and email %s with code %s', |
671 | + logger.info('Setting new password for %r and email %r with code %r', |
672 | app_name, email, code) |
673 | self.backend.set_new_password(app_name, email, code, password) |
674 | |
675 | |
676 | === modified file 'ubuntu_sso/qt/tests/test_controllers.py' |
677 | --- ubuntu_sso/qt/tests/test_controllers.py 2011-11-11 20:34:57 +0000 |
678 | +++ ubuntu_sso/qt/tests/test_controllers.py 2011-12-19 19:46:24 +0000 |
679 | @@ -669,6 +669,7 @@ |
680 | self.count_back = 0 |
681 | self.hide_value = False |
682 | self.text_value = 'mail@mail.com' |
683 | + self.current_user_page_id = 4 |
684 | |
685 | def wizard(self): |
686 | """Fake wizard function for view.""" |
687 | @@ -690,6 +691,10 @@ |
688 | def setText(self, text): |
689 | """Fake setText for QLineEdit.""" |
690 | self.text_value = text |
691 | + |
692 | + def visitedPages(self): |
693 | + """Return an int list of fake visited pages.""" |
694 | + return [1, 4, 6, 8] |
695 | # pylint: enable=C0103 |
696 | |
697 | |
698 | @@ -2094,5 +2099,17 @@ |
699 | """Test that on_password_changed execute the proper operation.""" |
700 | self.controller.on_password_changed('app_name', '') |
701 | self.assertTrue(self.controller.view.hide_value) |
702 | - self.assertEqual(self.controller.view.count_back, 2) |
703 | + times_visited = 2 |
704 | + self.assertEqual(self.controller.view.count_back, times_visited) |
705 | + self.assertEqual(self.controller.view.text_value, 'mail@mail.com') |
706 | + |
707 | + def test_on_password_changed_not_visited(self): |
708 | + """Test that on_password_changed execute the proper operation.""" |
709 | + current_user_page_id = 20 |
710 | + self.patch(self.controller.view, "current_user_page_id", |
711 | + current_user_page_id) |
712 | + self.controller.on_password_changed('app_name', '') |
713 | + self.assertTrue(self.controller.view.hide_value) |
714 | + times_visited = 4 |
715 | + self.assertEqual(self.controller.view.count_back, times_visited) |
716 | self.assertEqual(self.controller.view.text_value, 'mail@mail.com') |
717 | |
718 | === modified file 'ubuntu_sso/utils/__init__.py' |
719 | --- ubuntu_sso/utils/__init__.py 2011-10-06 19:38:19 +0000 |
720 | +++ ubuntu_sso/utils/__init__.py 2011-12-19 19:46:24 +0000 |
721 | @@ -45,7 +45,7 @@ |
722 | |
723 | CHECKING_INTERVAL = 60 * 60 # in seconds |
724 | ERROR_INTERVAL = 30 # in seconds |
725 | - SERVER_URL = "http://one.ubuntu.com/" |
726 | + SERVER_URL = "http://one.ubuntu.com/api/time" |
727 | |
728 | def __init__(self): |
729 | """Initialize this instance.""" |
730 | |
731 | === modified file 'ubuntu_sso/utils/tests/test_tcpactivation.py' |
732 | --- ubuntu_sso/utils/tests/test_tcpactivation.py 2011-10-28 10:41:18 +0000 |
733 | +++ ubuntu_sso/utils/tests/test_tcpactivation.py 2011-12-19 19:46:24 +0000 |
734 | @@ -51,12 +51,25 @@ |
735 | """Echo the data received.""" |
736 | self.transport.write(data) |
737 | |
738 | + #pylint:disable=E1101 |
739 | + def connectionLost(self, *a): |
740 | + self.factory.onConnectionLost.callback(self) |
741 | + #pylint:enable=E1101 |
742 | + |
743 | |
744 | class FakeServerFactory(protocol.Factory): |
745 | """A factory for the test server.""" |
746 | |
747 | protocol = FakeServerProtocol |
748 | |
749 | + #pylint:disable=W0201 |
750 | + def __init__(self, testcase=None): |
751 | + """Create a new instance for the testcase.""" |
752 | + if testcase is not None: |
753 | + self.onConnectionLost = defer.Deferred() |
754 | + testcase.addCleanup(lambda: self.onConnectionLost) |
755 | + #pylint:enable=W0201 |
756 | + |
757 | |
758 | class FakeTransport(object): |
759 | """A fake transport.""" |
760 | @@ -197,7 +210,7 @@ |
761 | @defer.inlineCallbacks |
762 | def test_is_already_running(self): |
763 | """The is_already_running method returns True if already started.""" |
764 | - f = FakeServerFactory() |
765 | + f = FakeServerFactory(self) |
766 | # pylint: disable=E1101 |
767 | listener = reactor.listenTCP(SAMPLE_PORT, f, |
768 | interface=tcpactivation.LOCALHOST) |
769 | @@ -319,18 +332,20 @@ |
770 | port = yield ai.get_port() |
771 | self.assertEqual(port, SAMPLE_PORT) |
772 | |
773 | + #pylint:disable=W0201 |
774 | @defer.inlineCallbacks |
775 | def test_get_port_fails_if_service_already_started(self): |
776 | """The get_port method fails if service already started.""" |
777 | ai1 = ActivationInstance(self.config) |
778 | port1 = yield ai1.get_port() |
779 | - f = FakeServerFactory() |
780 | + f = FakeServerFactory(self) |
781 | # pylint: disable=E1101 |
782 | listener = reactor.listenTCP(port1, f, |
783 | interface=tcpactivation.LOCALHOST) |
784 | self.addCleanup(listener.stopListening) |
785 | ai2 = ActivationInstance(self.config) |
786 | yield self.assertFailure(ai2.get_port(), AlreadyStartedError) |
787 | + #pylint:enable=W0201 |
788 | |
789 | |
790 | def server_test(config): |
791 | |
792 | === modified file 'ubuntu_sso/utils/tests/test_txsecrets.py' |
793 | --- ubuntu_sso/utils/tests/test_txsecrets.py 2011-10-28 10:41:18 +0000 |
794 | +++ ubuntu_sso/utils/tests/test_txsecrets.py 2011-12-19 19:46:24 +0000 |
795 | @@ -22,7 +22,7 @@ |
796 | import dbus.service |
797 | |
798 | from twisted.internet.defer import inlineCallbacks, returnValue |
799 | -from ubuntuone.devtools.testcase import DBusTestCase |
800 | +from ubuntuone.devtools.testcases.dbus import DBusTestCase |
801 | |
802 | from ubuntu_sso.utils import txsecrets |
803 | |
804 | |
805 | === modified file 'ubuntu_sso/utils/ui.py' |
806 | --- ubuntu_sso/utils/ui.py 2011-10-20 14:15:46 +0000 |
807 | +++ ubuntu_sso/utils/ui.py 2011-12-19 19:46:24 +0000 |
808 | @@ -88,8 +88,8 @@ |
809 | SUCCESS = _('You are now logged into %(app_name)s.') |
810 | SURNAME_ENTRY = _('Surname') |
811 | TC_BUTTON = _('Show Terms & Conditions') |
812 | -TC_NOT_ACCEPTED = _('Agreeing to the Ubuntu One Terms & Conditions is ' \ |
813 | - 'required to subscribe.') |
814 | +TC_NOT_ACCEPTED = _('Agreeing to the %(app_name)s Terms & Conditions is ' \ |
815 | + 'required to subscribe.') |
816 | TOS_LABEL = _("You can also find these terms at <a href='%(url)s'>%(url)s</a>") |
817 | TRY_AGAIN_BUTTON = _('Try again') |
818 | UNKNOWN_ERROR = _('There was an error when trying to complete the ' \ |
819 | |
820 | === added directory 'ubuntu_sso/utils/webclient' |
821 | === added file 'ubuntu_sso/utils/webclient/__init__.py' |
822 | --- ubuntu_sso/utils/webclient/__init__.py 1970-01-01 00:00:00 +0000 |
823 | +++ ubuntu_sso/utils/webclient/__init__.py 2011-12-19 19:46:24 +0000 |
824 | @@ -0,0 +1,48 @@ |
825 | +# -*- coding: utf-8 -*- |
826 | +# |
827 | +# Copyright 2011 Canonical Ltd. |
828 | +# |
829 | +# This program is free software: you can redistribute it and/or modify it |
830 | +# under the terms of the GNU General Public License version 3, as published |
831 | +# by the Free Software Foundation. |
832 | +# |
833 | +# This program is distributed in the hope that it will be useful, but |
834 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
835 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
836 | +# PURPOSE. See the GNU General Public License for more details. |
837 | +# |
838 | +# You should have received a copy of the GNU General Public License along |
839 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
840 | +"""A common webclient that can use a QtNetwork or libsoup backend.""" |
841 | + |
842 | +import sys |
843 | + |
844 | +# pylint: disable=W0611 |
845 | +from ubuntu_sso.utils.webclient.common import ( |
846 | + UnauthorizedError, |
847 | + WebClientError, |
848 | +) |
849 | + |
850 | + |
851 | +def is_qt4reactor_installed(): |
852 | + """Check if the qt4reactor is installed.""" |
853 | + reactor = sys.modules.get("twisted.internet.reactor") |
854 | + return reactor and getattr(reactor, "qApp", None) |
855 | + |
856 | + |
857 | +def webclient_module(): |
858 | + """Choose the module of the web client.""" |
859 | + if is_qt4reactor_installed(): |
860 | + from ubuntu_sso.utils.webclient import qtnetwork as web_module |
861 | + else: |
862 | + # the libsoup backend depends on the twisted + GI bug |
863 | + # meanwhile, use the txweb sample client |
864 | + #from ubuntu_sso.utils.webclient import libsoup as web_module |
865 | + from ubuntu_sso.utils.webclient import txweb as web_module |
866 | + return web_module |
867 | + |
868 | + |
869 | +def webclient_factory(*args, **kwargs): |
870 | + """Choose the type of the web client dynamically.""" |
871 | + web_module = webclient_module() |
872 | + return web_module.WebClient(*args, **kwargs) |
873 | |
874 | === added file 'ubuntu_sso/utils/webclient/common.py' |
875 | --- ubuntu_sso/utils/webclient/common.py 1970-01-01 00:00:00 +0000 |
876 | +++ ubuntu_sso/utils/webclient/common.py 2011-12-19 19:46:24 +0000 |
877 | @@ -0,0 +1,81 @@ |
878 | +# -*- coding: utf-8 -*- |
879 | +# |
880 | +# Copyright 2011 Canonical Ltd. |
881 | +# |
882 | +# This program is free software: you can redistribute it and/or modify it |
883 | +# under the terms of the GNU General Public License version 3, as published |
884 | +# by the Free Software Foundation. |
885 | +# |
886 | +# This program is distributed in the hope that it will be useful, but |
887 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
888 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
889 | +# PURPOSE. See the GNU General Public License for more details. |
890 | +# |
891 | +# You should have received a copy of the GNU General Public License along |
892 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
893 | +"""The common bits of a webclient.""" |
894 | + |
895 | +import time |
896 | + |
897 | +from oauth import oauth |
898 | +from twisted.internet import defer |
899 | + |
900 | + |
901 | +class WebClientError(Exception): |
902 | + """An http error happened while calling the webservice.""" |
903 | + |
904 | + |
905 | +class UnauthorizedError(WebClientError): |
906 | + """The request ended with bad_request, unauthorized or forbidden.""" |
907 | + |
908 | + |
909 | +class Response(object): |
910 | + """A reponse object.""" |
911 | + |
912 | + def __init__(self, content, headers=None): |
913 | + """Initialize this instance.""" |
914 | + self.content = content |
915 | + self.headers = headers |
916 | + |
917 | + |
918 | +class BaseWebClient(object): |
919 | + """The webclient base class, to be extended by backends.""" |
920 | + |
921 | + def __init__(self, username=None, password=None): |
922 | + """Initialize this instance.""" |
923 | + self.username = username |
924 | + self.password = password |
925 | + |
926 | + def request(self, url, method="GET", extra_headers=None, |
927 | + oauth_credentials=None): |
928 | + """Return a deferred that will be fired with a Response object.""" |
929 | + raise NotImplementedError |
930 | + |
931 | + def get_timestamp(self): |
932 | + """Get a timestamp synchronized with the server.""" |
933 | + # pylint: disable=W0511 |
934 | + # TODO: get the synchronized timestamp |
935 | + return defer.succeed(time.time()) |
936 | + |
937 | + @staticmethod |
938 | + def build_oauth_headers(method, url, credentials, timestamp): |
939 | + """Build an oauth request given some credentials.""" |
940 | + consumer = oauth.OAuthConsumer(credentials["consumer_key"], |
941 | + credentials["consumer_secret"]) |
942 | + token = oauth.OAuthToken(credentials["token"], |
943 | + credentials["token_secret"]) |
944 | + parameters = {} |
945 | + if timestamp: |
946 | + parameters["oauth_timestamp"] = timestamp |
947 | + request = oauth.OAuthRequest.from_consumer_and_token( |
948 | + http_url=url, |
949 | + http_method=method, |
950 | + parameters=parameters, |
951 | + oauth_consumer=consumer, |
952 | + token=token) |
953 | + sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1() |
954 | + request.sign_request(sig_method, consumer, token) |
955 | + return request.to_header() |
956 | + |
957 | + def shutdown(self): |
958 | + """Shut down all pending requests (if possible).""" |
959 | |
960 | === added file 'ubuntu_sso/utils/webclient/libsoup.py' |
961 | --- ubuntu_sso/utils/webclient/libsoup.py 1970-01-01 00:00:00 +0000 |
962 | +++ ubuntu_sso/utils/webclient/libsoup.py 2011-12-19 19:46:24 +0000 |
963 | @@ -0,0 +1,87 @@ |
964 | +# -*- coding: utf-8 -*- |
965 | +# |
966 | +# Copyright 2011 Canonical Ltd. |
967 | +# |
968 | +# This program is free software: you can redistribute it and/or modify it |
969 | +# under the terms of the GNU General Public License version 3, as published |
970 | +# by the Free Software Foundation. |
971 | +# |
972 | +# This program is distributed in the hope that it will be useful, but |
973 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
974 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
975 | +# PURPOSE. See the GNU General Public License for more details. |
976 | +# |
977 | +# You should have received a copy of the GNU General Public License along |
978 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
979 | +"""A webclient backend that uses libsoup.""" |
980 | + |
981 | +import httplib |
982 | + |
983 | +from twisted.internet import defer |
984 | + |
985 | +from ubuntu_sso.utils.webclient.common import ( |
986 | + BaseWebClient, |
987 | + Response, |
988 | + UnauthorizedError, |
989 | + WebClientError, |
990 | +) |
991 | + |
992 | + |
993 | +class WebClient(BaseWebClient): |
994 | + """A webclient with a libsoup backend.""" |
995 | + |
996 | + def __init__(self, *args, **kwargs): |
997 | + """Initialize this instance.""" |
998 | + super(WebClient, self).__init__(*args, **kwargs) |
999 | + # pylint: disable=E0611 |
1000 | + from gi.repository import Soup, SoupGNOME |
1001 | + self.soup = Soup |
1002 | + self.session = Soup.SessionAsync() |
1003 | + self.session.add_feature_by_type(SoupGNOME.ProxyResolverGNOME) |
1004 | + self.session.connect("authenticate", self._on_authenticate) |
1005 | + |
1006 | + def _on_message(self, session, message, d): |
1007 | + """Handle the result of an http message.""" |
1008 | + if message.status_code == httplib.OK: |
1009 | + response = Response(message.response_body.data) |
1010 | + d.callback(response) |
1011 | + elif message.status_code == httplib.UNAUTHORIZED: |
1012 | + e = UnauthorizedError(message.reason_phrase) |
1013 | + d.errback(e) |
1014 | + else: |
1015 | + e = WebClientError(message.reason_phrase) |
1016 | + d.errback(e) |
1017 | + |
1018 | + def _on_authenticate(self, sesion, message, auth, retrying, data=None): |
1019 | + """Handle the "authenticate" signal.""" |
1020 | + if not retrying and self.username and self.password: |
1021 | + auth.authenticate(self.username, self.password) |
1022 | + |
1023 | + @defer.inlineCallbacks |
1024 | + def request(self, url, method="GET", extra_headers=None, |
1025 | + oauth_credentials=None): |
1026 | + """Return a deferred that will be fired with a Response object.""" |
1027 | + if extra_headers: |
1028 | + headers = dict(extra_headers) |
1029 | + else: |
1030 | + headers = {} |
1031 | + |
1032 | + if oauth_credentials: |
1033 | + timestamp = yield self.get_timestamp() |
1034 | + oauth_headers = self.build_oauth_headers(method, url, |
1035 | + oauth_credentials, timestamp) |
1036 | + headers.update(oauth_headers) |
1037 | + |
1038 | + d = defer.Deferred() |
1039 | + message = self.soup.Message.new(method, url) |
1040 | + |
1041 | + for key, value in headers.iteritems(): |
1042 | + message.request_headers.append(key, value) |
1043 | + |
1044 | + self.session.queue_message(message, self._on_message, d) |
1045 | + response = yield d |
1046 | + defer.returnValue(response) |
1047 | + |
1048 | + def shutdown(self): |
1049 | + """End the soup session for this webclient.""" |
1050 | + self.session.abort() |
1051 | |
1052 | === added file 'ubuntu_sso/utils/webclient/qtnetwork.py' |
1053 | --- ubuntu_sso/utils/webclient/qtnetwork.py 1970-01-01 00:00:00 +0000 |
1054 | +++ ubuntu_sso/utils/webclient/qtnetwork.py 2011-12-19 19:46:24 +0000 |
1055 | @@ -0,0 +1,101 @@ |
1056 | +# -*- coding: utf-8 -*- |
1057 | +# |
1058 | +# Copyright 2011 Canonical Ltd. |
1059 | +# |
1060 | +# This program is free software: you can redistribute it and/or modify it |
1061 | +# under the terms of the GNU General Public License version 3, as published |
1062 | +# by the Free Software Foundation. |
1063 | +# |
1064 | +# This program is distributed in the hope that it will be useful, but |
1065 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1066 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1067 | +# PURPOSE. See the GNU General Public License for more details. |
1068 | +# |
1069 | +# You should have received a copy of the GNU General Public License along |
1070 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
1071 | +"""A webclient backend that uses QtNetwork.""" |
1072 | + |
1073 | +from PyQt4.QtCore import ( |
1074 | + QCoreApplication, |
1075 | + QUrl, |
1076 | +) |
1077 | +from PyQt4.QtNetwork import ( |
1078 | + QNetworkAccessManager, |
1079 | + QNetworkReply, |
1080 | + QNetworkRequest, |
1081 | +) |
1082 | +from twisted.internet import defer |
1083 | + |
1084 | +from ubuntu_sso.utils.webclient.common import ( |
1085 | + BaseWebClient, |
1086 | + Response, |
1087 | + UnauthorizedError, |
1088 | + WebClientError, |
1089 | +) |
1090 | + |
1091 | + |
1092 | +class WebClient(BaseWebClient): |
1093 | + """A webclient with a qtnetwork backend.""" |
1094 | + |
1095 | + def __init__(self, *args, **kwargs): |
1096 | + """Initialize this instance.""" |
1097 | + super(WebClient, self).__init__(*args, **kwargs) |
1098 | + self.nam = QNetworkAccessManager(QCoreApplication.instance()) |
1099 | + self.nam.finished.connect(self._handle_finished) |
1100 | + self.nam.authenticationRequired.connect(self._handle_authentication) |
1101 | + self.replies = {} |
1102 | + |
1103 | + @defer.inlineCallbacks |
1104 | + def request(self, url, method="GET", extra_headers=None, |
1105 | + oauth_credentials=None): |
1106 | + """Return a deferred that will be fired with a Response object.""" |
1107 | + request = QNetworkRequest(QUrl(url)) |
1108 | + |
1109 | + if extra_headers: |
1110 | + headers = dict(extra_headers) |
1111 | + else: |
1112 | + headers = {} |
1113 | + |
1114 | + if oauth_credentials: |
1115 | + timestamp = yield self.get_timestamp() |
1116 | + oauth_headers = self.build_oauth_headers(method, url, |
1117 | + oauth_credentials, timestamp) |
1118 | + headers.update(oauth_headers) |
1119 | + |
1120 | + for key, value in headers.iteritems(): |
1121 | + request.setRawHeader(key, value) |
1122 | + |
1123 | + d = defer.Deferred() |
1124 | + if method == "GET": |
1125 | + reply = self.nam.get(request) |
1126 | + elif method == "HEAD": |
1127 | + reply = self.nam.head(request) |
1128 | + else: |
1129 | + reply = self.nam.sendCustomRequest(request, method) |
1130 | + self.replies[reply] = d |
1131 | + result = yield d |
1132 | + defer.returnValue(result) |
1133 | + |
1134 | + def _handle_authentication(self, reply, authenticator): |
1135 | + """The reply needs authentication.""" |
1136 | + authenticator.setUser(self.username) |
1137 | + authenticator.setPassword(self.password) |
1138 | + |
1139 | + def _handle_finished(self, reply): |
1140 | + """The reply has finished processing.""" |
1141 | + assert reply in self.replies |
1142 | + d = self.replies.pop(reply) |
1143 | + error = reply.error() |
1144 | + if not error: |
1145 | + response = Response(reply.readAll()) |
1146 | + d.callback(response) |
1147 | + else: |
1148 | + if error == QNetworkReply.AuthenticationRequiredError: |
1149 | + exception = UnauthorizedError(reply.errorString()) |
1150 | + else: |
1151 | + exception = WebClientError(reply.errorString()) |
1152 | + d.errback(exception) |
1153 | + |
1154 | + def shutdown(self): |
1155 | + """Shut down all pending requests (if possible).""" |
1156 | + self.nam.deleteLater() |
1157 | |
1158 | === added directory 'ubuntu_sso/utils/webclient/tests' |
1159 | === added file 'ubuntu_sso/utils/webclient/tests/__init__.py' |
1160 | --- ubuntu_sso/utils/webclient/tests/__init__.py 1970-01-01 00:00:00 +0000 |
1161 | +++ ubuntu_sso/utils/webclient/tests/__init__.py 2011-12-19 19:46:24 +0000 |
1162 | @@ -0,0 +1,17 @@ |
1163 | +# -*- coding: utf-8 -*- |
1164 | +# |
1165 | +# Copyright 2011 Canonical Ltd. |
1166 | +# |
1167 | +# This program is free software: you can redistribute it and/or modify it |
1168 | +# under the terms of the GNU General Public License version 3, as published |
1169 | +# by the Free Software Foundation. |
1170 | +# |
1171 | +# This program is distributed in the hope that it will be useful, but |
1172 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1173 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1174 | +# PURPOSE. See the GNU General Public License for more details. |
1175 | +# |
1176 | +# You should have received a copy of the GNU General Public License along |
1177 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
1178 | + |
1179 | +"""Tests for the proxy-aware webclient.""" |
1180 | |
1181 | === added file 'ubuntu_sso/utils/webclient/tests/test_webclient.py' |
1182 | --- ubuntu_sso/utils/webclient/tests/test_webclient.py 1970-01-01 00:00:00 +0000 |
1183 | +++ ubuntu_sso/utils/webclient/tests/test_webclient.py 2011-12-19 19:46:24 +0000 |
1184 | @@ -0,0 +1,303 @@ |
1185 | +# -*- coding: utf-8 -*- |
1186 | +# |
1187 | +# Copyright 2011 Canonical Ltd. |
1188 | +# |
1189 | +# This program is free software: you can redistribute it and/or modify it |
1190 | +# under the terms of the GNU General Public License version 3, as published |
1191 | +# by the Free Software Foundation. |
1192 | +# |
1193 | +# This program is distributed in the hope that it will be useful, but |
1194 | +# WITHOUT AN WARRANTY; without even the implied warranties of |
1195 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1196 | +# PURPOSE. See the GNU General Public License for more details. |
1197 | +# |
1198 | +# You should have received a copy of the GNU General Public License along |
1199 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
1200 | +"""Integration tests for the proxy-enabled webclient.""" |
1201 | + |
1202 | +import os |
1203 | +import sys |
1204 | +import urllib |
1205 | + |
1206 | +from twisted.application import internet, service |
1207 | +from twisted.cred import checkers, portal |
1208 | +from twisted.internet import defer |
1209 | +from twisted.web import http, resource, server, guard |
1210 | + |
1211 | +from ubuntuone.devtools.testcases import TestCase |
1212 | + |
1213 | +from ubuntu_sso.utils import webclient |
1214 | + |
1215 | +ANY_VALUE = object() |
1216 | +SAMPLE_KEY = "result" |
1217 | +SAMPLE_VALUE = "sample result" |
1218 | +SAMPLE_RESOURCE = '{"%s": "%s"}' % (SAMPLE_KEY, SAMPLE_VALUE) |
1219 | +SAMPLE_USERNAME = "peddro" |
1220 | +SAMPLE_PASSWORD = "cantropus" |
1221 | +SAMPLE_CREDENTIALS = dict( |
1222 | + consumer_key="consumer key", |
1223 | + consumer_secret="consumer secret", |
1224 | + token="the real token", |
1225 | + token_secret="the token secret", |
1226 | +) |
1227 | +SAMPLE_HEADERS = {SAMPLE_KEY: SAMPLE_VALUE} |
1228 | + |
1229 | +SIMPLERESOURCE = "simpleresource" |
1230 | +THROWERROR = "throwerror" |
1231 | +UNAUTHORIZED = "unauthorized" |
1232 | +HEADONLY = "headonly" |
1233 | +VERIFYHEADERS = "verifyheaders" |
1234 | +GUARDED = "guarded" |
1235 | +OAUTHRESOURCE = "oauthresource" |
1236 | + |
1237 | + |
1238 | +def sample_get_credentials(): |
1239 | + """Will return the sample credentials right now.""" |
1240 | + return defer.succeed(SAMPLE_CREDENTIALS) |
1241 | + |
1242 | + |
1243 | +# pylint: disable=C0103 |
1244 | +# t.w.resource methods have freeform cased names |
1245 | + |
1246 | +class SimpleResource(resource.Resource): |
1247 | + """A simple web resource.""" |
1248 | + |
1249 | + def render_GET(self, request): |
1250 | + """Make a bit of html out of these resource's content.""" |
1251 | + return SAMPLE_RESOURCE |
1252 | + |
1253 | + |
1254 | +class HeadOnlyResource(resource.Resource): |
1255 | + """A resource that fails if called with a method other than HEAD.""" |
1256 | + |
1257 | + def render_HEAD(self, request): |
1258 | + """Return a bit of html.""" |
1259 | + return "OK" |
1260 | + |
1261 | + |
1262 | +class VerifyHeadersResource(resource.Resource): |
1263 | + """A resource that verifies the headers received.""" |
1264 | + |
1265 | + def render_GET(self, request): |
1266 | + """Make a bit of html out of these resource's content.""" |
1267 | + headers = request.requestHeaders.getRawHeaders(SAMPLE_KEY) |
1268 | + if headers != [SAMPLE_VALUE]: |
1269 | + request.setResponseCode(http.BAD_REQUEST) |
1270 | + return "ERROR: Expected header not present." |
1271 | + return SAMPLE_RESOURCE |
1272 | + |
1273 | + |
1274 | +class SimpleRealm(object): |
1275 | + """The same simple resource for all users.""" |
1276 | + |
1277 | + def requestAvatar(self, avatarId, mind, *interfaces): |
1278 | + """The avatar for this user.""" |
1279 | + if resource.IResource in interfaces: |
1280 | + return (resource.IResource, SimpleResource(), lambda: None) |
1281 | + raise NotImplementedError() |
1282 | + |
1283 | + |
1284 | +class OAuthCheckerResource(resource.Resource): |
1285 | + """A resource that verifies the request was oauth signed.""" |
1286 | + |
1287 | + def render_GET(self, request): |
1288 | + """Make a bit of html out of these resource's content.""" |
1289 | + header = request.requestHeaders.getRawHeaders("Authorization")[0] |
1290 | + if header.startswith("OAuth "): |
1291 | + return SAMPLE_RESOURCE |
1292 | + request.setResponseCode(http.BAD_REQUEST) |
1293 | + return "ERROR: Expected OAuth header not present." |
1294 | + |
1295 | + |
1296 | +class MockWebServer(object): |
1297 | + """A mock webserver for testing""" |
1298 | + |
1299 | + def __init__(self): |
1300 | + """Start up this instance.""" |
1301 | + root = resource.Resource() |
1302 | + root.putChild(SIMPLERESOURCE, SimpleResource()) |
1303 | + |
1304 | + root.putChild(THROWERROR, resource.NoResource()) |
1305 | + |
1306 | + unauthorized_resource = resource.ErrorPage(resource.http.UNAUTHORIZED, |
1307 | + "Unauthorized", "Unauthorized") |
1308 | + root.putChild(UNAUTHORIZED, unauthorized_resource) |
1309 | + root.putChild(HEADONLY, HeadOnlyResource()) |
1310 | + root.putChild(VERIFYHEADERS, VerifyHeadersResource()) |
1311 | + root.putChild(OAUTHRESOURCE, OAuthCheckerResource()) |
1312 | + |
1313 | + db = checkers.InMemoryUsernamePasswordDatabaseDontUse() |
1314 | + db.addUser(SAMPLE_USERNAME, SAMPLE_PASSWORD) |
1315 | + test_portal = portal.Portal(SimpleRealm(), [db]) |
1316 | + cred_factory = guard.BasicCredentialFactory("example.org") |
1317 | + guarded_resource = guard.HTTPAuthSessionWrapper(test_portal, |
1318 | + [cred_factory]) |
1319 | + root.putChild(GUARDED, guarded_resource) |
1320 | + |
1321 | + site = server.Site(root) |
1322 | + application = service.Application('web') |
1323 | + self.service_collection = service.IServiceCollection(application) |
1324 | + #pylint: disable=E1101 |
1325 | + self.tcpserver = internet.TCPServer(0, site) |
1326 | + self.tcpserver.setServiceParent(self.service_collection) |
1327 | + self.service_collection.startService() |
1328 | + |
1329 | + def get_url(self): |
1330 | + """Build the url for this mock server.""" |
1331 | + #pylint: disable=W0212 |
1332 | + port_num = self.tcpserver._port.getHost().port |
1333 | + return "http://localhost:%d/" % port_num |
1334 | + |
1335 | + def stop(self): |
1336 | + """Shut it down.""" |
1337 | + #pylint: disable=E1101 |
1338 | + return self.service_collection.stopService() |
1339 | + |
1340 | + |
1341 | +class FakeReactor(object): |
1342 | + """A fake reactor object.""" |
1343 | + qApp = "Sample qapp" |
1344 | + |
1345 | + |
1346 | +class ModuleSelectionTestCase(TestCase): |
1347 | + """Test the functions to choose the qtnet or libsoup backend.""" |
1348 | + |
1349 | + def test_is_qt4reactor_installed_not_installed(self): |
1350 | + """When the qt4reactor is not installed, it returns false.""" |
1351 | + self.patch(sys, "modules", {}) |
1352 | + self.assertFalse(webclient.is_qt4reactor_installed()) |
1353 | + |
1354 | + def test_is_qt4reactor_installed_installed(self): |
1355 | + """When the qt4reactor is installed, it returns true.""" |
1356 | + fake_sysmodules = {"twisted.internet.reactor": FakeReactor()} |
1357 | + self.patch(sys, "modules", fake_sysmodules) |
1358 | + self.assertTrue(webclient.is_qt4reactor_installed()) |
1359 | + |
1360 | + def assert_module_name(self, module, expected_name): |
1361 | + """Check the name of a given module.""" |
1362 | + module_filename = os.path.basename(module.__file__) |
1363 | + module_name = os.path.splitext(module_filename)[0] |
1364 | + self.assertEqual(module_name, expected_name) |
1365 | + |
1366 | + def test_webclient_module_qtnetwork(self): |
1367 | + """Test the module name for the qtnetwork case.""" |
1368 | + self.patch(webclient, "is_qt4reactor_installed", lambda: True) |
1369 | + module = webclient.webclient_module() |
1370 | + self.assert_module_name(module, "qtnetwork") |
1371 | + |
1372 | + def test_webclient_module_libsoup(self): |
1373 | + """Test the module name for the libsoup case.""" |
1374 | + self.patch(webclient, "is_qt4reactor_installed", lambda: False) |
1375 | + module = webclient.webclient_module() |
1376 | + # pylint: disable=W0511 |
1377 | + # TODO: the libsoup backend depends on the twisted + GI bug |
1378 | + # meanwhile, use the test txweb client |
1379 | + #self.assert_module_name(module, "libsoup") |
1380 | + self.assert_module_name(module, "txweb") |
1381 | + |
1382 | + |
1383 | +class WebClientTestCase(TestCase): |
1384 | + """Test for the webclient.""" |
1385 | + |
1386 | + timeout = 8 |
1387 | + |
1388 | + @defer.inlineCallbacks |
1389 | + def setUp(self): |
1390 | + yield super(WebClientTestCase, self).setUp() |
1391 | + self.ws = MockWebServer() |
1392 | + self.addCleanup(self.ws.stop) |
1393 | + self.base_url = self.ws.get_url() |
1394 | + self.wc = webclient.webclient_factory() |
1395 | + self.addCleanup(self.wc.shutdown) |
1396 | + # pylint: disable=W0511 |
1397 | + # TODO: skewed timestamp correction |
1398 | + |
1399 | + @defer.inlineCallbacks |
1400 | + def test_get_url(self): |
1401 | + """A url is successfully retrieved from the mock webserver.""" |
1402 | + result = yield self.wc.request(self.base_url + SIMPLERESOURCE) |
1403 | + self.assertEqual(SAMPLE_RESOURCE, result.content) |
1404 | + |
1405 | + @defer.inlineCallbacks |
1406 | + def test_get_url_error(self): |
1407 | + """The errback is called when there's some error.""" |
1408 | + yield self.assertFailure(self.wc.request(self.base_url + THROWERROR), |
1409 | + webclient.WebClientError) |
1410 | + |
1411 | + @defer.inlineCallbacks |
1412 | + def test_unauthorized(self): |
1413 | + """Detect when a request failed with the UNAUTHORIZED http code.""" |
1414 | + yield self.assertFailure(self.wc.request(self.base_url + UNAUTHORIZED), |
1415 | + webclient.UnauthorizedError) |
1416 | + |
1417 | + @defer.inlineCallbacks |
1418 | + def test_method_head(self): |
1419 | + """The HTTP method is used.""" |
1420 | + result = yield self.wc.request(self.base_url + HEADONLY, method="HEAD") |
1421 | + self.assertEqual("", result.content) |
1422 | + |
1423 | + @defer.inlineCallbacks |
1424 | + def test_send_extra_headers(self): |
1425 | + """The extra_headers are sent to the server.""" |
1426 | + result = yield self.wc.request(self.base_url + VERIFYHEADERS, |
1427 | + extra_headers=SAMPLE_HEADERS) |
1428 | + self.assertEqual(SAMPLE_RESOURCE, result.content) |
1429 | + |
1430 | + @defer.inlineCallbacks |
1431 | + def test_send_basic_auth(self): |
1432 | + """The basic authentication headers are sent.""" |
1433 | + other_wc = webclient.webclient_factory(username=SAMPLE_USERNAME, |
1434 | + password=SAMPLE_PASSWORD) |
1435 | + self.addCleanup(other_wc.shutdown) |
1436 | + result = yield other_wc.request(self.base_url + GUARDED) |
1437 | + self.assertEqual(SAMPLE_RESOURCE, result.content) |
1438 | + |
1439 | + @defer.inlineCallbacks |
1440 | + def test_request_is_oauth_signed(self): |
1441 | + """The request is oauth signed.""" |
1442 | + result = yield self.wc.request(self.base_url + OAUTHRESOURCE, |
1443 | + oauth_credentials=SAMPLE_CREDENTIALS) |
1444 | + self.assertEqual(SAMPLE_RESOURCE, result.content) |
1445 | + |
1446 | + |
1447 | +class OAuthTestCase(TestCase): |
1448 | + """Test for the oauth signing code.""" |
1449 | + |
1450 | + def parse_oauth_header(self, header): |
1451 | + """Parse an oauth header into a tuple of (method, params_dict).""" |
1452 | + params = {} |
1453 | + |
1454 | + method, params_string = header.split(" ", 1) |
1455 | + for p in params_string.split(","): |
1456 | + k, v = p.strip().split("=") |
1457 | + params[k] = urllib.unquote(v[1:-1]) |
1458 | + |
1459 | + return method, params |
1460 | + |
1461 | + def test_build_oauth_headers(self): |
1462 | + """Build the oauth headers for a sample request.""" |
1463 | + |
1464 | + sample_method = "GET" |
1465 | + sample_url = "http://one.ubuntu.com/" |
1466 | + timestamp = 1 |
1467 | + expected_params = { |
1468 | + "oauth_timestamp": str(timestamp), |
1469 | + "oauth_consumer_key": SAMPLE_CREDENTIALS["consumer_key"], |
1470 | + "oauth_signature_method": "HMAC-SHA1", |
1471 | + "oauth_token": SAMPLE_CREDENTIALS["token"], |
1472 | + "oauth_nonce": ANY_VALUE, |
1473 | + "oauth_signature": ANY_VALUE, |
1474 | + } |
1475 | + |
1476 | + module = webclient.webclient_module() |
1477 | + headers = module.BaseWebClient.build_oauth_headers(sample_method, |
1478 | + sample_url, SAMPLE_CREDENTIALS, timestamp) |
1479 | + |
1480 | + method, params = self.parse_oauth_header(headers["Authorization"]) |
1481 | + |
1482 | + self.assertEqual(method, "OAuth") |
1483 | + |
1484 | + for k, expected_value in expected_params.iteritems(): |
1485 | + self.assertIn(k, params) |
1486 | + if expected_value is not ANY_VALUE: |
1487 | + self.assertEqual(params[k], expected_value) |
1488 | |
1489 | === added file 'ubuntu_sso/utils/webclient/txweb.py' |
1490 | --- ubuntu_sso/utils/webclient/txweb.py 1970-01-01 00:00:00 +0000 |
1491 | +++ ubuntu_sso/utils/webclient/txweb.py 2011-12-19 19:46:24 +0000 |
1492 | @@ -0,0 +1,60 @@ |
1493 | +# -*- coding: utf-8 -*- |
1494 | +# |
1495 | +# Copyright 2011 Canonical Ltd. |
1496 | +# |
1497 | +# This program is free software: you can redistribute it and/or modify it |
1498 | +# under the terms of the GNU General Public License version 3, as published |
1499 | +# by the Free Software Foundation. |
1500 | +# |
1501 | +# This program is distributed in the hope that it will be useful, but |
1502 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1503 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1504 | +# PURPOSE. See the GNU General Public License for more details. |
1505 | +# |
1506 | +# You should have received a copy of the GNU General Public License along |
1507 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
1508 | +"""A webclient backend that uses twisted.web.client.""" |
1509 | + |
1510 | +import base64 |
1511 | + |
1512 | +from twisted.web import client, error, http |
1513 | +from twisted.internet import defer |
1514 | + |
1515 | +from ubuntu_sso.utils.webclient.common import ( |
1516 | + BaseWebClient, |
1517 | + Response, |
1518 | + UnauthorizedError, |
1519 | + WebClientError, |
1520 | +) |
1521 | + |
1522 | + |
1523 | +class WebClient(BaseWebClient): |
1524 | + """A simple web client that does not support proxies, yet.""" |
1525 | + |
1526 | + @defer.inlineCallbacks |
1527 | + def request(self, url, method="GET", extra_headers=None, |
1528 | + oauth_credentials=None): |
1529 | + """Get the page, or fail trying.""" |
1530 | + if extra_headers: |
1531 | + headers = dict(extra_headers) |
1532 | + else: |
1533 | + headers = {} |
1534 | + |
1535 | + if oauth_credentials: |
1536 | + timestamp = yield self.get_timestamp() |
1537 | + oauth_headers = self.build_oauth_headers(method, url, |
1538 | + oauth_credentials, timestamp) |
1539 | + headers.update(oauth_headers) |
1540 | + |
1541 | + if self.username and self.password: |
1542 | + auth = base64.b64encode(self.username + ":" + self.password) |
1543 | + headers["Authorization"] = "Basic " + auth |
1544 | + |
1545 | + try: |
1546 | + result = yield client.getPage(url, method=method, headers=headers) |
1547 | + response = Response(result) |
1548 | + defer.returnValue(response) |
1549 | + except error.Error as e: |
1550 | + if int(e.status) == http.UNAUTHORIZED: |
1551 | + raise UnauthorizedError(e.message) |
1552 | + raise WebClientError(e.message) |
+1