~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/remote.py

  • Committer: Robert Collins
  • Date: 2007-07-04 08:08:13 UTC
  • mfrom: (2572 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2587.
  • Revision ID: robertc@robertcollins.net-20070704080813-wzebx0r88fvwj5rq
Merge bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
"""Smart-server protocol, client and server.
18
 
 
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. ::
22
 
 
23
 
  SEP := '\001'
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
33
 
  ERROR_TRAILER := 
34
 
 
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.
38
 
 
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.)
45
 
 
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?
 
17
"""RemoteTransport client for the smart-server.
 
18
 
 
19
This module shouldn't be accessed directly.  The classes defined here should be
 
20
imported from bzrlib.smart.
49
21
"""
50
22
 
51
 
 
52
 
# TODO: _translate_error should be on the client, not the transport because
53
 
#     error coding is wire protocol specific.
54
 
 
55
 
# TODO: A plain integer from query_version is too simple; should give some
56
 
# capabilities too?
57
 
 
58
 
# TODO: Server should probably catch exceptions within itself and send them
59
 
# back across the network.  (But shouldn't catch KeyboardInterrupt etc)
60
 
# Also needs to somehow report protocol errors like bad requests.  Need to
61
 
# consider how we'll handle error reporting, e.g. if we get halfway through a
62
 
# bulk transfer and then something goes wrong.
63
 
 
64
 
# TODO: Standard marker at start of request/response lines?
65
 
 
66
 
# TODO: Make each request and response self-validatable, e.g. with checksums.
67
 
#
68
 
# TODO: get/put objects could be changed to gradually read back the data as it
69
 
# comes across the network
70
 
#
71
 
# TODO: What should the server do if it hits an error and has to terminate?
72
 
#
73
 
# TODO: is it useful to allow multiple chunks in the bulk data?
74
 
#
75
 
# TODO: If we get an exception during transmission of bulk data we can't just
76
 
# emit the exception because it won't be seen.
77
 
#   John proposes:  I think it would be worthwhile to have a header on each
78
 
#   chunk, that indicates it is another chunk. Then you can send an 'error'
79
 
#   chunk as long as you finish the previous chunk.
80
 
#
81
 
# TODO: Clone method on Transport; should work up towards parent directory;
82
 
# unclear how this should be stored or communicated to the server... maybe
83
 
# just pass it on all relevant requests?
84
 
#
85
 
# TODO: Better name than clone() for changing between directories.  How about
86
 
# open_dir or change_dir or chdir?
87
 
#
88
 
# TODO: Is it really good to have the notion of current directory within the
89
 
# connection?  Perhaps all Transports should factor out a common connection
90
 
# from the thing that has the directory context?
91
 
#
92
 
# TODO: Pull more things common to sftp and ssh to a higher level.
93
 
#
94
 
# TODO: The server that manages a connection should be quite small and retain
95
 
# minimum state because each of the requests are supposed to be stateless.
96
 
# Then we can write another implementation that maps to http.
97
 
#
98
 
# TODO: What to do when a client connection is garbage collected?  Maybe just
99
 
# abruptly drop the connection?
100
 
#
101
 
# TODO: Server in some cases will need to restrict access to files outside of
102
 
# a particular root directory.  LocalTransport doesn't do anything to stop you
103
 
# ascending above the base directory, so we need to prevent paths
104
 
# containing '..' in either the server or transport layers.  (Also need to
105
 
# consider what happens if someone creates a symlink pointing outside the 
106
 
# directory tree...)
107
 
#
108
 
# TODO: Server should rebase absolute paths coming across the network to put
109
 
# them under the virtual root, if one is in use.  LocalTransport currently
110
 
# doesn't do that; if you give it an absolute path it just uses it.
111
 
112
 
# XXX: Arguments can't contain newlines or ascii; possibly we should e.g.
113
 
# urlescape them instead.  Indeed possibly this should just literally be
114
 
# http-over-ssh.
115
 
#
116
 
# FIXME: This transport, with several others, has imperfect handling of paths
117
 
# within urls.  It'd probably be better for ".." from a root to raise an error
118
 
# rather than return the same directory as we do at present.
119
 
#
120
 
# TODO: Rather than working at the Transport layer we want a Branch,
121
 
# Repository or BzrDir objects that talk to a server.
122
 
#
123
 
# TODO: Probably want some way for server commands to gradually produce body
124
 
# data rather than passing it as a string; they could perhaps pass an
125
 
# iterator-like callback that will gradually yield data; it probably needs a
126
 
# close() method that will always be closed to do any necessary cleanup.
127
 
#
128
 
# TODO: Split the actual smart server from the ssh encoding of it.
129
 
#
130
 
# TODO: Perhaps support file-level readwrite operations over the transport
131
 
# too.
132
 
#
133
 
# TODO: SmartBzrDir class, proxying all Branch etc methods across to another
134
 
# branch doing file-level operations.
135
 
#
136
 
# TODO: jam 20060915 _decode_tuple is acting directly on input over
137
 
#       the socket, and it assumes everything is UTF8 sections separated
138
 
#       by \001. Which means a request like '\002' Will abort the connection
139
 
#       because of a UnicodeDecodeError. It does look like invalid data will
140
 
#       kill the SmartStreamServer, but only with an abort + exception, and 
141
 
#       the overall server shouldn't die.
 
23
__all__ = ['RemoteTransport', 'RemoteTCPTransport', 'RemoteSSHTransport']
142
24
 
143
25
from cStringIO import StringIO
144
 
import errno
145
 
import os
146
 
import socket
147
 
import sys
148
 
import tempfile
149
 
import threading
150
26
import urllib
151
27
import urlparse
152
28
 
153
29
from bzrlib import (
154
 
    bzrdir,
155
30
    errors,
156
 
    revision,
157
31
    transport,
158
 
    trace,
159
32
    urlutils,
160
33
    )
161
 
from bzrlib.bundle.serializer import write_bundle
162
 
from bzrlib.trace import mutter
163
 
from bzrlib.transport import local
 
34
from bzrlib.smart import client, medium, protocol
164
35
 
165
36
# must do this otherwise urllib can't parse the urls properly :(
166
 
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh']:
 
37
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh', 'bzr+http']:
167
38
    transport.register_urlparse_netloc_protocol(scheme)
168
39
del scheme
169
40
 
170
41
 
171
 
def _recv_tuple(from_file):
172
 
    req_line = from_file.readline()
173
 
    return _decode_tuple(req_line)
174
 
 
175
 
 
176
 
def _decode_tuple(req_line):
177
 
    if req_line == None or req_line == '':
178
 
        return None
179
 
    if req_line[-1] != '\n':
180
 
        raise errors.SmartProtocolError("request %r not terminated" % req_line)
181
 
    return tuple((a.decode('utf-8') for a in req_line[:-1].split('\x01')))
182
 
 
183
 
 
184
 
def _encode_tuple(args):
185
 
    """Encode the tuple args to a bytestream."""
186
 
    return '\x01'.join((a.encode('utf-8') for a in args)) + '\n'
187
 
 
188
 
 
189
 
class SmartProtocolBase(object):
190
 
    """Methods common to client and server"""
191
 
 
192
 
    def _send_bulk_data(self, body):
193
 
        """Send chunked body data"""
194
 
        assert isinstance(body, str)
195
 
        bytes = ''.join(('%d\n' % len(body), body, 'done\n'))
196
 
        self._write_and_flush(bytes)
197
 
 
198
 
    # TODO: this only actually accomodates a single block; possibly should support
199
 
    # multiple chunks?
200
 
    def _recv_bulk(self):
201
 
        chunk_len = self._in.readline()
202
 
        try:
203
 
            chunk_len = int(chunk_len)
204
 
        except ValueError:
205
 
            raise errors.SmartProtocolError("bad chunk length line %r" % chunk_len)
206
 
        bulk = self._in.read(chunk_len)
207
 
        if len(bulk) != chunk_len:
208
 
            raise errors.SmartProtocolError("short read fetching bulk data chunk")
209
 
        self._recv_trailer()
210
 
        return bulk
211
 
 
212
 
    def _recv_tuple(self):
213
 
        return _recv_tuple(self._in)
214
 
 
215
 
    def _recv_trailer(self):
216
 
        resp = self._recv_tuple()
217
 
        if resp == ('done', ):
218
 
            return
219
 
        else:
220
 
            self._translate_error(resp)
221
 
 
222
 
    def _serialise_offsets(self, offsets):
223
 
        """Serialise a readv offset list."""
224
 
        txt = []
225
 
        for start, length in offsets:
226
 
            txt.append('%d,%d' % (start, length))
227
 
        return '\n'.join(txt)
228
 
 
229
 
    def _write_and_flush(self, bytes):
230
 
        """Write bytes to self._out and flush it."""
231
 
        # XXX: this will be inefficient.  Just ask Robert.
232
 
        self._out.write(bytes)
233
 
        self._out.flush()
234
 
 
235
 
 
236
 
class SmartStreamServer(SmartProtocolBase):
237
 
    """Handles smart commands coming over a stream.
238
 
 
239
 
    The stream may be a pipe connected to sshd, or a tcp socket, or an
240
 
    in-process fifo for testing.
241
 
 
242
 
    One instance is created for each connected client; it can serve multiple
243
 
    requests in the lifetime of the connection.
244
 
 
245
 
    The server passes requests through to an underlying backing transport, 
246
 
    which will typically be a LocalTransport looking at the server's filesystem.
247
 
    """
248
 
 
249
 
    def __init__(self, in_file, out_file, backing_transport):
250
 
        """Construct new server.
251
 
 
252
 
        :param in_file: Python file from which requests can be read.
253
 
        :param out_file: Python file to write responses.
254
 
        :param backing_transport: Transport for the directory served.
255
 
        """
256
 
        self._in = in_file
257
 
        self._out = out_file
258
 
        self.smart_server = SmartServer(backing_transport)
259
 
        # server can call back to us to get bulk data - this is not really
260
 
        # ideal, they should get it per request instead
261
 
        self.smart_server._recv_body = self._recv_bulk
262
 
 
263
 
    def _recv_tuple(self):
264
 
        """Read a request from the client and return as a tuple.
265
 
        
266
 
        Returns None at end of file (if the client closed the connection.)
267
 
        """
268
 
        return _recv_tuple(self._in)
269
 
 
270
 
    def _send_tuple(self, args):
271
 
        """Send response header"""
272
 
        return self._write_and_flush(_encode_tuple(args))
273
 
 
274
 
    def _send_error_and_disconnect(self, exception):
275
 
        self._send_tuple(('error', str(exception)))
276
 
        ## self._out.close()
277
 
        ## self._in.close()
278
 
 
279
 
    def _serve_one_request(self):
280
 
        """Read one request from input, process, send back a response.
281
 
        
282
 
        :return: False if the server should terminate, otherwise None.
283
 
        """
284
 
        req_args = self._recv_tuple()
285
 
        if req_args == None:
286
 
            # client closed connection
287
 
            return False  # shutdown server
288
 
        try:
289
 
            response = self.smart_server.dispatch_command(req_args[0], req_args[1:])
290
 
            self._send_tuple(response.args)
291
 
            if response.body is not None:
292
 
                self._send_bulk_data(response.body)
293
 
        except KeyboardInterrupt:
294
 
            raise
295
 
        except Exception, e:
296
 
            # everything else: pass to client, flush, and quit
297
 
            self._send_error_and_disconnect(e)
298
 
            return False
299
 
 
300
 
    def serve(self):
301
 
        """Serve requests until the client disconnects."""
302
 
        # Keep a reference to stderr because the sys module's globals get set to
303
 
        # None during interpreter shutdown.
304
 
        from sys import stderr
305
 
        try:
306
 
            while self._serve_one_request() != False:
307
 
                pass
308
 
        except Exception, e:
309
 
            stderr.write("%s terminating on exception %s\n" % (self, e))
310
 
            raise
311
 
 
312
 
 
313
 
class SmartServerResponse(object):
314
 
    """Response generated by SmartServer."""
315
 
 
316
 
    def __init__(self, args, body=None):
317
 
        self.args = args
318
 
        self.body = body
319
 
 
320
 
# XXX: TODO: Create a SmartServerRequest which will take the responsibility
321
 
# for delivering the data for a request. This could be done with as the
322
 
# StreamServer, though that would create conflation between request and response
323
 
# which may be undesirable.
324
 
 
325
 
 
326
 
class SmartServer(object):
327
 
    """Protocol logic for smart server.
328
 
    
329
 
    This doesn't handle serialization at all, it just processes requests and
330
 
    creates responses.
331
 
    """
332
 
 
333
 
    # IMPORTANT FOR IMPLEMENTORS: It is important that SmartServer not contain
334
 
    # encoding or decoding logic to allow the wire protocol to vary from the
335
 
    # object protocol: we will want to tweak the wire protocol separate from
336
 
    # the object model, and ideally we will be able to do that without having
337
 
    # a SmartServer subclass for each wire protocol, rather just a Protocol
338
 
    # subclass.
339
 
 
340
 
    # TODO: Better way of representing the body for commands that take it,
341
 
    # and allow it to be streamed into the server.
342
 
    
343
 
    def __init__(self, backing_transport):
344
 
        self._backing_transport = backing_transport
345
 
        
346
 
    def do_hello(self):
347
 
        """Answer a version request with my version."""
348
 
        return SmartServerResponse(('ok', '1'))
349
 
 
350
 
    def do_has(self, relpath):
351
 
        r = self._backing_transport.has(relpath) and 'yes' or 'no'
352
 
        return SmartServerResponse((r,))
353
 
 
354
 
    def do_get(self, relpath):
355
 
        try:
356
 
            backing_bytes = self._backing_transport.get_bytes(relpath)
357
 
        except errors.ReadError:
358
 
            # cannot read the file
359
 
            return SmartServerResponse(('ReadError', ))
360
 
        return SmartServerResponse(('ok',), backing_bytes)
361
 
 
362
 
    def _deserialise_optional_mode(self, mode):
363
 
        # XXX: FIXME this should be on the protocol object.
364
 
        if mode == '':
365
 
            return None
366
 
        else:
367
 
            return int(mode)
368
 
 
369
 
    def do_append(self, relpath, mode):
370
 
        old_length = self._backing_transport.append_bytes(
371
 
            relpath, self._recv_body(), self._deserialise_optional_mode(mode))
372
 
        return SmartServerResponse(('appended', '%d' % old_length))
373
 
 
374
 
    def do_delete(self, relpath):
375
 
        self._backing_transport.delete(relpath)
376
 
 
377
 
    def do_iter_files_recursive(self, abspath):
378
 
        # XXX: the path handling needs some thought.
379
 
        #relpath = self._backing_transport.relpath(abspath)
380
 
        transport = self._backing_transport.clone(abspath)
381
 
        filenames = transport.iter_files_recursive()
382
 
        return SmartServerResponse(('names',) + tuple(filenames))
383
 
 
384
 
    def do_list_dir(self, relpath):
385
 
        filenames = self._backing_transport.list_dir(relpath)
386
 
        return SmartServerResponse(('names',) + tuple(filenames))
387
 
 
388
 
    def do_mkdir(self, relpath, mode):
389
 
        self._backing_transport.mkdir(relpath,
390
 
                                      self._deserialise_optional_mode(mode))
391
 
 
392
 
    def do_move(self, rel_from, rel_to):
393
 
        self._backing_transport.move(rel_from, rel_to)
394
 
 
395
 
    def do_put(self, relpath, mode):
396
 
        self._backing_transport.put_bytes(relpath,
397
 
                self._recv_body(),
398
 
                self._deserialise_optional_mode(mode))
399
 
 
400
 
    def _deserialise_offsets(self, text):
401
 
        # XXX: FIXME this should be on the protocol object.
402
 
        offsets = []
403
 
        for line in text.split('\n'):
404
 
            if not line:
405
 
                continue
406
 
            start, length = line.split(',')
407
 
            offsets.append((int(start), int(length)))
408
 
        return offsets
409
 
 
410
 
    def do_put_non_atomic(self, relpath, mode, create_parent, dir_mode):
411
 
        create_parent_dir = (create_parent == 'T')
412
 
        self._backing_transport.put_bytes_non_atomic(relpath,
413
 
                self._recv_body(),
414
 
                mode=self._deserialise_optional_mode(mode),
415
 
                create_parent_dir=create_parent_dir,
416
 
                dir_mode=self._deserialise_optional_mode(dir_mode))
417
 
 
418
 
    def do_readv(self, relpath):
419
 
        offsets = self._deserialise_offsets(self._recv_body())
420
 
        backing_bytes = ''.join(bytes for offset, bytes in
421
 
                             self._backing_transport.readv(relpath, offsets))
422
 
        return SmartServerResponse(('readv',), backing_bytes)
423
 
        
424
 
    def do_rename(self, rel_from, rel_to):
425
 
        self._backing_transport.rename(rel_from, rel_to)
426
 
 
427
 
    def do_rmdir(self, relpath):
428
 
        self._backing_transport.rmdir(relpath)
429
 
 
430
 
    def do_stat(self, relpath):
431
 
        stat = self._backing_transport.stat(relpath)
432
 
        return SmartServerResponse(('stat', str(stat.st_size), oct(stat.st_mode)))
433
 
        
434
 
    def do_get_bundle(self, path, revision_id):
435
 
        # open transport relative to our base
436
 
        t = self._backing_transport.clone(path)
437
 
        control, extra_path = bzrdir.BzrDir.open_containing_from_transport(t)
438
 
        repo = control.open_repository()
439
 
        tmpf = tempfile.TemporaryFile()
440
 
        base_revision = revision.NULL_REVISION
441
 
        write_bundle(repo, revision_id, base_revision, tmpf)
442
 
        tmpf.seek(0)
443
 
        return SmartServerResponse((), tmpf.read())
444
 
 
445
 
    def dispatch_command(self, cmd, args):
446
 
        func = getattr(self, 'do_' + cmd, None)
447
 
        if func is None:
448
 
            raise errors.SmartProtocolError("bad request %r" % (cmd,))
449
 
        try:
450
 
            result = func(*args)
451
 
            if result is None: 
452
 
                result = SmartServerResponse(('ok',))
453
 
            return result
454
 
        except errors.NoSuchFile, e:
455
 
            return SmartServerResponse(('NoSuchFile', e.path))
456
 
        except errors.FileExists, e:
457
 
            return SmartServerResponse(('FileExists', e.path))
458
 
        except errors.DirectoryNotEmpty, e:
459
 
            return SmartServerResponse(('DirectoryNotEmpty', e.path))
460
 
        except errors.ShortReadvError, e:
461
 
            return SmartServerResponse(('ShortReadvError',
462
 
                e.path, str(e.offset), str(e.length), str(e.actual)))
463
 
        except UnicodeError, e:
464
 
            # If it is a DecodeError, than most likely we are starting
465
 
            # with a plain string
466
 
            str_or_unicode = e.object
467
 
            if isinstance(str_or_unicode, unicode):
468
 
                val = u'u:' + str_or_unicode
469
 
            else:
470
 
                val = u's:' + str_or_unicode.encode('base64')
471
 
            # This handles UnicodeEncodeError or UnicodeDecodeError
472
 
            return SmartServerResponse((e.__class__.__name__,
473
 
                    e.encoding, val, str(e.start), str(e.end), e.reason))
474
 
        except errors.TransportNotPossible, e:
475
 
            if e.msg == "readonly transport":
476
 
                return SmartServerResponse(('ReadOnlyError', ))
477
 
            else:
478
 
                raise
479
 
 
480
 
 
481
 
class SmartTCPServer(object):
482
 
    """Listens on a TCP socket and accepts connections from smart clients"""
483
 
 
484
 
    def __init__(self, backing_transport=None, host='127.0.0.1', port=0):
485
 
        """Construct a new server.
486
 
 
487
 
        To actually start it running, call either start_background_thread or
488
 
        serve.
489
 
 
490
 
        :param host: Name of the interface to listen on.
491
 
        :param port: TCP port to listen on, or 0 to allocate a transient port.
492
 
        """
493
 
        if backing_transport is None:
494
 
            backing_transport = memory.MemoryTransport()
495
 
        self._server_socket = socket.socket()
496
 
        self._server_socket.bind((host, port))
497
 
        self.port = self._server_socket.getsockname()[1]
498
 
        self._server_socket.listen(1)
499
 
        self._server_socket.settimeout(1)
500
 
        self.backing_transport = backing_transport
501
 
 
502
 
    def serve(self):
503
 
        # let connections timeout so that we get a chance to terminate
504
 
        # Keep a reference to the exceptions we want to catch because the socket
505
 
        # module's globals get set to None during interpreter shutdown.
506
 
        from socket import timeout as socket_timeout
507
 
        from socket import error as socket_error
508
 
        self._should_terminate = False
509
 
        while not self._should_terminate:
510
 
            try:
511
 
                self.accept_and_serve()
512
 
            except socket_timeout:
513
 
                # just check if we're asked to stop
514
 
                pass
515
 
            except socket_error, e:
516
 
                trace.warning("client disconnected: %s", e)
517
 
                pass
518
 
 
519
 
    def get_url(self):
520
 
        """Return the url of the server"""
521
 
        return "bzr://%s:%d/" % self._server_socket.getsockname()
522
 
 
523
 
    def accept_and_serve(self):
524
 
        conn, client_addr = self._server_socket.accept()
525
 
        # For WIN32, where the timeout value from the listening socket
526
 
        # propogates to the newly accepted socket.
527
 
        conn.setblocking(True)
528
 
        conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
529
 
        from_client = conn.makefile('r')
530
 
        to_client = conn.makefile('w')
531
 
        handler = SmartStreamServer(from_client, to_client,
532
 
                self.backing_transport)
533
 
        connection_thread = threading.Thread(None, handler.serve, name='smart-server-child')
534
 
        connection_thread.setDaemon(True)
535
 
        connection_thread.start()
536
 
 
537
 
    def start_background_thread(self):
538
 
        self._server_thread = threading.Thread(None,
539
 
                self.serve,
540
 
                name='server-' + self.get_url())
541
 
        self._server_thread.setDaemon(True)
542
 
        self._server_thread.start()
543
 
 
544
 
    def stop_background_thread(self):
545
 
        self._should_terminate = True
546
 
        # self._server_socket.close()
547
 
        # we used to join the thread, but it's not really necessary; it will
548
 
        # terminate in time
549
 
        ## self._server_thread.join()
550
 
 
551
 
 
552
 
class SmartTCPServer_for_testing(SmartTCPServer):
553
 
    """Server suitable for use by transport tests.
554
 
    
555
 
    This server is backed by the process's cwd.
556
 
    """
557
 
 
558
 
    def __init__(self):
559
 
        self._homedir = os.getcwd()
560
 
        # The server is set up by default like for ssh access: the client
561
 
        # passes filesystem-absolute paths; therefore the server must look
562
 
        # them up relative to the root directory.  it might be better to act
563
 
        # a public server and have the server rewrite paths into the test
564
 
        # directory.
565
 
        SmartTCPServer.__init__(self, transport.get_transport("file:///"))
566
 
        
567
 
    def setUp(self):
568
 
        """Set up server for testing"""
569
 
        self.start_background_thread()
570
 
 
571
 
    def tearDown(self):
572
 
        self.stop_background_thread()
573
 
 
574
 
    def get_url(self):
575
 
        """Return the url of the server"""
576
 
        host, port = self._server_socket.getsockname()
577
 
        # XXX: I think this is likely to break on windows -- self._homedir will
578
 
        # have backslashes (and maybe a drive letter?).
579
 
        #  -- Andrew Bennetts, 2006-08-29
580
 
        return "bzr://%s:%d%s" % (host, port, urlutils.escape(self._homedir))
581
 
 
582
 
    def get_bogus_url(self):
583
 
        """Return a URL which will fail to connect"""
584
 
        return 'bzr://127.0.0.1:1/'
585
 
 
586
 
 
587
 
class SmartStat(object):
 
42
# Port 4155 is the default port for bzr://, registered with IANA.
 
43
BZR_DEFAULT_INTERFACE = '0.0.0.0'
 
44
BZR_DEFAULT_PORT = 4155
 
45
 
 
46
 
 
47
class _SmartStat(object):
588
48
 
589
49
    def __init__(self, size, mode):
590
50
        self.st_size = size
591
51
        self.st_mode = mode
592
52
 
593
53
 
594
 
class SmartTransport(transport.Transport):
 
54
class RemoteTransport(transport.Transport):
595
55
    """Connection to a smart server.
596
56
 
597
 
    The connection holds references to pipes that can be used to send requests
598
 
    to the server.
 
57
    The connection holds references to the medium that can be used to send
 
58
    requests to the server.
599
59
 
600
60
    The connection has a notion of the current directory to which it's
601
61
    connected; this is incorporated in filenames passed to the server.
603
63
    This supports some higher-level RPC operations and can also be treated 
604
64
    like a Transport to do file-like operations.
605
65
 
606
 
    The connection can be made over a tcp socket, or (in future) an ssh pipe
607
 
    or a series of http requests.  There are concrete subclasses for each
608
 
    type: SmartTCPTransport, etc.
 
66
    The connection can be made over a tcp socket, an ssh pipe or a series of
 
67
    http requests.  There are concrete subclasses for each type:
 
68
    RemoteTCPTransport, etc.
609
69
    """
610
70
 
611
 
    # IMPORTANT FOR IMPLEMENTORS: SmartTransport MUST NOT be given encoding
 
71
    # IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
612
72
    # responsibilities: Put those on SmartClient or similar. This is vital for
613
73
    # the ability to support multiple versions of the smart protocol over time:
614
 
    # SmartTransport is an adapter from the Transport object model to the 
 
74
    # RemoteTransport is an adapter from the Transport object model to the 
615
75
    # SmartClient model, not an encoder.
616
76
 
617
 
    def __init__(self, url, clone_from=None, client=None):
 
77
    def __init__(self, url, clone_from=None, medium=None, _client=None):
618
78
        """Constructor.
619
79
 
620
 
        :param client: ignored when clone_from is not None.
 
80
        :param clone_from: Another RemoteTransport instance that this one is
 
81
            being cloned from.  Attributes such as credentials and the medium
 
82
            will be reused.
 
83
        :param medium: The medium to use for this RemoteTransport. This must be
 
84
            supplied if clone_from is None.
 
85
        :param _client: Override the _SmartClient used by this transport.  This
 
86
            should only be used for testing purposes; normally this is
 
87
            determined from the medium.
621
88
        """
622
89
        ### Technically super() here is faulty because Transport's __init__
623
90
        ### fails to take 2 parameters, and if super were to choose a silly
624
91
        ### initialisation order things would blow up. 
625
92
        if not url.endswith('/'):
626
93
            url += '/'
627
 
        super(SmartTransport, self).__init__(url)
 
94
        super(RemoteTransport, self).__init__(url)
628
95
        self._scheme, self._username, self._password, self._host, self._port, self._path = \
629
96
                transport.split_url(url)
630
97
        if clone_from is None:
631
 
            if client is None:
632
 
                self._client = SmartStreamClient(self._connect_to_server)
633
 
            else:
634
 
                self._client = client
 
98
            self._medium = medium
635
99
        else:
636
100
            # credentials may be stripped from the base in some circumstances
637
101
            # as yet to be clearly defined or documented, so copy them.
638
102
            self._username = clone_from._username
639
103
            # reuse same connection
640
 
            self._client = clone_from._client
 
104
            self._medium = clone_from._medium
 
105
        assert self._medium is not None
 
106
        if _client is None:
 
107
            self._client = client._SmartClient(self._medium)
 
108
        else:
 
109
            self._client = _client
641
110
 
642
111
    def abspath(self, relpath):
643
112
        """Return the full url to the given relative path.
648
117
        return self._unparse_url(self._remote_path(relpath))
649
118
    
650
119
    def clone(self, relative_url):
651
 
        """Make a new SmartTransport related to me, sharing the same connection.
 
120
        """Make a new RemoteTransport related to me, sharing the same connection.
652
121
 
653
122
        This essentially opens a handle on a different remote directory.
654
123
        """
655
124
        if relative_url is None:
656
 
            return self.__class__(self.base, self)
 
125
            return RemoteTransport(self.base, self)
657
126
        else:
658
 
            return self.__class__(self.abspath(relative_url), self)
 
127
            return RemoteTransport(self.abspath(relative_url), self)
659
128
 
660
129
    def is_readonly(self):
661
130
        """Smart server transport can do read/write file operations."""
662
 
        return False
663
 
                                                   
 
131
        resp = self._call2('Transport.is_readonly')
 
132
        if resp == ('yes', ):
 
133
            return True
 
134
        elif resp == ('no', ):
 
135
            return False
 
136
        elif (resp == ('error', "Generic bzr smart protocol error: "
 
137
                                "bad request 'Transport.is_readonly'") or
 
138
              resp == ('error', "Generic bzr smart protocol error: "
 
139
                                "bad request u'Transport.is_readonly'")):
 
140
            # XXX: nasty hack: servers before 0.16 don't have a
 
141
            # 'Transport.is_readonly' verb, so we do what clients before 0.16
 
142
            # did: assume False.
 
143
            return False
 
144
        else:
 
145
            self._translate_error(resp)
 
146
        raise errors.UnexpectedSmartServerResponse(resp)
 
147
 
664
148
    def get_smart_client(self):
665
 
        return self._client
 
149
        return self._medium
 
150
 
 
151
    def get_smart_medium(self):
 
152
        return self._medium
666
153
                                                   
667
154
    def _unparse_url(self, path):
668
155
        """Return URL for a path.
685
172
        """Returns the Unicode version of the absolute path for relpath."""
686
173
        return self._combine_paths(self._path, relpath)
687
174
 
 
175
    def _call(self, method, *args):
 
176
        resp = self._call2(method, *args)
 
177
        self._translate_error(resp)
 
178
 
 
179
    def _call2(self, method, *args):
 
180
        """Call a method on the remote server."""
 
181
        return self._client.call(method, *args)
 
182
 
 
183
    def _call_with_body_bytes(self, method, args, body):
 
184
        """Call a method on the remote server with body bytes."""
 
185
        return self._client.call_with_body_bytes(method, args, body)
 
186
 
688
187
    def has(self, relpath):
689
188
        """Indicate whether a remote file of the given name exists or not.
690
189
 
691
190
        :see: Transport.has()
692
191
        """
693
 
        resp = self._client._call('has', self._remote_path(relpath))
 
192
        resp = self._call2('has', self._remote_path(relpath))
694
193
        if resp == ('yes', ):
695
194
            return True
696
195
        elif resp == ('no', ):
703
202
        
704
203
        :see: Transport.get_bytes()/get_file()
705
204
        """
 
205
        return StringIO(self.get_bytes(relpath))
 
206
 
 
207
    def get_bytes(self, relpath):
706
208
        remote = self._remote_path(relpath)
707
 
        resp = self._client._call('get', remote)
 
209
        request = self._medium.get_request()
 
210
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
 
211
        smart_protocol.call('get', remote)
 
212
        resp = smart_protocol.read_response_tuple(True)
708
213
        if resp != ('ok', ):
 
214
            smart_protocol.cancel_read_body()
709
215
            self._translate_error(resp, relpath)
710
 
        return StringIO(self._client._recv_bulk())
 
216
        return smart_protocol.read_body_bytes()
711
217
 
712
218
    def _serialise_optional_mode(self, mode):
713
219
        if mode is None:
716
222
            return '%d' % mode
717
223
 
718
224
    def mkdir(self, relpath, mode=None):
719
 
        resp = self._client._call('mkdir', 
720
 
                                  self._remote_path(relpath), 
721
 
                                  self._serialise_optional_mode(mode))
 
225
        resp = self._call2('mkdir', self._remote_path(relpath),
 
226
            self._serialise_optional_mode(mode))
722
227
        self._translate_error(resp)
723
228
 
724
229
    def put_bytes(self, relpath, upload_contents, mode=None):
725
230
        # FIXME: upload_file is probably not safe for non-ascii characters -
726
231
        # should probably just pass all parameters as length-delimited
727
232
        # strings?
728
 
        resp = self._client._call_with_upload(
729
 
            'put',
 
233
        if type(upload_contents) is unicode:
 
234
            # Although not strictly correct, we raise UnicodeEncodeError to be
 
235
            # compatible with other transports.
 
236
            raise UnicodeEncodeError(
 
237
                'undefined', upload_contents, 0, 1,
 
238
                'put_bytes must be given bytes, not unicode.')
 
239
        resp = self._call_with_body_bytes('put',
730
240
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
731
241
            upload_contents)
732
242
        self._translate_error(resp)
740
250
        if create_parent_dir:
741
251
            create_parent_str = 'T'
742
252
 
743
 
        resp = self._client._call_with_upload(
 
253
        resp = self._call_with_body_bytes(
744
254
            'put_non_atomic',
745
255
            (self._remote_path(relpath), self._serialise_optional_mode(mode),
746
256
             create_parent_str, self._serialise_optional_mode(dir_mode)),
769
279
        return self.append_bytes(relpath, from_file.read(), mode)
770
280
        
771
281
    def append_bytes(self, relpath, bytes, mode=None):
772
 
        resp = self._client._call_with_upload(
 
282
        resp = self._call_with_body_bytes(
773
283
            'append',
774
284
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
775
285
            bytes)
778
288
        self._translate_error(resp)
779
289
 
780
290
    def delete(self, relpath):
781
 
        resp = self._client._call('delete', self._remote_path(relpath))
 
291
        resp = self._call2('delete', self._remote_path(relpath))
782
292
        self._translate_error(resp)
783
293
 
784
294
    def readv(self, relpath, offsets):
795
305
                               limit=self._max_readv_combine,
796
306
                               fudge_factor=self._bytes_to_read_before_seek))
797
307
 
798
 
 
799
 
        resp = self._client._call_with_upload(
800
 
            'readv',
801
 
            (self._remote_path(relpath),),
802
 
            self._client._serialise_offsets((c.start, c.length) for c in coalesced))
 
308
        request = self._medium.get_request()
 
309
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
 
310
        smart_protocol.call_with_body_readv_array(
 
311
            ('readv', self._remote_path(relpath)),
 
312
            [(c.start, c.length) for c in coalesced])
 
313
        resp = smart_protocol.read_response_tuple(True)
803
314
 
804
315
        if resp[0] != 'readv':
805
316
            # This should raise an exception
 
317
            smart_protocol.cancel_read_body()
806
318
            self._translate_error(resp)
807
319
            return
808
320
 
809
 
        data = self._client._recv_bulk()
 
321
        # FIXME: this should know how many bytes are needed, for clarity.
 
322
        data = smart_protocol.read_body_bytes()
810
323
        # Cache the results, but only until they have been fulfilled
811
324
        data_map = {}
812
325
        for c_offset in coalesced:
825
338
                cur_offset_and_size = offset_stack.next()
826
339
 
827
340
    def rename(self, rel_from, rel_to):
828
 
        self._call('rename', 
 
341
        self._call('rename',
829
342
                   self._remote_path(rel_from),
830
343
                   self._remote_path(rel_to))
831
344
 
832
345
    def move(self, rel_from, rel_to):
833
 
        self._call('move', 
 
346
        self._call('move',
834
347
                   self._remote_path(rel_from),
835
348
                   self._remote_path(rel_to))
836
349
 
837
350
    def rmdir(self, relpath):
838
351
        resp = self._call('rmdir', self._remote_path(relpath))
839
352
 
840
 
    def _call(self, method, *args):
841
 
        resp = self._client._call(method, *args)
842
 
        self._translate_error(resp)
843
 
 
844
353
    def _translate_error(self, resp, orig_path=None):
845
354
        """Raise an exception from a response"""
846
355
        if resp is None:
871
380
            end = int(resp[4])
872
381
            reason = str(resp[5]) # reason must always be a string
873
382
            if val.startswith('u:'):
874
 
                val = val[2:]
 
383
                val = val[2:].decode('utf-8')
875
384
            elif val.startswith('s:'):
876
385
                val = val[2:].decode('base64')
877
386
            if what == 'UnicodeDecodeError':
889
398
        else:
890
399
            raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
891
400
 
892
 
    def _send_tuple(self, args):
893
 
        self._client._send_tuple(args)
894
 
 
895
 
    def _recv_tuple(self):
896
 
        return self._client._recv_tuple()
897
 
 
898
401
    def disconnect(self):
899
 
        self._client.disconnect()
 
402
        self._medium.disconnect()
900
403
 
901
404
    def delete_tree(self, relpath):
902
405
        raise errors.TransportNotPossible('readonly transport')
903
406
 
904
407
    def stat(self, relpath):
905
 
        resp = self._client._call('stat', self._remote_path(relpath))
 
408
        resp = self._call2('stat', self._remote_path(relpath))
906
409
        if resp[0] == 'stat':
907
 
            return SmartStat(int(resp[1]), int(resp[2], 8))
 
410
            return _SmartStat(int(resp[1]), int(resp[2], 8))
908
411
        else:
909
412
            self._translate_error(resp)
910
413
 
925
428
        return True
926
429
 
927
430
    def list_dir(self, relpath):
928
 
        resp = self._client._call('list_dir',
929
 
                                  self._remote_path(relpath))
 
431
        resp = self._call2('list_dir', self._remote_path(relpath))
930
432
        if resp[0] == 'names':
931
433
            return [name.encode('ascii') for name in resp[1:]]
932
434
        else:
933
435
            self._translate_error(resp)
934
436
 
935
437
    def iter_files_recursive(self):
936
 
        resp = self._client._call('iter_files_recursive',
937
 
                                  self._remote_path(''))
 
438
        resp = self._call2('iter_files_recursive', self._remote_path(''))
938
439
        if resp[0] == 'names':
939
440
            return resp[1:]
940
441
        else:
941
442
            self._translate_error(resp)
942
443
 
943
444
 
944
 
class SmartStreamClient(SmartProtocolBase):
945
 
    """Connection to smart server over two streams"""
946
 
 
947
 
    def __init__(self, connect_func):
948
 
        self._connect_func = connect_func
949
 
        self._connected = False
950
 
 
951
 
    def __del__(self):
952
 
        self.disconnect()
953
 
 
954
 
    def _ensure_connection(self):
955
 
        if not self._connected:
956
 
            self._in, self._out = self._connect_func()
957
 
            self._connected = True
958
 
 
959
 
    def _send_tuple(self, args):
960
 
        self._ensure_connection()
961
 
        return self._write_and_flush(_encode_tuple(args))
962
 
 
963
 
    def _send_bulk_data(self, body):
964
 
        self._ensure_connection()
965
 
        SmartProtocolBase._send_bulk_data(self, body)
966
 
        
967
 
    def _recv_bulk(self):
968
 
        self._ensure_connection()
969
 
        return SmartProtocolBase._recv_bulk(self)
970
 
 
971
 
    def _recv_tuple(self):
972
 
        self._ensure_connection()
973
 
        return SmartProtocolBase._recv_tuple(self)
974
 
 
975
 
    def _recv_trailer(self):
976
 
        self._ensure_connection()
977
 
        return SmartProtocolBase._recv_trailer(self)
978
 
 
979
 
    def disconnect(self):
980
 
        """Close connection to the server"""
981
 
        if self._connected:
982
 
            self._out.close()
983
 
            self._in.close()
984
 
 
985
 
    def _call(self, *args):
986
 
        self._send_tuple(args)
987
 
        return self._recv_tuple()
988
 
 
989
 
    def _call_with_upload(self, method, args, body):
990
 
        """Call an rpc, supplying bulk upload data.
991
 
 
992
 
        :param method: method name to call
993
 
        :param args: parameter args tuple
994
 
        :param body: upload body as a byte string
995
 
        """
996
 
        self._send_tuple((method,) + args)
997
 
        self._send_bulk_data(body)
998
 
        return self._recv_tuple()
999
 
 
1000
 
    def query_version(self):
1001
 
        """Return protocol version number of the server."""
1002
 
        # XXX: should make sure it's empty
1003
 
        self._send_tuple(('hello',))
1004
 
        resp = self._recv_tuple()
1005
 
        if resp == ('ok', '1'):
1006
 
            return 1
 
445
class RemoteTCPTransport(RemoteTransport):
 
446
    """Connection to smart server over plain tcp.
 
447
    
 
448
    This is essentially just a factory to get 'RemoteTransport(url,
 
449
        SmartTCPClientMedium).
 
