~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: John Arbash Meinel
  • Date: 2005-12-29 02:36:07 UTC
  • mto: (1185.50.36 bzr-jam-integration)
  • mto: This revision was merged to the branch mainline in revision 1536.
  • Revision ID: john@arbash-meinel.com-20051229023607-95f3ed4a0404cfa5
test_revision_info.py is actually a blackbox test.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
2
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
1
# Copyright (C) 2005 Robey Pointer <robey@lag.net>, Canonical Ltd
3
2
 
4
3
# This program is free software; you can redistribute it and/or modify
5
4
# it under the terms of the GNU General Public License as published by
20
19
import errno
21
20
import getpass
22
21
import os
23
 
import random
24
22
import re
25
23
import stat
26
 
import subprocess
27
24
import sys
28
 
import time
29
25
import urllib
30
26
import urlparse
 
27
import time
 
28
import random
 
29
import subprocess
31
30
import weakref
32
31
 
33
 
from bzrlib.config import config_dir, ensure_config_dir_exists
34
 
from bzrlib.errors import (ConnectionError,
35
 
                           FileExists, 
 
32
from bzrlib.errors import (FileExists, 
36
33
                           TransportNotPossible, NoSuchFile, PathNotChild,
37
34
                           TransportError,
38
 
                           LockError, 
39
 
                           PathError,
40
 
                           ParamikoNotPresent,
41
 
                           )
 
35
                           LockError)
 
36
from bzrlib.config import config_dir, ensure_config_dir_exists
 
37
from bzrlib.trace import mutter, warning, error
 
38
from bzrlib.transport import Transport, register_transport
42
39
from bzrlib.osutils import pathjoin, fancy_rename
43
 
from bzrlib.trace import mutter, warning, error
44
 
from bzrlib.transport import (
45
 
    register_urlparse_netloc_protocol,
46
 
    Server,
47
 
    Transport,
48
 
    urlescape,
49
 
    )
50
40
import bzrlib.ui
51
41
 
52
42
try:
53
43
    import paramiko
54
 
except ImportError, e:
55
 
    raise ParamikoNotPresent(e)
 
44
except ImportError:
 
45
    error('The SFTP transport requires paramiko.')
 
46
    raise
56
47
else:
57
48
    from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
58
49
                               SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
61
52
    from paramiko.sftp_file import SFTPFile
62
53
    from paramiko.sftp_client import SFTPClient
63
54
 
64
 
 
65
 
register_urlparse_netloc_protocol('sftp')
66
 
 
67
 
 
68
 
def _ignore_sigint():
69
 
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
70
 
    # doesn't handle it itself.
71
 
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
72
 
    import signal
73
 
    signal.signal(signal.SIGINT, signal.SIG_IGN)
74
 
    
75
 
 
76
 
def os_specific_subprocess_params():
77
 
    """Get O/S specific subprocess parameters."""
78
 
    if sys.platform == 'win32':
79
 
        # setting the process group and closing fds is not supported on 
80
 
        # win32
81
 
        return {}
82
 
    else:
83
 
        # We close fds other than the pipes as the child process does not need 
84
 
        # them to be open.
85
 
        #
86
 
        # We also set the child process to ignore SIGINT.  Normally the signal
87
 
        # would be sent to every process in the foreground process group, but
88
 
        # this causes it to be seen only by bzr and not by ssh.  Python will
89
 
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
90
 
        # to release locks or do other cleanup over ssh before the connection
91
 
        # goes away.  
92
 
        # <https://launchpad.net/products/bzr/+bug/5987>
93
 
        #
94
 
        # Running it in a separate process group is not good because then it
95
 
        # can't get non-echoed input of a password or passphrase.
96
 
        # <https://launchpad.net/products/bzr/+bug/40508>
97
 
        return {'preexec_fn': _ignore_sigint,
98
 
                'close_fds': True,
99
 
                }
100
 
 
101
 
 
102
 
# don't use prefetch unless paramiko version >= 1.5.2 (there were bugs earlier)
103
 
_default_do_prefetch = False
104
 
if getattr(paramiko, '__version_info__', (0, 0, 0)) >= (1, 5, 5):
105
 
    _default_do_prefetch = True
