Merge lp:~alecu/ubuntuone-client/proxy-tunnel-client into lp:ubuntuone-client

Proposed by Alejandro J. Cura
Status: Merged
Approved by: Alejandro J. Cura
Approved revision: 1212
Merged at revision: 1203
Proposed branch: lp:~alecu/ubuntuone-client/proxy-tunnel-client
Merge into: lp:ubuntuone-client
Prerequisite: lp:~alecu/ubuntuone-client/proxy-tunnel-server
Diff against target: 264 lines (+247/-0)
3 files modified
tests/proxy/test_tunnel_client.py (+144/-0)
tests/proxy/test_tunnel_server.py (+2/-0)
ubuntuone/proxy/tunnel_client.py (+101/-0)
To merge this branch: bzr merge lp:~alecu/ubuntuone-client/proxy-tunnel-client
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Manuel de la Peña (community) Approve
Review via email: mp+95077@code.launchpad.net

Commit message

- A client for the proxy tunnel, to be used by syncdaemon (LP: #929207).

Description of the change

- A client for the proxy tunnel, to be used by syncdaemon (LP: #929207)

To post a comment you must log in.
1208. By Alejandro J. Cura

merged with source branch

1209. By Alejandro J. Cura

Merged proxy-tunnel-server into proxy-tunnel-client.

1210. By Alejandro J. Cura

Merged proxy-tunnel-server into proxy-tunnel-client.

1211. By Alejandro J. Cura

Merged proxy-tunnel-server into proxy-tunnel-client.

Revision history for this message
Manuel de la Peña (mandel) wrote :

In TunnelClientProtocolTestCase I see that we do a number of times the following:

75 + other_proto = protocol.Protocol()
76 + other_factory = protocol.ClientFactory()
77 + other_factory.buildProtocol = lambda _addr: other_proto
78 + tunnel_client_factory = tunnel_client.TunnelClientFactory(host, port,
79 + other_factory)
80 + tunnel_client_proto = tunnel_client_factory.buildProtocol(fake_addr)
81 + tunnel_client_proto.transport = FakeTransport()
82 + tunnel_client_proto.connectionMade()

Would't it be a good idea to move that to the setUp?

In TunnelClientTestCase you refactor the tests as follows:

             @defer.inlineCallbacks
             def _assert_connection(self, is_ssl=False):
                 """Asseert the connection."""
                 tunnel_client = TunnelClient("0.0.0.0", self.tunnel_server.port)
                 factory = client.HTTPClientFactory(self.dest_url)
                 scheme, host, port, path = client._parse(self.dest_url)
                 if is_ssl:
                     context_factory = ssl.ClientContextFactory()
                     tunnel_client.connectSSL(host, port, factory, context_factory)
                 else:
                     tunnel_client.connectTCP(host, port, factory)
                 result = yield factory.deferred
                 self.assertEqual(result, SAMPLE_CONTENT)

            def test_connects_right(self):
                """Uses the CONNECT method on the tunnel."""
                self._assert_connection()

            def test_starts_tls_connection(self):
                """TLS is started after connecting; control passed to the client."""
                self._assert_connection(is_ssl=True)

Although if you think it does not bring anything to the code you can leave the tests as they are.

Since all the above comments are just about tests and writing less code I'll approve and will trust you to apply or not the comments if you feel they are needed :)

review: Approve
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

* Can you please use the same encoding definition header from old files in the new file? Also, can you add the encoding header to tests/proxy/test_tunnel_client.py?

The rest looks good!

review: Approve
1212. By Alejandro J. Cura

fixes requested on the reviews

Revision history for this message
Alejandro J. Cura (alecu) wrote :

> In TunnelClientProtocolTestCase I see that we do a number of times the
> following:
>
> 75 + other_proto = protocol.Protocol()
> 76 + other_factory = protocol.ClientFactory()
> 77 + other_factory.buildProtocol = lambda _addr: other_proto
> 78 + tunnel_client_factory =
> tunnel_client.TunnelClientFactory(host, port,
> 79 +
> other_factory)
> 80 + tunnel_client_proto =
> tunnel_client_factory.buildProtocol(fake_addr)
> 81 + tunnel_client_proto.transport = FakeTransport()
> 82 + tunnel_client_proto.connectionMade()
>
> Would't it be a good idea to move that to the setUp?

Good idea, I've moved it to the setUp.

> In TunnelClientTestCase you refactor the tests as follows:
>
> @defer.inlineCallbacks
> def _assert_connection(self, is_ssl=False):
> """Asseert the connection."""
> tunnel_client = TunnelClient("0.0.0.0",
> self.tunnel_server.port)
> factory = client.HTTPClientFactory(self.dest_url)
> scheme, host, port, path = client._parse(self.dest_url)
> if is_ssl:
> context_factory = ssl.ClientContextFactory()
> tunnel_client.connectSSL(host, port, factory,
> context_factory)
> else:
> tunnel_client.connectTCP(host, port, factory)
> result = yield factory.deferred
> self.assertEqual(result, SAMPLE_CONTENT)
>
> def test_connects_right(self):
> """Uses the CONNECT method on the tunnel."""
> self._assert_connection()
>
> def test_starts_tls_connection(self):
> """TLS is started after connecting; control passed to the
> client."""
> self._assert_connection(is_ssl=True)
>
> Although if you think it does not bring anything to the code you can leave the
> tests as they are.

This case is a bit more delicate, because not only the connectXXX method is different and needs to have an if like you've put, but also the url is different too. I think it's not worth it to refactor the test on this particular test.

thanks the review!

Revision history for this message
Alejandro J. Cura (alecu) wrote :

> * Can you please use the same encoding definition header from old files in the
> new file? Also, can you add the encoding header to
> tests/proxy/test_tunnel_client.py?

Fixed, thanks for pointing it out.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'tests/proxy/test_tunnel_client.py'
--- tests/proxy/test_tunnel_client.py 1970-01-01 00:00:00 +0000
+++ tests/proxy/test_tunnel_client.py 2012-03-09 14:41:44 +0000
@@ -0,0 +1,144 @@
1# -*- coding: utf8 -*-
2#
3# Copyright 2012 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"""Tests for the proxy tunnel."""
17
18from twisted.internet import defer, protocol, ssl
19from twisted.trial.unittest import TestCase
20from twisted.web import client
21
22from ubuntuone.devtools.testcases.squid import SquidTestCase
23
24from tests.proxy import (
25 FakeTransport,
26 MockWebServer,
27 SAMPLE_CONTENT,
28 SIMPLERESOURCE,
29)
30from ubuntuone.proxy import tunnel_client
31from ubuntuone.proxy.tunnel_client import CRLF, TunnelClient
32from ubuntuone.proxy.tunnel_server import TunnelServer
33
34
35FAKE_HEADER = (
36 "HTTP/1.0 200 Connected!" + CRLF +
37 "Header1: value1" + CRLF +
38 "Header2: value2" + CRLF +
39 CRLF
40)
41
42
43class SavingProtocol(protocol.Protocol):
44 """A protocol that saves all that it receives."""
45
46 def __init__(self):
47 """Initialize this protocol."""
48 self.saved_data = None
49
50 def connectionMade(self):
51 """The connection was made, start saving."""
52 self.saved_data = []
53
54 def dataReceived(self, data):
55 """Save the data received."""
56 self.saved_data.append(data)
57
58 @property
59 def content(self):
60 """All the content so far."""
61 return "".join(self.saved_data)
62
63
64class TunnelClientProtocolTestCase(TestCase):
65 """Tests for the client side tunnel protocol."""
66
67 timeout = 3
68
69 @defer.inlineCallbacks
70 def setUp(self):
71 """Initialize this testcase."""
72 yield super(TunnelClientProtocolTestCase, self).setUp()
73 self.host, self.port = "9.9.9.9", 8765
74 fake_addr = object()
75 self.other_proto = SavingProtocol()
76 other_factory = protocol.ClientFactory()
77 other_factory.buildProtocol = lambda _addr: self.other_proto
78 tunnel_client_factory = tunnel_client.TunnelClientFactory(self.host,
79 self.port, other_factory)
80 tunnel_client_proto = tunnel_client_factory.buildProtocol(fake_addr)
81 tunnel_client_proto.transport = FakeTransport()
82 tunnel_client_proto.connectionMade()
83 self.tunnel_client_proto = tunnel_client_proto
84
85 def test_sends_connect_request(self):
86 """Sends the expected CONNECT request."""
87 expected = tunnel_client.METHOD_LINE.format(self.host, self.port)
88 written = self.tunnel_client_proto.transport.getvalue()
89 first_line = written.split(CRLF)[0]
90 self.assertEqual(first_line + CRLF, expected)
91 self.assertTrue(written.endswith(CRLF * 2),
92 "Ends with a double CRLF")
93
94 def test_handles_successful_connection(self):
95 """A successful connection is handled."""
96 self.tunnel_client_proto.dataReceived(FAKE_HEADER)
97 self.assertEqual(self.tunnel_client_proto.status_code, "200")
98
99 def test_protocol_is_switched(self):
100 """The protocol is switched after the headers are received."""
101 expected = (SAMPLE_CONTENT + CRLF) * 2
102 self.tunnel_client_proto.dataReceived(FAKE_HEADER + SAMPLE_CONTENT)
103 self.other_proto.dataReceived(CRLF + SAMPLE_CONTENT + CRLF)
104 self.assertEqual(self.other_proto.content, expected)
105
106
107class TunnelClientTestCase(SquidTestCase):
108 """Test the client for the tunnel."""
109
110 timeout = 3
111
112 @defer.inlineCallbacks
113 def setUp(self):
114 """Initialize this testcase."""
115 yield super(TunnelClientTestCase, self).setUp()
116 self.ws = MockWebServer()
117 self.addCleanup(self.ws.stop)
118 self.dest_url = self.ws.get_iri().encode("utf-8") + SIMPLERESOURCE
119 self.dest_ssl_url = (self.ws.get_ssl_iri().encode("utf-8") +
120 SIMPLERESOURCE)
121 self.tunnel_server = TunnelServer()
122 self.addCleanup(self.tunnel_server.shutdown)
123
124 @defer.inlineCallbacks
125 def test_connects_right(self):
126 """Uses the CONNECT method on the tunnel."""
127 tunnel_client = TunnelClient("0.0.0.0", self.tunnel_server.port)
128 factory = client.HTTPClientFactory(self.dest_url)
129 scheme, host, port, path = client._parse(self.dest_url)
130 tunnel_client.connectTCP(host, port, factory)
131 result = yield factory.deferred
132 self.assertEqual(result, SAMPLE_CONTENT)
133
134 @defer.inlineCallbacks
135 def test_starts_tls_connection(self):
136 """TLS is started after connecting; control passed to the client."""
137 tunnel_client = TunnelClient("0.0.0.0",
138 self.tunnel_server.port)
139 factory = client.HTTPClientFactory(self.dest_ssl_url)
140 scheme, host, port, path = client._parse(self.dest_ssl_url)
141 context_factory = ssl.ClientContextFactory()
142 tunnel_client.connectSSL(host, port, factory, context_factory)
143 result = yield factory.deferred
144 self.assertEqual(result, SAMPLE_CONTENT)
0145
=== modified file 'tests/proxy/test_tunnel_server.py'
--- tests/proxy/test_tunnel_server.py 2012-03-01 23:13:50 +0000
+++ tests/proxy/test_tunnel_server.py 2012-03-09 14:41:44 +0000
@@ -1,3 +1,5 @@
1# -*- coding: utf8 -*-
2#
1# Copyright 2012 Canonical Ltd.3# Copyright 2012 Canonical Ltd.
2#4#
3# 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
46
=== added file 'ubuntuone/proxy/tunnel_client.py'
--- ubuntuone/proxy/tunnel_client.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/proxy/tunnel_client.py 2012-03-09 14:41:44 +0000
@@ -0,0 +1,101 @@
1# -*- coding: utf8 -*-
2#
3# Copyright 2012 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"""Client for the tunnel protocol."""
17
18from twisted.internet import protocol
19
20from ubuntuone.proxy.common import BaseTunnelProtocol, CRLF
21
22METHOD_LINE = "CONNECT {0}:{1} HTTP/1.0" + CRLF
23
24
25class TunnelClientProtocol(BaseTunnelProtocol):
26 """Client protocol for the handshake part of the tunnel."""
27
28 def connectionMade(self):
29 """The connection to the tunnel was made so send request."""
30 method_line = METHOD_LINE.format(self.factory.tunnel_host,
31 self.factory.tunnel_port)
32 headers = {
33 "User-Agent": "Ubuntu One tunnel client"
34 }
35 self.transport.write(method_line +
36 self.format_headers(headers) +
37 CRLF)
38
39 def handle_first_line(self, line):
40 """The first line received is the status line."""
41 try:
42 proto_version, self.status_code, description = line.split(" ", 2)
43 except ValueError:
44 self.transport.loseConnection()
45
46 def headers_done(self):
47 """All the headers have arrived. Time to switch protocols."""
48 remaining_data = self.clearLineBuffer()
49 if self.status_code != "200":
50 self.transport.loseConnection()
51 return
52 addr = self.transport.getPeer()
53 other_protocol = self.factory.other_factory.buildProtocol(addr)
54 self.transport.protocol = other_protocol
55 other_protocol.transport = self.transport
56 self.transport = None
57 if self.factory.context_factory:
58 other_protocol.transport.startTLS(self.factory.context_factory)
59 other_protocol.connectionMade()
60 if remaining_data:
61 other_protocol.dataReceived(remaining_data)
62
63
64class TunnelClientFactory(protocol.ClientFactory):
65 """A factory for Tunnel Client Protocols."""
66
67 protocol = TunnelClientProtocol
68
69 def __init__(self, tunnel_host, tunnel_port, other_factory,
70 context_factory=None):
71 """Initialize this factory."""
72 self.tunnel_host = tunnel_host
73 self.tunnel_port = tunnel_port
74 self.other_factory = other_factory
75 self.context_factory = context_factory
76
77
78class TunnelClient(object):
79 """A client for the proxy tunnel."""
80
81 def __init__(self, tunnel_host, tunnel_port):
82 """Initialize this client."""
83 self.tunnel_host = tunnel_host
84 self.tunnel_port = tunnel_port
85 # TODO: use tcp activation to start a tunnel process if not running
86
87 def connectTCP(self, host, port, other_factory, *args, **kwargs):
88 """A connectTCP going thru the tunnel."""
89 factory = TunnelClientFactory(host, port, other_factory)
90 from twisted.internet import reactor
91 return reactor.connectTCP(self.tunnel_host, self.tunnel_port,
92 factory, *args, **kwargs)
93
94 def connectSSL(self, host, port, other_factory,
95 context_factory, *args, **kwargs):
96 """A connectSSL going thru the tunnel."""
97 factory = TunnelClientFactory(host, port, other_factory,
98 context_factory)
99 from twisted.internet import reactor
100 return reactor.connectTCP(self.tunnel_host, self.tunnel_port,
101 factory, *args, **kwargs)

Subscribers

People subscribed via source and target branches