~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/remote.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-06-12 02:17:42 UTC
  • mfrom: (2521.1.1 56322)
  • Revision ID: pqm@pqm.ubuntu.com-20070612021742-uetsy3g747iq3xkk
mergeĀ initĀ --create-prefix

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
 
        backing_bytes = self._backing_transport.get_bytes(relpath)
356
 
        return SmartServerResponse(('ok',), backing_bytes)
357
 
 
358
 
    def _deserialise_optional_mode(self, mode):
359
 
        # XXX: FIXME this should be on the protocol object.
360
 
        if mode == '':
361
 
            return None
362
 
        else:
363
 
            return int(mode)
364
 
 
365
 
    def do_append(self, relpath, mode):
366
 
        old_length = self._backing_transport.append_bytes(
367
 
            relpath, self._recv_body(), self._deserialise_optional_mode(mode))
368
 
        return SmartServerResponse(('appended', '%d' % old_length))
369
 
 
370
 
    def do_delete(self, relpath):
371
 
        self._backing_transport.delete(relpath)
372
 
 
373
 
    def do_iter_files_recursive(self, abspath):
374
 
        # XXX: the path handling needs some thought.
375
 
        #relpath = self._backing_transport.relpath(abspath)
376
 
        transport = self._backing_transport.clone(abspath)
377
 
        filenames = transport.iter_files_recursive()
378
 
        return SmartServerResponse(('names',) + tuple(filenames))
379
 
 
380
 
    def do_list_dir(self, relpath):
381
 
        filenames = self._backing_transport.list_dir(relpath)
382
 
        return SmartServerResponse(('names',) + tuple(filenames))
383
 
 
384
 
    def do_mkdir(self, relpath, mode):
385
 
        self._backing_transport.mkdir(relpath,
386
 
                                      self._deserialise_optional_mode(mode))
387
 
 
388
 
    def do_move(self, rel_from, rel_to):
389
 
        self._backing_transport.move(rel_from, rel_to)
390
 
 
391
 
    def do_put(self, relpath, mode):
392
 
        self._backing_transport.put_bytes(relpath,
393
 
                self._recv_body(),
394
 
                self._deserialise_optional_mode(mode))
395
 
 
396
 
    def _deserialise_offsets(self, text):
397
 
        # XXX: FIXME this should be on the protocol object.
398
 
        offsets = []
399
 
        for line in text.split('\n'):
400
 
            if not line:
401
 
                continue
402
 
            start, length = line.split(',')
403
 
            offsets.append((int(start), int(length)))
404
 
        return offsets
405
 
 
406
 
    def do_put_non_atomic(self, relpath, mode, create_parent, dir_mode):
407
 
        create_parent_dir = (create_parent == 'T')
408
 
        self._backing_transport.put_bytes_non_atomic(relpath,
409
 
                self._recv_body(),
410
 
                mode=self._deserialise_optional_mode(mode),
411
 
                create_parent_dir=create_parent_dir,
412
 
                dir_mode=self._deserialise_optional_mode(dir_mode))
413
 
 
414
 
    def do_readv(self, relpath):
415
 
        offsets = self._deserialise_offsets(self._recv_body())
416
 
        backing_bytes = ''.join(bytes for offset, bytes in
417
 
                             self._backing_transport.readv(relpath, offsets))
418
 
        return SmartServerResponse(('readv',), backing_bytes)
419
 
        
420
 
    def do_rename(self, rel_from, rel_to):
421
 
        self._backing_transport.rename(rel_from, rel_to)
422
 
 
423
 
    def do_rmdir(self, relpath):
424
 
        self._backing_transport.rmdir(relpath)
425
 
 
426
 
    def do_stat(self, relpath):
427
 
        stat = self._backing_transport.stat(relpath)
428
 
        return SmartServerResponse(('stat', str(stat.st_size), oct(stat.st_mode)))
429
 
        
430
 
    def do_get_bundle(self, path, revision_id):
431
 
        # open transport relative to our base
432
 
        t = self._backing_transport.clone(path)
433
 
        control, extra_path = bzrdir.BzrDir.open_containing_from_transport(t)
