~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

MergeĀ fromĀ jam-integration

Show diffs side-by-side

added added

removed removed

Lines of Context:
29
29
import subprocess
30
30
import weakref
31
31
 
32
 
from bzrlib.errors import (FileExists, 
 
32
from bzrlib.config import config_dir, ensure_config_dir_exists
 
33
from bzrlib.errors import (ConnectionError,
 
34
                           FileExists, 
33
35
                           TransportNotPossible, NoSuchFile, PathNotChild,
34
36
                           TransportError,
35
 
                           LockError)
36
 
from bzrlib.config import config_dir
 
37
                           LockError
 
38
                           )
 
39
from bzrlib.osutils import pathjoin, fancy_rename
37
40
from bzrlib.trace import mutter, warning, error
38
 
from bzrlib.transport import Transport, register_transport
 
41
from bzrlib.transport import Transport, Server, urlescape
39
42
import bzrlib.ui
40
43
 
41
44
try:
98
101
 
99
102
class SFTPSubprocess:
100
103
    """A socket-like object that talks to an ssh subprocess via pipes."""
101
 
    def __init__(self, hostname, port=None, user=None):
102
 
        vendor = _get_ssh_vendor()
 
104
    def __init__(self, hostname, vendor, port=None, user=None):
103
105
        assert vendor in ['openssh', 'ssh']
104
106
        if vendor == 'openssh':