450
    """
 
451
 
 
452
    def __init__(self, url):
 
453
        _scheme, _username, _password, _host, _port, _path = \
 
454
            transport.split_url(url)
 
455
        if _port is None:
 
456
            _port = BZR_DEFAULT_PORT
1007
457
        else:
1008
 
            raise errors.SmartProtocolError("bad response %r" % (resp,))
1009
 
 
1010
 
 
1011
 
class SmartTCPTransport(SmartTransport):
1012
 
    """Connection to smart server over plain tcp"""
1013
 
 
1014
 
    def __init__(self, url, clone_from=None):
1015
 
        super(SmartTCPTransport, self).__init__(url, clone_from)
 
458
            try:
 
459
                _port = int(_port)
 
460
            except (ValueError, TypeError), e:
 
461
                raise errors.InvalidURL(
 
462
                    path=url, extra="invalid port %s" % _port)
 
463
        client_medium = medium.SmartTCPClientMedium(_host, _port)
 
464
        super(RemoteTCPTransport, self).__init__(url, medium=client_medium)
 
465
 
 
466
 
 
467
class RemoteSSHTransport(RemoteTransport):
 
468
    """Connection to smart server over SSH.
 
469
 
 
470
    This is essentially just a factory to get 'RemoteTransport(url,
 