434
 
        repo = control.open_repository()
435
 
        tmpf = tempfile.TemporaryFile()
436
 
        base_revision = revision.NULL_REVISION
437
 
        write_bundle(repo, revision_id, base_revision, tmpf)
438
 
        tmpf.seek(0)
439
 
        return SmartServerResponse((), tmpf.read())
440
 
 
441
 
    def dispatch_command(self, cmd, args):
442
 
        func = getattr(self, 'do_' + cmd, None)
443
 
        if func is None:
444
 
            raise errors.SmartProtocolError("bad request %r" % (cmd,))
445
 
        try:
446
 
            result = func(*args)
447
 
            if result is None: 
448
 
                result = SmartServerResponse(('ok',))
449
 
            return result
450
 
        except errors.NoSuchFile, e:
451
 
            return SmartServerResponse(('NoSuchFile', e.path))
452
 
        except errors.FileExists, e:
453
 
            return SmartServerResponse(('FileExists', e.path))
454
 
        except errors.DirectoryNotEmpty, e:
455
 
            return SmartServerResponse(('DirectoryNotEmpty', e.path))
456
 
        except errors.ShortReadvError, e:
457
 
            return SmartServerResponse(('ShortReadvError',
458
 
                e.path, str(e.offset), str(e.length), str(e.actual)))
459
 
        except UnicodeError, e:
460
 
            # If it is a DecodeError, than most likely we are starting
461
 
            # with a plain string
462
 
            str_or_unicode = e.object
463
 
            if isinstance(str_or_unicode, unicode):
464
 
                val = u'u:' + str_or_unicode
465
 
            else:
466
 
                val = u's:' + str_or_unicode.encode('base64')
467
 
            # This handles UnicodeEncodeError or UnicodeDecodeError
468
 
            return SmartServerResponse((e.__class__.__name__,
469
 
                    e.encoding, val, str(e.start), str(e.end), e.reason))
470
 
        except errors.TransportNotPossible, e:
471
 
            if e.msg == "readonly transport":
472
 
                return SmartServerResponse(('ReadOnlyError', ))
473
 
            else:
474
 
                raise
475
 
 
476
 
 
477
 
class SmartTCPServer(object):
478
 
    """Listens on a TCP socket and accepts connections from smart clients"""
479
 
 
480
 
    def __init__(self, backing_transport=None, host='127.0.0.1', port=0):
481
 
        """Construct a new server.
482
 
 
483
 
        To actually start it running, call either start_background_thread or
484
 
        serve.
485
 
 
486
 
        :param host: Name of the interface to listen on.
487
 
        :param port: TCP port to listen on, or 0 to allocate a transient port.
488
 
        """
489
 
        if backing_transport is None:
490
 
            backing_transport = memory.MemoryTransport()
491
 
        self._server_socket = socket.socket()
492
 
        self._server_socket.bind((host, port))
493
 
        self.port = self._server_socket.getsockname()[1]
494
 
        self._server_socket.listen(1)
495
 
        self._server_socket.settimeout(1)
496
 
        self.backing_transport = backing_transport
497
 
 
498
 
    def serve(self):
499
 
        # let connections timeout so that we get a chance to terminate
500
 
        # Keep a reference to the exceptions we want to catch because the socket
501
 
        # module's globals get set to None during interpreter shutdown.
502
 
        from socket import timeout as socket_timeout
503
 
        from socket import error as socket_error
504
 
        self._should_terminate = False
505
 
        while not self._should_terminate:
506
 
            try:
507
 
                self.accept_and_serve()
508
 
            except socket_timeout:
509
 
                # just check if we're asked to stop
510
 
                pass
511
 
            except socket_error, e:
512
 
                trace.warning("client disconnected: %s", e)
513
 
                pass
514
 
 
515
 
    def get_url(self):
516
 
        """Return the url of the server"""
517
 
        return "bzr://%s:%d/" % self._server_socket.getsockname()
518
 
 
519
 
    def accept_and_serve(self):
520
 
        conn, client_addr = self._server_socket.accept()
521
 
        # For WIN32, where the timeout value from the listening socket
522
 
        # propogates to the newly accepted socket.
