~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: Michael Ellerman
  • Date: 2005-12-10 22:11:13 UTC
  • mto: This revision was merged to the branch mainline in revision 1528.
  • Revision ID: michael@ellerman.id.au-20051210221113-99ca561aaab4661e
Simplify handling of DivergedBranches in cmd_pull()

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.config import config_dir, ensure_config_dir_exists
33
 
from bzrlib.errors import (ConnectionError,
34
 
                           FileExists, 
 
32
from bzrlib.errors import (FileExists, 
35
33
                           TransportNotPossible, NoSuchFile, PathNotChild,
36
34
                           TransportError,
37
 
                           LockError
38
 
                           )
39
 
from bzrlib.osutils import pathjoin, fancy_rename
 
35
                           LockError)
 
36
from bzrlib.config import config_dir
40
37
from bzrlib.trace import mutter, warning, error
41
 
from bzrlib.transport import Transport, Server, urlescape
 
38
from bzrlib.transport import Transport, register_transport
42
39
import bzrlib.ui
43
40
 
44
41
try:
101
98
 
102
99
class SFTPSubprocess:
103
100
    """A socket-like object that talks to an ssh subprocess via pipes."""
104
 
    def __init__(self, hostname, vendor, port=None, user=None):
 
101
    def __init__(self, hostname, port=None, user=None):
 
102
        vendor = _get_ssh_vendor()
105
103
        assert vendor in ['openssh', 'ssh']
106
104
        if vendor == 'openssh':