106
 
 
 
55
if 'sftp' not in urlparse.uses_netloc: urlparse.uses_netloc.append('sftp')
 
56
 
 
57
 
 
58
_close_fds = True
 
59
if sys.platform == 'win32':
 
60
    # close_fds not supported on win32
 
61
    _close_fds = False
107
62
 
108
63
_ssh_vendor = None
109
64
def _get_ssh_vendor():
114
69
 
115
70
    _ssh_vendor = 'none'
116
71
 
117
 
    if 'BZR_SSH' in os.environ:
118
 
        _ssh_vendor = os.environ['BZR_SSH']
119
 
        if _ssh_vendor == 'paramiko':
120
 
            _ssh_vendor = 'none'
121
 
        return _ssh_vendor
122
 
 
123
72
    try:
124
73
        p = subprocess.Popen(['ssh', '-V'],
 
74
                             close_fds=_close_fds,
125
75
                             stdin=subprocess.PIPE,
126
76
                             stdout=subprocess.PIPE,
127
 
                             stderr=subprocess.PIPE,
128
 
                             **os_specific_subprocess_params())
 
77
                             stderr=subprocess.PIPE)
129
78
        returncode = p.returncode
130
79
        stdout, stderr = p.communicate()
131
80
    except OSError:
150
99
 
151
100
class SFTPSubprocess:
152
101
    """A socket-like object that talks to an ssh subprocess via pipes."""
153
 
    def __init__(self, hostname, vendor, port=None, user=None):
 
102
    def __init__(self, hostname, port=None, user=None):
 
103
        vendor = _get_ssh_vendor()
154
104
        assert vendor in ['openssh', 'ssh']
155
105
        if vendor == 'openssh':