471
        SmartSSHClientMedium).
 
472
    """
 
473
 
 
474
    def __init__(self, url):
 
475
        _scheme, _username, _password, _host, _port, _path = \
 
476
            transport.split_url(url)
1016
477
        try:
1017
 
            self._port = int(self._port)
 
478
            if _port is not None:
 
479
                _port = int(_port)
1018
480
        except (ValueError, TypeError), e:
1019
 
            raise errors.InvalidURL(path=url, extra="invalid port %s" % self._port)
1020
 
        self._socket = None
1021
 
 
1022
 
    def _connect_to_server(self):
1023
 
        self._socket = socket.socket()
1024
 
        self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
1025
 
        result = self._socket.connect_ex((self._host, int(self._port)))
1026
 
        if result:
1027
 
            raise errors.ConnectionError("failed to connect to %s:%d: %s" %
1028
 
                    (self._host, self._port, os.strerror(result)))
1029
 
        # TODO: May be more efficient to just treat them as sockets
1030
 
        # throughout?  But what about pipes to ssh?...
1031
 
        to_server = self._socket.makefile('w')
1032
 
        from_server = self._socket.makefile('r')
1033
 
        return from_server, to_server
1034
 
 
1035
 
    def disconnect(self):
1036
 
        super(SmartTCPTransport, self).disconnect()
1037
 
        # XXX: Is closing the socket as well as closing the files really
1038
 
        # necessary?
1039
 
        if self._socket is not None:
1040
 
            self._socket.close()
1041
 
 
1042
 
try:
1043
 
    from bzrlib.transport import sftp, ssh
1044
 
except errors.ParamikoNotPresent:
1045
 
    # no paramiko, no SSHTransport.
1046
 
    pass
1047
 
else:
1048
 
    class SmartSSHTransport(SmartTransport):
1049
 
        """Connection to smart server over SSH."""
1050
 
 
1051
 
        def __init__(self, url, clone_from=None):
1052
 
            # TODO: all this probably belongs in the parent class.
1053
 
            super(SmartSSHTransport, self).__init__(url, clone_from)
1054
 
            try:
1055
 
                if self._port is not None:
1056
 
                    self._port = int(self._port)
1057
 
            except (ValueError, TypeError), e:
1058
 
                raise errors.InvalidURL(path=url, extra="invalid port %s" % self._port)
1059
 
 
1060
 
        def _connect_to_server(self):
1061
 
            executable = os.environ.get('BZR_REMOTE_PATH', 'bzr')
1062
 
            vendor = ssh._get_ssh_vendor()
1063
 
            self._ssh_connection = vendor.connect_ssh(self._username,
1064
 
                    self._password, self._host, self._port,
1065
 
                    command=[executable, 'serve', '--inet', '--directory=/',
1066
 
                             '--allow-writes'])
1067
 
            return self._ssh_connection.get_filelike_channels()
1068
 
 
1069
 
        def disconnect(self):
1070
 
            super(SmartSSHTransport, self).disconnect()
1071
 
            self._ssh_connection.close()
 
481
            raise errors.InvalidURL(path=url, extra="invalid port %s" % 
 
482
                _port)
 
483
        client_medium = medium.SmartSSHClientMedium(_host, _port,
 
484
                                                    _username, _password)
 
485
        super(RemoteSSHTransport, self).__init__(url, medium=client_medium)
 
486
 
 
487
 
 
488
class RemoteHTTPTransport(RemoteTransport):
 
489
    """Just a way to connect between a bzr+http:// url and http://.
 