107
105
            args = ['ssh',
128
126
    def send(self, data):
129
127
        return os.write(self.proc.stdin.fileno(), data)
130
128
 
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
 
 
138
129
    def recv(self, count):
139
130
        return os.read(self.proc.stdout.fileno(), count)
140
131
 
144
135
        self.proc.wait()
145
136
 
146
137
 
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
 
 
166
138
SYSTEM_HOSTKEYS = {}
167
139
BZR_HOSTKEYS = {}
168
140
 
172
144
# X seconds. But that requires a lot more fanciness.
173
145
_connected_hosts = weakref.WeakValueDictionary()
174
146
 
175
 
 
176
147
def load_host_keys():
177
148
    """
178
149
    Load system host keys (probably doesn't work on windows) and any
183
154
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
184
155
    except Exception, e:
185
156
        mutter('failed to load system host keys: ' + str(e))
186
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
157
    bzr_hostkey_path = os.path.join(config_dir(), 'ssh_host_keys')
187
158
    try:
188
159
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
189
160
    except Exception, e:
190
161
        mutter('failed to load bzr host keys: ' + str(e))
191
162
        save_host_keys()
192
163
 
193
 
 
194
164
def save_host_keys():
195
165
    """
196
166
    Save "discovered" host keys in $(config)/ssh_host_keys/.
197
167
    """
198
168
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
199
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
200
 
    ensure_config_dir_exists()
201
 
 
 
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())
202
172
    try:
203
173
        f = open(bzr_hostkey_path, 'w')
204
174
        f.write('# SSH host keys collected by bzr\n')
221
191
        self.lock_path = path + '.write-lock'
222
192
        self.transport = transport
223
193
        try:
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)
 
194
            self.lock_file = transport._sftp_open_exclusive(self.lock_path)
227
195
        except FileExists:
228
196
            raise LockError('File %r already locked' % (self.path,))
229
197
 
230
198
    def __del__(self):
231
199
        """Should this warn, or actually try to cleanup?"""
232
200
        if self.lock_file:
233
 
            warning("SFTPLock %r not explicitly unlocked" % (self.path,))
 
201
            warn("SFTPLock %r not explicitly unlocked" % (self.path,))
234
202
            self.unlock()
235
203
 
236
204
    def unlock(self):
244
212
            # What specific errors should we catch here?
245
213
            pass
246
214
 
247
 
 
248
215
class SFTPTransport (Transport):
249
216
    """
250
217
    Transport implementation for SFTP access.
255
222
        assert base.startswith('sftp://')
256
223
        self._parse_url(base)
257
224
        base = self._unparse_url()
258
 
        if base[-1] != '/':
259
 
            base = base + '/'
260
225
        super(SFTPTransport, self).__init__(base)
261
226
        if clone_from is None:
262
227
            self._sftp_connect()
289
254
        @param relpath: the relative path or path components
290
255
        @type relpath: str or list
291
256
        """
292
 
        return self._unparse_url(self._remote_path(relpath))
 
257
        return self._unparse_url(self._abspath(relpath))
293
258
    
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
 
        """
 
259
    def _abspath(self, relpath):
 
260
        """Return the absolute path segment without the SFTP URL."""
299
261
        # FIXME: share the common code across transports
300
262
        assert isinstance(relpath, basestring)
301
 
        relpath = urllib.unquote(relpath).split('/')
 
263
        relpath = [urllib.unquote(relpath)]
302
264
        basepath = self._path.split('/')
303
265
        if len(basepath) > 0 and basepath[-1] == '':
304
266
            basepath = basepath[:-1]
316
278
                basepath.append(p)
317
279
 
318
280
        path = '/'.join(basepath)
 
281
        # could still be a "relative" path here, but relative on the sftp server
319
282
        return path
320
283
 
321
284
    def relpath(self, abspath):
333
296
            extra = ': ' + ', '.join(error)
334
297
            raise PathNotChild(abspath, self.base, extra=extra)
335
298
        pl = len(self._path)
336
 
        return path[pl:].strip('/')
 
299
        return path[pl:].lstrip('/')
337
300
 
338
301
    def has(self, relpath):
339
302
        """
340
303
        Does the target location exist?
341
304
        """
342
305
        try:
343
 
            self._sftp.stat(self._remote_path(relpath))
 
306
            self._sftp.stat(self._abspath(relpath))
344
307
            return True
345
308
        except IOError:
346
309
            return False
352
315
        :param relpath: The relative path to the file
353
316
        """
354
317
        try:
355
 
            path = self._remote_path(relpath)
356
 
            f = self._sftp.file(path, mode='rb')
 
318
            path = self._abspath(relpath)
 
319
            f = self._sftp.file(path)
357
320
            if self._do_prefetch and hasattr(f, 'prefetch'):
358
321
                f.prefetch()
359
322
            return f
379
342
            f.prefetch()
380
343
        return f
381
344
 
382
 
    def put(self, relpath, f, mode=None):
 
345
    def put(self, relpath, f):
383
346
        """
384
347
        Copy the file-like or string object into the location.
385
348
 
386
349
        :param relpath: Location to put the contents, relative to base.
387
350
        :param f:       File-like or string object.
388
 
        :param mode: The final mode for the file
389
351
        """
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(),
 
352
        final_path = self._abspath(relpath)
 
353
        tmp_relpath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
396
354
                        os.getpid(), random.randint(0,0x7FFFFFFF))
397
 
        fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
398
 
        closed = False
 
355
        tmp_abspath = self._abspath(tmp_relpath)
 
356
        fout = self._sftp_open_exclusive(tmp_relpath)
 
357
 
399
358
        try:
400
359
            try:
401
 
                fout.set_pipelined(True)
402
360
                self._pump(f, fout)
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)
 
361
            except (paramiko.SSHException, IOError), e:
 
362
                self._translate_io_exception(e, relpath, ': unable to write')
410
363
        except Exception, e:
411
364
            # If we fail, try to clean up the temporary file
412
365
            # before we throw the exception
413
366
            # 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())
418
367
            try:
419
 
                if not closed:
420
 
                    fout.close()
 
368
                fout.close()
421
369
                self._sftp.remove(tmp_abspath)
422
370
            except:
423
 
                # raise the saved except
424
 
                raise e
425
 
            # raise the original with its traceback if we can.
426
 
            raise
 
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)
427
396
 
428
397
    def iter_files_recursive(self):
429
398
        """Walk the relative paths of all files in this transport."""
437
406
            else:
438
407
                yield relpath
439
408
 
440
 
    def mkdir(self, relpath, mode=None):
 
409
    def mkdir(self, relpath):
441
410
        """Create a directory at the given path."""
442
411
        try:
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
 
412
            path = self._abspath(relpath)
449
413
            self._sftp.mkdir(path)
450
 
            if mode is not None:
451
 
                self._sftp.chmod(path, mode=mode)
452
414
        except (paramiko.SSHException, IOError), e:
453
 
            self._translate_io_exception(e, path, ': unable to mkdir',
 
415
            self._translate_io_exception(e, relpath, ': unable to mkdir',
454
416
                failure_exc=FileExists)
455
417
 
456
418
    def _translate_io_exception(self, e, path, more_info='', failure_exc=NoSuchFile):
477
439
            # strange but true, for the paramiko server.
478
440
            if (e.args == ('Failure',)):
479
441
                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)
483
442
        raise e
484
443
 
485
444
    def append(self, relpath, f):
488
447
        location.
489
448
        """
490
449
        try:
491
 
            path = self._remote_path(relpath)
 
450
            path = self._abspath(relpath)
492
451
            fout = self._sftp.file(path, 'ab')
493
452
            self._pump(f, fout)
494
453
        except (IOError, paramiko.SSHException), e:
496
455
 
497
456
    def copy(self, rel_from, rel_to):
498
457
        """Copy the item at rel_from to the location at rel_to"""
499
 
        path_from = self._remote_path(rel_from)
500
 
        path_to = self._remote_path(rel_to)
 
458
        path_from = self._abspath(rel_from)
 
459
        path_to = self._abspath(rel_to)
501
460
        self._copy_abspaths(path_from, path_to)
502
461
 
503
 
    def _copy_abspaths(self, path_from, path_to, mode=None):
 
462
    def _copy_abspaths(self, path_from, path_to):
504
463
        """Copy files given an absolute path
505
464
 
506
465
        :param path_from: Path on remote server to read
515
474
        try:
516
475
            fin = self._sftp.file(path_from, 'rb')
517
476
            try:
518
 
                self._put(path_to, fin, mode=mode)
 
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()
519
483
            finally:
520
484
                fin.close()
521
485
        except (IOError, paramiko.SSHException), e:
522
486
            self._translate_io_exception(e, path_from, ': unable copy to: %r' % path_to)
523
487
 
524
 
    def copy_to(self, relpaths, other, mode=None, pb=None):
 
488
    def copy_to(self, relpaths, other, pb=None):
525
489
        """Copy a set of entries from self into another Transport.
526
490
 
527
491
        :param relpaths: A list/generator of entries to be copied.
533
497
            total = self._get_total(relpaths)
534
498
            count = 0
535
499
            for path in relpaths:
536
 
                path_from = self._remote_path(relpath)
537
 
                path_to = other._remote_path(relpath)
 
500
                path_from = self._abspath(relpath)
 
501
                path_to = other._abspath(relpath)
538
502
                self._update_pb(pb, 'copy-to', count, total)
539
 
                self._copy_abspaths(path_from, path_to, mode=mode)
 
503
                self._copy_abspaths(path_from, path_to)
540
504
                count += 1
541
505
            return count
542
506
        else:
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))
 
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)
556
514
 