105
107
            args = ['ssh',
126
128
    def send(self, data):
127
129
        return os.write(self.proc.stdin.fileno(), data)
128
130
 
 
131
    def recv_ready(self):
 
132
        # TODO: jam 20051215 this function is necessary to support the
 
133
        # pipelined() function. In reality, it probably should use
 
134
        # poll() or select() to actually return if there is data
 
135
        # available, otherwise we probably don't get any benefit
 
136
        return True
 
137
 
129
138
    def recv(self, count):
130
139
        return os.read(self.proc.stdout.fileno(), count)
131
140
 
135
144
        self.proc.wait()
136
145
 
137
146
 
 
147
class LoopbackSFTP(object):
 
148
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
 
149
 
 
150
    def __init__(self, sock):
 
151
        self.__socket = sock
 
152
 
 
153
    def send(self, data):
 
154
        return self.__socket.send(data)
 
155
 
 
156
    def recv(self, n):
 
157
        return self.__socket.recv(n)
 
158
 
 
159
    def recv_ready(self):
 
160
        return True
 
161
 
 
162
    def close(self):
 
163
        self.__socket.close()
 
164
 
 
165
 
138
166
SYSTEM_HOSTKEYS = {}
139
167
BZR_HOSTKEYS = {}
140
168
 
144
172
# X seconds. But that requires a lot more fanciness.
145
173
_connected_hosts = weakref.WeakValueDictionary()
146
174
 
 
175
 
147
176
def load_host_keys():
148
177
    """
149
178
    Load system host keys (probably doesn't work on windows) and any
154
183
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
155
184
    except Exception, e:
156
185
        mutter('failed to load system host keys: ' + str(e))
157
 
    bzr_hostkey_path = os.path.join(config_dir(), 'ssh_host_keys')
 
186
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
158
187
    try:
159
188
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
160
189
    except Exception, e:
161
190
        mutter('failed to load bzr host keys: ' + str(e))
162
191
        save_host_keys()
163
192
 
 
193
 
164
194
def save_host_keys():
165
195
    """
166
196
    Save "discovered" host keys in $(config)/ssh_host_keys/.
167
197
    """
168
198
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
169
 
    bzr_hostkey_path = os.path.join(config_dir(), 'ssh_host_keys')
170
 
    if not os.path.isdir(config_dir()):
171
 
        os.mkdir(config_dir())
 
199
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
200
    ensure_config_dir_exists()
 
201
 
172
202
    try:
173
203
        f = open(bzr_hostkey_path, 'w')
174
204
        f.write('# SSH host keys collected by bzr\n')
191
221
        self.lock_path = path + '.write-lock'
192
222
        self.transport = transport
193
223
        try:
194
 
            self.lock_file = transport._sftp_open_exclusive(self.lock_path)
 
224
            # RBC 20060103 FIXME should we be using private methods here ?
 
225
            abspath = transport._remote_path(self.lock_path)
 
226
            self.lock_file = transport._sftp_open_exclusive(abspath)
195
227
        except FileExists:
196
228
            raise LockError('File %r already locked' % (self.path,))
197
229
 
198
230
    def __del__(self):
199
231
        """Should this warn, or actually try to cleanup?"""
200
232
        if self.lock_file:
201
 
            warn("SFTPLock %r not explicitly unlocked" % (self.path,))
 
233
            warning("SFTPLock %r not explicitly unlocked" % (self.path,))
202
234
            self.unlock()
203
235
 
204
236
    def unlock(self):
212
244
            # What specific errors should we catch here?
213
245
            pass
214
246
 
 
247
 
215
248
class SFTPTransport (Transport):
216
249
    """
217
250
    Transport implementation for SFTP access.
222
255
        assert base.startswith('sftp://')
223
256
        self._parse_url(base)
224
257
        base = self._unparse_url()
 
258
        if base[-1] != '/':
 
259
            base = base + '/'
225
260
        super(SFTPTransport, self).__init__(base)
226
261
        if clone_from is None:
227
262
            self._sftp_connect()
254
289
        @param relpath: the relative path or path components
255
290
        @type relpath: str or list
256
291
        """
257
 
        return self._unparse_url(self._abspath(relpath))
 
292
        return self._unparse_url(self._remote_path(relpath))
258
293
    
259
 
    def _abspath(self, relpath):
260
 
        """Return the absolute path segment without the SFTP URL."""
 
294
    def _remote_path(self, relpath):
 
295
        """Return the path to be passed along the sftp protocol for relpath.
 
296
        
 
297
        relpath is a urlencoded string.
 
298
        """
261
299
        # FIXME: share the common code across transports
262
300
        assert isinstance(relpath, basestring)
263
 
        relpath = [urllib.unquote(relpath)]
 
301
        relpath = urllib.unquote(relpath).split('/')
264
302
        basepath = self._path.split('/')
265
303
        if len(basepath) > 0 and basepath[-1] == '':
266
304
            basepath = basepath[:-1]
278
316
                basepath.append(p)
279
317
 
280
318
        path = '/'.join(basepath)
281
 
        # could still be a "relative" path here, but relative on the sftp server
282
319
        return path
283
320
 
284
321
    def relpath(self, abspath):
296
333
            extra = ': ' + ', '.join(error)
297
334
            raise PathNotChild(abspath, self.base, extra=extra)
298
335
        pl = len(self._path)
299
 
        return path[pl:].lstrip('/')
 
336
        return path[pl:].strip('/')
300
337
 
301
338
    def has(self, relpath):
302
339
        """
303
340
        Does the target location exist?
304
341
        """
305
342
        try:
306
 
            self._sftp.stat(self._abspath(relpath))
 
343
            self._sftp.stat(self._remote_path(relpath))
307
344
            return True
308
345
        except IOError:
309
346
            return False
315
352
        :param relpath: The relative path to the file
316
353
        """
317
354
        try:
318
 
            path = self._abspath(relpath)
319
 
            f = self._sftp.file(path)
 
355
            path = self._remote_path(relpath)
 
356
            f = self._sftp.file(path, mode='rb')
320
357
            if self._do_prefetch and hasattr(f, 'prefetch'):
321
358
                f.prefetch()
322
359
            return f
342
379
            f.prefetch()
343
380
        return f
344
381
 
345
 
    def put(self, relpath, f):
 
382
    def put(self, relpath, f, mode=None):
346
383
        """
347
384
        Copy the file-like or string object into the location.
348
385
 
349
386
        :param relpath: Location to put the contents, relative to base.
350
387
        :param f:       File-like or string object.
 
388
        :param mode: The final mode for the file
351
389
        """
352
 
        final_path = self._abspath(relpath)
353
 
        tmp_relpath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
 
390
        final_path = self._remote_path(relpath)
 
391
        self._put(final_path, f, mode=mode)
 
392
 
 
393
    def _put(self, abspath, f, mode=None):
 
394
        """Helper function so both put() and copy_abspaths can reuse the code"""
 
395
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
354
396
                        os.getpid(), random.randint(0,0x7FFFFFFF))