490
    
 
491
    This connection operates slightly differently than the RemoteSSHTransport.
 
492
    It uses a plain http:// transport underneath, which defines what remote
 
493
    .bzr/smart URL we are connected to. From there, all paths that are sent are
 
494
    sent as relative paths, this way, the remote side can properly
 
495
    de-reference them, since it is likely doing rewrite rules to translate an
 
496
    HTTP path into a local path.
 
497
    """
 
498
 
 
499
    def __init__(self, url, http_transport=None):
 
500
        assert url.startswith('bzr+http://')
 
501
 
 
502
        if http_transport is None:
 
503
            http_url = url[len('bzr+'):]
 
504
            self._http_transport = transport.get_transport(http_url)
 
505
        else:
 
506
            self._http_transport = http_transport
 
507
        http_medium = self._http_transport.get_smart_medium()
 
508
        super(RemoteHTTPTransport, self).__init__(url, medium=http_medium)
 
509
 
 
510
    def _remote_path(self, relpath):
 
511
        """After connecting HTTP Transport only deals in relative URLs."""
 
512
        # Adjust the relpath based on which URL this smart transport is
 
513
        # connected to.
 
514
        base = urlutils.normalize_url(self._http_transport.base)
 
515
        url = urlutils.join(self.base[len('bzr+'):], relpath)
 
516
        url = urlutils.normalize_url(url)
 
517
        return urlutils.relative_url(base, url)
 
518
 
 
519
    def abspath(self, relpath):
 
520
        """Return the full url to the given relative path.
 
