1
# Copyright (C) 2006 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Smart-server protocol, client and server.
19
Requests are sent as a command and list of arguments, followed by optional
20
bulk body data. Responses are similarly a response and list of arguments,
21
followed by bulk body data. ::
24
Fields are separated by Ctrl-A.
25
BULK_DATA := CHUNK+ TRAILER
26
Chunks can be repeated as many times as necessary.
27
CHUNK := CHUNK_LEN CHUNK_BODY
28
CHUNK_LEN := DIGIT+ NEWLINE
29
Gives the number of bytes in the following chunk.
30
CHUNK_BODY := BYTE[chunk_len]
31
TRAILER := SUCCESS_TRAILER | ERROR_TRAILER
32
SUCCESS_TRAILER := 'done' NEWLINE
35
Paths are passed across the network. The client needs to see a namespace that
36
includes any repository that might need to be referenced, and the client needs
37
to know about a root directory beyond which it cannot ascend.
39
Servers run over ssh will typically want to be able to access any path the user
40
can access. Public servers on the other hand (which might be over http, ssh
41
or tcp) will typically want to restrict access to only a particular directory
42
and its children, so will want to do a software virtual root at that level.
43
In other words they'll want to rewrite incoming paths to be under that level
44
(and prevent escaping using ../ tricks.)
46
URLs that include ~ should probably be passed across to the server verbatim
47
and the server can expand them. This will proably not be meaningful when
48
limited to a directory?
53
# TODO: A plain integer from query_version is too simple; should give some
56
# TODO: Server should probably catch exceptions within itself and send them
57
# back across the network. (But shouldn't catch KeyboardInterrupt etc)
58
# Also needs to somehow report protocol errors like bad requests. Need to
59
# consider how we'll handle error reporting, e.g. if we get halfway through a
60
# bulk transfer and then something goes wrong.
62
# TODO: Standard marker at start of request/response lines?
64
# TODO: Client and server warnings perhaps should contain some non-ascii bytes
65
# to make sure the channel can carry them without trouble? Test for this?
67
# TODO: get/put objects could be changed to gradually read back the data as it
68
# comes across the network
70
# TODO: What should the server do if it hits an error and has to terminate?
72
# TODO: is it useful to allow multiple chunks in the bulk data?
74
# TODO: If we get an exception during transmission of bulk data we can't just
75
# emit the exception because it won't be seen.
77
# TODO: Clone method on Transport; should work up towards parent directory;
78
# unclear how this should be stored or communicated to the server... maybe
79
# just pass it on all relevant requests?
81
# TODO: Better name than clone() for changing between directories. How about
82
# open_dir or change_dir or chdir?
84
# TODO: Is it really good to have the notion of current directory within the
85
# connection? Perhaps all Transports should factor out a common connection
86
# from the thing that has the directory context?
88
# TODO: Pull more things common to sftp and ssh to a higher level.
90
# TODO: The server that manages a connection should be quite small and retain
91
# minimum state because each of the requests are supposed to be stateless.
92
# Then we can write another implementation that maps to http.
94
# TODO: What to do when a client connection is garbage collected? Maybe just
95
# abruptly drop the connection?
97
# TODO: Server in some cases will need to restrict access to files outside of
98
# a particular root directory. LocalTransport doesn't do anything to stop you
99
# ascending above the base directory, so we need to prevent paths
100
# containing '..' in either the server or transport layers. (Also need to
101
# consider what happens if someone creates a symlink pointing outside the
104
# TODO: Server should rebase absolute paths coming across the network to put
105
# them under the virtual root, if one is in use. LocalTransport currently
106
# doesn't do that; if you give it an absolute path it just uses it.
108
# XXX: Arguments can't contain newlines or ascii; possibly we should e.g.
109
# urlescape them instead. Indeed possibly this should just literally be
112
# FIXME: This transport, with several others, has imperfect handling of paths
113
# within urls. It'd probably be better for ".." from a root to raise an error
114
# rather than return the same directory as we do at present.
116
# TODO: Rather than working at the Transport layer we want a Branch,
117
# Repository or BzrDir objects that talk to a server.
119
# TODO: Probably want some way for server commands to gradually produce body
120
# data rather than passing it as a string; they could perhaps pass an
121
# iterator-like callback that will gradually yield data; it probably needs a
122
# close() method that will always be closed to do any necessary cleanup.
124
# TODO: Transport should probably not implicitly connect from its constructor;
125
# it's quite useful to be able to deal with Transports representing locations
126
# without actually opening it.
128
# TODO: Split the actual smart server from the ssh encoding of it.
130
# TODO: Perhaps support file-level readwrite operations over the transport
133
# TODO: SmartBzrDir class, proxying all Branch etc methods across to another
134
# branch doing file-level operations.
137
from cStringIO import StringIO
147
from bzrlib import bzrdir, errors, revision, transport, trace, urlutils
148
from bzrlib.transport import sftp, local
149
from bzrlib.bundle.serializer import write_bundle
150
from bzrlib.trace import mutter
152
# must do this otherwise urllib can't parse the urls properly :(
153
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh']:
154
transport.register_urlparse_netloc_protocol(scheme)
158
class BzrProtocolError(errors.TransportError):
159
"""Generic bzr smart protocol error: %(details)s"""
161
def __init__(self, details):
162
self.details = details
165
def _recv_tuple(from_file):
166
req_line = from_file.readline()
167
return _decode_tuple(req_line)
170
def _decode_tuple(req_line):
171
if req_line == None or req_line == '':
173
if req_line[-1] != '\n':
174
raise BzrProtocolError("request %r not terminated" % req_line)
175
return tuple((a.decode('utf-8') for a in req_line[:-1].split('\1')))
178
def _send_tuple(to_file, args):
179
to_file.write('\1'.join((a.encode('utf-8') for a in args)) + '\n')
183
class SmartProtocolBase(object):
184
"""Methods common to client and server"""
186
def _send_bulk_data(self, body):
187
"""Send chunked body data"""
188
assert isinstance(body, str)
189
self._out.write('%d\n' % len(body))
190
self._out.write(body)
191
self._out.write('done\n')
194
# TODO: this only actually accomodates a single block; possibly should support
196
def _recv_bulk(self):
197
chunk_len = self._in.readline()
199
chunk_len = int(chunk_len)
201
raise BzrProtocolError("bad chunk length line %r" % chunk_len)
202
bulk = self._in.read(chunk_len)
203
if len(bulk) != chunk_len:
204
raise BzrProtocolError("short read fetching bulk data chunk")
208
def _recv_tuple(self):
209
return _recv_tuple(self._in)
211
def _recv_trailer(self):
212
resp = self._recv_tuple()
213
if resp == ('done', ):
216
self._translate_error(resp)
219
class SmartStreamServer(SmartProtocolBase):
220
"""Handles smart commands coming over a stream.
222
The stream may be a pipe connected to sshd, or a tcp socket, or an
223
in-process fifo for testing.
225
One instance is created for each connected client; it can serve multiple
226
requests in the lifetime of the connection.
228
The server passes requests through to an underlying backing transport,
229
which will typically be a LocalTransport looking at the server's filesystem.
232
def __init__(self, in_file, out_file, backing_transport):
233
"""Construct new server.
235
:param in_file: Python file from which requests can be read.
236
:param out_file: Python file to write responses.
237
:param backing_transport: Transport for the directory served.
241
self.smart_server = SmartServer(backing_transport)
242
# server can call back to us to get bulk data - this is not really
243
# ideal, they should get it per request instead
244
self.smart_server._recv_body = self._recv_bulk
246
def _recv_tuple(self):
247
"""Read a request from the client and return as a tuple.
249
Returns None at end of file (if the client closed the connection.)
251
return _recv_tuple(self._in)
253
def _send_tuple(self, args):
254
"""Send response header"""
255
return _send_tuple(self._out, args)
257
def _send_error_and_disconnect(self, exception):
258
self._send_tuple(('error', str(exception)))
263
def _serve_one_request(self):
264
"""Read one request from input, process, send back a response.
266
:return: False if the server should terminate, otherwise None.
268
req_args = self._recv_tuple()
270
# client closed connection
271
return False # shutdown server
273
response = self.smart_server.dispatch_command(req_args[0], req_args[1:])
274
self._send_tuple(response.args)
275
if response.body is not None:
276
self._send_bulk_data(response.body)
277
except KeyboardInterrupt:
280
# everything else: pass to client, flush, and quit
281
self._send_error_and_disconnect(e)
285
"""Serve requests until the client disconnects."""
286
# Keep a reference to stderr because the sys module's globals get set to
287
# None during interpreter shutdown.
288
from sys import stderr
290
while self._serve_one_request() != False:
293
stderr.write("%s terminating on exception %s\n" % (self, e))
297
class SmartServerResponse(object):
298
"""Response generated by SmartServer."""
300
def __init__(self, args, body=None):
305
class SmartServer(object):
306
"""Protocol logic for smart.
308
This doesn't handle serialization at all, it just processes requests and
312
# TODO: Better way of representing the body for commands that take it,
313
# and allow it to be streamed into the server.
315
def __init__(self, backing_transport):
316
self._backing_transport = backing_transport
319
"""Answer a version request with my version."""
320
return SmartServerResponse(('ok', '1'))
322
def do_has(self, relpath):
323
r = self._backing_transport.has(relpath) and 'yes' or 'no'
324
return SmartServerResponse((r,))
326
def do_get(self, relpath):
327
backing_file = self._backing_transport.get(relpath)
328
return SmartServerResponse(('ok',), backing_file.read())
330
def _optional_mode(self, mode):
336
def do_append(self, relpath, mode):
337
old_length = self._backing_transport.append(relpath, StringIO(self._recv_body()),
338
self._optional_mode(mode))
339
return SmartServerResponse(('appended', '%d' % old_length))
341
def do_delete(self, relpath):
342
self._backing_transport.delete(relpath)
344
def do_iter_files_recursive(self, abspath):
345
# XXX: the path handling needs some thought.
346
#relpath = self._backing_transport.relpath(abspath)
347
transport = self._backing_transport.clone(abspath)
348
filenames = transport.iter_files_recursive()
349
return SmartServerResponse(('names',) + tuple(filenames))
351
def do_list_dir(self, relpath):
352
filenames = self._backing_transport.list_dir(relpath)
353
return SmartServerResponse(('names',) + tuple(filenames))
355
def do_mkdir(self, relpath, mode):
356
self._backing_transport.mkdir(relpath, self._optional_mode(mode))
358
def do_move(self, rel_from, rel_to):
359
self._backing_transport.move(rel_from, rel_to)
361
def do_put(self, relpath, mode):
362
self._backing_transport.put(relpath,
363
StringIO(self._recv_body()),
364
self._optional_mode(mode))
366
def do_rename(self, rel_from, rel_to):
367
self._backing_transport.rename(rel_from, rel_to)
369
def do_rmdir(self, relpath):
370
self._backing_transport.rmdir(relpath)
372
def do_stat(self, relpath):
373
stat = self._backing_transport.stat(relpath)
374
return SmartServerResponse(('stat', str(stat.st_size), oct(stat.st_mode)))
376
def do_get_bundle(self, path, revision_id):
377
# open transport relative to our base
378
t = self._backing_transport.clone(path)
379
control, extra_path = bzrdir.BzrDir.open_containing_from_transport(t)
380
repo = control.open_repository()
381
tmpf = tempfile.TemporaryFile()
382
base_revision = revision.NULL_REVISION
383
write_bundle(repo, revision_id, base_revision, tmpf)
385
return SmartServerResponse((), tmpf.read())
387
def dispatch_command(self, cmd, args):
388
func = getattr(self, 'do_' + cmd, None)
390
raise BzrProtocolError("bad request %r" % (cmd,))
394
result = SmartServerResponse(('ok',))
396
except errors.NoSuchFile, e:
397
return SmartServerResponse(('NoSuchFile', e.path))
398
except errors.FileExists, e:
399
return SmartServerResponse(('FileExists', e.path))
400
except errors.DirectoryNotEmpty, e:
401
return SmartServerResponse(('DirectoryNotEmpty', e.path))
404
class SmartTCPServer(object):
405
"""Listens on a TCP socket and accepts connections from smart clients"""
407
def __init__(self, backing_transport=None, host='127.0.0.1', port=0):
408
"""Construct a new server.
410
To actually start it running, call either start_background_thread or
413
:param host: Name of the interface to listen on.
414
:param port: TCP port to listen on, or 0 to allocate a transient port.
416
if backing_transport is None:
417
backing_transport = memory.MemoryTransport()
418
self._server_socket = socket.socket()
419
self._server_socket.bind((host, port))
420
self.port = self._server_socket.getsockname()[1]
421
self._server_socket.listen(1)
422
self._server_socket.settimeout(1)
423
self.backing_transport = backing_transport
426
# let connections timeout so that we get a chance to terminate
427
# Keep a reference to the exceptions we want to catch because the socket
428
# module's globals get set to None during interpreter shutdown.
429
from socket import timeout as socket_timeout
430
from socket import error as socket_error
431
self._should_terminate = False
432
while not self._should_terminate:
434
self.accept_and_serve()
435
except socket_timeout:
436
# just check if we're asked to stop
438
except socket_error, e:
439
trace.warning("client disconnected: %s", e)
443
"""Return the url of the server"""
444
return "bzr://%s:%d/" % self._server_socket.getsockname()
446
def accept_and_serve(self):
447
conn, client_addr = self._server_socket.accept()
448
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
449
from_client = conn.makefile('r')
450
to_client = conn.makefile('w')
451
handler = SmartStreamServer(from_client, to_client,
452
self.backing_transport)
453
connection_thread = threading.Thread(None, handler.serve, name='smart-server-child')
454
connection_thread.setDaemon(True)
455
connection_thread.start()
457
def start_background_thread(self):
458
self._server_thread = threading.Thread(None,
460
name='server-' + self.get_url())
461
self._server_thread.setDaemon(True)
462
self._server_thread.start()
464
def stop_background_thread(self):
465
self._should_terminate = True
466
# self._server_socket.close()
467
# we used to join the thread, but it's not really necessary; it will
469
## self._server_thread.join()
472
class SmartTCPServer_for_testing(SmartTCPServer):
473
"""Server suitable for use by transport tests.
475
This server is backed by the process's cwd.
479
self._homedir = os.getcwd()
480
# The server is set up by default like for ssh access: the client
481
# passes filesystem-absolute paths; therefore the server must look
482
# them up relative to the root directory. it might be better to act
483
# a public server and have the server rewrite paths into the test
485
SmartTCPServer.__init__(self, transport.get_transport("file:///"))
488
"""Set up server for testing"""
489
self.start_background_thread()
492
self.stop_background_thread()
495
"""Return the url of the server"""
496
host, port = self._server_socket.getsockname()
497
# XXX: I think this is likely to break on windows -- self._homedir will
498
# have backslashes (and maybe a drive letter?).
499
# -- Andrew Bennetts, 2006-08-29
500
return "bzr://%s:%d%s" % (host, port, urlutils.escape(self._homedir))
502
def get_bogus_url(self):
503
"""Return a URL which will fail to connect"""
504
return 'bzr://127.0.0.1:1/'
507
class SmartStat(object):
509
def __init__(self, size, mode):
514
class SmartTransport(sftp.SFTPUrlHandling):
515
"""Connection to a smart server.
517
The connection holds references to pipes that can be used to send requests
520
The connection has a notion of the current directory to which it's
521
connected; this is incorporated in filenames passed to the server.
523
This supports some higher-level RPC operations and can also be treated
524
like a Transport to do file-like operations.
526
The connection can be made over a tcp socket, or (in future) an ssh pipe
527
or a series of http requests. There are concrete subclasses for each
528
type: SmartTCPTransport, etc.
531
def __init__(self, server_url, clone_from=None):
532
### Technically super() here is faulty because Transport's __init__
533
### fails to take 2 parameters, and if super were to choose a silly
534
### initialisation order things would blow up.
535
super(SmartTransport, self).__init__(server_url)
536
if clone_from is None:
537
self._client = SmartStreamClient(self._connect_to_server)
539
# credentials may be stripped from the base in some circumstances
540
# as yet to be clearly defined or documented, so copy them.
541
self._username = clone_from._username
542
# reuse same connection
543
self._client = clone_from._client
545
def clone(self, relative_url):
546
"""Make a new SmartTransport related to me, sharing the same connection.
548
This essentially opens a handle on a different remote directory.
550
if relative_url is None:
551
return self.__class__(self.base, self)
553
return self.__class__(self.abspath(relative_url), self)
555
def is_readonly(self):
556
"""Smart server transport can do read/write file operations."""
559
def get_smart_client(self):
562
def _unparse_url(self, path):
563
"""Return URL for a path.
565
:see: SFTPUrlHandling._unparse_url
567
# TODO: Eventually it should be possible to unify this with
568
# SFTPUrlHandling._unparse_url?
571
path = urllib.quote(path)
572
netloc = urllib.quote(self._host)
573
if self._username is not None:
574
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
575
if self._port is not None:
576
netloc = '%s:%d' % (netloc, self._port)
577
return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
579
def _remote_path(self, relpath):
580
return self._combine_paths(self._path, relpath)
582
def has(self, relpath):
583
"""Indicate whether a remote file of the given name exists or not.
585
:see: Transport.has()
587
resp = self._client._call('has', self._remote_path(relpath))
588
if resp == ('yes', ):
590
elif resp == ('no', ):
593
self._translate_error(resp)
595
def get(self, relpath):
596
"""Return file-like object reading the contents of a remote file.
598
:see: Transport.get()
600
remote = self._remote_path(relpath)
601
resp = self._client._call('get', remote)
603
self._translate_error(resp, relpath)
604
return StringIO(self._client._recv_bulk())
606
def _optional_mode(self, mode):
612
def mkdir(self, relpath, mode=None):
613
resp = self._client._call('mkdir',
614
self._remote_path(relpath),
615
self._optional_mode(mode))
616
self._translate_error(resp)
618
def put(self, relpath, upload_file, mode=None):
619
# FIXME: upload_file is probably not safe for non-ascii characters -
620
# should probably just pass all parameters as length-delimited
622
# XXX: wrap strings in a StringIO. There should be a better way of
624
if isinstance(upload_file, str):
625
upload_file = StringIO(upload_file)
626
resp = self._client._call_with_upload('put',
627
(self._remote_path(relpath),
628
self._optional_mode(mode)),
630
self._translate_error(resp)
632
def append(self, relpath, from_file, mode=None):
633
resp = self._client._call_with_upload('append',
634
(self._remote_path(relpath),
635
self._optional_mode(mode)),
637
if resp[0] == 'appended':
639
self._translate_error(resp)
641
def delete(self, relpath):
642
resp = self._client._call('delete', self._remote_path(relpath))
643
self._translate_error(resp)
645
def rename(self, rel_from, rel_to):
647
self._remote_path(rel_from),
648
self._remote_path(rel_to))
650
def move(self, rel_from, rel_to):
652
self._remote_path(rel_from),
653
self._remote_path(rel_to))
655
def rmdir(self, relpath):
656
resp = self._call('rmdir', self._remote_path(relpath))
658
def _call(self, method, *args):
659
resp = self._client._call(method, *args)
660
self._translate_error(resp)
662
def _translate_error(self, resp, orig_path=None):
663
"""Raise an exception from a response"""
667
elif what == 'NoSuchFile':
668
if orig_path is not None:
669
error_path = orig_path
672
raise errors.NoSuchFile(error_path)
673
elif what == 'error':
674
raise BzrProtocolError(unicode(resp[1]))
675
elif what == 'FileExists':
676
raise errors.FileExists(resp[1])
677
elif what == 'DirectoryNotEmpty':
678
raise errors.DirectoryNotEmpty(resp[1])
680
raise BzrProtocolError('unexpected smart server error: %r' % (resp,))
682
def _send_tuple(self, args):
683
self._client._send_tuple(args)
685
def _recv_tuple(self):
686
return self._client._recv_tuple()
688
def disconnect(self):
689
self._client.disconnect()
691
def delete_tree(self, relpath):
692
raise errors.TransportNotPossible('readonly transport')
694
def stat(self, relpath):
695
resp = self._client._call('stat', self._remote_path(relpath))
696
if resp[0] == 'stat':
697
return SmartStat(int(resp[1]), int(resp[2], 8))
699
self._translate_error(resp)
701
## def lock_read(self, relpath):
702
## """Lock the given file for shared (read) access.
703
## :return: A lock object, which should be passed to Transport.unlock()
705
## # The old RemoteBranch ignore lock for reading, so we will
706
## # continue that tradition and return a bogus lock object.
707
## class BogusLock(object):
708
## def __init__(self, path):
712
## return BogusLock(relpath)
717
def list_dir(self, relpath):
718
resp = self._client._call('list_dir',
719
self._remote_path(relpath))
720
if resp[0] == 'names':
721
return [name.encode('ascii') for name in resp[1:]]
723
self._translate_error(resp)
725
def iter_files_recursive(self):
726
resp = self._client._call('iter_files_recursive',
727
self._remote_path(''))
728
if resp[0] == 'names':
731
self._translate_error(resp)
734
class SmartStreamClient(SmartProtocolBase):
735
"""Connection to smart server over two streams"""
737
def __init__(self, connect_func):
738
self._connect_func = connect_func
739
self._connected = False
744
def _ensure_connection(self):
745
if not self._connected:
746
self._in, self._out = self._connect_func()
747
self._connected = True
749
def _send_tuple(self, args):
750
self._ensure_connection()
751
_send_tuple(self._out, args)
753
def _send_bulk_data(self, body):
754
self._ensure_connection()
755
SmartProtocolBase._send_bulk_data(self, body)
757
def _recv_bulk(self):
758
self._ensure_connection()
759
return SmartProtocolBase._recv_bulk(self)
761
def _recv_tuple(self):
762
self._ensure_connection()
763
return SmartProtocolBase._recv_tuple(self)
765
def _recv_trailer(self):
766
self._ensure_connection()
767
return SmartProtocolBase._recv_trailer(self)
769
def disconnect(self):
770
"""Close connection to the server"""
775
def _call(self, *args):
776
self._send_tuple(args)
777
return self._recv_tuple()
779
def _call_with_upload(self, method, args, body):
780
"""Call an rpc, supplying bulk upload data.
782
:param method: method name to call
783
:param args: parameter args tuple
784
:param body: upload body as a byte string
786
self._send_tuple((method,) + args)
787
self._send_bulk_data(body)
788
return self._recv_tuple()
790
def query_version(self):
791
"""Return protocol version number of the server."""
792
# XXX: should make sure it's empty
793
self._send_tuple(('hello',))
794
resp = self._recv_tuple()
795
if resp == ('ok', '1'):
798
raise BzrProtocolError("bad response %r" % (resp,))
802
class SmartTCPTransport(SmartTransport):
803
"""Connection to smart server over plain tcp"""
805
def __init__(self, url, clone_from=None):
806
super(SmartTCPTransport, self).__init__(url, clone_from)
807
self._scheme, self._username, self._password, self._host, self._port, self._path = \
808
transport.split_url(url)
810
self._port = int(self._port)
811
except (ValueError, TypeError), e:
812
raise errors.InvalidURL(path=url, extra="invalid port %s" % self._port)
815
def _connect_to_server(self):
816
self._socket = socket.socket()
817
self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
818
result = self._socket.connect_ex((self._host, int(self._port)))
820
raise errors.ConnectionError("failed to connect to %s:%d: %s" %
821
(self._host, self._port, os.strerror(result)))
822
# TODO: May be more efficient to just treat them as sockets
823
# throughout? But what about pipes to ssh?...
824
to_server = self._socket.makefile('w')
825
from_server = self._socket.makefile('r')
826
return from_server, to_server
828
def disconnect(self):
829
super(SmartTCPTransport, self).disconnect()
830
# XXX: Is closing the socket as well as closing the files really
832
if self._socket is not None:
836
class SmartSSHTransport(SmartTransport):
837
"""Connection to smart server over SSH."""
839
def __init__(self, url, clone_from=None):
840
# TODO: all this probably belongs in the parent class.
841
super(SmartSSHTransport, self).__init__(url, clone_from)
842
self._scheme, self._username, self._password, self._host, self._port, self._path = \
843
transport.split_url(url)
845
if self._port is not None:
846
self._port = int(self._port)
847
except (ValueError, TypeError), e:
848
raise errors.InvalidURL(path=url, extra="invalid port %s" % self._port)
850
def _connect_to_server(self):
851
# XXX: don't hardcode vendor
852
# XXX: cannot pass password to SSHSubprocess yet
853
if self._password is not None:
854
raise errors.InvalidURL("SSH smart transport doesn't handle passwords")
855
self._ssh_connection = sftp.SSHSubprocess(self._host, 'openssh',
856
port=self._port, user=self._username,
857
command=['bzr', 'serve', '--inet'])
858
return self._ssh_connection.get_filelike_channels()
860
def disconnect(self):
861
super(SmartSSHTransport, self).disconnect()
862
self._ssh_connection.close()
865
def get_test_permutations():
866
"""Return (transport, server) permutations for testing"""
867
return [(SmartTCPTransport, SmartTCPServer_for_testing)]