523
 
        conn.setblocking(True)
524
 
        conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
525
 
        from_client = conn.makefile('r')
526
 
        to_client = conn.makefile('w')
527
 
        handler = SmartStreamServer(from_client, to_client,
528
 
                self.backing_transport)
529
 
        connection_thread = threading.Thread(None, handler.serve, name='smart-server-child')
530
 
        connection_thread.setDaemon(True)
531
 
        connection_thread.start()
532
 
 
533
 
    def start_background_thread(self):
534
 
        self._server_thread = threading.Thread(None,
535
 
                self.serve,
536
 
                name='server-' + self.get_url())
537
 
        self._server_thread.setDaemon(True)
538
 
        self._server_thread.start()
539
 
 
540
 
    def stop_background_thread(self):
541
 
        self._should_terminate = True
542
 
        # self._server_socket.close()
543
 
        # we used to join the thread, but it's not really necessary; it will
544
 
        # terminate in time
545
 
        ## self._server_thread.join()
546
 
 
547
 
 
548
 
class SmartTCPServer_for_testing(SmartTCPServer):
549
 
    """Server suitable for use by transport tests.
550
 
    
551
 
    This server is backed by the process's cwd.
552
 
    """
553
 
 
554
 
    def __init__(self):
555
 
        self._homedir = os.getcwd()
556
 
        # The server is set up by default like for ssh access: the client
557
 
        # passes filesystem-absolute paths; therefore the server must look
558
 
        # them up relative to the root directory.  it might be better to act
559
 
        # a public server and have the server rewrite paths into the test
560
 
        # directory.
561
 
        SmartTCPServer.__init__(self, transport.get_transport("file:///"))
562
 
        
563
 
    def setUp(self):
564
 
        """Set up server for testing"""
565
 
        self.start_background_thread()
566
 
 
567
 
    def tearDown(self):
568
 
        self.stop_background_thread()
569
 
 
570
 
    def get_url(self):
571
 
        """Return the url of the server"""
572
 
        host, port = self._server_socket.getsockname()
573
 
        # XXX: I think this is likely to break on windows -- self._homedir will
574
 
        # have backslashes (and maybe a drive letter?).
575
 
        #  -- Andrew Bennetts, 2006-08-29
576
 
        return "bzr://%s:%d%s" % (host, port, urlutils.escape(self._homedir))
577
 
 
578
 
    def get_bogus_url(self):
579
 
        """Return a URL which will fail to connect"""
580
 
        return 'bzr://127.0.0.1:1/'
581
 
 
582
 
 
583
 
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):
584
48
 
585
49
    def __init__(self, size, mode):
586
50
        self.st_size = size
587
51
        self.st_mode = mode
588
52
 
589
53
 
590
 
class SmartTransport(transport.Transport):
 
54
class RemoteTransport(transport.Transport):
591
55
    """Connection to a smart server.
592
56
 
593
 
    The connection holds references to pipes that can be used to send requests
594
 
    to the server.
 
57
    The connection holds references to the medium that can be used to send
 
58
    requests to the server.
595
59
 
596
60
    The connection has a notion of the current directory to which it's
597
61
    connected; this is incorporated in filenames passed to the server.
599
63
    This supports some higher-level RPC operations and can also be treated 
600
64
    like a Transport to do file-like operations.
601
65
 
602
 
    The connection can be made over a tcp socket, or (in future) an ssh pipe
603
 
    or a series of http requests.  There are concrete subclasses for each
604
 
    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.
605
69
    """
606
70
 
607
 
    # IMPORTANT FOR IMPLEMENTORS: SmartTransport MUST NOT be given encoding
 
71
    # IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
608
72
    # responsibilities: Put those on SmartClient or similar. This is vital for
609
73
    # the ability to support multiple versions of the smart protocol over time:
610
 
    # SmartTransport is an adapter from the Transport object model to the 
 
74
    # RemoteTransport is an adapter from the Transport object model to the 
611
75
    # SmartClient model, not an encoder.
612
76
 
613
 
    def __init__(self, url, clone_from=None, client=None):
 
77
    def __init__(self, url, clone_from=None, medium=None, _client=None):