521
        
 
522
        :param relpath: the relative path or path components
 
523
        :type relpath: str or list
 
524
        """
 
525
        return self._unparse_url(self._combine_paths(self._path, relpath))
 
526
 
 
527
    def clone(self, relative_url):
 
528
        """Make a new RemoteHTTPTransport related to me.
 
529
 
 
530
        This is re-implemented rather than using the default
 
531
        RemoteTransport.clone() because we must be careful about the underlying
 
532
        http transport.
 
533
 
 
534
        Also, the cloned smart transport will POST to the same .bzr/smart
 
535
        location as this transport (although obviously the relative paths in the
 
536
        smart requests may be different).  This is so that the server doesn't
 
537
        have to handle .bzr/smart requests at arbitrary places inside .bzr
 
538
        directories, just at the initial URL the user uses.
 
539
 
 
540
        The exception is parent paths (i.e. relative_url of "..").
 
541
        """
 
542
        if relative_url:
 
543
            abs_url = self.abspath(relative_url)
 
544
        else:
 
545
            abs_url = self.base
 
546
        # We either use the exact same http_transport (for child locations), or
 
547
        # a clone of the underlying http_transport (for parent locations).  This
 
548
        # means we share the connection.
 
549
        norm_base = urlutils.normalize_url(self.base)
 
550
        norm_abs_url = urlutils.normalize_url(abs_url)
 
551
        normalized_rel_url = urlutils.relative_url(norm_base, norm_abs_url)
 
552
        if normalized_rel_url == ".." or normalized_rel_url.startswith("../"):
 
553
            http_transport = self._http_transport.clone(normalized_rel_url)
 
554
        else:
 
555
            http_transport = self._http_transport
 
556
        return RemoteHTTPTransport(abs_url, http_transport=http_transport)
1072
557
 
1073
558
 
1074
559
def get_test_permutations():
1075
 
    """Return (transport, server) permutations for testing"""
1076
 
    return [(SmartTCPTransport, SmartTCPServer_for_testing)]
 
560
    """Return (transport, server) permutations for testing."""
 
561
    ### We may need a little more test framework support to construct an
 
562
    ### appropriate RemoteTransport in the future.
 
563
    from bzrlib.smart import server
 
564
    return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]