156
106
            args = ['ssh',
170
120
                args.extend(['-l', user])
171
121
            args.extend(['-s', 'sftp', hostname])
172
122
 
173
 
        self.proc = subprocess.Popen(args,
 
123
        self.proc = subprocess.Popen(args, close_fds=_close_fds,
174
124
                                     stdin=subprocess.PIPE,
175
 
                                     stdout=subprocess.PIPE,
176
 
                                     **os_specific_subprocess_params())
 
125
                                     stdout=subprocess.PIPE)
177
126
 
178
127
    def send(self, data):
179
128
        return os.write(self.proc.stdin.fileno(), data)
194
143
        self.proc.wait()
195
144
 
196
145
 
197
 
class LoopbackSFTP(object):
198
 
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
199
 
 
200
 
    def __init__(self, sock):
201
 
        self.__socket = sock
202
 
 
203
 
    def send(self, data):
204
 
        return self.__socket.send(data)
205
 
 
206
 
    def recv(self, n):
207
 
        return self.__socket.recv(n)
208
 
 
209
 
    def recv_ready(self):
210
 
        return True
211
 
 
212
 
    def close(self):
213
 
        self.__socket.close()
214
 
 
215
 
 
216
146
SYSTEM_HOSTKEYS = {}
217
147
BZR_HOSTKEYS = {}
218
148
 
222
152
# X seconds. But that requires a lot more fanciness.
223
153
_connected_hosts = weakref.WeakValueDictionary()
224
154
 
225
 
def clear_connection_cache():
226
 
    """Remove all hosts from the SFTP connection cache.
227
 
 
228
 
    Primarily useful for test cases wanting to force garbage collection.
229
 
    """
230
 
    _connected_hosts.clear()
231
 
 
232
 
 
233
155
def load_host_keys():
234
156
    """
235
157
    Load system host keys (probably doesn't work on windows) and any
247
169
        mutter('failed to load bzr host keys: ' + str(e))
248
170
        save_host_keys()
249
171
 
250
 
 
251
172
def save_host_keys():
252
173
    """
253
174
    Save "discovered" host keys in $(config)/ssh_host_keys/.
278
199
        self.lock_path = path + '.write-lock'
279
200
        self.transport = transport
280
201
        try:
281
 
            # RBC 20060103 FIXME should we be using private methods here ?
282
 
            abspath = transport._remote_path(self.lock_path)
 
202
            abspath = transport._abspath(self.lock_path)
283
203
            self.lock_file = transport._sftp_open_exclusive(abspath)
284
204
        except FileExists:
285
205
            raise LockError('File %r already locked' % (self.path,))
287
207
    def __del__(self):
288
208
        """Should this warn, or actually try to cleanup?"""
289
209
        if self.lock_file:
290
 
            warning("SFTPLock %r not explicitly unlocked" % (self.path,))
 
210
            warn("SFTPLock %r not explicitly unlocked" % (self.path,))
291
211
            self.unlock()
292
212
 
293
213
    def unlock(self):
305
225
    """
306
226
    Transport implementation for SFTP access.
307
227
    """
308
 
    _do_prefetch = _default_do_prefetch
 
228
    _do_prefetch = False # Right now Paramiko's prefetch support causes things to hang
309
229
 
310
230
    def __init__(self, base, clone_from=None):
311
231
        assert base.startswith('sftp://')
312
232
        self._parse_url(base)
313
233
        base = self._unparse_url()
314
 
        if base[-1] != '/':
315
 
            base += '/'
316
234
        super(SFTPTransport, self).__init__(base)
317
235
        if clone_from is None:
318
236
            self._sftp_connect()
345
263
        @param relpath: the relative path or path components
346
264
        @type relpath: str or list
347
265
        """
348
 
        return self._unparse_url(self._remote_path(relpath))
 
266
        return self._unparse_url(self._abspath(relpath))
349
267
    
350
 
    def _remote_path(self, relpath):
351
 
        """Return the path to be passed along the sftp protocol for relpath.
352
 
        
353
 
        relpath is a urlencoded string.
354
 
        """
 
268
    def _abspath(self, relpath):
 
269
        """Return the absolute path segment without the SFTP URL."""
355
270
        # FIXME: share the common code across transports
356
271
        assert isinstance(relpath, basestring)
357
 
        relpath = urllib.unquote(relpath).split('/')
 
272
        relpath = [urllib.unquote(relpath)]
358
273
        basepath = self._path.split('/')
359
274
        if len(basepath) > 0 and basepath[-1] == '':
360
275
            basepath = basepath[:-1]
372
287
                basepath.append(p)
373
288
 
374
289
        path = '/'.join(basepath)
 
290
        # could still be a "relative" path here, but relative on the sftp server
375
291
        return path
376
292
 
377
293
    def relpath(self, abspath):
389
305
            extra = ': ' + ', '.join(error)
390
306
            raise PathNotChild(abspath, self.base, extra=extra)
391
307
        pl = len(self._path)
392
 
        return path[pl:].strip('/')
 
308
        return path[pl:].lstrip('/')
393
309
 
394
310
    def has(self, relpath):
395
311
        """
396
312
        Does the target location exist?
397
313
        """
398
314
        try:
399
 
            self._sftp.stat(self._remote_path(relpath))
 
315
            self._sftp.stat(self._abspath(relpath))
400
316
            return True
401
317
        except IOError:
402
318
            return False
403
319
 
404
 
    def get(self, relpath):
 
320
    def get(self, relpath, decode=False):
405
321
        """
406
322
        Get the file at the given relative path.
407
323
 
408
324
        :param relpath: The relative path to the file
409
325
        """
410
326
        try:
411
 
            path = self._remote_path(relpath)
 
327
            path = self._abspath(relpath)
412
328
            f = self._sftp.file(path, mode='rb')
413
 
            if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
 
329
            if self._do_prefetch and hasattr(f, 'prefetch'):
414
330
                f.prefetch()
415
331
            return f
416
332
        except (IOError, paramiko.SSHException), e:
443
359
        :param f:       File-like or string object.
444
360
        :param mode: The final mode for the file
445
361
        """
446
 
        final_path = self._remote_path(relpath)
 
362
        final_path = self._abspath(relpath)
447
363
        self._put(final_path, f, mode=mode)
448
364
 
449
365
    def _put(self, abspath, f, mode=None):
462
378
                self._sftp.chmod(tmp_abspath, mode)
463
379
            fout.close()
464
380
            closed = True
465
 
            self._rename_and_overwrite(tmp_abspath, abspath)
 
381
            self._rename(tmp_abspath, abspath)
466
382
        except Exception, e:
467
383
            # If we fail, try to clean up the temporary file
468
384
            # before we throw the exception
476
392
                    fout.close()
477
393
                self._sftp.remove(tmp_abspath)
478
394
            except:
479
 
                # raise the saved except
480
 
                raise e
481
 
            # raise the original with its traceback if we can.
482
 
            raise
 
395
                pass
 
396
            raise e
483
397
 
484
398
    def iter_files_recursive(self):
485
399
        """Walk the relative paths of all files in this transport."""
496
410
    def mkdir(self, relpath, mode=None):
497
411
        """Create a directory at the given path."""
498
412
        try:
499
 
            path = self._remote_path(relpath)
 
413
            path = self._abspath(relpath)
500
414
            # In the paramiko documentation, it says that passing a mode flag 
501
415
            # will filtered against the server umask.
502
416
            # StubSFTPServer does not do this, which would be nice, because it is
509
423
            self._translate_io_exception(e, path, ': unable to mkdir',
510
424
                failure_exc=FileExists)
511
425
 
512
 
    def _translate_io_exception(self, e, path, more_info='', 
513
 
                                failure_exc=PathError):
 
426
    def _translate_io_exception(self, e, path, more_info='', failure_exc=NoSuchFile):
514
427
        """Translate a paramiko or IOError into a friendlier exception.
515
428
 
516
429
        :param e: The original exception
520
433
        :param failure_exc: Paramiko has the super fun ability to raise completely
521
434
                           opaque errors that just set "e.args = ('Failure',)" with
522
435
                           no more information.
523
 
                           If this parameter is set, it defines the exception 
524
 
                           to raise in these cases.
 
436
                           This sometimes means FileExists, but it also sometimes
 
437
                           means NoSuchFile
525
438
        """
526
439
        # paramiko seems to generate detailless errors.
527
440
        self._translate_error(e, path, raise_generic=False)
539
452
            mutter('Raising exception with errno %s', e.errno)
540
453
        raise e
541
454
 
542
 
    def append(self, relpath, f, mode=None):
 
455
    def append(self, relpath, f):
543
456
        """
544
457
        Append the text in the file-like object into the final
545
458
        location.
546
459
        """
547
460
        try:
548
 
            path = self._remote_path(relpath)
 
461
            path = self._abspath(relpath)
549
462
            fout = self._sftp.file(path, 'ab')
550
 
            if mode is not None:
551
 
                self._sftp.chmod(path, mode)
552
 
            result = fout.tell()
553
463
            self._pump(f, fout)
554
 
            return result
555
464
        except (IOError, paramiko.SSHException), e:
556
465
            self._translate_io_exception(e, relpath, ': unable to append')
557
466
 
558
 
    def rename(self, rel_from, rel_to):
559
 
        """Rename without special overwriting"""
 
467
    def copy(self, rel_from, rel_to):
 
468
        """Copy the item at rel_from to the location at rel_to"""
 
469
        path_from = self._abspath(rel_from)
 
470
        path_to = self._abspath(rel_to)
 
471
        self._copy_abspaths(path_from, path_to)
 
472
 
 
473
    def _copy_abspaths(self, path_from, path_to, mode=None):
 
474
        """Copy files given an absolute path
 
475
 
 
476
        :param path_from: Path on remote server to read
 
477
        :param path_to: Path on remote server to write
 
478
        :return: None
 
479
 
 
480
        TODO: Should the destination location be atomically created?
 
481
              This has not been specified
 
482
        TODO: This should use some sort of remote copy, rather than
 
483
              pulling the data locally, and then writing it remotely
 
484
        """
560
485
        try:
561
 
            self._sftp.rename(self._remote_path(rel_from),
562
 
                              self._remote_path(rel_to))
 
486
            fin = self._sftp.file(path_from, 'rb')
 
487
            try:
 
488
                self._put(path_to, fin, mode=mode)
 
489
            finally:
 
490
                fin.close()
563
491
        except (IOError, paramiko.SSHException), e:
564
 
            self._translate_io_exception(e, rel_from,
565
 
                    ': unable to rename to %r' % (rel_to))
566
 
 
567
 
    def _rename_and_overwrite(self, abs_from, abs_to):
 
492
            self._translate_io_exception(e, path_from, ': unable copy to: %r' % path_to)
 
493
 
 
494
    def copy_to(self, relpaths, other, mode=None, pb=None):
 
495
        """Copy a set of entries from self into another Transport.
 
496
 
 
497
        :param relpaths: A list/generator of entries to be copied.
 
498
        """
 
499
        if isinstance(other, SFTPTransport) and other._sftp is self._sftp:
 
500
            # Both from & to are on the same remote filesystem
 
501
            # We can use a remote copy, instead of pulling locally, and pushing
 
502
 
 
503
            total = self._get_total(relpaths)
 
504
            count = 0
 
505
            for path in relpaths:
 
506
                path_from = self._abspath(relpath)
 
507
                path_to = other._abspath(relpath)
 
508
                self._update_pb(pb, 'copy-to', count, total)
 
509
                self._copy_abspaths(path_from, path_to, mode=mode)
 
510
                count += 1
 
511
            return count
 
512
        else:
 
513
            return super(SFTPTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
 
514
 
 
515
    def _rename(self, abs_from, abs_to):
568
516
        """Do a fancy rename on the remote server.
569
517
        
570
518
        Using the implementation provided by osutils.
578
526
 
579
527
    def move(self, rel_from, rel_to):
580
528
        """Move the item at rel_from to the location at rel_to"""
581
 
        path_from = self._remote_path(rel_from)
582
 
        path_to = self._remote_path(rel_to)
583
 
        self._rename_and_overwrite(path_from, path_to)
 
529
        path_from = self._abspath(rel_from)
 
530
        path_to = self._abspath(rel_to)
 
531
        self._rename(path_from, path_to)
584
532
 
585
533
    def delete(self, relpath):
586
534
        """Delete the item at relpath"""
587
 
        path = self._remote_path(relpath)
 
535
        path = self._abspath(relpath)
588
536
        try:
589
537
            self._sftp.remove(path)
590
538
        except (IOError, paramiko.SSHException), e:
599
547
        Return a list of all files at the given location.
600
548
        """
601
549
        # does anything actually use this?
602
 
        path = self._remote_path(relpath)
 
550
        path = self._abspath(relpath)
603
551
        try:
604
552
            return self._sftp.listdir(path)
605
553
        except (IOError, paramiko.SSHException), e:
606
554
            self._translate_io_exception(e, path, ': failed to list_dir')
607
555
 
608
 
    def rmdir(self, relpath):
609
 
        """See Transport.rmdir."""
610
 
        path = self._remote_path(relpath)
611
 
        try:
612
 
            return self._sftp.rmdir(path)
613
 
        except (IOError, paramiko.SSHException), e:
614
 
            self._translate_io_exception(e, path, ': failed to rmdir')
615
 
 
616
556
    def stat(self, relpath):
617
557
        """Return the stat information for a file."""
618
 
        path = self._remote_path(relpath)
 
558
        path = self._abspath(relpath)
619
559
        try:
620
560
            return self._sftp.stat(path)
621
561
        except (IOError, paramiko.SSHException), e:
647
587
        # that we have taken the lock.
648
588
        return SFTPLock(relpath, self)
649
589
 
 
590
 
650
591
    def _unparse_url(self, path=None):
651
592
        if path is None:
652
593
            path = self._path
653
594
        path = urllib.quote(path)
654
 
        # handle homedir paths
655
 
        if not path.startswith('/'):
656
 
            path = "/~/" + path
 
595
        if path.startswith('/'):
 
596
            path = '/%2F' + path[1:]
 
597
        else:
 
598
            path = '/' + path
657
599
        netloc = urllib.quote(self._host)
658
600
        if self._username is not None:
659
601
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
660
602
        if self._port is not None:
661
603
            netloc = '%s:%d' % (netloc, self._port)
 
604
 
662
605
        return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
663
606
 
664
607
    def _split_url(self, url):
692
635
        # as a homedir relative path (the path begins with a double slash
693
636
        # if it is absolute).
694
637
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
695
 
        # RBC 20060118 we are not using this as its too user hostile. instead
696
 
        # we are following lftp and using /~/foo to mean '~/foo'.
697
 
        # handle homedir paths
698
 
        if path.startswith('/~/'):
699
 
            path = path[3:]
700
 
        elif path == '/~':
701
 
            path = ''
 
638
        if path.startswith('/'):
 
639
            path = path[1:]
 
640
 
702
641
        return (username, password, host, port, path)
703
642
 
704
643
    def _parse_url(self, url):
722
661
            pass
723
662
        
724
663
        vendor = _get_ssh_vendor()
725
 
        if vendor == 'loopback':
726
 
            sock = socket.socket()
727
 
            sock.connect((self._host, self._port))
728
 
            self._sftp = SFTPClient(LoopbackSFTP(sock))
729
 
        elif vendor != 'none':
730
 
            sock = SFTPSubprocess(self._host, vendor, self._port,
731
 
                                  self._username)
 
664
        if vendor != 'none':
 
665
            sock = SFTPSubprocess(self._host, self._port, self._username)
732
666
            self._sftp = SFTPClient(sock)
733
667
        else:
734
668
            self._paramiko_connect()
742
676
 
743
677
        try:
744
678
            t = paramiko.Transport((self._host, self._port or 22))
745
 
            t.set_log_channel('bzr.paramiko')
746
679
            t.start_client()
747
680
        except paramiko.SSHException, e:
748
681
            raise ConnectionError('Unable to reach SSH host %s:%d' %
809
742
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
810
743
            return
811
744
 
 
745
 
812
746
        if self._password:
813
747
            try:
814
748
                transport.auth_password(username, self._password)
881
815
            self._translate_io_exception(e, abspath, ': unable to open',
882
816
                failure_exc=FileExists)
883
817
 
884
 
 
885
 
# ------------- server test implementation --------------
886
 
import socket
887
 
import threading
888
 
 
889
 
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
890
 
 
891
 
STUB_SERVER_KEY = """
892
 
-----BEGIN RSA PRIVATE KEY-----
893
 
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
894
 
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
895
 
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
896
 
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
897
 
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
898
 
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
899
 
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
900
 
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
901
 
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
902
 
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
903
 
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
904
 
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
905
 
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
906
 
-----END RSA PRIVATE KEY-----
907
 
"""
908
 
    
909
 
 
910
 
class SingleListener(threading.Thread):
911
 
 
912
 
    def __init__(self, callback):
913
 
        threading.Thread.__init__(self)
914
 
        self._callback = callback
915
 
        self._socket = socket.socket()
916
 
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
917
 
        self._socket.bind(('localhost', 0))
918
 
        self._socket.listen(1)
919
 
        self.port = self._socket.getsockname()[1]
920
 
        self.stop_event = threading.Event()
921
 
 
922
 
    def run(self):
923
 
        s, _ = self._socket.accept()
924
 
        # now close the listen socket
925
 
        self._socket.close()
926
 
        try:
927
 
            self._callback(s, self.stop_event)
928
 
        except socket.error:
929
 
            pass #Ignore socket errors
930
 
        except Exception, x:
931
 
            # probably a failed test
932
 
            warning('Exception from within unit test server thread: %r' % x)
933
 
 
934
 
    def stop(self):
935
 
        self.stop_event.set()
936
 
        # use a timeout here, because if the test fails, the server thread may
937
 
        # never notice the stop_event.
938
 
        self.join(5.0)
939
 
 
940
 
 
941
 
class SFTPServer(Server):
942
 
    """Common code for SFTP server facilities."""
943
 
 
944
 
    def __init__(self):
945
 
        self._original_vendor = None
946
 
        self._homedir = None
947
 
        self._server_homedir = None
948
 
        self._listener = None
949
 
        self._root = None
950
 
        self._vendor = 'none'
951
 
        # sftp server logs
952
 
        self.logs = []
953
 
 
954
 
    def _get_sftp_url(self, path):
955
 
        """Calculate an sftp url to this server for path."""
956
 
        return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
957
 
 
958
 
    def log(self, message):
959
 
        """StubServer uses this to log when a new server is created."""
960
 
        self.logs.append(message)
961
 
 
962
 
    def _run_server(self, s, stop_event):
963
 
        ssh_server = paramiko.Transport(s)
964
 
        key_file = os.path.join(self._homedir, 'test_rsa.key')
965
 
        file(key_file, 'w').write(STUB_SERVER_KEY)
966
 
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
967
 
        ssh_server.add_server_key(host_key)
968
 
        server = StubServer(self)
969
 
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
970
 
                                         StubSFTPServer, root=self._root,
971
 
                                         home=self._server_homedir)
972
 
        event = threading.Event()
973
 
        ssh_server.start_server(event, server)
974
 
        event.wait(5.0)
975
 
        stop_event.wait(30.0)
976
 
    
977
 
    def setUp(self):
978
 
        global _ssh_vendor
979
 
        self._original_vendor = _ssh_vendor
980
 
        _ssh_vendor = self._vendor
981
 
        self._homedir = os.getcwdu()
982
 
        if self._server_homedir is None:
983
 
            self._server_homedir = self._homedir
984
 
        self._root = '/'
985
 
        # FIXME WINDOWS: _root should be _server_homedir[0]:/
986
 
        self._listener = SingleListener(self._run_server)
987
 
        self._listener.setDaemon(True)
988
 
        self._listener.start()
989
 
 
990
 
    def tearDown(self):
991
 
        """See bzrlib.transport.Server.tearDown."""
992
 
        global _ssh_vendor
993
 
        self._listener.stop()
994
 
        _ssh_vendor = self._original_vendor
995
 
 
996
 
 
997
 
class SFTPFullAbsoluteServer(SFTPServer):
998
 
    """A test server for sftp transports, using absolute urls and ssh."""
999
 
 
1000
 
    def get_url(self):
1001
 
        """See bzrlib.transport.Server.get_url."""
1002
 
        return self._get_sftp_url(urlescape(self._homedir[1:]))
1003
 
 
1004
 
 
1005
 
class SFTPServerWithoutSSH(SFTPServer):
1006
 
    """An SFTP server that uses a simple TCP socket pair rather than SSH."""
1007
 
 
1008
 
    def __init__(self):
1009
 
        super(SFTPServerWithoutSSH, self).__init__()
1010
 
        self._vendor = 'loopback'
1011
 
 
1012
 
    def _run_server(self, sock, stop_event):
1013
 
        class FakeChannel(object):
1014
 
            def get_transport(self):
1015
 
                return self
1016
 
            def get_log_channel(self):
1017
 
                return 'paramiko'
1018
 
            def get_name(self):
1019
 
                return '1'
1020
 
            def get_hexdump(self):
1021
 
                return False
1022
 
 
1023
 
        server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
1024
 
                                     root=self._root, home=self._server_homedir)
1025
 
        server.start_subsystem('sftp', None, sock)
1026
 
        server.finish_subsystem()
1027
 
 
1028
 
 
1029
 
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
1030
 
    """A test server for sftp transports, using absolute urls."""
1031
 
 
1032
 
    def get_url(self):
1033
 
        """See bzrlib.transport.Server.get_url."""
1034
 
        return self._get_sftp_url(urlescape(self._homedir[1:]))
1035
 
 
1036
 
 
1037
 
class SFTPHomeDirServer(SFTPServerWithoutSSH):
1038
 
    """A test server for sftp transports, using homedir relative urls."""
1039
 
 
1040
 
    def get_url(self):
1041
 
        """See bzrlib.transport.Server.get_url."""
1042
 
        return self._get_sftp_url("~/")
1043
 
 
1044
 
 
1045
 
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
1046
 
    """A test servere for sftp transports, using absolute urls to non-home."""
1047
 
 
1048
 
    def setUp(self):
1049
 
        self._server_homedir = '/dev/noone/runs/tests/here'
1050
 
        super(SFTPSiblingAbsoluteServer, self).setUp()
1051
 
 
1052
 
 
1053
 
def get_test_permutations():
1054
 
    """Return the permutations to be used in testing."""
1055
 
    return [(SFTPTransport, SFTPAbsoluteServer),
1056
 
            (SFTPTransport, SFTPHomeDirServer),
1057
 
            (SFTPTransport, SFTPSiblingAbsoluteServer),
1058
 
            ]