355
 
        tmp_abspath = self._abspath(tmp_relpath)
356
 
        fout = self._sftp_open_exclusive(tmp_relpath)
357
 
 
 
397
        fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
 
398
        closed = False
358
399
        try:
359
400
            try:
 
401
                fout.set_pipelined(True)
360
402
                self._pump(f, fout)
361
 
            except (paramiko.SSHException, IOError), e:
362
 
                self._translate_io_exception(e, relpath, ': unable to write')
 
403
            except (IOError, paramiko.SSHException), e:
 
404
                self._translate_io_exception(e, tmp_abspath)
 
405
            if mode is not None:
 
406
                self._sftp.chmod(tmp_abspath, mode)
 
407
            fout.close()
 
408
            closed = True
 
409
            self._rename(tmp_abspath, abspath)
363
410
        except Exception, e:
364
411
            # If we fail, try to clean up the temporary file
365
412
            # before we throw the exception
366
413
            # but don't let another exception mess things up
 
414
            # Write out the traceback, because otherwise
 
415
            # the catch and throw destroys it
 
416
            import traceback
 
417
            mutter(traceback.format_exc())
367
418
            try:
368
 
                fout.close()
 
419
                if not closed:
 
420
                    fout.close()
369
421
                self._sftp.remove(tmp_abspath)
370
422
            except:
371
 
                pass
372
 
            raise e
373
 
        else:
374
 
            # sftp rename doesn't allow overwriting, so play tricks:
375
 
            tmp_safety = 'bzr.tmp.%.9f.%d.%d' % (time.time(), os.getpid(), random.randint(0, 0x7FFFFFFF))
376
 
            tmp_safety = self._abspath(tmp_safety)
377
 
            try:
378
 
                self._sftp.rename(final_path, tmp_safety)
379
 
                file_existed = True
380
 
            except:
381
 
                file_existed = False
382
 
            success = False
383
 
            try:
384
 
                try:
385
 
                    self._sftp.rename(tmp_abspath, final_path)
386
 
                except (paramiko.SSHException, IOError), e:
387
 
                    self._translate_io_exception(e, relpath, ': unable to rename')
388
 
                else:
389
 
                    success = True
390
 
            finally:
391
 
                if file_existed:
392
 
                    if success:
393
 
                        self._sftp.unlink(tmp_safety)
394
 
                    else:
395
 
                        self._sftp.rename(tmp_safety, final_path)
 
423
                # raise the saved except
 
424
                raise e
 
425
            # raise the original with its traceback if we can.
 
426
            raise
396
427
 
397
428
    def iter_files_recursive(self):
398
429
        """Walk the relative paths of all files in this transport."""
406
437
            else:
407
438
                yield relpath
408
439
 
409
 
    def mkdir(self, relpath):
 
440
    def mkdir(self, relpath, mode=None):
410
441
        """Create a directory at the given path."""
411
442
        try:
412
 
            path = self._abspath(relpath)
 
443
            path = self._remote_path(relpath)
 
444
            # In the paramiko documentation, it says that passing a mode flag 
 
445
            # will filtered against the server umask.
 
446
            # StubSFTPServer does not do this, which would be nice, because it is
 
447
            # what we really want :)
 
448
            # However, real servers do use umask, so we really should do it that way
413
449
            self._sftp.mkdir(path)
 
450
            if mode is not None:
 
451
                self._sftp.chmod(path, mode=mode)
414
452
        except (paramiko.SSHException, IOError), e:
