Merge lp:~denys.duchier/bzr/bzr.ssl into lp:~bzr/bzr/trunk-old

Proposed by Denys Duchier
Status: Superseded
Proposed branch: lp:~denys.duchier/bzr/bzr.ssl
Merge into: lp:~bzr/bzr/trunk-old
Diff against target: 460 lines
To merge this branch: bzr merge lp:~denys.duchier/bzr/bzr.ssl
Reviewer Review Type Date Requested Status
Andrew Bennetts Pending
Review via email: mp+10175@code.launchpad.net

This proposal supersedes a proposal from 2009-08-14.

This proposal has been superseded by a proposal from 2009-08-14.

To post a comment you must log in.
Revision history for this message
Denys Duchier (denys.duchier) wrote : Posted in a previous version of this proposal

This branch introduces support for the bzr protocol over SSL.

Server Side
---------------

The smart server can be started with options --keyfile and --certfile to
request that client connections be SSL encrypted:

    bzr serve --keyfile FILE --certfile FILE ...

Client Side
--------------

a bzrs:// url causes the client to open an SSL connection to a bzr smart server.

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

Thanks, that's an interesting feature to add!

Does this do authentication at all, or do you plan to add it?

--
Martin <http://launchpad.net/~mbp/>

Revision history for this message
Denys Duchier (denys.duchier) wrote : Posted in a previous version of this proposal

Martin Pool <email address hidden> writes:

> Does this do authentication at all, or do you plan to add it?

I wanted to use python's built-in SSL support. Verification of
certificates is only supported starting with python 2.6. Thus
certificate-based authentication is very easy to add, but can only be
activated for recent python distros. If there is interest, I can add
support for, it of course.

I am also interested in supporting client authentication and
authorization, but that's a more complex proposition and should be
attempted in a separate branch.

Cheers,

--Denys

Revision history for this message
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal

>>>>> "Denys" == Denys Duchier <email address hidden> writes:

    Denys> Martin Pool <email address hidden> writes:
    >> Does this do authentication at all, or do you plan to add it?

    Denys> I wanted to use python's built-in SSL support.
    Denys> Verification of certificates is only supported
    Denys> starting with python 2.6.

That's the plan for the http client anyway.

    Denys> Thus certificate-based authentication is very easy to
    Denys> add, but can only be activated for recent python
    Denys> distros. If there is interest, I can add support for,
    Denys> it of course.

Preliminary work on the test infrastructure is in
bzrlib/tests/ssl_certs, bzrlib/tests/https_server.py and all.

It would be nice to reuse/stay compatible with that.

It's already possible to create the key and cert files and
version the results instead of embedding opaque versions like you
did in creds.py.

    Denys> I am also interested in supporting client
    Denys> authentication and authorization, but that's a more
    Denys> complex proposition and should be attempted in a
    Denys> separate branch.

Indeed.

Revision history for this message
Denys Duchier (denys.duchier) wrote : Posted in a previous version of this proposal

Vincent Ladeuil <email address hidden> writes:

> Preliminary work on the test infrastructure is in
> bzrlib/tests/ssl_certs, bzrlib/tests/https_server.py and all.
>
> It would be nice to reuse/stay compatible with that.
>
> It's already possible to create the key and cert files and
> version the results instead of embedding opaque versions like you
> did in creds.py.

Ah yes! I had not noticed ssl_certs. This is perfect; I'll use it
instead of my hack. Did you have more than this reuse in mind?

Cheers,

--Denys

Revision history for this message
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal

>>>>> "Denys" == Denys Duchier <email address hidden> writes:

    Denys> Ah yes! I had not noticed ssl_certs. This is
    Denys> perfect; I'll use it instead of my hack. Did you have
    Denys> more than this reuse in mind?

_ssl_wrap_socket in bzrlib/transport/http/_urllib2_wrappers.py
looks very much like your wrap_socket :-)

I'm pretty sure the expand* should go somewhere else, closer to
the CLI UI... i.e. higher in the stack.

Then, since this is now needed in two different places, that may
need to be moved to osutils.py instead.

Other than that, the way you reuse the tests in
blackbox/test_server.py is not exactly what we prefer :)