614
78
        """Constructor.
615
79
 
616
 
        :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.
617
88
        """
618
89
        ### Technically super() here is faulty because Transport's __init__
619
90
        ### fails to take 2 parameters, and if super were to choose a silly
620
91
        ### initialisation order things would blow up. 
621
92
        if not url.endswith('/'):
622
93
            url += '/'
623
 
        super(SmartTransport, self).__init__(url)
 
94
        super(RemoteTransport, self).__init__(url)
624
95
        self._scheme, self._username, self._password, self._host, self._port, self._path = \
625
96
                transport.split_url(url)
626
97
        if clone_from is None:
627
 
            if client is None:
628
 
                self._client = SmartStreamClient(self._connect_to_server)
629
 
            else:
630
 
                self._client = client
 
98
            self._medium = medium
631
99
        else:
632
100
            # credentials may be stripped from the base in some circumstances
633
101
            # as yet to be clearly defined or documented, so copy them.
634
102
            self._username = clone_from._username
635
103
            # reuse same connection
636
 
            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
637
110
 
638
111
    def abspath(self, relpath):
639
112
        """Return the full url to the given relative path.
644
117
        return self._unparse_url(self._remote_path(relpath))
645
118
    
646
119
    def clone(self, relative_url):
647
 
        """Make a new SmartTransport related to me, sharing the same connection.
 
120
        """Make a new RemoteTransport related to me, sharing the same connection.
648
121
 
649
122
        This essentially opens a handle on a different remote directory.
650
123
        """
651
124
        if relative_url is None:
652
 
            return self.__class__(self.base, self)
 
125
            return RemoteTransport(self.base, self)
653
126
        else:
654
 
            return self.__class__(self.abspath(relative_url), self)
 
127
            return RemoteTransport(self.abspath(relative_url), self)
655
128
 
656
129
    def is_readonly(self):
657
130
        """Smart server transport can do read/write file operations."""
658
 
        return False
659
 
                                                   
 
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
        assert False, 'weird response %r' % (resp,)
 
147
 
660
148
    def get_smart_client(self):
661
 
        return self._client
 
149
        return self._medium
 
150
 
 
151
    def get_smart_medium(self):
 
152
        return self._medium
662
153
                                                   
663
154
    def _unparse_url(self, path):
664
155
        """Return URL for a path.
681
172
        """Returns the Unicode version of the absolute path for relpath."""
682
173
        return self._combine_paths(self._path, relpath)
683
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
 
684
187
    def has(self, relpath):
685
188
        """Indicate whether a remote file of the given name exists or not.
686
189
 
687
190
        :see: Transport.has()
688
191
        """
689
 
        resp = self._client._call('has', self._remote_path(relpath))
 
192
        resp = self._call2('has', self._remote_path(relpath))
690
193
        if resp == ('yes', ):
691
194
            return True
692
195
        elif resp == ('no', ):
699
202
        
700
203
        :see: Transport.get_bytes()/get_file()
701
204
        """
 
205
        return StringIO(self.get_bytes(relpath))
 
206
 
 
207
    def get_bytes(self, relpath):
702
208
        remote = self._remote_path(relpath)
703
 
        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)
704
213
        if resp != ('ok', ):
 
214
            smart_protocol.cancel_read_body()
705
215
            self._translate_error(resp, relpath)
706
 
        return StringIO(self._client._recv_bulk())
 
216
        return smart_protocol.read_body_bytes()
707
217
 
708
218
    def _serialise_optional_mode(self, mode):
709
219
        if mode is None:
712
222
            return '%d' % mode
713
223
 
714
224
    def mkdir(self, relpath, mode=None):
715
 
        resp = self._client._call('mkdir', 
716
 
                                  self._remote_path(relpath), 
717
 
                                  self._serialise_optional_mode(mode))
 
225
        resp = self._call2('mkdir', self._remote_path(relpath),
 
226
            self._serialise_optional_mode(mode))
718
227
        self._translate_error(resp)
719
228
 
720
229
    def put_bytes(self, relpath, upload_contents, mode=None):
721
230
        # FIXME: upload_file is probably not safe for non-ascii characters -