557
515
    def move(self, rel_from, rel_to):
558
516
        """Move the item at rel_from to the location at rel_to"""
559
 
        path_from = self._remote_path(rel_from)
560
 
        path_to = self._remote_path(rel_to)
561
 
        self._rename(path_from, path_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)
562
523
 
563
524
    def delete(self, relpath):
564
525
        """Delete the item at relpath"""
565
 
        path = self._remote_path(relpath)
 
526
        path = self._abspath(relpath)
566
527
        try:
567
528
            self._sftp.remove(path)
568
529
        except (IOError, paramiko.SSHException), e:
577
538
        Return a list of all files at the given location.
578
539
        """
579
540
        # does anything actually use this?
580
 
        path = self._remote_path(relpath)
 
541
        path = self._abspath(relpath)
581
542
        try:
582
543
            return self._sftp.listdir(path)
583
544
        except (IOError, paramiko.SSHException), e:
585
546
 
586
547
    def stat(self, relpath):
587
548
        """Return the stat information for a file."""
588
 
        path = self._remote_path(relpath)
 
549
        path = self._abspath(relpath)
589
550
        try:
590
551
            return self._sftp.stat(path)
591
552
        except (IOError, paramiko.SSHException), e:
617
578
        # that we have taken the lock.
618
579
        return SFTPLock(relpath, self)
619
580
 
 
581
 
620
582
    def _unparse_url(self, path=None):
621
583
        if path is None:
622
584
            path = self._path
623
585
        path = urllib.quote(path)
624
 
        # handle homedir paths
625
 
        if not path.startswith('/'):
626
 
            path = "/~/" + path
 
586
        if path.startswith('/'):
 
587
            path = '/%2F' + path[1:]
 
588
        else:
 
589
            path = '/' + path
627
590
        netloc = urllib.quote(self._host)
628
591
        if self._username is not None:
629
592
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
663
626
        # as a homedir relative path (the path begins with a double slash
664
627
        # if it is absolute).
665
628
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
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 = ''
 
629
        if path.startswith('/'):
 
630
            path = path[1:]
 
631
 
673
632
        return (username, password, host, port, path)
674
633
 
675
634
    def _parse_url(self, url):
693
652
            pass
694
653
        
695
654
        vendor = _get_ssh_vendor()
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)
 
655
        if vendor != 'none':
 
656
            sock = SFTPSubprocess(self._host, self._port, self._username)
703
657
            self._sftp = SFTPClient(sock)
704
658
        else:
705
659
            self._paramiko_connect()
713
667
 
714
668
        try:
715
669
            t = paramiko.Transport((self._host, self._port or 22))
716
 
            t.set_log_channel('bzr.paramiko')
717
670
            t.start_client()
718
671
        except paramiko.SSHException, e:
719
672
            raise ConnectionError('Unable to reach SSH host %s:%d' %
738
691
            save_host_keys()
739
692
        if server_key != our_server_key:
740
693
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
741
 
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
 
694
            filename2 = os.path.join(config_dir(), 'ssh_host_keys')
742
695
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
743
696
                (self._host, our_server_key_hex, server_key_hex),
744
697
                ['Try editing %s or %s' % (filename1, filename2)])
780
733
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
781
734
            return
782
735
 
 
736
 
783
737
        if self._password:
784
738
            try:
785
739
                transport.auth_password(username, self._password)
822
776
            pass
823
777
        return False
824
778
 
825
 
    def _sftp_open_exclusive(self, abspath, mode=None):
 
779
    def _sftp_open_exclusive(self, relpath):
826
780
        """Open a remote path exclusively.