415
 
            self._translate_io_exception(e, relpath, ': unable to mkdir',
 
453
            self._translate_io_exception(e, path, ': unable to mkdir',
416
454
                failure_exc=FileExists)
417
455
 
418
456
    def _translate_io_exception(self, e, path, more_info='', failure_exc=NoSuchFile):
439
477
            # strange but true, for the paramiko server.
440
478
            if (e.args == ('Failure',)):
441
479
                raise failure_exc(path, str(e) + more_info)
 
480
            mutter('Raising exception with args %s', e.args)
 
481
        if hasattr(e, 'errno'):
 
482
            mutter('Raising exception with errno %s', e.errno)
442
483
        raise e
443
484
 
444
485
    def append(self, relpath, f):
447
488
        location.
448
489
        """
449
490
        try:
450
 
            path = self._abspath(relpath)
 
491
            path = self._remote_path(relpath)
451
492
            fout = self._sftp.file(path, 'ab')
452
493
            self._pump(f, fout)
453
494
        except (IOError, paramiko.SSHException), e:
455
496
 
456
497
    def copy(self, rel_from, rel_to):
457
498
        """Copy the item at rel_from to the location at rel_to"""
458
 
        path_from = self._abspath(rel_from)
459
 
        path_to = self._abspath(rel_to)
 
499
        path_from = self._remote_path(rel_from)
 
500
        path_to = self._remote_path(rel_to)
460
501
        self._copy_abspaths(path_from, path_to)
461
502
 
462
 
    def _copy_abspaths(self, path_from, path_to):
 
503
    def _copy_abspaths(self, path_from, path_to, mode=None):
463
504
        """Copy files given an absolute path
464
505
 
465
506
        :param path_from: Path on remote server to read
474
515
        try:
475
516
            fin = self._sftp.file(path_from, 'rb')
476
517
            try:
477
 
                fout = self._sftp.file(path_to, 'wb')
478
 
                try:
479
 
                    fout.set_pipelined(True)
480
 
                    self._pump(fin, fout)
481
 
                finally:
482
 
                    fout.close()
 
518
                self._put(path_to, fin, mode=mode)
483
519
            finally:
484
520
                fin.close()
485
521
        except (IOError, paramiko.SSHException), e:
486
522
            self._translate_io_exception(e, path_from, ': unable copy to: %r' % path_to)
487
523
 
488
 
    def copy_to(self, relpaths, other, pb=None):
 
524
    def copy_to(self, relpaths, other, mode=None, pb=None):
489
525
        """Copy a set of entries from self into another Transport.
490
526
 
491
527
        :param relpaths: A list/generator of entries to be copied.
497
533
            total = self._get_total(relpaths)
498
534
            count = 0
499
535
            for path in relpaths:
500
 
                path_from = self._abspath(relpath)
501
 
                path_to = other._abspath(relpath)
 
536
                path_from = self._remote_path(relpath)
 
537
                path_to = other._remote_path(relpath)
502
538
                self._update_pb(pb, 'copy-to', count, total)
503
 
                self._copy_abspaths(path_from, path_to)
 
539
                self._copy_abspaths(path_from, path_to, mode=mode)
504
540
                count += 1
505
541
            return count
506
542
        else:
507
 
            return super(SFTPTransport, self).copy_to(relpaths, other, pb=pb)
508
 
 
509
 
        # The dummy implementation just does a simple get + put
510
 
        def copy_entry(path):
511
 
            other.put(path, self.get(path))
512
 
 
513
 
        return self._iterate_over(relpaths, copy_entry, pb, 'copy_to', expand=False)
 
543
            return super(SFTPTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
 
544
 
 
545
    def _rename(self, abs_from, abs_to):
 
546
        """Do a fancy rename on the remote server.
 
547
        
 
548
        Using the implementation provided by osutils.
 
549
        """
 
550
        try:
 
551
            fancy_rename(abs_from, abs_to,
 
552
                    rename_func=self._sftp.rename,
 
553
                    unlink_func=self._sftp.remove)
 
554
        except (IOError, paramiko.SSHException), e:
 
555
            self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
514
556
 
515
557
    def move(self, rel_from, rel_to):
516
558
        """Move the item at rel_from to the location at rel_to"""
517
 
        path_from = self._abspath(rel_from)
518
 
        path_to = self._abspath(rel_to)
519
 
        try:
520
 
            self._sftp.rename(path_from, path_to)
521
 
        except (IOError, paramiko.SSHException), e:
522
 
            self._translate_io_exception(e, path_from, ': unable to move to: %r' % path_to)
 
559
        path_from = self._remote_path(rel_from)
 
560
        path_to = self._remote_path(rel_to)
 
561
        self._rename(path_from, path_to)
523
562
 
524
563
    def delete(self, relpath):
525
564
        """Delete the item at relpath"""
526
 
        path = self._abspath(relpath)
 
565
        path = self._remote_path(relpath)
527
566
        try:
528
567
            self._sftp.remove(path)
529
568
        except (IOError, paramiko.SSHException), e:
538
577
        Return a list of all files at the given location.
539
578
        """
540
579
        # does anything actually use this?
541
 
        path = self._abspath(relpath)
 
580
        path = self._remote_path(relpath)
542
581
        try:
543
582
            return self._sftp.listdir(path)
544
583
        except (IOError, paramiko.SSHException), e:
546
585
 
547
586
    def stat(self, relpath):
548
587
        """Return the stat information for a file."""
549
 
        path = self._abspath(relpath)
 
588
        path = self._remote_path(relpath)
550
589
        try:
551
590
            return self._sftp.stat(path)
552
591
        except (IOError, paramiko.SSHException), e:
578
617
        # that we have taken the lock.
579
618
        return SFTPLock(relpath, self)
580
619
 
581
 
 
582
620
    def _unparse_url(self, path=None):
583
621
        if path is None:
584
622
            path = self._path
585
623
        path = urllib.quote(path)
586
 
        if path.startswith('/'):
587
 
            path = '/%2F' + path[1:]
588
 
        else:
589
 
            path = '/' + path
 
624
        # handle homedir paths
 
625
        if not path.startswith('/'):
 
626
            path = "/~/" + path
590
627
        netloc = urllib.quote(self._host)
591
628
        if self._username is not None:
592
629
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
626
663
        # as a homedir relative path (the path begins with a double slash
627
664
        # if it is absolute).
628
665
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
629
 
        if path.startswith('/'):
630
 
            path = path[1:]
631
 
 
 
666
        # RBC 20060118 we are not using this as its too user hostile. instead
 
667
        # we are following lftp and using /~/foo to mean '~/foo'.
 
668
        # handle homedir paths
 
669
        if path.startswith('/~/'):
 
670
            path = path[3:]
 
671
        elif path == '/~':
 
672
            path = ''
632
673
        return (username, password, host, port, path)
633
674
 
634
675
    def _parse_url(self, url):
652
693
            pass
653
694
        
654
695
        vendor = _get_ssh_vendor()
655
 
        if vendor != 'none':
656
 
            sock = SFTPSubprocess(self._host, self._port, self._username)
 
696
        if vendor == 'loopback':
 
697
            sock = socket.socket()
 
698
            sock.connect((self._host, self._port))
 
699
            self._sftp = SFTPClient(LoopbackSFTP(sock))
 
700
        elif vendor != 'none':
 
701
            sock = SFTPSubprocess(self._host, vendor, self._port,
 
702
                                  self._username)
657
703
            self._sftp = SFTPClient(sock)
658
704
        else:
659
705
            self._paramiko_connect()
667
713
 
668
714
        try:
669
715
            t = paramiko.Transport((self._host, self._port or 22))
 
716
            t.set_log_channel('bzr.paramiko')
670
717
            t.start_client()
671
718
        except paramiko.SSHException, e:
672
719
            raise ConnectionError('Unable to reach SSH host %s:%d' %
691
738
            save_host_keys()
692
739
        if server_key != our_server_key:
693
740
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
694
 
            filename2 = os.path.join(config_dir(), 'ssh_host_keys')
 
741
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
695
742
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
696
743
                (self._host, our_server_key_hex, server_key_hex),
697
744
                ['Try editing %s or %s' % (filename1, filename2)])
733
780
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
734
781
            return
735
782
 
736
 
 
737
783
        if self._password:
738
784
            try:
739
785
                transport.auth_password(username, self._password)
776
822
            pass
777
823
        return False
778
824
 
779
 
    def _sftp_open_exclusive(self, relpath):
 
825
    def _sftp_open_exclusive(self, abspath, mode=None):
780
826
        """Open a remote path exclusively.
781
827
 
782
828
        SFTP supports O_EXCL (SFTP_FLAG_EXCL), which fails if
787
833
        WARNING: This breaks the SFTPClient abstraction, so it
788
834
        could easily break against an updated version of paramiko.
789
835
 
790
 
        :param relpath: The relative path, where the file should be opened
 
836
        :param abspath: The remote absolute path where the file should be opened
 
837
        :param mode: The mode permissions bits for the new file
791
838
        """
792
 
        path = self._sftp._adjust_cwd(self._abspath(relpath))
 
839
        path = self._sftp._adjust_cwd(abspath)
793
840
        attr = SFTPAttributes()
794
 
        mode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
 
841
        if mode is not None:
 
842
            attr.st_mode = mode
 
843
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
795
844
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
796
845
        try:
797
 
            t, msg = self._sftp._request(CMD_OPEN, path, mode, attr)
 
846
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
798
847
            if t != CMD_HANDLE:
799
848
                raise TransportError('Expected an SFTP handle')
800
849
            handle = msg.get_string()
801
 
            return SFTPFile(self._sftp, handle, 'w', -1)
 
850
            return SFTPFile(self._sftp, handle, 'wb', -1)
802
851
        except (paramiko.SSHException, IOError), e:
803
 
            self._translate_io_exception(e, relpath, ': unable to open',
 
852
            self._translate_io_exception(e, abspath, ': unable to open',
804
853
                failure_exc=FileExists)
805
854
 
 
855
 
 
856
# ------------- server test implementation --------------
 
857
import socket
 
858
import threading
 
859
 
 
860
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
 
861
 
 
862
STUB_SERVER_KEY = """
 
863
-----BEGIN RSA PRIVATE KEY-----
 
864
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
 
865
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
 
866
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
 
867
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
 
868
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
 
869
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
 
870
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
 
871
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
 
872
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
 
873
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
 
874
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
 
875
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
 
876
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
 
877
-----END RSA PRIVATE KEY-----
 
878
"""
 
879
    
 
880
 
 
881
class SingleListener(threading.Thread):
 
882
 
 
883
    def __init__(self, callback):
 
884
        threading.Thread.__init__(self)
 
885
        self._callback = callback
 
886
        self._socket = socket.socket()
 
887
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
888
        self._socket.bind(('localhost', 0))
 
889
        self._socket.listen(1)
 
890
        self.port = self._socket.getsockname()[1]
 
891
        self.stop_event = threading.Event()
 
892
 
 
893
    def run(self):
 
894
        s, _ = self._socket.accept()
 
895
        # now close the listen socket
 
896
        self._socket.close()
 
897
        try:
 
898
            self._callback(s, self.stop_event)
 
899
        except socket.error:
 
900
            pass #Ignore socket errors
 
901
        except Exception, x:
 
902
            # probably a failed test
 
903
            warning('Exception from within unit test server thread: %r' % x)
 
904
 
 
905
    def stop(self):
 
906
        self.stop_event.set()
 
907
        # use a timeout here, because if the test fails, the server thread may
 
908
        # never notice the stop_event.
 
909
        self.join(5.0)
 
910
 
 
911
 
 
912
class SFTPServer(Server):
 
913
    """Common code for SFTP server facilities."""
 
914
 
 
915
    def __init__(self):
 
916
        self._original_vendor = None
 
917
        self._homedir = None
 
918
        self._server_homedir = None
 
919
        self._listener = None
 
920
        self._root = None
 
921
        self._vendor = 'none'
 
922
        # sftp server logs
 
923
        self.logs = []
 
924
 
 
925
    def _get_sftp_url(self, path):
 
926
        """Calculate an sftp url to this server for path."""
 
927
        return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
 
928
 
 
929
    def log(self, message):
 
930
        """StubServer uses this to log when a new server is created."""
 
931
        self.logs.append(message)
 
932
 
 
933
    def _run_server(self, s, stop_event):
 
934
        ssh_server = paramiko.Transport(s)
 
935
        key_file = os.path.join(self._homedir, 'test_rsa.key')
 
936
        file(key_file, 'w').write(STUB_SERVER_KEY)
 
937
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
 
938
        ssh_server.add_server_key(host_key)
 
939
        server = StubServer(self)
 
940
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
 
941
                                         StubSFTPServer, root=self._root,
 
942
                                         home=self._server_homedir)
 
943
        event = threading.Event()
 
944
        ssh_server.start_server(event, server)
 
945
        event.wait(5.0)
 
946
        stop_event.wait(30.0)
 
947
    
 
948
    def setUp(self):
 
949
        global _ssh_vendor
 
950
        self._original_vendor = _ssh_vendor
 
951
        _ssh_vendor = self._vendor
 
952
        self._homedir = os.getcwdu()
 
953
        if self._server_homedir is None:
 
954
            self._server_homedir = self._homedir
 
955
        self._root = '/'
 
956
        # FIXME WINDOWS: _root should be _server_homedir[0]:/
 
957
        self._listener = SingleListener(self._run_server)
 
958
        self._listener.setDaemon(True)
 
959
        self._listener.start()
 
960
 
 
961
    def tearDown(self):
 
962
        """See bzrlib.transport.Server.tearDown."""
 
963
        global _ssh_vendor
 
964
        self._listener.stop()
 
965
        _ssh_vendor = self._original_vendor
 
966
 
 
967
 
 
968
class SFTPServerWithoutSSH(SFTPServer):
 
969
    """
 
970
    Common code for an SFTP server over a clear TCP loopback socket,
 
971
    instead of over an SSH secured socket.
 
972
    """
 
973
 
 
974
    def __init__(self):
 
975
        super(SFTPServerWithoutSSH, self).__init__()
 
976
        self._vendor = 'loopback'
 
977
 
 
978
    def _run_server(self, sock, stop_event):
 
979
        class FakeChannel(object):
 
980
            def get_transport(self):
 
981
                return self
 
982
            def get_log_channel(self):
 
983
                return 'paramiko'
 
984
            def get_name(self):
 
985
                return '1'
 
986
            def get_hexdump(self):
 
987
                return False
 
988
 
 
989
        server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
 
990
                                     root=self._root, home=self._server_homedir)
 