You want to move these tests in their own class and make them
parameterized instead.

So I'm voting resubmit (the points above are worth doing a new
submission with them fixed but the overall patch sounds good) and
with that sorted out, I'd leave it to Andrew to review the smart
server part of it.

 review: resubmit

 reviewer: spiv

review: Needs Resubmitting
lp:~denys.duchier/bzr/bzr.ssl updated
4610. By Denys Duchier

load_tests to multiply bzr tests bare and over SSL

4611. By Denys Duchier

reinstate useful comments on ssl_wrap_socket.

4612. By Denys Duchier

improved refactoring. added doc comments.

4613. By Denys Duchier

renamed: bare->tcp ssl->ssl/tcp.

4614. By Denys Duchier

prune doc string for RemoteSSLTransport.

4615. By Denys Duchier

document bzr server over SSL.

4616. By Denys Duchier

sanity check for --keyfile and --certfile options

4617. By Denys Duchier

tests for options --keyfile and --certfile

Unmerged revisions

4617. By Denys Duchier

tests for options --keyfile and --certfile

4616. By Denys Duchier

sanity check for --keyfile and --certfile options

4615. By Denys Duchier

document bzr server over SSL.

4614. By Denys Duchier

prune doc string for RemoteSSLTransport.

4613. By Denys Duchier

renamed: bare->tcp ssl->ssl/tcp.

4612. By Denys Duchier

improved refactoring. added doc comments.

4611. By Denys Duchier

reinstate useful comments on ssl_wrap_socket.

4610. By Denys Duchier

load_tests to multiply bzr tests bare and over SSL

4609. By Denys Duchier

cleaned up ssl tests

4608. By Denys Duchier