827
781
 
828
782
        SFTP supports O_EXCL (SFTP_FLAG_EXCL), which fails if
833
787
        WARNING: This breaks the SFTPClient abstraction, so it
834
788
        could easily break against an updated version of paramiko.
835
789
 
836
 
        :param abspath: The remote absolute path where the file should be opened
837
 
        :param mode: The mode permissions bits for the new file
 
790
        :param relpath: The relative path, where the file should be opened
838
791
        """
839
 
        path = self._sftp._adjust_cwd(abspath)
 
792
        path = self._sftp._adjust_cwd(self._abspath(relpath))
840
793
        attr = SFTPAttributes()
841
 
        if mode is not None:
842
 
            attr.st_mode = mode
843
 
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
 
794
        mode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
844
795
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
845
796
        try:
846
 
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
 
797
            t, msg = self._sftp._request(CMD_OPEN, path, mode, attr)
847
798
            if t != CMD_HANDLE:
848
799
                raise TransportError('Expected an SFTP handle')
849
800
            handle = msg.get_string()
850
 
            return SFTPFile(self._sftp, handle, 'wb', -1)
 
801
            return SFTPFile(self._sftp, handle, 'w', -1)
851
802
        except (paramiko.SSHException, IOError), e:
852
 
            self._translate_io_exception(e, abspath, ': unable to open',
 
803
            self._translate_io_exception(e, relpath, ': unable to open',
853
804
                failure_exc=FileExists)
854
805
 
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
 
            ]