991
        server.start_subsystem('sftp', None, sock)
 
992
        server.finish_subsystem()
 
993
 
 
994
 
 
995
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
 
996
    """A test server for sftp transports, using absolute urls."""
 
997
 
 
998
    def get_url(self):
 
999
        """See bzrlib.transport.Server.get_url."""
 
1000
        return self._get_sftp_url(urlescape(self._homedir[1:]))
 
1001
 
 
1002
 
 
1003
class SFTPHomeDirServer(SFTPServerWithoutSSH):
 
1004
    """A test server for sftp transports, using homedir relative urls."""
 
1005
 
 
1006
    def get_url(self):
 
1007
        """See bzrlib.transport.Server.get_url."""
 
1008
        return self._get_sftp_url("~/")
 
1009
 
 
1010
 
 
1011
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
 
1012
    """A test servere for sftp transports, using absolute urls to non-home."""
 
1013
 
 
1014
    def setUp(self):
 
1015
        self._server_homedir = '/dev/noone/runs/tests/here'
 
1016
        super(SFTPSiblingAbsoluteServer, self).setUp()
 
1017
 
 
1018
 
 
1019
def get_test_permutations():
 
1020
    """Return the permutations to be used in testing."""
 
1021
    return [(SFTPTransport, SFTPAbsoluteServer),
 
1022
            (SFTPTransport, SFTPHomeDirServer),
 
1023
            (SFTPTransport, SFTPSiblingAbsoluteServer),
 
1024
            ]