Merge lp:~nataliabidart/ubuntu-sso-client/stable-3-0-update into lp:ubuntu-sso-client/stable-3-0

Proposed by Natalia Bidart
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
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.

To post a comment you must log in.
Revision history for this message
Roberto Alsina (ralsina) wrote :

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bin/ubuntu-sso-login'
--- bin/ubuntu-sso-login 2011-09-27 14:06:12 +0000
+++ bin/ubuntu-sso-login 2011-12-19 19:46:24 +0000
@@ -1,9 +1,5 @@
1#!/usr/bin/python1#!/usr/bin/env python
22# -*- coding: utf-8 -*-
3# ubuntu-sso-login - Client side log-in utility for Ubuntu One
4#
5# Author: Rodney Dawes <rodney.dawes@canonical.com>
6# Author: Natalia B. Bidart <natalia.bidart@canonical.com>
7#3#
8# Copyright 2009-2010 Canonical Ltd.4# Copyright 2009-2010 Canonical Ltd.
9#5#
@@ -19,7 +15,7 @@
19# You should have received a copy of the GNU General Public License along15# You should have received a copy of the GNU General Public License along
20# with this program. If not, see <http://www.gnu.org/licenses/>.16# with this program. If not, see <http://www.gnu.org/licenses/>.
2117
22"""Run the dbus service for UserManagement and ApplicationCredentials."""18"""Start the sso service."""
2319
24# Invalid name "ubuntu-sso-login", pylint: disable=C010320# Invalid name "ubuntu-sso-login", pylint: disable=C0103
2521
@@ -41,60 +37,19 @@
41# val = globals()[globalname]37# val = globals()[globalname]
42# KeyError: 'ROUND_CEiLiNG'38# KeyError: 'ROUND_CEiLiNG'
4339
44import signal
45import sys40import sys
4641
47import dbus.mainloop.glib42if sys.platform == 'win32':
48import dbus.service43 from PyQt4 import QtGui
49import gtk44 # need to create the QApplication before installing the reactor
5045 QtGui.QApplication(sys.argv)
51from dbus.mainloop.glib import DBusGMainLoop46
5247 # pylint: disable=F0401
53from ubuntu_sso import (DBUS_BUS_NAME, DBUS_ACCOUNT_PATH, DBUS_CRED_PATH,48 import qt4reactor
54 DBUS_CREDENTIALS_PATH)49 qt4reactor.install()
55from ubuntu_sso.main import SSOLogin, CredentialsManagement50
5651from ubuntu_sso.main import main
57from ubuntu_sso.logger import setup_logging
58
59# Invalid name "ubuntu-sso-login"
60# pylint: disable=C0103
61
62
63logger = setup_logging("ubuntu-sso-login")
64dbus.mainloop.glib.threads_init()
65gtk.gdk.threads_init()
66DBusGMainLoop(set_as_default=True)
67
68
69def sighup_handler(*a, **kw):
70 """Stop the service."""
71 # This handler may be called in any thread, so is not thread safe.
72 # See the link below for info:
73 # www.listware.net/201004/gtk-devel-list/115067-unix-signals-in-glib.html
74 #
75 # gtk.main_quit and the logger methods are safe to be called from any
76 # thread. Just don't call other random stuff here.
77 logger.info("Stoping Ubuntu SSO login manager since SIGHUP was received.")
78 gtk.main_quit()
7952
8053
81if __name__ == "__main__":54if __name__ == "__main__":
82 # Register DBus service for making sure we run only one instance55 main()
83 bus = dbus.SessionBus()
84 name = bus.request_name(DBUS_BUS_NAME,
85 dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
86 if name == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
87 logger.error("Ubuntu SSO login manager already running, quitting.")
88 sys.exit(0)
89
90 logger.debug("Hooking up SIGHUP with handler %r.", sighup_handler)
91 signal.signal(signal.SIGHUP, sighup_handler)
92
93 logger.info("Starting Ubuntu SSO login manager for bus %r.", DBUS_BUS_NAME)
94 bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus())
95 SSOLogin(bus_name, object_path=DBUS_ACCOUNT_PATH)
96 CredentialsManagement(timeout_func=gtk.timeout_add,
97 shutdown_func=gtk.main_quit,
98 bus_name=bus_name, object_path=DBUS_CREDENTIALS_PATH)
99
100 gtk.main()
10156
=== removed file 'bin/windows-ubuntu-sso-login'
--- bin/windows-ubuntu-sso-login 2011-11-10 20:14:29 +0000
+++ bin/windows-ubuntu-sso-login 1970-01-01 00:00:00 +0000
@@ -1,86 +0,0 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# Authors: Manuel de la Pena <manuel@canonical.com>
4# Alejandro J. Cura <alecu@canonical.com>
5#
6# Copyright 2011 Canonical Ltd.
7#
8# This program is free software: you can redistribute it and/or modify it
9# under the terms of the GNU General Public License version 3, as published
10# by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranties of
14# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15# PURPOSE. See the GNU General Public License for more details.
16#
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"""Start the sso service on a windows machine."""
20
21# disable the name warning and complains about twisted
22# pylint: disable=C0103, E1101, F0401
23import sys
24
25from PyQt4 import QtGui
26# need to create the QApplication before installing the reactor
27app = QtGui.QApplication(sys.argv)
28import qt4reactor
29qt4reactor.install()
30
31from twisted.internet import reactor, defer
32from twisted.spread.pb import PBServerFactory
33from twisted.internet.task import LoopingCall
34from twisted.python import log
35
36from ubuntu_sso.logger import setup_logging
37from ubuntu_sso.main.windows import (
38 CredentialsManagement,
39 LOCALHOST,
40 SSOLogin,
41 UbuntuSSORoot,
42 get_activation_config,
43)
44from ubuntu_sso.utils import tcpactivation
45
46
47logger = setup_logging("windows-ubuntu-sso-login")
48
49
50def add_timeout(interval, callback, *args, **kwargs):
51 """Add a timeout callback as a task."""
52 time_out_task = LoopingCall(callback, *args, **kwargs)
53 time_out_task.start(interval/1000, now=False)
54
55
56@defer.inlineCallbacks
57def main():
58 """Initialize and start this process."""
59 ai = tcpactivation.ActivationInstance(get_activation_config())
60 port = yield ai.get_port()
61
62 login = SSOLogin('ignored')
63 creds_management = CredentialsManagement(add_timeout, reactor.stop)
64 root = UbuntuSSORoot(sso_login=login, cred_manager=creds_management)
65
66 reactor.listenTCP(port, PBServerFactory(root), interface=LOCALHOST)
67
68
69def handle_already_started(failure):
70 """Handle the already started error by shutting down this process."""
71 failure.trap(tcpactivation.AlreadyStartedError)
72 print "Ubuntu SSO login manager already running."
73 reactor.stop()
74
75
76def utter_failure(failure):
77 """Handle an utter failure by logging it and quiting."""
78 log.err(failure)
79 reactor.stop()
80
81
82if __name__ == '__main__':
83 d = main()
84 d.addErrback(handle_already_started)
85 d.addErrback(utter_failure)
86 reactor.run()
870
=== modified file 'run-tests.bat'
--- run-tests.bat 2011-11-10 19:45:36 +0000
+++ run-tests.bat 2011-12-19 19:46:24 +0000
@@ -55,7 +55,7 @@
55"%PYTHONEXEPATH%\python.exe" setup.py build55"%PYTHONEXEPATH%\python.exe" setup.py build
56ECHO Running tests56ECHO Running tests
57:: execute the tests with a number of ignored linux only modules57:: execute the tests with a number of ignored linux only modules
58"%PYTHONEXEPATH%\python.exe" "%PYTHONEXEPATH%\Scripts\u1trial" -c ubuntu_sso -i "test_gui.py, test_linux.py, test_txsecrets.py" --reactor=qt4 --gui %*58"%PYTHONEXEPATH%\python.exe" "%PYTHONEXEPATH%\Scripts\u1trial" -c -i "test_gui.py, test_linux.py, test_txsecrets.py" --reactor=qt4 --gui %* ubuntu_sso
59:: Clean the build from the setupt.py59:: Clean the build from the setupt.py
60ECHO Cleaning the generated code before running the style checks...60ECHO Cleaning the generated code before running the style checks...
61"%PYTHONEXEPATH%\python.exe" setup.py clean61"%PYTHONEXEPATH%\python.exe" setup.py clean
6262
=== modified file 'ubuntu_sso/gtk/gui.py'
--- ubuntu_sso/gtk/gui.py 2011-11-14 12:02:39 +0000
+++ ubuntu_sso/gtk/gui.py 2011-12-19 19:46:24 +0000
@@ -743,7 +743,7 @@
743 # check T&C743 # check T&C
744 if not self.yes_to_tc_checkbutton.get_active():744 if not self.yes_to_tc_checkbutton.get_active():
745 self._set_warning_message(self.tc_warning_label,745 self._set_warning_message(self.tc_warning_label,
746 TC_NOT_ACCEPTED)746 TC_NOT_ACCEPTED % {'app_name': self.app_name})
747 error = True747 error = True
748748
749 captcha_solution = self.captcha_solution_entry.get_text()749 captcha_solution = self.captcha_solution_entry.get_text()
750750
=== modified file 'ubuntu_sso/gtk/tests/test_gui.py'
--- ubuntu_sso/gtk/tests/test_gui.py 2011-11-11 17:12:19 +0000
+++ ubuntu_sso/gtk/tests/test_gui.py 2011-12-19 19:46:24 +0000
@@ -1420,7 +1420,7 @@
1420 self.ui.join_ok_button.clicked()1420 self.ui.join_ok_button.clicked()
14211421
1422 self.assert_correct_label_warning(self.ui.tc_warning_label,1422 self.assert_correct_label_warning(self.ui.tc_warning_label,
1423 gui.TC_NOT_ACCEPTED)1423 gui.TC_NOT_ACCEPTED % {'app_name': APP_NAME})
1424 self.assertNotIn('register_user', self.ui.backend._called)1424 self.assertNotIn('register_user', self.ui.backend._called)
14251425
1426 def test_warning_is_shown_if_not_captcha_solution(self):1426 def test_warning_is_shown_if_not_captcha_solution(self):
14271427
=== modified file 'ubuntu_sso/main/__init__.py'
--- ubuntu_sso/main/__init__.py 2011-09-27 14:06:12 +0000
+++ ubuntu_sso/main/__init__.py 2011-12-19 19:46:24 +0000
@@ -338,16 +338,16 @@
338338
339if sys.platform == 'win32':339if sys.platform == 'win32':
340 from ubuntu_sso.main import windows340 from ubuntu_sso.main import windows
341 SSOLogin = windows.SSOLogin341 source = windows
342 CredentialsManagement = windows.CredentialsManagement
343 TIMEOUT_INTERVAL = 10000000000 # forever342 TIMEOUT_INTERVAL = 10000000000 # forever
344 thread_execute = windows.blocking
345 get_sso_login_backend = windows.get_sso_login_backend
346else:343else:
347 from ubuntu_sso.main import linux344 from ubuntu_sso.main import linux
348 SSOLogin = linux.SSOLogin345 source = linux
349 CredentialsManagement = linux.CredentialsManagement346
350 thread_execute = linux.blocking347CredentialsManagement = source.CredentialsManagement
351 get_sso_login_backend = linux.get_sso_login_backend348get_sso_login_backend = source.get_sso_login_backend
349main = source.main
350SSOLogin = source.SSOLogin
351thread_execute = source.blocking
352352
353# pylint: enable=C0103353# pylint: enable=C0103
354354
=== modified file 'ubuntu_sso/main/linux.py'
--- ubuntu_sso/main/linux.py 2011-09-29 20:49:45 +0000
+++ ubuntu_sso/main/linux.py 2011-12-19 19:46:24 +0000
@@ -1,11 +1,6 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2#2#
3# ubuntu_sso.main - main login handling interface3# Copyright 2009-2011 Canonical Ltd.
4#
5# Author: Natalia Bidart <natalia.bidart@canonical.com>
6# Author: Alejandro J. Cura <alecu@canonical.com>
7#
8# Copyright 2009 Canonical Ltd.
9#4#
10# This program is free software: you can redistribute it and/or modify it5# This program is free software: you can redistribute it and/or modify it
11# under the terms of the GNU General Public License version 3, as published6# under the terms of the GNU General Public License version 3, as published
@@ -28,12 +23,19 @@
28"""23"""
2924
30import threading25import threading
26import signal
27import sys
3128
29import dbus.mainloop.glib
32import dbus.service30import dbus.service
31import gtk
32
3333
34from ubuntu_sso import (34from ubuntu_sso import (
35 DBUS_ACCOUNT_PATH,35 DBUS_ACCOUNT_PATH,
36 DBUS_BUS_NAME,
36 DBUS_CREDENTIALS_IFACE,37 DBUS_CREDENTIALS_IFACE,
38 DBUS_CREDENTIALS_PATH,
37 DBUS_IFACE_USER_NAME,39 DBUS_IFACE_USER_NAME,
38 NO_OP,40 NO_OP,
39)41)
@@ -50,7 +52,7 @@
50# pylint: disable=C010352# pylint: disable=C0103
5153
5254
53logger = setup_logging("ubuntu_sso.main")55logger = setup_logging("ubuntu_sso.main.linux")
5456
5557
56def blocking(f, app_name, result_cb, error_cb):58def blocking(f, app_name, result_cb, error_cb):
@@ -410,3 +412,42 @@
410def get_sso_login_backend():412def get_sso_login_backend():
411 """Get the backend for the Login service."""413 """Get the backend for the Login service."""
412 raise NotImplementedError()414 raise NotImplementedError()
415
416
417def sighup_handler(*a, **kw):
418 """Stop the service."""
419 # This handler may be called in any thread, so is not thread safe.
420 # See the link below for info:
421 # www.listware.net/201004/gtk-devel-list/115067-unix-signals-in-glib.html
422 #
423 # gtk.main_quit and the logger methods are safe to be called from any
424 # thread. Just don't call other random stuff here.
425 logger.info("Stoping Ubuntu SSO login manager since SIGHUP was received.")
426 gtk.main_quit()
427
428
429def main():
430 """Run the backend service."""
431 dbus.mainloop.glib.threads_init()
432 gtk.gdk.threads_init()
433 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
434
435 bus = dbus.SessionBus()
436 # Register DBus service for making sure we run only one instance
437 name = bus.request_name(DBUS_BUS_NAME,
438 dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
439 if name == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
440 logger.error("Ubuntu SSO login manager already running, quitting.")
441 sys.exit(0)
442
443 logger.debug("Hooking up SIGHUP with handler %r.", sighup_handler)
444 signal.signal(signal.SIGHUP, sighup_handler)
445
446 logger.info("Starting Ubuntu SSO login manager for bus %r.", DBUS_BUS_NAME)
447 bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus())
448 SSOLogin(bus_name, object_path=DBUS_ACCOUNT_PATH)
449 CredentialsManagement(timeout_func=gtk.timeout_add,
450 shutdown_func=gtk.main_quit,
451 bus_name=bus_name, object_path=DBUS_CREDENTIALS_PATH)
452
453 gtk.main()
413454
=== modified file 'ubuntu_sso/main/tests/test_linux.py'
--- ubuntu_sso/main/tests/test_linux.py 2011-11-14 18:31:46 +0000
+++ ubuntu_sso/main/tests/test_linux.py 2011-12-19 19:46:24 +0000
@@ -585,6 +585,7 @@
585 self.memento = MementoHandler()585 self.memento = MementoHandler()
586 self.memento.setLevel(logging.DEBUG)586 self.memento.setLevel(logging.DEBUG)
587 ubuntu_sso.main.logger.addHandler(self.memento)587 ubuntu_sso.main.logger.addHandler(self.memento)
588 self.addCleanup(ubuntu_sso.main.logger.removeHandler, self.memento)
588589
589 @defer.inlineCallbacks590 @defer.inlineCallbacks
590 def tearDown(self):591 def tearDown(self):
@@ -1204,7 +1205,9 @@
12041205
1205 self.memento = MementoHandler()1206 self.memento = MementoHandler()
1206 self.memento.setLevel(logging.DEBUG)1207 self.memento.setLevel(logging.DEBUG)
1207 ubuntu_sso.main.logger.addHandler(self.memento)1208 ubuntu_sso.main.linux.logger.addHandler(self.memento)
1209 self.addCleanup(ubuntu_sso.main.linux.logger.removeHandler,
1210 self.memento)
12081211
1209 def assert_dbus_signal_correct(self, signal, signature):1212 def assert_dbus_signal_correct(self, signal, signature):
1210 """Check that 'signal' is a dbus signal with proper 'signature'."""1213 """Check that 'signal' is a dbus signal with proper 'signature'."""
12111214
=== modified file 'ubuntu_sso/main/tests/test_windows.py' (properties changed: -x to +x)
--- ubuntu_sso/main/tests/test_windows.py 2011-11-02 16:56:12 +0000
+++ ubuntu_sso/main/tests/test_windows.py 2011-12-19 19:46:24 +0000
@@ -28,6 +28,7 @@
28 DeadReferenceError,28 DeadReferenceError,
29 PBClientFactory,29 PBClientFactory,
30 PBServerFactory,30 PBServerFactory,
31 Broker,
31)32)
32from ubuntu_sso import main33from ubuntu_sso import main
33from ubuntu_sso.main import windows34from ubuntu_sso.main import windows
@@ -46,7 +47,7 @@
4647
47# because we are using twisted we have java like names C0103 and48# because we are using twisted we have java like names C0103 and
48# the issues that mocker brings with it like W010449# the issues that mocker brings with it like W0104
49# pylint: disable=C0103,W010450# pylint: disable=C0103,W0104,E1101,W0201
5051
5152
52class SaveProtocolServerFactory(PBServerFactory):53class SaveProtocolServerFactory(PBServerFactory):
@@ -59,6 +60,77 @@
59 self.protocolInstance = protocol60 self.protocolInstance = protocol
6061
6162
63class SaveClientFactory(PBClientFactory):
64 """Client Factory that knows when we disconnected."""
65
66 def __init__(self, connected_d, disconnected_d):
67 """Create a new instance."""
68 PBClientFactory.__init__(self)
69 self.connected_d = connected_d
70 self.disconnected_d = disconnected_d
71
72 def clientConnectionMade(self, broker):
73 """Connection made."""
74 PBClientFactory.clientConnectionMade(self, broker)
75 self.connected_d.callback(True)
76
77 def clientConnectionLost(self, connector, reason, reconnecting=0):
78 """Connection lost."""
79 self.disconnected_d.callback(True)
80
81
82class ServerProtocol(Broker):
83 """Server protocol that allows us to clean the tests."""
84
85 def connectionLost(self, *a):
86 self.factory.onConnectionLost.callback(self)
87
88
89class ConnectedTestCase(TestCase):
90 """Base test case with a client and a server."""
91
92 @defer.inlineCallbacks
93 def setUp(self):
94 """Set up for the tests."""
95 yield super(ConnectedTestCase, self).setUp()
96 self.server_disconnected = defer.Deferred()
97 self.client_disconnected = defer.Deferred()
98 self.listener = None
99 self.connector = None
100 self.server_factory = None
101 self.client_factory = None
102
103 def setup_client_server(self, sso_root):
104 """Set tests."""
105 port = get_sso_pb_port()
106 self.listener = self._listen_server(sso_root, self.server_disconnected,
107 port)
108 connected = defer.Deferred()
109 self.connector = self._connect_client(connected,
110 self.client_disconnected, port)
111 self.addCleanup(self.teardown_client_server)
112 return connected
113
114 def _listen_server(self, sso_root, d, port):
115 """Start listenting."""
116 self.server_factory = SaveProtocolServerFactory(sso_root)
117 self.server_factory.onConnectionLost = d
118 self.server_factory.protocol = ServerProtocol
119 return reactor.listenTCP(port, self.server_factory)
120
121 def _connect_client(self, d1, d2, port):
122 """Connect client."""
123 self.client_factory = SaveClientFactory(d1, d2)
124 return reactor.connectTCP(LOCALHOST, port, self.client_factory)
125
126 def teardown_client_server(self):
127 """Clean resources."""
128 self.connector.disconnect()
129 d = defer.maybeDeferred(self.listener.stopListening)
130 return defer.gatherResults([d, self.client_disconnected,
131 self.server_disconnected])
132
133
62class FakeDecoratedObject(object):134class FakeDecoratedObject(object):
63 """An object that has decorators."""135 """An object that has decorators."""
64136
@@ -240,7 +312,7 @@
240 self.assertEqual(name, self.signal_names[signal_index])312 self.assertEqual(name, self.signal_names[signal_index])
241313
242314
243class SSOLoginTestCase(SignalHandlingTestCase):315class SSOLoginTestCase(ConnectedTestCase, SignalHandlingTestCase):
244 """Test the login class."""316 """Test the login class."""
245317
246 signal_names = [318 signal_names = [
@@ -268,35 +340,12 @@
268 self.login = SSOLogin(None)340 self.login = SSOLogin(None)
269 # start pb341 # start pb
270 self.sso_root = UbuntuSSORoot(sso_login=self.login)342 self.sso_root = UbuntuSSORoot(sso_login=self.login)
271 self.server_factory = SaveProtocolServerFactory(self.sso_root)
272 # pylint: disable=E1101343 # pylint: disable=E1101
273 port = get_sso_pb_port()344 yield self.setup_client_server(self.sso_root)
274 self.listener = reactor.listenTCP(port, self.server_factory)
275 self.client_factory = PBClientFactory()
276 self.connector = reactor.connectTCP(LOCALHOST, port,
277 self.client_factory)
278 self.client = yield self._get_client()345 self.client = yield self._get_client()
279 # pylint: enable=E1101346 # pylint: enable=E1101
280347
281 @defer.inlineCallbacks348 @defer.inlineCallbacks
282 def tearDown(self):
283 """Clean reactor."""
284 yield super(SSOLoginTestCase, self).tearDown()
285 if self.server_factory.protocolInstance is not None:
286 self.server_factory.protocolInstance.transport.loseConnection()
287 yield defer.gatherResults([self._tearDownServer(),
288 self._tearDownClient()])
289
290 def _tearDownServer(self):
291 """Teardown the server."""
292 return defer.maybeDeferred(self.listener.stopListening)
293
294 def _tearDownClient(self):
295 """Tear down the client."""
296 self.connector.disconnect()
297 return defer.succeed(None)
298
299 @defer.inlineCallbacks
300 def _get_client(self):349 def _get_client(self):
301 """Get the client."""350 """Get the client."""
302 # request the remote object and create a client351 # request the remote object and create a client
@@ -541,7 +590,7 @@
541 self.mocker.verify()590 self.mocker.verify()
542591
543592
544class CredentialsManagementTestCase(TestCase):593class CredentialsManagementTestCase(ConnectedTestCase, TestCase):
545 """Test the management class."""594 """Test the management class."""
546595
547 @defer.inlineCallbacks596 @defer.inlineCallbacks
@@ -556,35 +605,12 @@
556 self.creds.root = self.root605 self.creds.root = self.root
557 # start pb606 # start pb
558 self.sso_root = UbuntuSSORoot(cred_manager=self.creds)607 self.sso_root = UbuntuSSORoot(cred_manager=self.creds)
559 self.server_factory = SaveProtocolServerFactory(self.sso_root)
560 # pylint: disable=E1101608 # pylint: disable=E1101
561 port = get_sso_pb_port()609 yield self.setup_client_server(self.sso_root)
562 self.listener = reactor.listenTCP(port, self.server_factory)
563 self.client_factory = PBClientFactory()
564 self.connector = reactor.connectTCP(LOCALHOST, port,
565 self.client_factory)
566 self.client = yield self._get_client()610 self.client = yield self._get_client()
567 # pylint: enable=E1101611 # pylint: enable=E1101
568612
569 @defer.inlineCallbacks613 @defer.inlineCallbacks
570 def tearDown(self):
571 """Clean reactor."""
572 yield super(CredentialsManagementTestCase, self).tearDown()
573 if self.server_factory.protocolInstance is not None:
574 self.server_factory.protocolInstance.transport.loseConnection()
575 yield defer.gatherResults([self._tearDownServer(),
576 self._tearDownClient()])
577
578 def _tearDownServer(self):
579 """Teardown the server."""
580 return defer.maybeDeferred(self.listener.stopListening)
581
582 def _tearDownClient(self):
583 """Tear down the client."""
584 self.connector.disconnect()
585 return defer.succeed(None)
586
587 @defer.inlineCallbacks
588 def _get_client(self):614 def _get_client(self):
589 """Get the client."""615 """Get the client."""
590 # request the remote object and create a client616 # request the remote object and create a client
@@ -592,6 +618,7 @@
592 remote = yield root.callRemote('get_cred_manager')618 remote = yield root.callRemote('get_cred_manager')
593 client = CredentialsManagementClient(remote)619 client = CredentialsManagementClient(remote)
594 yield client.register_to_signals()620 yield client.register_to_signals()
621 self.addCleanup(client.unregister_to_signals)
595 # set the cb622 # set the cb
596 for signal_name in ['on_authorization_denied_cb',623 for signal_name in ['on_authorization_denied_cb',
597 'on_credentials_found_cb',624 'on_credentials_found_cb',
598625
=== modified file 'ubuntu_sso/main/windows.py'
--- ubuntu_sso/main/windows.py 2011-09-27 14:06:12 +0000
+++ ubuntu_sso/main/windows.py 2011-12-19 19:46:24 +0000
@@ -1,6 +1,4 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2# Authors: Manuel de la Pena <manuel@canonical.com>
3# Alejandro J. Cura <alecu@canonical.com>
4#2#
5# Copyright 2011 Canonical Ltd.3# Copyright 2011 Canonical Ltd.
6#4#
@@ -26,10 +24,12 @@
26# pylint: enable=F040124# pylint: enable=F0401
2725
28from twisted.internet import defer, reactor26from twisted.internet import defer, reactor
27from twisted.internet.task import LoopingCall
29from twisted.internet.threads import deferToThread28from twisted.internet.threads import deferToThread
30from twisted.spread.pb import (29from twisted.spread.pb import (
31 DeadReferenceError,30 DeadReferenceError,
32 PBClientFactory,31 PBClientFactory,
32 PBServerFactory,
33 Referenceable,33 Referenceable,
34 Root,34 Root,
35)35)
@@ -41,7 +41,12 @@
41 SSOLoginRoot,41 SSOLoginRoot,
42 except_to_errdict,42 except_to_errdict,
43)43)
44from ubuntu_sso.utils.tcpactivation import ActivationConfig, ActivationClient44from ubuntu_sso.utils.tcpactivation import (
45 ActivationClient,
46 ActivationConfig,
47 ActivationInstance,
48 AlreadyStartedError,
49)
4550
4651
47logger = setup_logging("ubuntu_sso.main.windows")52logger = setup_logging("ubuntu_sso.main.windows")
@@ -857,3 +862,41 @@
857 root = UbuntuSSOClient()862 root = UbuntuSSOClient()
858 yield root.connect()863 yield root.connect()
859 defer.returnValue(root.sso_login)864 defer.returnValue(root.sso_login)
865
866
867def add_timeout(interval, callback, *args, **kwargs):
868 """Add a timeout callback as a task."""
869 time_out_task = LoopingCall(callback, *args, **kwargs)
870 time_out_task.start(interval / 1000, now=False)
871
872
873# the reactor does have run and stop methods
874# pylint: disable=E1101
875
876@defer.inlineCallbacks
877def start_service():
878 """Initialize and start this process."""
879 try:
880 ai = ActivationInstance(get_activation_config())
881 port = yield ai.get_port()
882 logger.info("Starting Ubuntu SSO login manager for port %r.", port)
883
884 login = SSOLogin('ignored')
885 creds_management = CredentialsManagement(add_timeout, reactor.stop)
886 root = UbuntuSSORoot(sso_login=login, cred_manager=creds_management)
887
888 reactor.listenTCP(port, PBServerFactory(root), interface=LOCALHOST)
889 except AlreadyStartedError:
890 logger.error("Ubuntu SSO login manager already running, quitting.")
891 reactor.stop()
892 except:
893 logger.exception('Can not start Ubuntu SSO login manager:')
894 reactor.stop()
895
896
897def main():
898 """Run the backend service."""
899 start_service()
900 reactor.run()
901
902# pylint: enable=E1101
860903
=== modified file 'ubuntu_sso/qt/controllers.py'
--- ubuntu_sso/qt/controllers.py 2011-11-11 20:34:57 +0000
+++ ubuntu_sso/qt/controllers.py 2011-12-19 19:46:24 +0000
@@ -828,8 +828,12 @@
828 email = unicode(self.view.wizard().forgotten.ui.email_line_edit.text())828 email = unicode(self.view.wizard().forgotten.ui.email_line_edit.text())
829 self.view.wizard().current_user.ui.email_edit.setText(email)829 self.view.wizard().current_user.ui.email_edit.setText(email)
830 self.view.wizard().overlay.hide()830 self.view.wizard().overlay.hide()
831 self.view.wizard().back()831 current_user_id = self.view.wizard().current_user_page_id
832 self.view.wizard().back()832 visited_pages = self.view.wizard().visitedPages()
833 for index in reversed(visited_pages):
834 if index == current_user_id:
835 break
836 self.view.wizard().back()
833837
834 def on_password_change_error(self, app_name, error):838 def on_password_change_error(self, app_name, error):
835 """Let the user know that there was an error."""839 """Let the user know that there was an error."""
@@ -844,7 +848,7 @@
844 email = unicode(self.view.wizard().forgotten.ui.email_line_edit.text())848 email = unicode(self.view.wizard().forgotten.ui.email_line_edit.text())
845 code = unicode(self.view.ui.reset_code_line_edit.text())849 code = unicode(self.view.ui.reset_code_line_edit.text())
846 password = unicode(self.view.ui.password_line_edit.text())850 password = unicode(self.view.ui.password_line_edit.text())
847 logger.info('Settig new password for %s and email %s with code %s',851 logger.info('Setting new password for %r and email %r with code %r',
848 app_name, email, code)852 app_name, email, code)
849 self.backend.set_new_password(app_name, email, code, password)853 self.backend.set_new_password(app_name, email, code, password)
850854
851855
=== modified file 'ubuntu_sso/qt/tests/test_controllers.py'
--- ubuntu_sso/qt/tests/test_controllers.py 2011-11-11 20:34:57 +0000
+++ ubuntu_sso/qt/tests/test_controllers.py 2011-12-19 19:46:24 +0000
@@ -669,6 +669,7 @@
669 self.count_back = 0669 self.count_back = 0
670 self.hide_value = False670 self.hide_value = False
671 self.text_value = 'mail@mail.com'671 self.text_value = 'mail@mail.com'
672 self.current_user_page_id = 4
672673
673 def wizard(self):674 def wizard(self):
674 """Fake wizard function for view."""675 """Fake wizard function for view."""
@@ -690,6 +691,10 @@
690 def setText(self, text):691 def setText(self, text):
691 """Fake setText for QLineEdit."""692 """Fake setText for QLineEdit."""
692 self.text_value = text693 self.text_value = text
694
695 def visitedPages(self):
696 """Return an int list of fake visited pages."""
697 return [1, 4, 6, 8]
693 # pylint: enable=C0103698 # pylint: enable=C0103
694699
695700
@@ -2094,5 +2099,17 @@
2094 """Test that on_password_changed execute the proper operation."""2099 """Test that on_password_changed execute the proper operation."""
2095 self.controller.on_password_changed('app_name', '')2100 self.controller.on_password_changed('app_name', '')
2096 self.assertTrue(self.controller.view.hide_value)2101 self.assertTrue(self.controller.view.hide_value)
2097 self.assertEqual(self.controller.view.count_back, 2)2102 times_visited = 2
2103 self.assertEqual(self.controller.view.count_back, times_visited)
2104 self.assertEqual(self.controller.view.text_value, 'mail@mail.com')
2105
2106 def test_on_password_changed_not_visited(self):
2107 """Test that on_password_changed execute the proper operation."""
2108 current_user_page_id = 20
2109 self.patch(self.controller.view, "current_user_page_id",
2110 current_user_page_id)
2111 self.controller.on_password_changed('app_name', '')
2112 self.assertTrue(self.controller.view.hide_value)
2113 times_visited = 4
2114 self.assertEqual(self.controller.view.count_back, times_visited)
2098 self.assertEqual(self.controller.view.text_value, 'mail@mail.com')2115 self.assertEqual(self.controller.view.text_value, 'mail@mail.com')
20992116
=== modified file 'ubuntu_sso/utils/__init__.py'
--- ubuntu_sso/utils/__init__.py 2011-10-06 19:38:19 +0000
+++ ubuntu_sso/utils/__init__.py 2011-12-19 19:46:24 +0000
@@ -45,7 +45,7 @@
4545
46 CHECKING_INTERVAL = 60 * 60 # in seconds46 CHECKING_INTERVAL = 60 * 60 # in seconds
47 ERROR_INTERVAL = 30 # in seconds47 ERROR_INTERVAL = 30 # in seconds
48 SERVER_URL = "http://one.ubuntu.com/"48 SERVER_URL = "http://one.ubuntu.com/api/time"
4949
50 def __init__(self):50 def __init__(self):
51 """Initialize this instance."""51 """Initialize this instance."""
5252
=== modified file 'ubuntu_sso/utils/tests/test_tcpactivation.py'
--- ubuntu_sso/utils/tests/test_tcpactivation.py 2011-10-28 10:41:18 +0000
+++ ubuntu_sso/utils/tests/test_tcpactivation.py 2011-12-19 19:46:24 +0000
@@ -51,12 +51,25 @@
51 """Echo the data received."""51 """Echo the data received."""
52 self.transport.write(data)52 self.transport.write(data)
5353
54 #pylint:disable=E1101
55 def connectionLost(self, *a):
56 self.factory.onConnectionLost.callback(self)
57 #pylint:enable=E1101
58
5459
55class FakeServerFactory(protocol.Factory):60class FakeServerFactory(protocol.Factory):
56 """A factory for the test server."""61 """A factory for the test server."""
5762
58 protocol = FakeServerProtocol63 protocol = FakeServerProtocol
5964
65 #pylint:disable=W0201
66 def __init__(self, testcase=None):
67 """Create a new instance for the testcase."""
68 if testcase is not None:
69 self.onConnectionLost = defer.Deferred()
70 testcase.addCleanup(lambda: self.onConnectionLost)
71 #pylint:enable=W0201
72
6073
61class FakeTransport(object):74class FakeTransport(object):
62 """A fake transport."""75 """A fake transport."""
@@ -197,7 +210,7 @@
197 @defer.inlineCallbacks210 @defer.inlineCallbacks
198 def test_is_already_running(self):211 def test_is_already_running(self):
199 """The is_already_running method returns True if already started."""212 """The is_already_running method returns True if already started."""
200 f = FakeServerFactory()213 f = FakeServerFactory(self)
201 # pylint: disable=E1101214 # pylint: disable=E1101
202 listener = reactor.listenTCP(SAMPLE_PORT, f,215 listener = reactor.listenTCP(SAMPLE_PORT, f,
203 interface=tcpactivation.LOCALHOST)216 interface=tcpactivation.LOCALHOST)
@@ -319,18 +332,20 @@
319 port = yield ai.get_port()332 port = yield ai.get_port()
320 self.assertEqual(port, SAMPLE_PORT)333 self.assertEqual(port, SAMPLE_PORT)
321334
335 #pylint:disable=W0201
322 @defer.inlineCallbacks336 @defer.inlineCallbacks
323 def test_get_port_fails_if_service_already_started(self):337 def test_get_port_fails_if_service_already_started(self):
324 """The get_port method fails if service already started."""338 """The get_port method fails if service already started."""
325 ai1 = ActivationInstance(self.config)339 ai1 = ActivationInstance(self.config)
326 port1 = yield ai1.get_port()340 port1 = yield ai1.get_port()
327 f = FakeServerFactory()341 f = FakeServerFactory(self)
328 # pylint: disable=E1101342 # pylint: disable=E1101
329 listener = reactor.listenTCP(port1, f,343 listener = reactor.listenTCP(port1, f,
330 interface=tcpactivation.LOCALHOST)344 interface=tcpactivation.LOCALHOST)
331 self.addCleanup(listener.stopListening)345 self.addCleanup(listener.stopListening)
332 ai2 = ActivationInstance(self.config)346 ai2 = ActivationInstance(self.config)
333 yield self.assertFailure(ai2.get_port(), AlreadyStartedError)347 yield self.assertFailure(ai2.get_port(), AlreadyStartedError)
348 #pylint:enable=W0201
334349
335350
336def server_test(config):351def server_test(config):
337352
=== modified file 'ubuntu_sso/utils/tests/test_txsecrets.py'
--- ubuntu_sso/utils/tests/test_txsecrets.py 2011-10-28 10:41:18 +0000
+++ ubuntu_sso/utils/tests/test_txsecrets.py 2011-12-19 19:46:24 +0000
@@ -22,7 +22,7 @@
22import dbus.service22import dbus.service
2323
24from twisted.internet.defer import inlineCallbacks, returnValue24from twisted.internet.defer import inlineCallbacks, returnValue
25from ubuntuone.devtools.testcase import DBusTestCase25from ubuntuone.devtools.testcases.dbus import DBusTestCase
2626
27from ubuntu_sso.utils import txsecrets27from ubuntu_sso.utils import txsecrets
2828
2929
=== modified file 'ubuntu_sso/utils/ui.py'
--- ubuntu_sso/utils/ui.py 2011-10-20 14:15:46 +0000
+++ ubuntu_sso/utils/ui.py 2011-12-19 19:46:24 +0000
@@ -88,8 +88,8 @@
88SUCCESS = _('You are now logged into %(app_name)s.')88SUCCESS = _('You are now logged into %(app_name)s.')
89SURNAME_ENTRY = _('Surname')89SURNAME_ENTRY = _('Surname')
90TC_BUTTON = _('Show Terms & Conditions')90TC_BUTTON = _('Show Terms & Conditions')
91TC_NOT_ACCEPTED = _('Agreeing to the Ubuntu One Terms & Conditions is ' \91TC_NOT_ACCEPTED = _('Agreeing to the %(app_name)s Terms & Conditions is ' \
92 'required to subscribe.')92 'required to subscribe.')
93TOS_LABEL = _("You can also find these terms at <a href='%(url)s'>%(url)s</a>")93TOS_LABEL = _("You can also find these terms at <a href='%(url)s'>%(url)s</a>")
94TRY_AGAIN_BUTTON = _('Try again')94TRY_AGAIN_BUTTON = _('Try again')
95UNKNOWN_ERROR = _('There was an error when trying to complete the ' \95UNKNOWN_ERROR = _('There was an error when trying to complete the ' \
9696
=== added directory 'ubuntu_sso/utils/webclient'
=== added file 'ubuntu_sso/utils/webclient/__init__.py'
--- ubuntu_sso/utils/webclient/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntu_sso/utils/webclient/__init__.py 2011-12-19 19:46:24 +0000
@@ -0,0 +1,48 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16"""A common webclient that can use a QtNetwork or libsoup backend."""
17
18import sys
19
20# pylint: disable=W0611
21from ubuntu_sso.utils.webclient.common import (
22 UnauthorizedError,
23 WebClientError,
24)
25
26
27def is_qt4reactor_installed():
28 """Check if the qt4reactor is installed."""
29 reactor = sys.modules.get("twisted.internet.reactor")
30 return reactor and getattr(reactor, "qApp", None)
31
32
33def webclient_module():
34 """Choose the module of the web client."""
35 if is_qt4reactor_installed():
36 from ubuntu_sso.utils.webclient import qtnetwork as web_module
37 else:
38 # the libsoup backend depends on the twisted + GI bug
39 # meanwhile, use the txweb sample client
40 #from ubuntu_sso.utils.webclient import libsoup as web_module
41 from ubuntu_sso.utils.webclient import txweb as web_module
42 return web_module
43
44
45def webclient_factory(*args, **kwargs):
46 """Choose the type of the web client dynamically."""
47 web_module = webclient_module()
48 return web_module.WebClient(*args, **kwargs)
049
=== added file 'ubuntu_sso/utils/webclient/common.py'
--- ubuntu_sso/utils/webclient/common.py 1970-01-01 00:00:00 +0000
+++ ubuntu_sso/utils/webclient/common.py 2011-12-19 19:46:24 +0000
@@ -0,0 +1,81 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16"""The common bits of a webclient."""
17
18import time
19
20from oauth import oauth
21from twisted.internet import defer
22
23
24class WebClientError(Exception):
25 """An http error happened while calling the webservice."""
26
27
28class UnauthorizedError(WebClientError):
29 """The request ended with bad_request, unauthorized or forbidden."""
30
31
32class Response(object):
33 """A reponse object."""
34
35 def __init__(self, content, headers=None):
36 """Initialize this instance."""
37 self.content = content
38 self.headers = headers
39
40
41class BaseWebClient(object):
42 """The webclient base class, to be extended by backends."""
43
44 def __init__(self, username=None, password=None):
45 """Initialize this instance."""
46 self.username = username
47 self.password = password
48
49 def request(self, url, method="GET", extra_headers=None,
50 oauth_credentials=None):
51 """Return a deferred that will be fired with a Response object."""
52 raise NotImplementedError
53
54 def get_timestamp(self):
55 """Get a timestamp synchronized with the server."""
56 # pylint: disable=W0511
57 # TODO: get the synchronized timestamp
58 return defer.succeed(time.time())
59
60 @staticmethod
61 def build_oauth_headers(method, url, credentials, timestamp):
62 """Build an oauth request given some credentials."""
63 consumer = oauth.OAuthConsumer(credentials["consumer_key"],
64 credentials["consumer_secret"])
65 token = oauth.OAuthToken(credentials["token"],
66 credentials["token_secret"])
67 parameters = {}
68 if timestamp:
69 parameters["oauth_timestamp"] = timestamp
70 request = oauth.OAuthRequest.from_consumer_and_token(
71 http_url=url,
72 http_method=method,
73 parameters=parameters,
74 oauth_consumer=consumer,
75 token=token)
76 sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
77 request.sign_request(sig_method, consumer, token)
78 return request.to_header()
79
80 def shutdown(self):
81 """Shut down all pending requests (if possible)."""
082
=== added file 'ubuntu_sso/utils/webclient/libsoup.py'
--- ubuntu_sso/utils/webclient/libsoup.py 1970-01-01 00:00:00 +0000
+++ ubuntu_sso/utils/webclient/libsoup.py 2011-12-19 19:46:24 +0000
@@ -0,0 +1,87 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16"""A webclient backend that uses libsoup."""
17
18import httplib
19
20from twisted.internet import defer
21
22from ubuntu_sso.utils.webclient.common import (
23 BaseWebClient,
24 Response,
25 UnauthorizedError,
26 WebClientError,
27)
28
29
30class WebClient(BaseWebClient):
31 """A webclient with a libsoup backend."""
32
33 def __init__(self, *args, **kwargs):
34 """Initialize this instance."""
35 super(WebClient, self).__init__(*args, **kwargs)
36 # pylint: disable=E0611
37 from gi.repository import Soup, SoupGNOME
38 self.soup = Soup
39 self.session = Soup.SessionAsync()
40 self.session.add_feature_by_type(SoupGNOME.ProxyResolverGNOME)
41 self.session.connect("authenticate", self._on_authenticate)
42
43 def _on_message(self, session, message, d):
44 """Handle the result of an http message."""
45 if message.status_code == httplib.OK:
46 response = Response(message.response_body.data)
47 d.callback(response)
48 elif message.status_code == httplib.UNAUTHORIZED:
49 e = UnauthorizedError(message.reason_phrase)
50 d.errback(e)
51 else:
52 e = WebClientError(message.reason_phrase)
53 d.errback(e)
54
55 def _on_authenticate(self, sesion, message, auth, retrying, data=None):
56 """Handle the "authenticate" signal."""
57 if not retrying and self.username and self.password:
58 auth.authenticate(self.username, self.password)
59
60 @defer.inlineCallbacks
61 def request(self, url, method="GET", extra_headers=None,
62 oauth_credentials=None):
63 """Return a deferred that will be fired with a Response object."""
64 if extra_headers:
65 headers = dict(extra_headers)
66 else:
67 headers = {}
68
69 if oauth_credentials:
70 timestamp = yield self.get_timestamp()
71 oauth_headers = self.build_oauth_headers(method, url,
72 oauth_credentials, timestamp)
73 headers.update(oauth_headers)
74
75 d = defer.Deferred()
76 message = self.soup.Message.new(method, url)
77
78 for key, value in headers.iteritems():
79 message.request_headers.append(key, value)
80
81 self.session.queue_message(message, self._on_message, d)
82 response = yield d
83 defer.returnValue(response)
84
85 def shutdown(self):
86 """End the soup session for this webclient."""
87 self.session.abort()
088
=== added file 'ubuntu_sso/utils/webclient/qtnetwork.py'
--- ubuntu_sso/utils/webclient/qtnetwork.py 1970-01-01 00:00:00 +0000
+++ ubuntu_sso/utils/webclient/qtnetwork.py 2011-12-19 19:46:24 +0000
@@ -0,0 +1,101 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16"""A webclient backend that uses QtNetwork."""
17
18from PyQt4.QtCore import (
19 QCoreApplication,
20 QUrl,
21)
22from PyQt4.QtNetwork import (
23 QNetworkAccessManager,
24 QNetworkReply,
25 QNetworkRequest,
26)
27from twisted.internet import defer
28
29from ubuntu_sso.utils.webclient.common import (
30 BaseWebClient,
31 Response,
32 UnauthorizedError,
33 WebClientError,
34)
35
36
37class WebClient(BaseWebClient):
38 """A webclient with a qtnetwork backend."""
39
40 def __init__(self, *args, **kwargs):
41 """Initialize this instance."""
42 super(WebClient, self).__init__(*args, **kwargs)
43 self.nam = QNetworkAccessManager(QCoreApplication.instance())
44 self.nam.finished.connect(self._handle_finished)
45 self.nam.authenticationRequired.connect(self._handle_authentication)
46 self.replies = {}
47
48 @defer.inlineCallbacks
49 def request(self, url, method="GET", extra_headers=None,
50 oauth_credentials=None):
51 """Return a deferred that will be fired with a Response object."""
52 request = QNetworkRequest(QUrl(url))
53
54 if extra_headers:
55 headers = dict(extra_headers)
56 else:
57 headers = {}
58
59 if oauth_credentials:
60 timestamp = yield self.get_timestamp()
61 oauth_headers = self.build_oauth_headers(method, url,
62 oauth_credentials, timestamp)
63 headers.update(oauth_headers)
64
65 for key, value in headers.iteritems():
66 request.setRawHeader(key, value)
67
68 d = defer.Deferred()
69 if method == "GET":
70 reply = self.nam.get(request)
71 elif method == "HEAD":
72 reply = self.nam.head(request)
73 else:
74 reply = self.nam.sendCustomRequest(request, method)
75 self.replies[reply] = d
76 result = yield d
77 defer.returnValue(result)
78
79 def _handle_authentication(self, reply, authenticator):
80 """The reply needs authentication."""
81 authenticator.setUser(self.username)
82 authenticator.setPassword(self.password)
83
84 def _handle_finished(self, reply):
85 """The reply has finished processing."""
86 assert reply in self.replies
87 d = self.replies.pop(reply)
88 error = reply.error()
89 if not error:
90 response = Response(reply.readAll())
91 d.callback(response)
92 else:
93 if error == QNetworkReply.AuthenticationRequiredError:
94 exception = UnauthorizedError(reply.errorString())
95 else:
96 exception = WebClientError(reply.errorString())
97 d.errback(exception)
98
99 def shutdown(self):
100 """Shut down all pending requests (if possible)."""
101 self.nam.deleteLater()
0102
=== added directory 'ubuntu_sso/utils/webclient/tests'
=== added file 'ubuntu_sso/utils/webclient/tests/__init__.py'
--- ubuntu_sso/utils/webclient/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntu_sso/utils/webclient/tests/__init__.py 2011-12-19 19:46:24 +0000
@@ -0,0 +1,17 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Tests for the proxy-aware webclient."""
018
=== added file 'ubuntu_sso/utils/webclient/tests/test_webclient.py'
--- ubuntu_sso/utils/webclient/tests/test_webclient.py 1970-01-01 00:00:00 +0000
+++ ubuntu_sso/utils/webclient/tests/test_webclient.py 2011-12-19 19:46:24 +0000
@@ -0,0 +1,303 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT AN WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16"""Integration tests for the proxy-enabled webclient."""
17
18import os
19import sys
20import urllib
21
22from twisted.application import internet, service
23from twisted.cred import checkers, portal
24from twisted.internet import defer
25from twisted.web import http, resource, server, guard
26
27from ubuntuone.devtools.testcases import TestCase
28
29from ubuntu_sso.utils import webclient
30
31ANY_VALUE = object()
32SAMPLE_KEY = "result"
33SAMPLE_VALUE = "sample result"
34SAMPLE_RESOURCE = '{"%s": "%s"}' % (SAMPLE_KEY, SAMPLE_VALUE)
35SAMPLE_USERNAME = "peddro"
36SAMPLE_PASSWORD = "cantropus"
37SAMPLE_CREDENTIALS = dict(
38 consumer_key="consumer key",
39 consumer_secret="consumer secret",
40 token="the real token",
41 token_secret="the token secret",
42)
43SAMPLE_HEADERS = {SAMPLE_KEY: SAMPLE_VALUE}
44
45SIMPLERESOURCE = "simpleresource"
46THROWERROR = "throwerror"
47UNAUTHORIZED = "unauthorized"
48HEADONLY = "headonly"
49VERIFYHEADERS = "verifyheaders"
50GUARDED = "guarded"
51OAUTHRESOURCE = "oauthresource"
52
53
54def sample_get_credentials():
55 """Will return the sample credentials right now."""
56 return defer.succeed(SAMPLE_CREDENTIALS)
57
58
59# pylint: disable=C0103
60# t.w.resource methods have freeform cased names
61
62class SimpleResource(resource.Resource):
63 """A simple web resource."""
64
65 def render_GET(self, request):
66 """Make a bit of html out of these resource's content."""
67 return SAMPLE_RESOURCE
68
69
70class HeadOnlyResource(resource.Resource):
71 """A resource that fails if called with a method other than HEAD."""
72
73 def render_HEAD(self, request):
74 """Return a bit of html."""
75 return "OK"
76
77
78class VerifyHeadersResource(resource.Resource):
79 """A resource that verifies the headers received."""
80
81 def render_GET(self, request):
82 """Make a bit of html out of these resource's content."""
83 headers = request.requestHeaders.getRawHeaders(SAMPLE_KEY)
84 if headers != [SAMPLE_VALUE]:
85 request.setResponseCode(http.BAD_REQUEST)
86 return "ERROR: Expected header not present."
87 return SAMPLE_RESOURCE
88
89
90class SimpleRealm(object):
91 """The same simple resource for all users."""
92
93 def requestAvatar(self, avatarId, mind, *interfaces):
94 """The avatar for this user."""
95 if resource.IResource in interfaces:
96 return (resource.IResource, SimpleResource(), lambda: None)
97 raise NotImplementedError()
98
99
100class OAuthCheckerResource(resource.Resource):
101 """A resource that verifies the request was oauth signed."""
102
103 def render_GET(self, request):
104 """Make a bit of html out of these resource's content."""
105 header = request.requestHeaders.getRawHeaders("Authorization")[0]
106 if header.startswith("OAuth "):
107 return SAMPLE_RESOURCE
108 request.setResponseCode(http.BAD_REQUEST)
109 return "ERROR: Expected OAuth header not present."
110
111
112class MockWebServer(object):
113 """A mock webserver for testing"""
114
115 def __init__(self):
116 """Start up this instance."""
117 root = resource.Resource()
118 root.putChild(SIMPLERESOURCE, SimpleResource())
119
120 root.putChild(THROWERROR, resource.NoResource())
121
122 unauthorized_resource = resource.ErrorPage(resource.http.UNAUTHORIZED,
123 "Unauthorized", "Unauthorized")
124 root.putChild(UNAUTHORIZED, unauthorized_resource)
125 root.putChild(HEADONLY, HeadOnlyResource())
126 root.putChild(VERIFYHEADERS, VerifyHeadersResource())
127 root.putChild(OAUTHRESOURCE, OAuthCheckerResource())
128
129 db = checkers.InMemoryUsernamePasswordDatabaseDontUse()
130 db.addUser(SAMPLE_USERNAME, SAMPLE_PASSWORD)
131 test_portal = portal.Portal(SimpleRealm(), [db])
132 cred_factory = guard.BasicCredentialFactory("example.org")
133 guarded_resource = guard.HTTPAuthSessionWrapper(test_portal,
134 [cred_factory])
135 root.putChild(GUARDED, guarded_resource)
136
137 site = server.Site(root)
138 application = service.Application('web')
139 self.service_collection = service.IServiceCollection(application)
140 #pylint: disable=E1101
141 self.tcpserver = internet.TCPServer(0, site)
142 self.tcpserver.setServiceParent(self.service_collection)
143 self.service_collection.startService()
144
145 def get_url(self):
146 """Build the url for this mock server."""
147 #pylint: disable=W0212
148 port_num = self.tcpserver._port.getHost().port
149 return "http://localhost:%d/" % port_num
150
151 def stop(self):
152 """Shut it down."""
153 #pylint: disable=E1101
154 return self.service_collection.stopService()
155
156
157class FakeReactor(object):
158 """A fake reactor object."""
159 qApp = "Sample qapp"
160
161
162class ModuleSelectionTestCase(TestCase):
163 """Test the functions to choose the qtnet or libsoup backend."""
164
165 def test_is_qt4reactor_installed_not_installed(self):
166 """When the qt4reactor is not installed, it returns false."""
167 self.patch(sys, "modules", {})
168 self.assertFalse(webclient.is_qt4reactor_installed())
169
170 def test_is_qt4reactor_installed_installed(self):
171 """When the qt4reactor is installed, it returns true."""
172 fake_sysmodules = {"twisted.internet.reactor": FakeReactor()}
173 self.patch(sys, "modules", fake_sysmodules)
174 self.assertTrue(webclient.is_qt4reactor_installed())
175
176 def assert_module_name(self, module, expected_name):
177 """Check the name of a given module."""
178 module_filename = os.path.basename(module.__file__)
179 module_name = os.path.splitext(module_filename)[0]
180 self.assertEqual(module_name, expected_name)
181
182 def test_webclient_module_qtnetwork(self):
183 """Test the module name for the qtnetwork case."""
184 self.patch(webclient, "is_qt4reactor_installed", lambda: True)
185 module = webclient.webclient_module()
186 self.assert_module_name(module, "qtnetwork")
187
188 def test_webclient_module_libsoup(self):
189 """Test the module name for the libsoup case."""
190 self.patch(webclient, "is_qt4reactor_installed", lambda: False)
191 module = webclient.webclient_module()
192 # pylint: disable=W0511
193 # TODO: the libsoup backend depends on the twisted + GI bug
194 # meanwhile, use the test txweb client
195 #self.assert_module_name(module, "libsoup")
196 self.assert_module_name(module, "txweb")
197
198
199class WebClientTestCase(TestCase):
200 """Test for the webclient."""
201
202 timeout = 8
203
204 @defer.inlineCallbacks
205 def setUp(self):
206 yield super(WebClientTestCase, self).setUp()
207 self.ws = MockWebServer()
208 self.addCleanup(self.ws.stop)
209 self.base_url = self.ws.get_url()
210 self.wc = webclient.webclient_factory()
211 self.addCleanup(self.wc.shutdown)
212 # pylint: disable=W0511
213 # TODO: skewed timestamp correction
214
215 @defer.inlineCallbacks
216 def test_get_url(self):
217 """A url is successfully retrieved from the mock webserver."""
218 result = yield self.wc.request(self.base_url + SIMPLERESOURCE)
219 self.assertEqual(SAMPLE_RESOURCE, result.content)
220
221 @defer.inlineCallbacks
222 def test_get_url_error(self):
223 """The errback is called when there's some error."""
224 yield self.assertFailure(self.wc.request(self.base_url + THROWERROR),
225 webclient.WebClientError)
226
227 @defer.inlineCallbacks
228 def test_unauthorized(self):
229 """Detect when a request failed with the UNAUTHORIZED http code."""
230 yield self.assertFailure(self.wc.request(self.base_url + UNAUTHORIZED),
231 webclient.UnauthorizedError)
232
233 @defer.inlineCallbacks
234 def test_method_head(self):
235 """The HTTP method is used."""
236 result = yield self.wc.request(self.base_url + HEADONLY, method="HEAD")
237 self.assertEqual("", result.content)
238
239 @defer.inlineCallbacks
240 def test_send_extra_headers(self):
241 """The extra_headers are sent to the server."""
242 result = yield self.wc.request(self.base_url + VERIFYHEADERS,
243 extra_headers=SAMPLE_HEADERS)
244 self.assertEqual(SAMPLE_RESOURCE, result.content)
245
246 @defer.inlineCallbacks
247 def test_send_basic_auth(self):
248 """The basic authentication headers are sent."""
249 other_wc = webclient.webclient_factory(username=SAMPLE_USERNAME,
250 password=SAMPLE_PASSWORD)
251 self.addCleanup(other_wc.shutdown)
252 result = yield other_wc.request(self.base_url + GUARDED)
253 self.assertEqual(SAMPLE_RESOURCE, result.content)
254
255 @defer.inlineCallbacks
256 def test_request_is_oauth_signed(self):
257 """The request is oauth signed."""
258 result = yield self.wc.request(self.base_url + OAUTHRESOURCE,
259 oauth_credentials=SAMPLE_CREDENTIALS)
260 self.assertEqual(SAMPLE_RESOURCE, result.content)
261
262
263class OAuthTestCase(TestCase):
264 """Test for the oauth signing code."""
265
266 def parse_oauth_header(self, header):
267 """Parse an oauth header into a tuple of (method, params_dict)."""
268 params = {}
269
270 method, params_string = header.split(" ", 1)
271 for p in params_string.split(","):
272 k, v = p.strip().split("=")
273 params[k] = urllib.unquote(v[1:-1])
274
275 return method, params
276
277 def test_build_oauth_headers(self):
278 """Build the oauth headers for a sample request."""
279
280 sample_method = "GET"
281 sample_url = "http://one.ubuntu.com/"
282 timestamp = 1
283 expected_params = {
284 "oauth_timestamp": str(timestamp),
285 "oauth_consumer_key": SAMPLE_CREDENTIALS["consumer_key"],
286 "oauth_signature_method": "HMAC-SHA1",
287 "oauth_token": SAMPLE_CREDENTIALS["token"],
288 "oauth_nonce": ANY_VALUE,
289 "oauth_signature": ANY_VALUE,
290 }
291
292 module = webclient.webclient_module()
293 headers = module.BaseWebClient.build_oauth_headers(sample_method,
294 sample_url, SAMPLE_CREDENTIALS, timestamp)
295
296 method, params = self.parse_oauth_header(headers["Authorization"])
297
298 self.assertEqual(method, "OAuth")
299
300 for k, expected_value in expected_params.iteritems():
301 self.assertIn(k, params)
302 if expected_value is not ANY_VALUE:
303 self.assertEqual(params[k], expected_value)
0304
=== added file 'ubuntu_sso/utils/webclient/txweb.py'
--- ubuntu_sso/utils/webclient/txweb.py 1970-01-01 00:00:00 +0000
+++ ubuntu_sso/utils/webclient/txweb.py 2011-12-19 19:46:24 +0000
@@ -0,0 +1,60 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16"""A webclient backend that uses twisted.web.client."""
17
18import base64
19
20from twisted.web import client, error, http
21from twisted.internet import defer
22
23from ubuntu_sso.utils.webclient.common import (
24 BaseWebClient,
25 Response,
26 UnauthorizedError,
27 WebClientError,
28)
29
30
31class WebClient(BaseWebClient):
32 """A simple web client that does not support proxies, yet."""
33
34 @defer.inlineCallbacks
35 def request(self, url, method="GET", extra_headers=None,
36 oauth_credentials=None):
37 """Get the page, or fail trying."""
38 if extra_headers:
39 headers = dict(extra_headers)
40 else:
41 headers = {}
42
43 if oauth_credentials:
44 timestamp = yield self.get_timestamp()
45 oauth_headers = self.build_oauth_headers(method, url,
46 oauth_credentials, timestamp)
47 headers.update(oauth_headers)
48
49 if self.username and self.password:
50 auth = base64.b64encode(self.username + ":" + self.password)
51 headers["Authorization"] = "Basic " + auth
52
53 try:
54 result = yield client.getPage(url, method=method, headers=headers)
55 response = Response(result)
56 defer.returnValue(response)
57 except error.Error as e:
58 if int(e.status) == http.UNAUTHORIZED:
59 raise UnauthorizedError(e.message)
60 raise WebClientError(e.message)

Subscribers

People subscribed via source and target branches