722
231
        # should probably just pass all parameters as length-delimited
723
232
        # strings?
724
 
        resp = self._client._call_with_upload(
725
 
            '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',
726
240
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
727
241
            upload_contents)
728
242
        self._translate_error(resp)
736
250
        if create_parent_dir:
737
251
            create_parent_str = 'T'
738
252
 
739
 
        resp = self._client._call_with_upload(
 
253
        resp = self._call_with_body_bytes(
740
254
            'put_non_atomic',
741
255
            (self._remote_path(relpath), self._serialise_optional_mode(mode),
742
256
             create_parent_str, self._serialise_optional_mode(dir_mode)),
765
279
        return self.append_bytes(relpath, from_file.read(), mode)
766
280
        
767
281
    def append_bytes(self, relpath, bytes, mode=None):
768
 
        resp = self._client._call_with_upload(
 
282
        resp = self._call_with_body_bytes(
769
283
            'append',
770
284
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
771
285
            bytes)
774
288
        self._translate_error(resp)
775
289
 
776
290
    def delete(self, relpath):
777
 
        resp = self._client._call('delete', self._remote_path(relpath))
 
291
        resp = self._call2('delete', self._remote_path(relpath))
778
292
        self._translate_error(resp)
779
293
 
780
294
    def readv(self, relpath, offsets):
791
305
                               limit=self._max_readv_combine,
792
306
                               fudge_factor=self._bytes_to_read_before_seek))
793
307
 
794
 
 
795
 
        resp = self._client._call_with_upload(
796
 
            'readv',
797
 
            (self._remote_path(relpath),),
798
 
            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)
799
314
 
800
315
        if resp[0] != 'readv':
801
316
            # This should raise an exception
 
317
            smart_protocol.cancel_read_body()
802
318
            self._translate_error(resp)
803
319
            return
804
320
 
805
 
        data = self._client._recv_bulk()
 
321
        # FIXME: this should know how many bytes are needed, for clarity.
 
322
        data = smart_protocol.read_body_bytes()
806
323
        # Cache the results, but only until they have been fulfilled
807
324
        data_map = {}
808
325
        for c_offset in coalesced:
821
338
                cur_offset_and_size = offset_stack.next()
822
339
 
823
340
    def rename(self, rel_from, rel_to):
824
 
        self._call('rename', 
 
341
        self._call('rename',
825
342
                   self._remote_path(rel_from),
826
343
                   self._remote_path(rel_to))
827
344
 
828
345
    def move(self, rel_from, rel_to):
829
 
        self._call('move', 
 
346
        self._call('move',
830
347
                   self._remote_path(rel_from),
831
348
                   self._remote_path(rel_to))
832
349
 
833
350
    def rmdir(self, relpath):
834
351
        resp = self._call('rmdir', self._remote_path(relpath))
835
352
 
836
 
    def _call(self, method, *args):
837
 
        resp = self._client._call(method, *args)
838
 
        self._translate_error(resp)
839
 
 
840
353
    def _translate_error(self, resp, orig_path=None):
841
354
        """Raise an exception from a response"""
842
355
        if resp is None:
867
380
            end = int(resp[4])
868
381
            reason = str(resp[5]) # reason must always be a string
869
382
            if val.startswith('u:'):
870
 
                val = val[2:]
 
383
                val = val[2:].decode('utf-8')
871
384
            elif val.startswith('s:'):
872
385
                val = val[2:].decode('base64')
873
386
            if what == 'UnicodeDecodeError':
879
392
        else:
880
393
            raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
881
394
 
882
 
    def _send_tuple(self, args):
883
 
        self._client._send_tuple(args)
884
 
 
885
 
    def _recv_tuple(self):
886
 
        return self._client._recv_tuple()
887
 
 
888
395
    def disconnect(self):
889
 
        self._client.disconnect()
 
396
        self._medium.disconnect()
890
397
 
891
398
    def delete_tree(self, relpath):
892
399
        raise errors.TransportNotPossible('readonly transport')
893
400
 
894
401
    def stat(self, relpath):
895
 
        resp = self._client._call('stat', self._remote_path(relpath))
 
402
        resp = self._call2('stat', self._remote_path(relpath))
896
403
        if resp[0] == 'stat':
897
 
            return SmartStat(int(resp[1]), int(resp[2], 8))
 
404
            return _SmartStat(int(resp[1]), int(resp[2], 8))
898
405
        else:
899
406
            self._translate_error(resp)
900
407
 
915
422
        return True
916
423
 
917
424
    def list_dir(self, relpath):
918
 
        resp = self._client._call('list_dir',
919
 
                                  self._remote_path(relpath))
 
425
        resp = self._call2('list_dir', self._remote_path(relpath))
920
426
        if resp[0] == 'names':
921
427
            return [name.encode('ascii') for name in resp[1:]]
922
428
        else:
923
429
            self._translate_error(resp)
924
430
 
925
431
    def iter_files_recursive(self):
926
 
        resp = self._client._call('iter_files_recursive',
927
 
                                  self._remote_path(''))
 
432
        resp = self._call2('iter_files_recursive', self._remote_path(''))
928
433
        if resp[0] == 'names':
929
434
            return resp[1:]
930
435
        else:
931
436
            self._translate_error(resp)
932
437
 
933
438
 
934
 
class SmartStreamClient(SmartProtocolBase):
935
 
    """Connection to smart server over two streams"""
936
 
 
937
 
    def __init__(self, connect_func):
938
 
        self._connect_func = connect_func
939
 
        self._connected = False
940
 
 
941
 
    def __del__(self):
942
 
        self.disconnect()
943
 
 
944
 
    def _ensure_connection(self):
945
 
        if not self._connected:
946
 
            self._in, self._out = self._connect_func()
947
 
            self._connected = True
948
 
 
949
 
    def _send_tuple(self, args):
950
 
        self._ensure_connection()
951
 
        return self._write_and_flush(_encode_tuple(args))
952
 
 
953
 
    def _send_bulk_data(self, body):
954
 
        self._ensure_connection()
955
 
        SmartProtocolBase._send_bulk_data(self, body)
956
 
        
957
 
    def _recv_bulk(self):
958
 
        self._ensure_connection()
959
 
        return SmartProtocolBase._recv_bulk(self)
960
 
 
961
 
    def _recv_tuple(self):
962
 
        self._ensure_connection()
963
 
        return SmartProtocolBase._recv_tuple(self)
964
 
 
965
 
    def _recv_trailer(self):
966
 
        self._ensure_connection()
967
 
        return SmartProtocolBase._recv_trailer(self)
968
 
 
969
 
    def disconnect(self):
970
 
        """Close connection to the server"""
971
 
        if self._connected:
972
 
            self._out.close()
973
 
            self._in.close()
974
 
 
975
 
    def _call(self, *args):
976
 
        self._send_tuple(args)
977
 
        return self._recv_tuple()
978
 
 
979
 
    def _call_with_upload(self, method, args, body):
980
 
        """Call an rpc, supplying bulk upload data.
981
 
 
982
 
        :param method: method name to call
983
 
        :param args: parameter args tuple
984
 
        :param body: upload body as a byte string
985
 
        """
986
 
        self._send_tuple((method,) + args)
987
 
        self._send_bulk_data(body)
988
 
        return self._recv_tuple()
989
 
 
990
 
    def query_version(self):
991
 
        """Return protocol version number of the server."""
992
 
        # XXX: should make sure it's empty
993
 
        self._send_tuple(('hello',))
994
 
        resp = self._recv_tuple()
995
 
        if resp == ('ok', '1'):
996
 
            return 1
 
439
class RemoteTCPTransport(RemoteTransport):
 
440
    """Connection to smart server over plain tcp.
 
441
    
 
442
    This is essentially just a factory to get 'RemoteTransport(url,
 
443
        SmartTCPClientMedium).
 
444
    """
 
445
 
 
446
    def __init__(self, url):
 
447
        _scheme, _username, _password, _host, _port, _path = \
 
448
            transport.split_url(url)
 
449
        if _port is None:
 
450
            _port = BZR_DEFAULT_PORT
997
451
        else:
998
 
            raise errors.SmartProtocolError("bad response %r" % (resp,))
999
 
 
1000
 
 
1001
 
class SmartTCPTransport(SmartTransport):
1002
 
    """Connection to smart server over plain tcp"""
1003
 
 
1004
 
    def __init__(self, url, clone_from=None):
1005
 
        super(SmartTCPTransport, self).__init__(url, clone_from)
 
452
            try:
 
453
                _port = int(_port)
 
454
            except (ValueError, TypeError), e:
 
455
                raise errors.InvalidURL(
 
456
                    path=url, extra="invalid port %s" % _port)
 
457
        client_medium = medium.SmartTCPClientMedium(_host, _port)
 
458
        super(RemoteTCPTransport, self).__init__(url, medium=client_medium)
 
459
 
 
460
 
 
461
class RemoteSSHTransport(RemoteTransport):
 
462
    """Connection to smart server over SSH.
 
463
 
 
464
    This is essentially just a factory to get 'RemoteTransport(url,
 
465
        SmartSSHClientMedium).
 
466
    """
 
467
 
 
468
    def __init__(self, url):
 
469
        _scheme, _username, _password, _host, _port, _path = \
 
470
            transport.split_url(url)
1006
471
        try:
1007
 
            self._port = int(self._port)
 
472
            if _port is not None:
 
473
                _port = int(_port)
1008
474
        except (ValueError, TypeError), e:
1009
 
            raise errors.InvalidURL(path=url, extra="invalid port %s" % self._port)
1010
 
        self._socket = None
1011
 
 
1012
 
    def _connect_to_server(self):
1013
 
        self._socket = socket.socket()
1014
 
        self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
1015
 
        result = self._socket.connect_ex((self._host, int(self._port)))
1016
 
        if result:
1017
 
            raise errors.ConnectionError("failed to connect to %s:%d: %s" %
1018
 
                    (self._host, self._port, os.strerror(result)))
1019
 
        # TODO: May be more efficient to just treat them as sockets
1020
 
        # throughout?  But what about pipes to ssh?...
1021
 
        to_server = self._socket.makefile('w')
1022
 
        from_server = self._socket.makefile('r')
1023
 
        return from_server, to_server
1024
 
 
1025
 
    def disconnect(self):
1026
 
        super(SmartTCPTransport, self).disconnect()
1027
 
        # XXX: Is closing the socket as well as closing the files really
1028
 
        # necessary?
1029
 
        if self._socket is not None:
1030
 
            self._socket.close()
1031
 
 
1032
 
try:
1033
 
    from bzrlib.transport import sftp, ssh
1034
 
except errors.ParamikoNotPresent:
1035
 
    # no paramiko, no SSHTransport.
1036
 
    pass
1037
 
else:
1038
 
    class SmartSSHTransport(SmartTransport):
1039
 
        """Connection to smart server over SSH."""
1040
 
 
1041
 
        def __init__(self, url, clone_from=None):
1042
 
            # TODO: all this probably belongs in the parent class.
1043
 
            super(SmartSSHTransport, self).__init__(url, clone_from)
1044
 
            try:
1045
 
                if self._port is not None:
1046
 
                    self._port = int(self._port)
1047
 
            except (ValueError, TypeError), e:
1048
 
                raise errors.InvalidURL(path=url, extra="invalid port %s" % self._port)
1049
 
 
1050
 
        def _connect_to_server(self):
1051
 
            executable = os.environ.get('BZR_REMOTE_PATH', 'bzr')
1052
 
            vendor = ssh._get_ssh_vendor()
1053
 
            self._ssh_connection = vendor.connect_ssh(self._username,
1054
 
                    self._password, self._host, self._port,
1055
 
                    command=[executable, 'serve', '--inet', '--directory=/',
1056
 
                             '--allow-writes'])
1057
 
            return self._ssh_connection.get_filelike_channels()
1058
 
 
1059
 
        def disconnect(self):
1060
 
            super(SmartSSHTransport, self).disconnect()
1061
 
            self._ssh_connection.close()
 
475
            raise errors.InvalidURL(path=url, extra="invalid port %s" % 
 
476
                _port)
 
477
        client_medium = medium.SmartSSHClientMedium(_host, _port,
 
478
                                                    _username, _password)
 
479
        super(RemoteSSHTransport, self).__init__(url, medium=client_medium)
 
480
 
 
481
 
 
482
class RemoteHTTPTransport(RemoteTransport):
 
483
    """Just a way to connect between a bzr+http:// url and http://.
 
484
    
 
485
    This connection operates slightly differently than the RemoteSSHTransport.
 
486
    It uses a plain http:// transport underneath, which defines what remote
 
487
    .bzr/smart URL we are connected to. From there, all paths that are sent are
 
488
    sent as relative paths, this way, the remote side can properly
 
489
    de-reference them, since it is likely doing rewrite rules to translate an
 
490
    HTTP path into a local path.
 
491
    """
 
492
 
 
493
    def __init__(self, url, http_transport=None):
 
494
        assert url.startswith('bzr+http://')
 
495
 
 
496
        if http_transport is None:
 
497
            http_url = url[len('bzr+'):]
 
498
            self._http_transport = transport.get_transport(http_url)
 
499
        else:
 
500
            self._http_transport = http_transport
 
501
        http_medium = self._http_transport.get_smart_medium()
 
502
        super(RemoteHTTPTransport, self).__init__(url, medium=http_medium)
 
503
 
 
504
    def _remote_path(self, relpath):
 
505
        """After connecting HTTP Transport only deals in relative URLs."""
 
506
        # Adjust the relpath based on which URL this smart transport is
 
507
        # connected to.
 
508
        base = urlutils.normalize_url(self._http_transport.base)
 
509
        url = urlutils.join(self.base[len('bzr+'):], relpath)
 
510
        url = urlutils.normalize_url(url)
 
511
        return urlutils.relative_url(base, url)
 
512
 
 
513
    def abspath(self, relpath):
 
514
        """Return the full url to the given relative path.
 
515
        
 
516
        :param relpath: the relative path or path components
 
517
        :type relpath: str or list
 
518
        """
 
519
        return self._unparse_url(self._combine_paths(self._path, relpath))
 
520
 
 
521
    def clone(self, relative_url):
 
522
        """Make a new RemoteHTTPTransport related to me.
 
523
 
 
524
        This is re-implemented rather than using the default
 
525
        RemoteTransport.clone() because we must be careful about the underlying
 
526
        http transport.
 
527
 
 
528
        Also, the cloned smart transport will POST to the same .bzr/smart
 
529
        location as this transport (although obviously the relative paths in the
 
530
        smart requests may be different).  This is so that the server doesn't
 
531
        have to handle .bzr/smart requests at arbitrary places inside .bzr
 
532
        directories, just at the initial URL the user uses.
 
533
 
 
534
        The exception is parent paths (i.e. relative_url of "..").
 
535
        """
 
536
        if relative_url:
 
537
            abs_url = self.abspath(relative_url)
 
538
        else:
 
539
            abs_url = self.base
 
540
        # We either use the exact same http_transport (for child locations), or
 
541
        # a clone of the underlying http_transport (for parent locations).  This
 
542
        # means we share the connection.
 
543
        norm_base = urlutils.normalize_url(self.base)
 
544
        norm_abs_url = urlutils.normalize_url(abs_url)
 
545
        normalized_rel_url = urlutils.relative_url(norm_base, norm_abs_url)
 
546
        if normalized_rel_url == ".." or normalized_rel_url.startswith("../"):
 
547
            http_transport = self._http_transport.clone(normalized_rel_url)
 
548
        else:
 
549
            http_transport = self._http_transport
 
550
        return RemoteHTTPTransport(abs_url, http_transport=http_transport)
1062
551
 
1063
552
 
1064
553
def get_test_permutations():
1065
 
    """Return (transport, server) permutations for testing"""
1066
 
    return [(SmartTCPTransport, SmartTCPServer_for_testing)]
 
554
    """Return (transport, server) permutations for testing."""
 
555
    ### We may need a little more test framework support to construct an
 
556
    ### appropriate RemoteTransport in the future.
 
557
    from bzrlib.smart import server
 
558
    return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]