moved ssl_wrap_socket to osutils as suggested by Vincent Ladeuil

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/builtins.py'
2--- bzrlib/builtins.py 2009-08-10 08:25:53 +0000
3+++ bzrlib/builtins.py 2009-08-14 20:35:20 +0000
4@@ -4701,6 +4701,12 @@
5 '--allow-writes enables write access to the contents of '
6 'the served directory and below.'
7 ),
8+ Option('keyfile',
9+ help="Path to server's private key (for SSL).",
10+ type=str),
11+ Option('certfile',
12+ help="Path to server's certificate (for SSL).",
13+ type=str),
14 ]
15
16 def get_host_and_port(self, port):
17@@ -4723,10 +4729,14 @@
18 return host, port
19
20 def run(self, port=None, inet=False, directory=None, allow_writes=False,
21- protocol=None):
22+ protocol=None, keyfile=None, certfile=None):
23 from bzrlib.transport import get_transport, transport_server_registry
24 if directory is None:
25 directory = os.getcwd()
26+ if keyfile:
27+ keyfile = os.path.expanduser(keyfile)
28+ if certfile:
29+ certfile = os.path.expanduser(certfile)
30 if protocol is None:
31 protocol = transport_server_registry.get()
32 host, port = self.get_host_and_port(port)
33@@ -4734,7 +4744,7 @@
34 if not allow_writes:
35 url = 'readonly+' + url
36 transport = get_transport(url)
37- protocol(transport, host, port, inet)
38+ protocol(transport, host, port, inet, keyfile=keyfile, certfile=certfile)
39
40
41 class cmd_join(Command):
42
43=== modified file 'bzrlib/osutils.py'
44--- bzrlib/osutils.py 2009-07-23 16:01:17 +0000
45+++ bzrlib/osutils.py 2009-08-14 20:35:20 +0000
46@@ -1900,3 +1900,26 @@
47 if use_cache:
48 _cached_concurrency = concurrency
49 return concurrency
50+
51+_ssl_wrap_socket = None
52+
53+def ssl_wrap_socket(sock, keyfile=None, certfile=None, server_side=False):
54+ """Wrap SSL around a socket and return a SSLSocket object.
55+
56+ :param sock: the original socket object.
57+ :param keyfile: path to the server's private key (only for a server).
58+ :param certfile: path to the server's certificate (only for a server).
59+ :param server_side: boolean (default False. Pass True for a server).
60+ """
61+ global _ssl_wrap_socket
62+ if _ssl_wrap_socket is None:
63+ try:
64+ import ssl
65+ _ssl_wrap_socket = ssl.wrap_socket
66+ except ImportError:
67+ from httplib import FakeSocket
68+ def _ssl_wrap_socket(sock, keyfile=None, certfile=None,
69+ server_side=False):
70+ return FakeSocket(sock, socket.ssl(sock, keyfile, certfile))
71+ return _ssl_wrap_socket(sock, keyfile=keyfile, certfile=certfile,
72+ server_side=server_side)
73
74=== modified file 'bzrlib/smart/medium.py'
75--- bzrlib/smart/medium.py 2009-08-07 05:56:29 +0000
76+++ bzrlib/smart/medium.py 2009-08-14 20:35:21 +0000
77@@ -211,6 +211,7 @@
78 # None during interpreter shutdown.
79 from sys import stderr
80 try:
81+ self._before_serve()
82 while not self.finished:
83 server_protocol = self._build_protocol()
84 self._serve_one_request(server_protocol)
85@@ -218,6 +219,11 @@
86 stderr.write("%s terminating on exception %s\n" % (self, e))
87 raise
88
89+ def _before_serve(self):
90+ """Executed before the serve loop. Maybe used to setup e.g. ssl.
91+ """
92+ pass
93+
94 def _build_protocol(self):
95 """Identifies the version of the incoming request, and returns an
96 a protocol object that can interpret it.
97@@ -260,7 +266,8 @@
98
99 class SmartServerSocketStreamMedium(SmartServerStreamMedium):
100
101- def __init__(self, sock, backing_transport, root_client_path='/'):
102+ def __init__(self, sock, backing_transport, root_client_path='/',
103+ keyfile=None, certfile=None):
104 """Constructor.
105
106 :param sock: the socket the server will read from. It will be put
107@@ -269,8 +276,17 @@
108 SmartServerStreamMedium.__init__(
109 self, backing_transport, root_client_path=root_client_path)
110 sock.setblocking(True)
111+ self._keyfile = keyfile
112+ self._certfile = certfile
113 self.socket = sock
114
115+ def _before_serve(self):
116+ """Setup ssl if a certificate was provided."""
117+ if self._certfile:
118+ self.socket = osutils.ssl_wrap_socket(
119+ self.socket, keyfile=self._keyfile, certfile=self._certfile,
120+ server_side=True)
121+
122 def _serve_one_request_unguarded(self, protocol):
123 while protocol.next_read_size():
124 # We can safely try to read large chunks. If there is less data
125@@ -815,13 +831,14 @@
126 class SmartTCPClientMedium(SmartClientStreamMedium):
127 """A client medium using TCP."""
128
129- def __init__(self, host, port, base):
130+ def __init__(self, host, port, base, use_ssl=False):
131 """Creates a client that will connect on the first use."""
132 SmartClientStreamMedium.__init__(self, base)
133 self._connected = False
134 self._host = host
135 self._port = port
136 self._socket = None
137+ self._use_ssl = use_ssl
138
139 def _accept_bytes(self, bytes):
140 """See SmartClientMedium.accept_bytes."""
141@@ -873,6 +890,9 @@
142 err_msg = err.args[1]
143 raise errors.ConnectionError("failed to connect to %s:%d: %s" %
144 (self._host, port, err_msg))
145+ if self._use_ssl:
146+ self._socket = osutils.ssl_wrap_socket(self._socket,
147+ server_side=False)
148 self._connected = True
149
150 def _flush(self):
151
152=== modified file 'bzrlib/smart/server.py'
153--- bzrlib/smart/server.py 2009-07-20 11:27:05 +0000
154+++ bzrlib/smart/server.py 2009-08-14 20:35:21 +0000
155@@ -43,7 +43,7 @@
156 """
157
158 def __init__(self, backing_transport, host='127.0.0.1', port=0,
159- root_client_path='/'):
160+ root_client_path='/', keyfile=None, certfile=None):
161 """Construct a new server.
162
163 To actually start it running, call either start_background_thread or
164@@ -54,6 +54,8 @@
165 :param port: TCP port to listen on, or 0 to allocate a transient port.
166 :param root_client_path: The client path that will correspond to root
167 of backing_transport.
168+ :param keyfile: Path to server's private key (for SSL).
169+ :param certfile: Path to server's certificate (for SSL).
170 """
171 # let connections timeout so that we get a chance to terminate
172 # Keep a reference to the exceptions we want to catch because the socket
173@@ -84,6 +86,8 @@
174 self._started = threading.Event()
175 self._stopped = threading.Event()
176 self.root_client_path = root_client_path
177+ self._keyfile = keyfile
178+ self._certfile = certfile
179
180 def serve(self, thread_name_suffix=''):
181 self._should_terminate = False
182@@ -150,7 +154,10 @@
183
184 def get_url(self):
185 """Return the url of the server"""
186- return "bzr://%s:%d/" % self._sockname
187+ if self._certfile:
188+ return "bzrs://%s:%d/" % self._sockname
189+ else:
190+ return "bzr://%s:%d/" % self._sockname
191
192 def serve_conn(self, conn, thread_name_suffix):
193 # For WIN32, where the timeout value from the listening socket
194@@ -158,7 +165,8 @@
195 conn.setblocking(True)
196 conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
197 handler = medium.SmartServerSocketStreamMedium(
198- conn, self.backing_transport, self.root_client_path)
199+ conn, self.backing_transport, self.root_client_path,
200+ keyfile=self._keyfile, certfile=self._certfile)
201 thread_name = 'smart-server-child' + thread_name_suffix
202 connection_thread = threading.Thread(
203 None, handler.serve, name=thread_name)
204@@ -313,7 +321,8 @@
205 return transport.get_transport(url)
206
207
208-def serve_bzr(transport, host=None, port=None, inet=False):
209+def serve_bzr(transport, host=None, port=None, inet=False,
210+ keyfile=None, certfile=None):
211 from bzrlib import lockdir, ui
212 from bzrlib.transport import get_transport
213 from bzrlib.transport.chroot import ChrootServer
214@@ -328,7 +337,8 @@
215 host = medium.BZR_DEFAULT_INTERFACE
216 if port is None:
217 port = medium.BZR_DEFAULT_PORT
218- smart_server = SmartTCPServer(transport, host=host, port=port)
219+ smart_server = SmartTCPServer(transport, host=host, port=port,
220+ keyfile=keyfile, certfile=certfile)
221 trace.note('listening on port: %s' % smart_server.port)
222 # For the duration of this server, no UI output is permitted. note
223 # that this may cause problems with blackbox tests. This should be
224
225=== modified file 'bzrlib/tests/blackbox/test_serve.py'
226--- bzrlib/tests/blackbox/test_serve.py 2009-07-20 11:27:05 +0000
227+++ bzrlib/tests/blackbox/test_serve.py 2009-08-14 20:35:21 +0000
228@@ -40,7 +40,7 @@
229 from bzrlib.transport import get_transport, remote
230
231
232-class TestBzrServe(TestCaseWithTransport):
233+class TestBzrServeCommon(TestCaseWithTransport):
234
235 def assertInetServerShutsdownCleanly(self, process):
236 """Shutdown the server process looking for errors."""
237@@ -71,6 +71,8 @@
238 finally:
239 branch.unlock()
240
241+class TestBzrServeInet(TestBzrServeCommon):
242+
243 def start_server_inet(self, extra_options=()):
244 """Start a bzr server subprocess using the --inet option.
245
246@@ -90,23 +92,6 @@
247 transport = remote.RemoteTransport(url, medium=client_medium)
248 return process, transport
249
250- def start_server_port(self, extra_options=()):
251- """Start a bzr server subprocess.
252-
253- :param extra_options: extra options to give the server.
254- :return: a tuple with the bzr process handle for passing to
255- finish_bzr_subprocess, and the base url for the server.
256- """
257- # Serve from the current directory
258- args = ['serve', '--port', 'localhost:0']
259- args.extend(extra_options)
260- process = self.start_bzr_subprocess(args, skip_if_plan_to_signal=True)
261- port_line = process.stderr.readline()
262- prefix = 'listening on port: '
263- self.assertStartsWith(port_line, prefix)
264- port = int(port_line[len(prefix):])
265- return process,'bzr://localhost:%d/' % port
266-
267 def test_bzr_serve_inet_readonly(self):
268 """bzr server should provide a read only filesystem by default."""
269 process, transport = self.start_server_inet()
270@@ -124,35 +109,7 @@
271 self.make_read_requests(branch)
272 self.assertInetServerShutsdownCleanly(process)
273
274- def test_bzr_serve_port_readonly(self):
275- """bzr server should provide a read only filesystem by default."""
276- process, url = self.start_server_port()
277- transport = get_transport(url)
278- self.assertRaises(errors.TransportNotPossible, transport.mkdir, 'adir')
279- self.assertServerFinishesCleanly(process)
280-
281- def test_bzr_serve_port_readwrite(self):
282- # Make a branch
283- self.make_branch('.')
284-
285- process, url = self.start_server_port(['--allow-writes'])
286-
287- # Connect to the server
288- branch = Branch.open(url)
289- self.make_read_requests(branch)
290- self.assertServerFinishesCleanly(process)
291-
292- def test_bzr_serve_supports_protocol(self):
293- # Make a branch
294- self.make_branch('.')
295-
296- process, url = self.start_server_port(['--allow-writes',
297- '--protocol=bzr'])
298-
299- # Connect to the server
300- branch = Branch.open(url)
301- self.make_read_requests(branch)
302- self.assertServerFinishesCleanly(process)
303+class TestBzrServeSSH(TestBzrServeCommon):
304
305 def test_bzr_connect_to_bzr_ssh(self):
306 """User acceptance that get_transport of a bzr+ssh:// behaves correctly.
307@@ -244,6 +201,76 @@
308 self.command_executed)
309
310
311+class TestBzrServePort(TestBzrServeCommon):
312+
313+ def start_server_port(self, extra_options=()):
314+ """Start a bzr server subprocess.
315+
316+ :param extra_options: extra options to give the server.
317+ :return: a tuple with the bzr process handle for passing to
318+ finish_bzr_subprocess, and the base url for the server.
319+ """
320+ # Serve from the current directory
321+ args = ['serve', '--port', 'localhost:0']
322+ args.extend(extra_options)
323+ urlfmt = 'bzr://localhost:%d/'
324+ if self.use_ssl:
325+ from bzrlib.tests.ssl_certs import build_path
326+ keyfile = build_path('server_without_pass.key')
327+ certfile = build_path('server.crt')
328+ args.extend(("--keyfile", keyfile,
329+ "--certfile", certfile))
330+ urlfmt = 'bzrs://localhost:%d/'
331+ process = self.start_bzr_subprocess(args, skip_if_plan_to_signal=True)
332+ port_line = process.stderr.readline()
333+ prefix = 'listening on port: '
334+ self.assertStartsWith(port_line, prefix)
335+ port = int(port_line[len(prefix):])
336+ return process, urlfmt % port
337+
338+ def test_bzr_serve_port_readonly(self):
339+ """bzr server should provide a read only filesystem by default."""
340+ process, url = self.start_server_port()
341+ transport = get_transport(url)
342+ self.assertRaises(errors.TransportNotPossible, transport.mkdir, 'adir')
343+ self.assertServerFinishesCleanly(process)
344+
345+ def test_bzr_serve_port_readwrite(self):
346+ # Make a branch
347+ self.make_branch('.')
348+
349+ process, url = self.start_server_port(['--allow-writes'])
350+
351+ # Connect to the server
352+ branch = Branch.open(url)
353+ self.make_read_requests(branch)
354+ self.assertServerFinishesCleanly(process)
355+
356+ def test_bzr_serve_supports_protocol(self):
357+ # Make a branch
358+ self.make_branch('.')
359+
360+ process, url = self.start_server_port(['--allow-writes',
361+ '--protocol=bzr'])
362+
363+ # Connect to the server
364+ branch = Branch.open(url)
365+ self.make_read_requests(branch)
366+ self.assertServerFinishesCleanly(process)
367+
368+def serve_port_scenarios():
369+ return (('bare', dict(use_ssl=False)),
370+ ('ssl' , dict(use_ssl=True)))
371+
372+def load_tests(basic_tests, module, loader):
373+ from bzrlib import tests
374+ suite = loader.suiteClass()
375+ serve_port_tests, remaining_tests = tests.split_suite_by_condition(
376+ basic_tests, tests.condition_isinstance(TestBzrServePort))
377+ tests.multiply_tests(serve_port_tests, serve_port_scenarios(), suite)
378+ suite.addTest(remaining_tests)
379+ return suite
380+
381 class TestCmdServeChrooting(TestCaseWithTransport):
382
383 def test_serve_tcp(self):
384
385=== modified file 'bzrlib/transport/__init__.py'
386--- bzrlib/transport/__init__.py 2009-08-04 11:40:59 +0000
387+++ bzrlib/transport/__init__.py 2009-08-14 20:35:21 +0000
388@@ -1826,9 +1826,14 @@
389 register_transport_proto('bzr://',
390 help="Fast access using the Bazaar smart server.",
391 register_netloc=True)
392+register_transport_proto('bzrs://',
393+ help="Fast access using the Bazaar smart server over SSL.",
394+ register_netloc=True)
395
396 register_lazy_transport('bzr://', 'bzrlib.transport.remote',
397 'RemoteTCPTransport')
398+register_lazy_transport('bzrs://', 'bzrlib.transport.remote',
399+ 'RemoteSSLTransport')
400 register_transport_proto('bzr-v2://', register_netloc=True)
401
402 register_lazy_transport('bzr-v2://', 'bzrlib.transport.remote',
403
404=== modified file 'bzrlib/transport/http/_urllib2_wrappers.py'
405--- bzrlib/transport/http/_urllib2_wrappers.py 2009-05-04 15:21:26 +0000
406+++ bzrlib/transport/http/_urllib2_wrappers.py 2009-08-14 20:35:21 +0000
407@@ -280,19 +280,6 @@
408 self._wrap_socket_for_reporting(self.sock)
409
410
411-# Build the appropriate socket wrapper for ssl
412-try:
413- # python 2.6 introduced a better ssl package
414- import ssl
415- _ssl_wrap_socket = ssl.wrap_socket
416-except ImportError:
417- # python versions prior to 2.6 don't have ssl and ssl.wrap_socket instead
418- # they use httplib.FakeSocket
419- def _ssl_wrap_socket(sock, key_file, cert_file):
420- ssl_sock = socket.ssl(sock, key_file, cert_file)
421- return httplib.FakeSocket(sock, ssl_sock)
422-
423-
424 class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
425
426 def __init__(self, host, port=None, key_file=None, cert_file=None,
427@@ -313,7 +300,8 @@
428 self.connect_to_origin()
429
430 def connect_to_origin(self):
431- ssl_sock = _ssl_wrap_socket(self.sock, self.key_file, self.cert_file)
432+ ssl_sock = osutils.ssl_wrap_socket(
433+ self.sock, self.key_file, self.cert_file)
434 # Wrap the ssl socket before anybody use it
435 self._wrap_socket_for_reporting(ssl_sock)
436
437
438=== modified file 'bzrlib/transport/remote.py'
439--- bzrlib/transport/remote.py 2009-03-24 01:53:42 +0000
440+++ bzrlib/transport/remote.py 2009-08-14 20:35:21 +0000
441@@ -485,6 +485,19 @@
442 return client_medium, None
443
444
445+class RemoteSSLTransport(RemoteTransport):
446+ """Connection to smart server over ssl.
447+
448+ This is essentially just a factory to get 'RemoteTransport(url,
449+ SmartTCPClientMedium).
450+ """
451+
452+ def _build_medium(self):
453+ client_medium = medium.SmartTCPClientMedium(
454+ self._host, self._port, self.base, use_ssl=True)
455+ return client_medium, None
456+
457+
458 class RemoteTCPTransportV2Only(RemoteTransport):
459 """Connection to smart server over plain tcp with the client hard-coded to
460 assume protocol v2 and remote server version <= 1.6.